UNPKG

@localsecurity/name-id

Version:

A utility to create and parse lexicographically sortable ID's with embeded model names and created timestamp

390 lines (389 loc) 15.9 kB
// src/name-id.ts var Models = /* @__PURE__ */ ((Models2) => { Models2["ActivityLog"] = "ActivityLog"; Models2["Address"] = "Address"; Models2["AppliedCameraPlan"] = "AppliedCameraPlan"; Models2["AppliedLivePatrol"] = "AppliedLivePatrol"; Models2["AppliedOperatorMinutes"] = "AppliedOperatorMinutes"; Models2["AppliedProduct"] = "AppliedProduct"; Models2["AppliedSetupFee"] = "AppliedSetupFee"; Models2["AudioCustomMessageType"] = "AudioCustomMessageType"; Models2["AuditLog"] = "AuditLog"; Models2["CallLog"] = "CallLog"; Models2["CallNotice"] = "CallNotice"; Models2["Camera"] = "Camera"; Models2["CameraAccessUrls"] = "CameraAccessUrls"; Models2["CameraCredentials"] = "CameraCredentials"; Models2["CameraEvent"] = "CameraEvent"; Models2["CameraImmixConfig"] = "CameraImmixConfig"; Models2["CameraPlan"] = "CameraPlan"; Models2["CameraQualitySettings"] = "CameraQualitySettings"; Models2["CameraSceneDetectionResult"] = "CameraSceneDetectionResult"; Models2["CameraSceneDetectionResultItem"] = "CameraSceneDetectionResultItem"; Models2["CameraSceneDetectionSettings"] = "CameraSceneDetectionSettings"; Models2["CameraSceneDetectionThreshold"] = "CameraSceneDetectionThreshold"; Models2["Contact"] = "Contact"; Models2["Customer"] = "Customer"; Models2["CustomerUserPermission"] = "CustomerUserPermission"; Models2["CustomerUserPermissionSet"] = "CustomerUserPermissionSet"; Models2["DashboardDetail"] = "DashboardDetail"; Models2["EmailLog"] = "EmailLog"; Models2["ExchangeRateSetting"] = "ExchangeRateSetting"; Models2["Incident"] = "Incident"; Models2["IncidentActionDetail"] = "IncidentActionDetail"; Models2["IncidentActionList"] = "IncidentActionList"; Models2["IncidentBundleTime"] = "IncidentBundleTime"; Models2["IncidentCodeDetail"] = "IncidentCodeDetail"; Models2["IncidentDataToShare"] = "IncidentDataToShare"; Models2["IncidentNote"] = "IncidentNote"; Models2["IncidentShareActionItems"] = "IncidentShareActionItems"; Models2["IncidentShareContactInformation"] = "IncidentShareContactInformation"; Models2["IncidentShareEntry"] = "IncidentShareEntry"; Models2["IncidentShareEntryAccess"] = "IncidentShareEntryAccess"; Models2["IncidentShareEntyNote"] = "IncidentShareEntyNote"; Models2["IncidentShareEvent"] = "IncidentShareEvent"; Models2["IncidentShareIntegrator"] = "IncidentShareIntegrator"; Models2["IncidentShareSettings"] = "IncidentShareSettings"; Models2["IncidentShareSite"] = "IncidentShareSite"; Models2["Invoice"] = "Invoice"; Models2["InvoicePayment"] = "InvoicePayment"; Models2["InvoiceTransferPayment"] = "InvoiceTransferPayment"; Models2["LivePatrol"] = "LivePatrol"; Models2["Master"] = "Master"; Models2["Notification"] = "Notification"; Models2["NotificationSetting"] = "NotificationSetting"; Models2["ObjectGroup"] = "ObjectGroup"; Models2["OperatorMinutes"] = "OperatorMinutes"; Models2["PaymentTransferSettings"] = "PaymentTransferSettings"; Models2["Phone"] = "Phone"; Models2["ProcedureTask"] = "ProcedureTask"; Models2["Product"] = "Product"; Models2["RapidSOSActivity"] = "RapidSOSActivity"; Models2["RapidSOSAttachment"] = "RapidSOSAttachment"; Models2["RapidSOSCivicAddress"] = "RapidSOSCivicAddress"; Models2["RapidSOSDetails"] = "RapidSOSDetails"; Models2["RapidSOSEmergencyContact"] = "RapidSOSEmergencyContact"; Models2["RapidSOSEventDetails"] = "RapidSOSEventDetails"; Models2["RapidSOSIncident"] = "RapidSOSIncident"; Models2["RapidSOSLocation"] = "RapidSOSLocation"; Models2["RapidSOSLogMessage"] = "RapidSOSLogMessage"; Models2["RspndrActivity"] = "RspndrActivity"; Models2["RspndrEvents"] = "RspndrEvents"; Models2["RspndrIncident"] = "RspndrIncident"; Models2["RspndrIncidentCheckItem"] = "RspndrIncidentCheckItem"; Models2["RspndrIncidentGroundingRule"] = "RspndrIncidentGroundingRule"; Models2["SMSLog"] = "SMSLog"; Models2["SOP"] = "SOP"; Models2["SecondaryCameraEvent"] = "SecondaryCameraEvent"; Models2["SetupFee"] = "SetupFee"; Models2["SignalTestLog"] = "SignalTestLog"; Models2["Site"] = "Site"; Models2["SiteEmergencyContact"] = "SiteEmergencyContact"; Models2["SiteIntegration"] = "SiteIntegration"; Models2["SiteSubscription"] = "SiteSubscription"; Models2["SiteSupvisdSetting"] = "SiteSupvisdSetting"; Models2["SiteVMSDetails"] = "SiteVMSDetails"; Models2["StripePaymentMethod"] = "StripePaymentMethod"; Models2["StripePayoutDetails"] = "StripePayoutDetails"; Models2["SubscriptionStripeDetail"] = "SubscriptionStripeDetail"; Models2["SupvisdEventRule"] = "SupvisdEventRule"; Models2["Tax"] = "Tax"; Models2["TaxType"] = "TaxType"; Models2["TaxesTaxType"] = "TaxesTaxType"; Models2["TrackTikActivity"] = "TrackTikActivity"; Models2["TrackTikDetails"] = "TrackTikDetails"; Models2["TrackTikIncident"] = "TrackTikIncident"; Models2["TrackTikTask"] = "TrackTikTask"; Models2["TransactionLineItem"] = "TransactionLineItem"; Models2["TransactionLineItemTax"] = "TransactionLineItemTax"; Models2["Trigger"] = "Trigger"; Models2["User"] = "User"; Models2["UserVMSDetails"] = "UserVMSDetails"; Models2["VMSDetails"] = "VMSDetails"; Models2["WalletTransfer"] = "WalletTransfer"; Models2["WebhookHealth"] = "WebhookHealth"; return Models2; })(Models || {}); var nameIdStringRegex = new RegExp( `^(${Object.values(Models).map((n) => n.toLowerCase()).join("|")})_[2-7a-z]{22,}$` ); var 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; function isUUID(value) { const regexMatch = typeof value === "string" && uuidStringRegex.test(value); return regexMatch; } function isString(value) { try { return typeof value == "string"; } catch (e) { return 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; } function isHasUUID(value) { try { if (isString(value)) return isUUID(value); if (isString(value["id"])) return isUUID(value["id"]); return false; } catch (e) { return false; } } function isModelName(value) { return typeof value === "string" && Object.values(Models).includes(value); } 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"); } function extractModelFromNameIdSkipValidation(nameId) { let parts = nameId.split("_")[0].split(""); const firstLetter = parts.shift()?.toUpperCase(); return firstLetter + parts.join(""); } function extractTimestampFromNameId(nameId, offset = 15e11) { const timestamp = new Date( (offset / 1e3 + parseInt( nameId.split("_")[1].slice(0, 6).split("").reverse().join(""), 32 )) * 1e3 ); return timestamp; } var MutableModel = class { 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 == void 0) this.createdAt = (/* @__PURE__ */ new Date()).toISOString(); if (this.updatedAt == void 0) this.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); if (this.id == void 0) this.id = this.nameId; } }; var NameID = 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 = 15e11; 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 = /* @__PURE__ */ 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 = /* @__PURE__ */ new Date()) { time = new Date(time); const timestampMillis = time.getTime(); const timestampSeconds = Math.floor( (timestampMillis - this._offset) / 1e3 ); 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 name2 = extractModelFromNameIdSkipValidation(value); const timestamp2 = extractTimestampFromNameId(value); return { name: name2, timestamp: timestamp2 }; } if (!isNameId(value)) throw Error("Invalid NameIdString"); const name = extractModelFromNameId(value); const timestamp = extractTimestampFromNameId(value); return { name, timestamp }; } }; export { MutableModel, NameID, extractModelFromNameId, extractModelFromNameIdSkipValidation, extractTimestampFromNameId, isHasUUID, isModelName, isNameId, isString, isUUID, nameIdStringRegex, uuidStringRegex };