@eristack/entity-id
Version:
UUID v7 entity identifiers — sortable, parseable, Drizzle column helper
156 lines (152 loc) • 4.69 kB
JavaScript
// src/core/errors.ts
var ENTITY_ID_PARSE_CODE = "ENTITY_ID_PARSE";
var EntityIdParseError = class extends Error {
code = ENTITY_ID_PARSE_CODE;
constructor(message) {
super(message);
this.name = "EntityIdParseError";
}
};
// src/core/types.ts
var ENTITY_ID_VERSION = 7;
var MAX_ENTITY_ID_UNIX_MS = 281474976710655;
// src/core/internal.ts
var HYPHENATED = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
function bytesToCanonicalUuid(bytes) {
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
const canonical = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
return canonical;
}
function canonicalUuidToBytes(id) {
const compact = id.replace(/-/g, "").toLowerCase();
if (compact.length !== 32 || !/^[0-9a-f]+$/.test(compact)) {
throw new EntityIdParseError("Entity id must be 32 hex digits (optional hyphens)");
}
const bytes = new Uint8Array(16);
for (let i = 0; i < 16; i++) {
bytes[i] = Number.parseInt(compact.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function assertEntityIdV7(bytes) {
const version = bytes[6] >> 4 & 15;
if (version !== ENTITY_ID_VERSION) {
throw new EntityIdParseError(
`Entity id must be UUID version ${ENTITY_ID_VERSION}, got version ${version}`
);
}
const variant = bytes[8] >> 6 & 3;
if (variant !== 2) {
throw new EntityIdParseError("Entity id must use RFC 4122 variant (10xxxxxx)");
}
}
function unixMsFromV7Bytes(bytes) {
const ms = BigInt(bytes[0]) << 40n | BigInt(bytes[1]) << 32n | BigInt(bytes[2]) << 24n | BigInt(bytes[3]) << 16n | BigInt(bytes[4]) << 8n | BigInt(bytes[5]);
return Number(ms);
}
function generateEntityIdBytes(unixMs, random) {
if (!Number.isFinite(unixMs) || unixMs < 0 || unixMs > MAX_ENTITY_ID_UNIX_MS) {
throw new EntityIdParseError(
`Unix timestamp ms out of range for UUID v7 (0..${MAX_ENTITY_ID_UNIX_MS})`
);
}
if (random.length < 10) {
throw new EntityIdParseError("Random bytes must be at least 10 octets");
}
const ts = BigInt(Math.trunc(unixMs));
const bytes = new Uint8Array(16);
bytes[0] = Number(ts >> 40n & 0xffn);
bytes[1] = Number(ts >> 32n & 0xffn);
bytes[2] = Number(ts >> 24n & 0xffn);
bytes[3] = Number(ts >> 16n & 0xffn);
bytes[4] = Number(ts >> 8n & 0xffn);
bytes[5] = Number(ts & 0xffn);
bytes[6] = random[0] & 15 | 112;
bytes[7] = random[1];
bytes[8] = random[2] & 63 | 128;
bytes[9] = random[3];
bytes[10] = random[4];
bytes[11] = random[5];
bytes[12] = random[6];
bytes[13] = random[7];
bytes[14] = random[8];
bytes[15] = random[9];
return bytes;
}
function isHyphenatedUuidLower(value) {
return HYPHENATED.test(value);
}
// src/core/entity-id.ts
function randomOctets(length) {
const bytes = new Uint8Array(length);
crypto.getRandomValues(bytes);
return bytes;
}
function entityIdFactory() {
return generateEntityId();
}
function generateEntityId(unixMs = Date.now()) {
const rnd = randomOctets(10);
const bytes = generateEntityIdBytes(unixMs, rnd);
return bytesToCanonicalUuid(bytes);
}
function generateEntityIdAt(unixMs, random) {
const bytes = generateEntityIdBytes(unixMs, random);
return bytesToCanonicalUuid(bytes);
}
function normalizeEntityId(value) {
const trimmed = value.trim().toLowerCase();
const canonical = trimmed.includes("-") ? trimmed : `${trimmed.slice(0, 8)}-${trimmed.slice(8, 12)}-${trimmed.slice(12, 16)}-${trimmed.slice(16, 20)}-${trimmed.slice(20)}`;
if (!isHyphenatedUuidLower(canonical)) {
throw new EntityIdParseError("Entity id must be a lowercase UUID string");
}
const bytes = canonicalUuidToBytes(canonical);
assertEntityIdV7(bytes);
return canonical;
}
function parseEntityId(value) {
return normalizeEntityId(value);
}
function isValidEntityId(value) {
try {
parseEntityId(value);
return true;
} catch {
return false;
}
}
function entityIdToUnixMs(id) {
const bytes = canonicalUuidToBytes(id);
assertEntityIdV7(bytes);
return unixMsFromV7Bytes(bytes);
}
function entityIdToDate(id) {
return new Date(entityIdToUnixMs(id));
}
function compareEntityIds(a, b) {
return a.localeCompare(b);
}
function entityIdEquals(a, b) {
try {
return parseEntityId(a) === parseEntityId(b);
} catch {
return false;
}
}
export {
ENTITY_ID_PARSE_CODE,
EntityIdParseError,
ENTITY_ID_VERSION,
MAX_ENTITY_ID_UNIX_MS,
entityIdFactory,
generateEntityId,
generateEntityIdAt,
normalizeEntityId,
parseEntityId,
isValidEntityId,
entityIdToUnixMs,
entityIdToDate,
compareEntityIds,
entityIdEquals
};
//# sourceMappingURL=chunk-7V2KY5JP.js.map