@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
JavaScript
// 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
};