UNPKG

@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
/** * 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;