@smonn/ids 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +400 -18
- package/dist/cli.mjs +195 -16
- package/dist/cli.mjs.map +1 -1
- package/dist/drizzle-CeSni5PB.d.mts +44 -0
- package/dist/drizzle-CeSni5PB.d.mts.map +1 -0
- package/dist/drizzle.d.mts +2 -0
- package/dist/drizzle.mjs +42 -0
- package/dist/drizzle.mjs.map +1 -0
- package/dist/express.d.mts +92 -0
- package/dist/express.d.mts.map +1 -0
- package/dist/express.mjs +90 -0
- package/dist/express.mjs.map +1 -0
- package/dist/hono.d.mts +75 -0
- package/dist/hono.d.mts.map +1 -0
- package/dist/hono.mjs +63 -0
- package/dist/hono.mjs.map +1 -0
- package/dist/kysely.d.mts +55 -0
- package/dist/kysely.d.mts.map +1 -0
- package/dist/kysely.mjs +42 -0
- package/dist/kysely.mjs.map +1 -0
- package/dist/{opaque-B4ps7Pqk.mjs → opaque-goLnFoo7.mjs} +29 -13
- package/dist/opaque-goLnFoo7.mjs.map +1 -0
- package/dist/opaque.d.mts +33 -9
- package/dist/opaque.d.mts.map +1 -1
- package/dist/opaque.mjs +1 -1
- package/dist/prisma.d.mts +84 -0
- package/dist/prisma.d.mts.map +1 -0
- package/dist/prisma.mjs +53 -0
- package/dist/prisma.mjs.map +1 -0
- package/dist/reverse--n4D2yxu.mjs +87 -0
- package/dist/reverse--n4D2yxu.mjs.map +1 -0
- package/dist/reverse.d.mts +76 -0
- package/dist/reverse.d.mts.map +1 -0
- package/dist/reverse.mjs +2 -0
- package/dist/wrapped-Dw5mHQhn.mjs +363 -0
- package/dist/wrapped-Dw5mHQhn.mjs.map +1 -0
- package/dist/wrapped.d.mts +86 -8
- package/dist/wrapped.d.mts.map +1 -1
- package/dist/wrapped.mjs +1 -335
- package/package.json +38 -3
- package/dist/opaque-B4ps7Pqk.mjs.map +0 -1
- package/dist/wrapped.mjs.map +0 -1
package/dist/opaque.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { i as
|
|
1
|
+
import { i as importOpaqueKey, n as decodeOpaqueKey, r as encodeOpaqueKey, t as createOpaqueTimestampId } from "./opaque-goLnFoo7.mjs";
|
|
2
2
|
export { createOpaqueTimestampId, decodeOpaqueKey, encodeOpaqueKey, importOpaqueKey };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { i as ParseResult, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/prisma.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Minimum codec interface required by the Prisma adapter.
|
|
6
|
+
*
|
|
7
|
+
* Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,
|
|
8
|
+
* ReverseTimestampCodec, and WrappedKeyCodec all expose `safeParse`. The adapter
|
|
9
|
+
* never calls key-dependent methods.
|
|
10
|
+
*
|
|
11
|
+
* Intentionally the same structural shape as the Drizzle adapter's IdColumnCodec.
|
|
12
|
+
* Do NOT import IdColumnCodec from `@smonn/ids/drizzle` — that would create
|
|
13
|
+
* cross-adapter coupling.
|
|
14
|
+
*/
|
|
15
|
+
type IdColumnCodec<Brand extends string> = {
|
|
16
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.
|
|
20
|
+
*
|
|
21
|
+
* **Prisma casting caveat:** Prisma cannot fully brand a generated model field
|
|
22
|
+
* type at the schema level. The `read` function asserts `Id<Brand>` at the
|
|
23
|
+
* TypeScript level, but Prisma's generated types for the model field will not
|
|
24
|
+
* reflect this branding. Callers consuming the validated value from a Prisma
|
|
25
|
+
* result component may need an explicit `as Id<Brand>` cast at the call site.
|
|
26
|
+
*/
|
|
27
|
+
type IdTransform<Brand extends string> = {
|
|
28
|
+
/**
|
|
29
|
+
* Read transform: validates the raw database value via `safeParse` and returns
|
|
30
|
+
* `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a
|
|
31
|
+
* different brand.
|
|
32
|
+
*
|
|
33
|
+
* Use in a Prisma `$extends` result component's `compute` function.
|
|
34
|
+
*/
|
|
35
|
+
read(value: unknown): Id<Brand>;
|
|
36
|
+
/**
|
|
37
|
+
* Write transform: passes `Id<Brand>` through as its canonical string form.
|
|
38
|
+
* `Id<Brand>` is already the canonical string, so this is an identity function
|
|
39
|
+
* at runtime.
|
|
40
|
+
*
|
|
41
|
+
* Use in a Prisma `$extends` query component or explicit `data` mapping.
|
|
42
|
+
*/
|
|
43
|
+
write(value: Id<Brand>): string;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Creates a read/write transform pair for use with Prisma's `$extends` extension model.
|
|
47
|
+
*
|
|
48
|
+
* Works with any codec variant exposing `safeParse` (TimestampCodec,
|
|
49
|
+
* OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).
|
|
50
|
+
*
|
|
51
|
+
* **Prisma casting caveat:** Prisma's `$extends` result component can add
|
|
52
|
+
* typed computed accessors to model instances, but cannot retroactively
|
|
53
|
+
* re-type an existing schema field at the Prisma Client level. The `read`
|
|
54
|
+
* function asserts `Id<Brand>`, but callers will need an explicit
|
|
55
|
+
* `as Id<Brand>` cast at consumption sites where Prisma's generated types
|
|
56
|
+
* are expected.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* import { idField } from "@smonn/ids/prisma";
|
|
61
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
62
|
+
*
|
|
63
|
+
* const usr = createTimestampId("usr");
|
|
64
|
+
* const userIdField = idField(usr);
|
|
65
|
+
*
|
|
66
|
+
* const xprisma = prisma.$extends({
|
|
67
|
+
* result: {
|
|
68
|
+
* user: {
|
|
69
|
+
* id: {
|
|
70
|
+
* needs: { id: true },
|
|
71
|
+
* compute(user) {
|
|
72
|
+
* // Cast required: Prisma cannot brand the generated type at schema level
|
|
73
|
+
* return userIdField.read(user.id) as Id<"usr">;
|
|
74
|
+
* },
|
|
75
|
+
* },
|
|
76
|
+
* },
|
|
77
|
+
* },
|
|
78
|
+
* });
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;
|
|
82
|
+
//#endregion
|
|
83
|
+
export { IdColumnCodec, IdTransform, idField };
|
|
84
|
+
//# sourceMappingURL=prisma.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/prisma.ts"],"mappings":";;;;;AAaA;;;;;;;;;KAAY,aAAA;EACV,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;AAAA;;;;;;;;;;KAY7B,WAAA;;;;;;;;EAQV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EAQT;AAuClB;;;;;;EAvCE,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;AAAA;;;;;;;;;AAuCsE;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
|
package/dist/prisma.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/prisma.ts
|
|
2
|
+
/**
|
|
3
|
+
* Creates a read/write transform pair for use with Prisma's `$extends` extension model.
|
|
4
|
+
*
|
|
5
|
+
* Works with any codec variant exposing `safeParse` (TimestampCodec,
|
|
6
|
+
* OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).
|
|
7
|
+
*
|
|
8
|
+
* **Prisma casting caveat:** Prisma's `$extends` result component can add
|
|
9
|
+
* typed computed accessors to model instances, but cannot retroactively
|
|
10
|
+
* re-type an existing schema field at the Prisma Client level. The `read`
|
|
11
|
+
* function asserts `Id<Brand>`, but callers will need an explicit
|
|
12
|
+
* `as Id<Brand>` cast at consumption sites where Prisma's generated types
|
|
13
|
+
* are expected.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { idField } from "@smonn/ids/prisma";
|
|
18
|
+
* import { createTimestampId } from "@smonn/ids";
|
|
19
|
+
*
|
|
20
|
+
* const usr = createTimestampId("usr");
|
|
21
|
+
* const userIdField = idField(usr);
|
|
22
|
+
*
|
|
23
|
+
* const xprisma = prisma.$extends({
|
|
24
|
+
* result: {
|
|
25
|
+
* user: {
|
|
26
|
+
* id: {
|
|
27
|
+
* needs: { id: true },
|
|
28
|
+
* compute(user) {
|
|
29
|
+
* // Cast required: Prisma cannot brand the generated type at schema level
|
|
30
|
+
* return userIdField.read(user.id) as Id<"usr">;
|
|
31
|
+
* },
|
|
32
|
+
* },
|
|
33
|
+
* },
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
function idField(codec) {
|
|
39
|
+
return {
|
|
40
|
+
read(value) {
|
|
41
|
+
const result = codec.safeParse(value);
|
|
42
|
+
if (!result.ok) throw new Error(`[ids] invalid ID from database: ${result.error}`);
|
|
43
|
+
return result.id;
|
|
44
|
+
},
|
|
45
|
+
write(value) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { idField };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=prisma.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prisma.mjs","names":[],"sources":["../src/prisma.ts"],"sourcesContent":["import type { Id, ParseResult } from \"./types.js\";\n\n/**\n * Minimum codec interface required by the Prisma adapter.\n *\n * Any codec variant satisfies this type — TimestampCodec, OpaqueTimestampCodec,\n * ReverseTimestampCodec, and WrappedKeyCodec all expose `safeParse`. The adapter\n * never calls key-dependent methods.\n *\n * Intentionally the same structural shape as the Drizzle adapter's IdColumnCodec.\n * Do NOT import IdColumnCodec from `@smonn/ids/drizzle` — that would create\n * cross-adapter coupling.\n */\nexport type IdColumnCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\n\n/**\n * Read/write transform pair for integrating `Id<Brand>` with Prisma extensions.\n *\n * **Prisma casting caveat:** Prisma cannot fully brand a generated model field\n * type at the schema level. The `read` function asserts `Id<Brand>` at the\n * TypeScript level, but Prisma's generated types for the model field will not\n * reflect this branding. Callers consuming the validated value from a Prisma\n * result component may need an explicit `as Id<Brand>` cast at the call site.\n */\nexport type IdTransform<Brand extends string> = {\n /**\n * Read transform: validates the raw database value via `safeParse` and returns\n * `Id<Brand>`. Throws if the value is missing, malformed, or belongs to a\n * different brand.\n *\n * Use in a Prisma `$extends` result component's `compute` function.\n */\n read(value: unknown): Id<Brand>;\n /**\n * Write transform: passes `Id<Brand>` through as its canonical string form.\n * `Id<Brand>` is already the canonical string, so this is an identity function\n * at runtime.\n *\n * Use in a Prisma `$extends` query component or explicit `data` mapping.\n */\n write(value: Id<Brand>): string;\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Works with any codec variant exposing `safeParse` (TimestampCodec,\n * OpaqueTimestampCodec, ReverseTimestampCodec, WrappedKeyCodec).\n *\n * **Prisma casting caveat:** Prisma's `$extends` result component can add\n * typed computed accessors to model instances, but cannot retroactively\n * re-type an existing schema field at the Prisma Client level. The `read`\n * function asserts `Id<Brand>`, but callers will need an explicit\n * `as Id<Brand>` cast at consumption sites where Prisma's generated types\n * are expected.\n *\n * @example\n * ```ts\n * import { idField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const userIdField = idField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * user: {\n * id: {\n * needs: { id: true },\n * compute(user) {\n * // Cast required: Prisma cannot brand the generated type at schema level\n * return userIdField.read(user.id) as Id<\"usr\">;\n * },\n * },\n * },\n * },\n * });\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new Error(`[ids] invalid ID from database: ${result.error}`);\n }\n return result.id;\n },\n write(value: Id<Brand>): string {\n return value;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiFA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,MAAM,SAAS,MAAM,UAAU,KAAK;GACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,MAAM,mCAAmC,OAAO,OAAO;GAEnE,OAAO,OAAO;EAChB;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;CACF;AACF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { a as toWireId, i as payloadBytesFromId, n as registerBrand, s as validateBrand, t as wireMethods } from "./codec-shell-dWpxoFmy.mjs";
|
|
2
|
+
import { r as writeTimestamp } from "./timestamp-bytes-B57RM7Ho.mjs";
|
|
3
|
+
//#region src/layouts/reverse-timestamp.ts
|
|
4
|
+
const randomByteLength = 10;
|
|
5
|
+
/** Writes inverted timestamp bytes, then fills random portion. */
|
|
6
|
+
function buildReversePayload(ms, rng, buffer, randomView) {
|
|
7
|
+
writeTimestamp(ms, buffer);
|
|
8
|
+
for (let i = 0; i < 6; i++) buffer[i] = ~buffer[i] & 255;
|
|
9
|
+
rng(randomView);
|
|
10
|
+
}
|
|
11
|
+
/** Writes inverted timestamp bytes, then fills random portion with a sentinel. */
|
|
12
|
+
function buildReverseSentinelPayload(ms, fill, buffer, randomView) {
|
|
13
|
+
writeTimestamp(ms, buffer);
|
|
14
|
+
for (let i = 0; i < 6; i++) buffer[i] = ~buffer[i] & 255;
|
|
15
|
+
randomView.fill(fill);
|
|
16
|
+
}
|
|
17
|
+
/** Decodes the original timestamp by inverting the first 6 payload bytes. */
|
|
18
|
+
function extractReverseTimestampFromId(prefix, id) {
|
|
19
|
+
const bytes = payloadBytesFromId(prefix, id);
|
|
20
|
+
let ms = 0;
|
|
21
|
+
for (let i = 0; i < 6; i++) ms = ms * 256 + (~bytes[i] & 255);
|
|
22
|
+
return new Date(ms);
|
|
23
|
+
}
|
|
24
|
+
/** Layout ops binder for the Reverse Timestamp variant. */
|
|
25
|
+
function createReverseTimestampLayoutOps(prefix, rng) {
|
|
26
|
+
const buffer = new Uint8Array(16);
|
|
27
|
+
const randomView = new Uint8Array(buffer.buffer, 6, randomByteLength);
|
|
28
|
+
return {
|
|
29
|
+
generateAt: (ms) => {
|
|
30
|
+
buildReversePayload(ms, rng, buffer, randomView);
|
|
31
|
+
return toWireId(prefix, buffer);
|
|
32
|
+
},
|
|
33
|
+
extractTimestamp: (id) => extractReverseTimestampFromId(prefix, id),
|
|
34
|
+
minIdForTime: (ms) => {
|
|
35
|
+
buildReverseSentinelPayload(ms, 0, buffer, randomView);
|
|
36
|
+
return toWireId(prefix, buffer);
|
|
37
|
+
},
|
|
38
|
+
maxIdForTime: (ms) => {
|
|
39
|
+
buildReverseSentinelPayload(ms, 255, buffer, randomView);
|
|
40
|
+
return toWireId(prefix, buffer);
|
|
41
|
+
},
|
|
42
|
+
exampleWireId: (ms) => {
|
|
43
|
+
buildReversePayload(ms, rng, buffer, randomView);
|
|
44
|
+
return toWireId(prefix, buffer);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/reverse.ts
|
|
50
|
+
function defaultRng(target) {
|
|
51
|
+
crypto.getRandomValues(target);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).
|
|
55
|
+
*
|
|
56
|
+
* IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,
|
|
57
|
+
* so lexicographic ID order equals descending creation-time order. `extractTimestamp`
|
|
58
|
+
* inverts back to recover the original millisecond.
|
|
59
|
+
*
|
|
60
|
+
* @param brand - Entity type brand validated once at construction.
|
|
61
|
+
* @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
62
|
+
*/
|
|
63
|
+
function createReverseTimestampId(brand, opts = {}) {
|
|
64
|
+
validateBrand(brand);
|
|
65
|
+
registerBrand(brand, opts.allowDuplicateBrand);
|
|
66
|
+
const now = opts.now ?? Date.now;
|
|
67
|
+
const rng = opts.rng ?? defaultRng;
|
|
68
|
+
const prefix = `${brand}_`;
|
|
69
|
+
const wire = wireMethods(prefix);
|
|
70
|
+
const layout = createReverseTimestampLayoutOps(prefix, rng);
|
|
71
|
+
return {
|
|
72
|
+
generate: () => layout.generateAt(now()),
|
|
73
|
+
generateAt: (date) => layout.generateAt(date.getTime()),
|
|
74
|
+
is: wire.is,
|
|
75
|
+
parse: wire.parse,
|
|
76
|
+
safeParse: wire.safeParse,
|
|
77
|
+
extractTimestamp: layout.extractTimestamp,
|
|
78
|
+
minIdForTime: (date) => layout.minIdForTime(date.getTime()),
|
|
79
|
+
maxIdForTime: (date) => layout.maxIdForTime(date.getTime()),
|
|
80
|
+
toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(now())),
|
|
81
|
+
"~standard": wire["~standard"]
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { createReverseTimestampId as t };
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=reverse--n4D2yxu.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse--n4D2yxu.mjs","names":[],"sources":["../src/layouts/reverse-timestamp.ts","../src/reverse.ts"],"sourcesContent":["import type { Id, Prefix } from \"../types.js\";\nimport { payloadBytesFromId, toWireId } from \"../wire/envelope.js\";\nimport { payloadByteLength } from \"../wire/invariants.js\";\nimport { timestampByteLength, writeTimestamp } from \"../wire/timestamp-bytes.js\";\n\nconst randomByteLength: number = payloadByteLength - timestampByteLength;\n\n/** Writes inverted timestamp bytes, then fills random portion. */\nfunction buildReversePayload(\n ms: number,\n rng: (target: Uint8Array) => void,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n for (let i = 0; i < timestampByteLength; i++) {\n buffer[i] = ~buffer[i]! & 0xff;\n }\n rng(randomView);\n}\n\n/** Writes inverted timestamp bytes, then fills random portion with a sentinel. */\nfunction buildReverseSentinelPayload(\n ms: number,\n fill: number,\n buffer: Uint8Array,\n randomView: Uint8Array,\n): void {\n writeTimestamp(ms, buffer);\n for (let i = 0; i < timestampByteLength; i++) {\n buffer[i] = ~buffer[i]! & 0xff;\n }\n randomView.fill(fill);\n}\n\n/** Decodes the original timestamp by inverting the first 6 payload bytes. */\nfunction extractReverseTimestampFromId<Brand extends string>(\n prefix: Prefix<Brand>,\n id: Id<Brand>,\n): Date {\n const bytes = payloadBytesFromId(prefix, id);\n let ms = 0;\n for (let i = 0; i < timestampByteLength; i++) {\n ms = ms * 256 + (~bytes[i]! & 0xff);\n }\n return new Date(ms);\n}\n\n/** Layout ops binder for the Reverse Timestamp variant. */\nexport function createReverseTimestampLayoutOps<Brand extends string>(\n prefix: Prefix<Brand>,\n rng: (target: Uint8Array) => void,\n) {\n const buffer = new Uint8Array(payloadByteLength);\n const randomView = new Uint8Array(buffer.buffer, timestampByteLength, randomByteLength);\n\n return {\n generateAt: (ms: number): Id<Brand> => {\n buildReversePayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n extractTimestamp: (id: Id<Brand>): Date => extractReverseTimestampFromId(prefix, id),\n minIdForTime: (ms: number): Id<Brand> => {\n buildReverseSentinelPayload(ms, 0x00, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n maxIdForTime: (ms: number): Id<Brand> => {\n buildReverseSentinelPayload(ms, 0xff, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n exampleWireId: (ms: number): Id<Brand> => {\n buildReversePayload(ms, rng, buffer, randomView);\n return toWireId(prefix, buffer);\n },\n };\n}\n","import { validateBrand } from \"./brand.js\";\nimport { createReverseTimestampLayoutOps } from \"./layouts/reverse-timestamp.js\";\nimport { registerBrand } from \"./registry.js\";\nimport type { Id, JsonSchema, ParseResult, Prefix, StandardSchemaProps } from \"./types.js\";\nimport { wireMethods } from \"./wire/codec-shell.js\";\n\n/**\n * Configuration options for a Reverse Timestamp codec instance.\n */\nexport type ReverseTimestampOptions = {\n /** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */\n now?: () => number;\n /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */\n rng?: (target: Uint8Array) => void;\n /** If true, silences the duplicate-brand warning in non-production environments. */\n allowDuplicateBrand?: boolean;\n};\n\n/**\n * A brand-scoped codec for generating and validating Reverse Timestamp IDs.\n *\n * Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a\n * 16-byte payload (6-byte bitwise-inverted ms timestamp + 10 random bytes). IDs sort\n * by creation time in **descending** (newest-first) order.\n *\n * Range queries across a time interval [t_old, t_new] should scan from\n * `minIdForTime(t_new)` to `maxIdForTime(t_old)` — the reversed sort order means\n * newer timestamps produce lexicographically smaller IDs.\n *\n * Constructed via `createReverseTimestampId(brand)` from `@smonn/ids/reverse`.\n */\nexport type ReverseTimestampCodec<Brand extends string> = {\n /** Produces a new canonical ID using the codec's `now` and `rng`. */\n generate(): Id<Brand>;\n /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */\n generateAt(date: Date): Id<Brand>;\n /**\n * Strict type guard: `true` only for already-canonical strings for this brand.\n * For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.\n */\n is(value: unknown): value is Id<Brand>;\n /**\n * Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.\n */\n parse(value: unknown): Id<Brand>;\n /**\n * Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.\n */\n safeParse(value: unknown): ParseResult<Brand>;\n /**\n * Decodes the creation `Date` from an `Id<Brand>` by inverting the timestamp bytes.\n * Trusts the type — use `safeParse()` at boundaries first.\n */\n extractTimestamp(id: Id<Brand>): Date;\n /**\n * Lexicographically smallest ID for any ID generated at `date` (random portion `0x00`).\n * Because timestamps are inverted, a newer `date` yields a lexicographically smaller result —\n * use `minIdForTime(t_new)` as the lower bound when scanning [t_old, t_new].\n * Throws on invalid dates.\n */\n minIdForTime(date: Date): Id<Brand>;\n /**\n * Lexicographically largest ID for any ID generated at `date` (random portion `0xff`).\n * Because timestamps are inverted, an older `date` yields a lexicographically larger result —\n * use `maxIdForTime(t_old)` as the upper bound when scanning [t_old, t_new].\n * Throws on invalid dates.\n */\n maxIdForTime(date: Date): Id<Brand>;\n /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */\n toJsonSchema(): JsonSchema;\n /** Standard Schema validate entry point. */\n readonly \"~standard\": StandardSchemaProps<Brand>;\n};\n\nfunction defaultRng(target: Uint8Array): void {\n crypto.getRandomValues(target as Uint8Array<ArrayBuffer>);\n}\n\n/**\n * Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).\n *\n * IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,\n * so lexicographic ID order equals descending creation-time order. `extractTimestamp`\n * inverts back to recover the original millisecond.\n *\n * @param brand - Entity type brand validated once at construction.\n * @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.\n */\nexport function createReverseTimestampId<Brand extends string>(\n brand: Brand,\n opts: ReverseTimestampOptions = {},\n): ReverseTimestampCodec<Brand> {\n validateBrand(brand);\n registerBrand(brand, opts.allowDuplicateBrand);\n\n const now = opts.now ?? Date.now;\n const rng = opts.rng ?? defaultRng;\n const prefix: Prefix<Brand> = `${brand}_`;\n const wire = wireMethods(prefix);\n const layout = createReverseTimestampLayoutOps(prefix, rng);\n\n return {\n generate: () => layout.generateAt(now()),\n generateAt: (date: Date) => layout.generateAt(date.getTime()),\n is: wire.is,\n parse: wire.parse,\n safeParse: wire.safeParse,\n extractTimestamp: layout.extractTimestamp,\n minIdForTime: (date: Date) => layout.minIdForTime(date.getTime()),\n maxIdForTime: (date: Date) => layout.maxIdForTime(date.getTime()),\n toJsonSchema: () => wire.toJsonSchema(brand, layout.exampleWireId(now())),\n \"~standard\": wire[\"~standard\"],\n };\n}\n"],"mappings":";;;AAKA,MAAM,mBAAA;;AAGN,SAAS,oBACP,IACA,KACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,OAAO,KAAK,CAAC,OAAO,KAAM;CAE5B,IAAI,UAAU;AAChB;;AAGA,SAAS,4BACP,IACA,MACA,QACA,YACM;CACN,eAAe,IAAI,MAAM;CACzB,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,OAAO,KAAK,CAAC,OAAO,KAAM;CAE5B,WAAW,KAAK,IAAI;AACtB;;AAGA,SAAS,8BACP,QACA,IACM;CACN,MAAM,QAAQ,mBAAmB,QAAQ,EAAE;CAC3C,IAAI,KAAK;CACT,KAAK,IAAI,IAAI,GAAG,IAAA,GAAyB,KACvC,KAAK,KAAK,OAAO,CAAC,MAAM,KAAM;CAEhC,OAAO,IAAI,KAAK,EAAE;AACpB;;AAGA,SAAgB,gCACd,QACA,KACA;CACA,MAAM,SAAS,IAAI,WAAA,EAA4B;CAC/C,MAAM,aAAa,IAAI,WAAW,OAAO,QAAA,GAA6B,gBAAgB;CAEtF,OAAO;EACL,aAAa,OAA0B;GACrC,oBAAoB,IAAI,KAAK,QAAQ,UAAU;GAC/C,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,mBAAmB,OAAwB,8BAA8B,QAAQ,EAAE;EACnF,eAAe,OAA0B;GACvC,4BAA4B,IAAI,GAAM,QAAQ,UAAU;GACxD,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,eAAe,OAA0B;GACvC,4BAA4B,IAAI,KAAM,QAAQ,UAAU;GACxD,OAAO,SAAS,QAAQ,MAAM;EAChC;EACA,gBAAgB,OAA0B;GACxC,oBAAoB,IAAI,KAAK,QAAQ,UAAU;GAC/C,OAAO,SAAS,QAAQ,MAAM;EAChC;CACF;AACF;;;ACDA,SAAS,WAAW,QAA0B;CAC5C,OAAO,gBAAgB,MAAiC;AAC1D;;;;;;;;;;;AAYA,SAAgB,yBACd,OACA,OAAgC,CAAC,GACH;CAC9B,cAAc,KAAK;CACnB,cAAc,OAAO,KAAK,mBAAmB;CAE7C,MAAM,MAAM,KAAK,OAAO,KAAK;CAC7B,MAAM,MAAM,KAAK,OAAO;CACxB,MAAM,SAAwB,GAAG,MAAM;CACvC,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,SAAS,gCAAgC,QAAQ,GAAG;CAE1D,OAAO;EACL,gBAAgB,OAAO,WAAW,IAAI,CAAC;EACvC,aAAa,SAAe,OAAO,WAAW,KAAK,QAAQ,CAAC;EAC5D,IAAI,KAAK;EACT,OAAO,KAAK;EACZ,WAAW,KAAK;EAChB,kBAAkB,OAAO;EACzB,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,eAAe,SAAe,OAAO,aAAa,KAAK,QAAQ,CAAC;EAChE,oBAAoB,KAAK,aAAa,OAAO,OAAO,cAAc,IAAI,CAAC,CAAC;EACxE,aAAa,KAAK;CACpB;AACF"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { a as StandardSchemaProps, i as ParseResult, n as JsonSchema, t as Id } from "./types-g7CiQDyE.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/reverse.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for a Reverse Timestamp codec instance.
|
|
6
|
+
*/
|
|
7
|
+
type ReverseTimestampOptions = {
|
|
8
|
+
/** Returns the current timestamp in milliseconds. Defaults to `Date.now`. */now?: () => number; /** Writes random bytes into `target` for ID generation. Defaults to `crypto.getRandomValues`. */
|
|
9
|
+
rng?: (target: Uint8Array) => void; /** If true, silences the duplicate-brand warning in non-production environments. */
|
|
10
|
+
allowDuplicateBrand?: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* A brand-scoped codec for generating and validating Reverse Timestamp IDs.
|
|
14
|
+
*
|
|
15
|
+
* Wire format: `{brand}_` plus 26 lowercase Crockford base32 characters encoding a
|
|
16
|
+
* 16-byte payload (6-byte bitwise-inverted ms timestamp + 10 random bytes). IDs sort
|
|
17
|
+
* by creation time in **descending** (newest-first) order.
|
|
18
|
+
*
|
|
19
|
+
* Range queries across a time interval [t_old, t_new] should scan from
|
|
20
|
+
* `minIdForTime(t_new)` to `maxIdForTime(t_old)` — the reversed sort order means
|
|
21
|
+
* newer timestamps produce lexicographically smaller IDs.
|
|
22
|
+
*
|
|
23
|
+
* Constructed via `createReverseTimestampId(brand)` from `@smonn/ids/reverse`.
|
|
24
|
+
*/
|
|
25
|
+
type ReverseTimestampCodec<Brand extends string> = {
|
|
26
|
+
/** Produces a new canonical ID using the codec's `now` and `rng`. */generate(): Id<Brand>; /** Produces a new canonical ID with timestamp bytes from `date` and a fresh random tail. Throws on invalid dates. */
|
|
27
|
+
generateAt(date: Date): Id<Brand>;
|
|
28
|
+
/**
|
|
29
|
+
* Strict type guard: `true` only for already-canonical strings for this brand.
|
|
30
|
+
* For untrusted input, use `safeParse()` or `parse()` instead. See ADR-0003.
|
|
31
|
+
*/
|
|
32
|
+
is(value: unknown): value is Id<Brand>;
|
|
33
|
+
/**
|
|
34
|
+
* Lenient parse: normalises case and Crockford aliases, returns canonical `Id<Brand>`, or throws.
|
|
35
|
+
*/
|
|
36
|
+
parse(value: unknown): Id<Brand>;
|
|
37
|
+
/**
|
|
38
|
+
* Lenient parse without throwing: normalises to canonical form, or returns `{ ok: false, error }`.
|
|
39
|
+
*/
|
|
40
|
+
safeParse(value: unknown): ParseResult<Brand>;
|
|
41
|
+
/**
|
|
42
|
+
* Decodes the creation `Date` from an `Id<Brand>` by inverting the timestamp bytes.
|
|
43
|
+
* Trusts the type — use `safeParse()` at boundaries first.
|
|
44
|
+
*/
|
|
45
|
+
extractTimestamp(id: Id<Brand>): Date;
|
|
46
|
+
/**
|
|
47
|
+
* Lexicographically smallest ID for any ID generated at `date` (random portion `0x00`).
|
|
48
|
+
* Because timestamps are inverted, a newer `date` yields a lexicographically smaller result —
|
|
49
|
+
* use `minIdForTime(t_new)` as the lower bound when scanning [t_old, t_new].
|
|
50
|
+
* Throws on invalid dates.
|
|
51
|
+
*/
|
|
52
|
+
minIdForTime(date: Date): Id<Brand>;
|
|
53
|
+
/**
|
|
54
|
+
* Lexicographically largest ID for any ID generated at `date` (random portion `0xff`).
|
|
55
|
+
* Because timestamps are inverted, an older `date` yields a lexicographically larger result —
|
|
56
|
+
* use `maxIdForTime(t_old)` as the upper bound when scanning [t_old, t_new].
|
|
57
|
+
* Throws on invalid dates.
|
|
58
|
+
*/
|
|
59
|
+
maxIdForTime(date: Date): Id<Brand>; /** JSON Schema for the canonical wire form (`pattern` is canonical-only). */
|
|
60
|
+
toJsonSchema(): JsonSchema; /** Standard Schema validate entry point. */
|
|
61
|
+
readonly "~standard": StandardSchemaProps<Brand>;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Creates a Reverse Timestamp codec for `brand` (three lowercase a–z characters).
|
|
65
|
+
*
|
|
66
|
+
* IDs sort newest-first: the 48-bit timestamp field is bitwise-inverted before encoding,
|
|
67
|
+
* so lexicographic ID order equals descending creation-time order. `extractTimestamp`
|
|
68
|
+
* inverts back to recover the original millisecond.
|
|
69
|
+
*
|
|
70
|
+
* @param brand - Entity type brand validated once at construction.
|
|
71
|
+
* @param opts - Optional `now`, `rng`, and `allowDuplicateBrand` overrides.
|
|
72
|
+
*/
|
|
73
|
+
declare function createReverseTimestampId<Brand extends string>(brand: Brand, opts?: ReverseTimestampOptions): ReverseTimestampCodec<Brand>;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { ReverseTimestampCodec, ReverseTimestampOptions, createReverseTimestampId };
|
|
76
|
+
//# sourceMappingURL=reverse.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reverse.d.mts","names":[],"sources":["../src/reverse.ts"],"mappings":";;;;;AASA;KAAY,uBAAA;+EAEV,GAAA;EAEA,GAAA,IAAO,MAAA,EAAQ,UAAA;EAEf,mBAAA;AAAA;;AAAA;AAgBF;;;;;;;;;;;KAAY,qBAAA;uEAEV,QAAA,IAAY,EAAA,CAAG,KAAA;EAEf,UAAA,CAAW,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;EAK3B,EAAA,CAAG,KAAA,YAAiB,KAAA,IAAS,EAAA,CAAG,KAAA;;;;EAIhC,KAAA,CAAM,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;EAI1B,SAAA,CAAU,KAAA,YAAiB,WAAA,CAAY,KAAA;;;;;EAKvC,gBAAA,CAAiB,EAAA,EAAI,EAAA,CAAG,KAAA,IAAS,IAAA;;;;;;;EAOjC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;;;;;;;EAO7B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,EAAA,CAAG,KAAA;EAE7B,YAAA,IAAgB,UAAA;WAEP,WAAA,EAAa,mBAAA,CAAoB,KAAA;AAAA;;;;;;;;;;;iBAiB5B,wBAAA,uBACd,KAAA,EAAO,KAAA,EACP,IAAA,GAAM,uBAAA,GACL,qBAAA,CAAsB,KAAA"}
|
package/dist/reverse.mjs
ADDED