@localsecurity/name-id
Version:
A utility to create and parse lexicographically sortable ID's with embeded model names and created timestamp
485 lines (484 loc) • 19.2 kB
JavaScript
/**
* Models // todo -> add Models / ModelNames enum to types package
* */
var Models;
(function (Models) {
Models["ActivityLog"] = "ActivityLog";
Models["Address"] = "Address";
Models["AppliedCameraPlan"] = "AppliedCameraPlan";
Models["AppliedLivePatrol"] = "AppliedLivePatrol";
Models["AppliedOperatorMinutes"] = "AppliedOperatorMinutes";
Models["AppliedProduct"] = "AppliedProduct";
Models["AppliedSetupFee"] = "AppliedSetupFee";
Models["AudioCustomMessageType"] = "AudioCustomMessageType";
Models["AuditLog"] = "AuditLog";
Models["CallLog"] = "CallLog";
Models["CallNotice"] = "CallNotice";
Models["Camera"] = "Camera";
Models["CameraAccessUrls"] = "CameraAccessUrls";
Models["CameraCredentials"] = "CameraCredentials";
Models["CameraEvent"] = "CameraEvent";
Models["CameraImmixConfig"] = "CameraImmixConfig";
Models["CameraPlan"] = "CameraPlan";
Models["CameraQualitySettings"] = "CameraQualitySettings";
Models["CameraSceneDetectionResult"] = "CameraSceneDetectionResult";
Models["CameraSceneDetectionResultItem"] = "CameraSceneDetectionResultItem";
Models["CameraSceneDetectionSettings"] = "CameraSceneDetectionSettings";
Models["CameraSceneDetectionThreshold"] = "CameraSceneDetectionThreshold";
Models["Contact"] = "Contact";
Models["Customer"] = "Customer";
Models["CustomerUserPermission"] = "CustomerUserPermission";
Models["CustomerUserPermissionSet"] = "CustomerUserPermissionSet";
Models["DashboardDetail"] = "DashboardDetail";
Models["EmailLog"] = "EmailLog";
Models["ExchangeRateSetting"] = "ExchangeRateSetting";
Models["Incident"] = "Incident";
Models["IncidentActionDetail"] = "IncidentActionDetail";
Models["IncidentActionList"] = "IncidentActionList";
Models["IncidentBundleTime"] = "IncidentBundleTime";
Models["IncidentCodeDetail"] = "IncidentCodeDetail";
Models["IncidentDataToShare"] = "IncidentDataToShare";
Models["IncidentNote"] = "IncidentNote";
Models["IncidentShareActionItems"] = "IncidentShareActionItems";
Models["IncidentShareContactInformation"] = "IncidentShareContactInformation";
Models["IncidentShareEntry"] = "IncidentShareEntry";
Models["IncidentShareEntryAccess"] = "IncidentShareEntryAccess";
Models["IncidentShareEntyNote"] = "IncidentShareEntyNote";
Models["IncidentShareEvent"] = "IncidentShareEvent";
Models["IncidentShareIntegrator"] = "IncidentShareIntegrator";
Models["IncidentShareSettings"] = "IncidentShareSettings";
Models["IncidentShareSite"] = "IncidentShareSite";
Models["Invoice"] = "Invoice";
Models["InvoicePayment"] = "InvoicePayment";
Models["InvoiceTransferPayment"] = "InvoiceTransferPayment";
Models["LivePatrol"] = "LivePatrol";
Models["Master"] = "Master";
Models["Notification"] = "Notification";
Models["NotificationSetting"] = "NotificationSetting";
Models["ObjectGroup"] = "ObjectGroup";
Models["OperatorMinutes"] = "OperatorMinutes";
Models["PaymentTransferSettings"] = "PaymentTransferSettings";
Models["Phone"] = "Phone";
Models["ProcedureTask"] = "ProcedureTask";
Models["Product"] = "Product";
Models["RapidSOSActivity"] = "RapidSOSActivity";
Models["RapidSOSAttachment"] = "RapidSOSAttachment";
Models["RapidSOSCivicAddress"] = "RapidSOSCivicAddress";
Models["RapidSOSDetails"] = "RapidSOSDetails";
Models["RapidSOSEmergencyContact"] = "RapidSOSEmergencyContact";
Models["RapidSOSEventDetails"] = "RapidSOSEventDetails";
Models["RapidSOSIncident"] = "RapidSOSIncident";
Models["RapidSOSLocation"] = "RapidSOSLocation";
Models["RapidSOSLogMessage"] = "RapidSOSLogMessage";
Models["RspndrActivity"] = "RspndrActivity";
Models["RspndrEvents"] = "RspndrEvents";
Models["RspndrIncident"] = "RspndrIncident";
Models["RspndrIncidentCheckItem"] = "RspndrIncidentCheckItem";
Models["RspndrIncidentGroundingRule"] = "RspndrIncidentGroundingRule";
Models["SMSLog"] = "SMSLog";
Models["SOP"] = "SOP";
Models["SecondaryCameraEvent"] = "SecondaryCameraEvent";
Models["SetupFee"] = "SetupFee";
Models["SignalTestLog"] = "SignalTestLog";
Models["Site"] = "Site";
Models["SiteEmergencyContact"] = "SiteEmergencyContact";
Models["SiteIntegration"] = "SiteIntegration";
Models["SiteSubscription"] = "SiteSubscription";
Models["SiteSupvisdSetting"] = "SiteSupvisdSetting";
Models["SiteVMSDetails"] = "SiteVMSDetails";
Models["StripePaymentMethod"] = "StripePaymentMethod";
Models["StripePayoutDetails"] = "StripePayoutDetails";
Models["SubscriptionStripeDetail"] = "SubscriptionStripeDetail";
Models["SupvisdEventRule"] = "SupvisdEventRule";
Models["Tax"] = "Tax";
Models["TaxType"] = "TaxType";
Models["TaxesTaxType"] = "TaxesTaxType";
Models["TrackTikActivity"] = "TrackTikActivity";
Models["TrackTikDetails"] = "TrackTikDetails";
Models["TrackTikIncident"] = "TrackTikIncident";
Models["TrackTikTask"] = "TrackTikTask";
Models["TransactionLineItem"] = "TransactionLineItem";
Models["TransactionLineItemTax"] = "TransactionLineItemTax";
Models["Trigger"] = "Trigger";
Models["User"] = "User";
Models["UserVMSDetails"] = "UserVMSDetails";
Models["VMSDetails"] = "VMSDetails";
Models["WalletTransfer"] = "WalletTransfer";
Models["WebhookHealth"] = "WebhookHealth";
})(Models || (Models = {}));
/**
* A sortable ID in the format "{model}_{reversedTimestamp}{random}".
*/
const nameIdStringRegex = new RegExp(`^(${Object.values(Models)
.map((n) => n.toLowerCase())
.join("|")})_[2-7a-z]{22,}$`);
/**
* A valid uuid v4
*/
const uuidStringRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
/**
* Checks if the value is a valid uuid v4
* @param value A uuid string hopefully
* @returns true or false
*/
function isUUID(value) {
const regexMatch = typeof value === "string" && uuidStringRegex.test(value);
return regexMatch;
}
/**
* Checks if the value is a String
* @param value A string hopefully
* @returns true or false
*/
function isString(value) {
try {
return typeof value == "string";
}
catch (e) {
return false;
}
}
/**
* Checks if the value is a NameID (a sortable ID in the format "{model}_{reversedTimestamp}{random}")
* @param value A valid NameID string hopefully
* @returns true or false
*/
function isNameId(value) {
const parts = typeof value == "string" ? value.split("_") : [];
if (parts.length !== 2)
return false;
const timestamp = parts[1].slice(0, 6);
if (timestamp.length !== 6)
return false;
const regexMatch = typeof value === "string" && nameIdStringRegex.test(value);
if (regexMatch == false)
return false;
return true;
}
/**
* Checks if the value, or value['id'], is a valid uuid v4
* @param value A string or object with an 'id' property
* @returns true or false
* @example
* isHasUUID("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx") // true
* isHasUUID({"id":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"}) // true
*/
function isHasUUID(value) {
try {
if (isString(value))
return isUUID(value);
if (isString(value["id"]))
return isUUID(value["id"]);
return false;
}
catch (e) {
return false;
}
}
/**
* Checks if the value is a known name for a Model
* @param value
* @returns true or false
*/
function isModelName(value) {
return (typeof value === "string" && Object.values(Models).includes(value));
}
/**
* Extracts the Model Name from a NameID
* @param nameId A string in the format "{model}_{reversedTimestamp}{random}"
* @returns A ModelName
* @example
* extractModelFromNameId('user_012345234567abcdefghijklmnopqrstuv') // returns 'User'
*/
function extractModelFromNameId(nameId) {
const typenames = Object.values(Models);
const nameIdPrefix = nameId.split("_")[0];
for (let name of typenames) {
if (name.toLowerCase() === nameIdPrefix) {
return name;
}
}
throw Error("get __typename failed");
}
/**
* Extracts the Model Name from a NameID with a novel (unknown) model prefix
* @param nameId A string in the format "{model}_{reversedTimestamp}{random}"
* @returns A ModelName
* @example
* extractModelFromNameId('cat_012345234567abcdefghijklmnopqrstuv') // returns 'Cat'
*/
function extractModelFromNameIdSkipValidation(nameId) {
let parts = nameId.split("_")[0].split("");
const firstLetter = parts.shift()?.toUpperCase();
return firstLetter + parts.join("");
}
/**
* Extracts the createdAt timestamp from a NameID
* @param nameId A string in the format "{model}_{reversedTimestamp}{random}"
* @param offset Optionally you can include an offset if a custom offset was used
* @returns a Date
*/
function extractTimestampFromNameId(nameId, offset = 1500000000000) {
const timestamp = new Date((offset / 1000 +
parseInt(nameId.split("_")[1].slice(0, 6).split("").reverse().join(""), 32)) *
1000);
return timestamp;
}
/**
* Create a new Model Object with a valid NameID from an existing Model Object or a Model Name
*/
class MutableModel {
get __typename() {
const typenames = Object.values(Models);
const nameIdPrefix = this.nameId.split("_")[0];
for (let name of typenames) {
if (name.toLowerCase() === nameIdPrefix) {
return name;
}
}
throw Error("get __typename failed");
}
/**
* @param init An existing Model Object, or a Model Name
*/
constructor(init) {
if (init instanceof Object) {
const maybeTypename = init.__typename;
const hasTypename = typeof maybeTypename === "string" && isModelName(maybeTypename)
? maybeTypename
: false;
const maybeNameId = init.nameId;
const hasNameId = typeof maybeNameId === "string" && isNameId(maybeNameId)
? maybeNameId
: false;
if (hasNameId) {
Object.assign(this, init);
}
else if (hasTypename) {
this.nameId = new NameID(hasTypename).value;
Object.assign(this, init);
}
else {
throw Error("no valid __typename or name id in object");
}
}
else {
this.nameId = new NameID(init).value;
}
if (this.createdAt == undefined)
this.createdAt = new Date().toISOString();
if (this.updatedAt == undefined)
this.updatedAt = new Date().toISOString();
if (this.id == undefined)
this.id = this.nameId;
}
}
/**
* Represents a sortable ID.
*
* This class encapsulates the generation, validation, and manipulation of sortable IDs,
* which are used as primary keys in our database, particularly for time-series data.
*/
class NameID {
/**
* @param { ModelName | Model | NameId } input - Can be a ModelName, a Model object (with __typename and createdAt), or a valid nameId string.
* @param { string | number | Date } time - Defaults to now.
* @param {boolean} skip - Skip 'Is Valid Model Name' check.
* @throws { Error } - Throws an error if the input is invalid.
*
* @example
* const NameID_For_Model = new NameID("User");
* const NameID_From_Model = new NameID({ __typename: "User", ...});
* const NameID_From_Model_With_UUID = new NameID({id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", __typename: "User", ... });
*/
constructor(input, time = Date.now(), skip = false) {
this._lexicographicBase32 = "234567abcdefghijklmnopqrstuvwxyz";
this._randomLength = 16;
this._offset = 1500000000000; // 2022-11-11T01:53:20 UTC
if (isModelName(input)) {
this._value = this.generateNameID(input, time);
}
else if (isNameId(input)) {
let name = extractModelFromNameId(input);
time = extractTimestampFromNameId(input, this._offset);
this._value = this.generateNameID(name, time);
}
else if (typeof input["__typename"] == "string" &&
isModelName(input["__typename"]) &&
isHasUUID(input)) {
this._value = this.fromUUID(input);
}
else if (typeof input["__typename"] == "string" &&
isModelName(input["__typename"])) {
let name = input["__typename"];
time = input["createdAt"] ?? time;
this._value = this.generateNameID(name, time);
}
else if (typeof input["nameId"] == "string" &&
isNameId(input["nameId"])) {
let name = extractModelFromNameId(input["nameId"]);
time = extractTimestampFromNameId(input["nameId"], this._offset);
this._value = this.generateNameID(name, time);
}
else if (skip) {
if (typeof input !== "string")
throw Error("You must pass a string with skip");
else
this._value = this.generateNameID(input, time);
}
else {
throw Error(`invalid new NameID(${JSON.stringify({ input })}`);
}
}
/**
* Returns The string representation of the NameID.
* @example
* const nameId = new NameID("User");
* console.log(nameId.toString()); // Output: user_012345234567abcdefghijklmnopqrstuv
*/
get value() {
return this._value;
}
/**
* Convert a legacy object with a uuid id to a NameIdString
* @param object Any valid Model object
* @returns
*/
fromUUID(object) {
let id = "";
const model = object["__typename"];
const hasFX = typeof object["country"] == "string" ? object["country"] : false;
const hasId = typeof object["id"] == "string" ? object["id"].replace(/-/g, "") : false;
const hasUsername = typeof object["username"] == "string" ? object["username"] : false;
if (hasId)
id = hasId;
else if (hasFX)
id = hasFX;
else if (hasUsername)
id = hasUsername;
else
throw Error(`no id or username or country: ${JSON.stringify(object)}`);
return `${model}_${this.generateEncodedTimestamp(object["createdAt"])}${id}`;
}
/**
* Generates a sortable ID with model prefix and reversed timestamp.
*
* This function creates a unique identifier suitable for use as a primary key
* in a database, particularly for time-series data or event-driven systems.
* It combines a model identifier, a reversed timestamp for reverse chronological
* sorting, and a random component for uniqueness.
*
* @param name - The model type name (e.g., "User").
* @param time - The timestamp to encode. If not provided, the current time is used.
*
* @returns The generated sortable ID in the format "{model}_{reversedTimestamp}{random}".
*
* @example
* const userId = generateNameID("User");
* const cameraEventId = generateNameID("CameraEvent", new Date("2024-08-15T12:30:00Z"));
*/
generateNameID(name, time = new Date()) {
const encodedTimestamp = this.generateEncodedTimestamp(time);
let randomPart = "";
for (let i = 0; i < this._randomLength; i++) {
randomPart +=
this._lexicographicBase32[Math.floor(Math.random() * this._lexicographicBase32.length)];
}
return `${name.toLowerCase()}_${encodedTimestamp}${randomPart}`;
}
generateEncodedTimestamp(time = new Date()) {
time = new Date(time);
const timestampMillis = time.getTime();
const timestampSeconds = Math.floor((timestampMillis - this._offset) / 1000);
const timestampBase32 = timestampSeconds.toString(32).padStart(6, "0");
const encodedTimestamp = timestampBase32.split("").reverse().join("");
return encodedTimestamp;
}
/**
* Checks if a given ID is valid based on the expected format.
*
* This function validates if a string conforms to the expected sortable ID
* format: "model_reversedTimestamp_random".
* @returns {boolean} - True if the ID is valid, false otherwise.
*
* @example
* const validId = generateNameID("user");
* const invalidId = "user-12345-abcde";
*
* console.log(isValidId(validId)); // true
* console.log(isValidId(invalidId)); // false
*/
get isValid() {
return isNameId(this._value);
}
/**
* Decodes the timestamp from a NameID.
*
* @returns {Date | null} - The decoded timestamp as a Date object, or null if the ID is invalid.
*
* @example
* const { timestamp } = new NameID("event_abcdefghi");
* console.log(timestamp.toISOString());
*/
get timestamp() {
try {
if (!this.isValid)
throw Error("invalid nameId");
return extractTimestampFromNameId(this._value, this._offset);
}
catch (e) {
return null;
}
}
/**
* Generates a sortable ID with model prefix and reversed timestamp.
*
* This function creates a unique identifier suitable for use as a primary key
* in a database, particularly for time-series data or event-driven systems.
* It combines a model identifier, a reversed timestamp for reverse chronological
* sorting, and a random component for uniqueness.
*
* @param value - Can be a ModelName, a Model object (with __typename and createdAt), or a valid nameId string.
* @param time - The timestamp to encode. If not provided, the current time is used.
*
* @returns The generated sortable ID in the format "model_{reversedTimestamp}{random}".
*
* @example
* const userId = generateNameID("User");
* const cameraEventId = generateNameID("CameraEvent", new Date("2024-08-15T12:30:00Z"));
*/
static from(value, time) {
return new NameID(value);
}
/**
* Checks if the input is a value nameId
* @param value A nameId.
* @param skip Skip 'Is Valid Model Name' check.
* @returns A model name and timestamp
*/
static parse(value, skip = false) {
if (skip) {
const name = extractModelFromNameIdSkipValidation(value);
const timestamp = extractTimestampFromNameId(value);
return { name, timestamp };
}
if (!isNameId(value))
throw Error("Invalid NameIdString");
const name = extractModelFromNameId(value);
const timestamp = extractTimestampFromNameId(value);
return { name, timestamp };
}
}
/**
* Classes
*/
export { NameID, MutableModel };
/**
* Regexp
*/
export { uuidStringRegex, nameIdStringRegex };
/**
* Utilities
*/
export { isUUID, isString, isNameId, isHasUUID, isModelName, extractModelFromNameId, extractTimestampFromNameId, extractModelFromNameIdSkipValidation, };
export default NameID;