@smonn/ids 0.15.0 → 1.0.0-rc.1

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.
Files changed (46) hide show
  1. package/README.md +3 -3
  2. package/dist/{adapter-types-7wWdELSh.mjs → adapter-types-CjzFNDcJ.mjs} +7 -2
  3. package/dist/adapter-types-CjzFNDcJ.mjs.map +1 -0
  4. package/dist/cli.mjs +30 -22
  5. package/dist/cli.mjs.map +1 -1
  6. package/dist/drizzle.d.mts +202 -3
  7. package/dist/drizzle.d.mts.map +1 -1
  8. package/dist/drizzle.mjs +245 -5
  9. package/dist/drizzle.mjs.map +1 -1
  10. package/dist/express.d.mts +44 -2
  11. package/dist/express.d.mts.map +1 -1
  12. package/dist/express.mjs +60 -2
  13. package/dist/express.mjs.map +1 -1
  14. package/dist/fastify.d.mts +49 -2
  15. package/dist/fastify.d.mts.map +1 -1
  16. package/dist/fastify.mjs +61 -2
  17. package/dist/fastify.mjs.map +1 -1
  18. package/dist/hono.d.mts +44 -2
  19. package/dist/hono.d.mts.map +1 -1
  20. package/dist/hono.mjs +54 -2
  21. package/dist/hono.mjs.map +1 -1
  22. package/dist/kysely.d.mts +103 -2
  23. package/dist/kysely.d.mts.map +1 -1
  24. package/dist/kysely.mjs +105 -2
  25. package/dist/kysely.mjs.map +1 -1
  26. package/dist/mikro-orm.d.mts +81 -3
  27. package/dist/mikro-orm.d.mts.map +1 -1
  28. package/dist/mikro-orm.mjs +86 -4
  29. package/dist/mikro-orm.mjs.map +1 -1
  30. package/dist/nestjs.mjs +1 -1
  31. package/dist/{opaque-COAcIIY4.mjs → opaque-Dle3CmSE.mjs} +18 -10
  32. package/dist/opaque-Dle3CmSE.mjs.map +1 -0
  33. package/dist/opaque.d.mts +16 -10
  34. package/dist/opaque.d.mts.map +1 -1
  35. package/dist/opaque.mjs +1 -1
  36. package/dist/prisma.d.mts +135 -3
  37. package/dist/prisma.d.mts.map +1 -1
  38. package/dist/prisma.mjs +141 -3
  39. package/dist/prisma.mjs.map +1 -1
  40. package/dist/typeorm.d.mts +84 -1
  41. package/dist/typeorm.d.mts.map +1 -1
  42. package/dist/typeorm.mjs +87 -2
  43. package/dist/typeorm.mjs.map +1 -1
  44. package/package.json +1 -1
  45. package/dist/adapter-types-7wWdELSh.mjs.map +0 -1
  46. package/dist/opaque-COAcIIY4.mjs.map +0 -1
package/dist/prisma.d.mts CHANGED
@@ -1,9 +1,29 @@
1
1
  import { t as Id } from "./types-hGBnCpJj.mjs";
2
2
  import { n as IdColumnCodec } from "./adapter-types-Bia_w9sg.mjs";
3
3
  import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcKKOG.mjs";
4
+ import { ModelQueryOptionsCb } from "@prisma/client/runtime/library";
4
5
 
5
6
  //#region src/adapters/prisma.d.ts
6
7
  /**
8
+ * Extension of {@link IdColumnCodec} that also exposes synchronous `generate()`.
9
+ * Required by {@link idField} so that {@link IdTransform.defaultQuery} can produce
10
+ * IDs at write time. Every full codec variant (Timestamp, Reverse Timestamp) satisfies
11
+ * this; async-generate codecs (Opaque, Signed, Wrapped, Digest) do not and are
12
+ * therefore unsupported by `defaultQuery`.
13
+ */
14
+ type IdGeneratingCodec<Brand extends string> = IdColumnCodec<Brand> & {
15
+ generate(): Id<Brand>;
16
+ };
17
+ /**
18
+ * The per-model object returned by {@link IdTransform.defaultQuery}, suitable for
19
+ * the model-level value inside a Prisma `$extends({ query: { modelName: … } })` block.
20
+ * Structurally identical to `{ [operation: string]: ModelQueryOptionsCb }` from
21
+ * `@prisma/client/runtime/library`.
22
+ */
23
+ type IdQueryField = {
24
+ [operation: string]: ModelQueryOptionsCb;
25
+ };
26
+ /**
7
27
  * Typed `$extends` result-component field definition produced by
8
28
  * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`
9
29
  * return type is statically `Id<Brand>`, so the extended-client model field is
@@ -14,6 +34,15 @@ type IdComputeField<Brand extends string> = {
14
34
  compute: (model: Record<string, unknown>) => Id<Brand>;
15
35
  };
16
36
  /**
37
+ * Typed `$extends` result-component field definition produced by
38
+ * {@link IdTransform.computeNullableField} — like {@link IdComputeField} but
39
+ * `compute` returns `Id<Brand> | null` for nullable columns.
40
+ */
41
+ type NullableIdComputeField<Brand extends string> = {
42
+ needs: Record<string, boolean>;
43
+ compute: (model: Record<string, unknown>) => Id<Brand> | null;
44
+ };
45
+ /**
17
46
  * Read/write transform pair and `$extends` result-component factory for
18
47
  * integrating `Id<Brand>` with Prisma extensions.
19
48
  */
@@ -25,6 +54,11 @@ type IdTransform<Brand extends string> = {
25
54
  */
26
55
  read(value: unknown): Id<Brand>;
27
56
  /**
57
+ * Nullable read transform: returns `null` when `value` is `null` or `undefined`;
58
+ * otherwise delegates to {@link read}. Use for optional foreign keys.
59
+ */
60
+ readNullable(value: unknown): Id<Brand> | null;
61
+ /**
28
62
  * Write transform: passes `Id<Brand>` through as its canonical string form.
29
63
  * `Id<Brand>` is already the canonical string, so this is an identity function
30
64
  * at runtime.
@@ -51,16 +85,61 @@ type IdTransform<Brand extends string> = {
51
85
  * ```
52
86
  */
53
87
  computeField(fieldName: string): IdComputeField<Brand>;
88
+ /**
89
+ * Creates a `$extends` query-component model slice that auto-generates
90
+ * `Id<Brand>` values for `create`, `createMany`, and `upsert` operations
91
+ * when the field is absent, `undefined`, or `null` in `args.data` (or
92
+ * `args.create` for upsert). Explicitly supplied values are always passed
93
+ * through unchanged.
94
+ *
95
+ * @param fieldName - The model field to auto-generate (e.g. `"id"`).
96
+ * @returns An {@link IdQueryField} suitable for the model-level value inside
97
+ * a Prisma `$extends({ query: { modelName: … } })` block.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const xprisma = prisma.$extends({
102
+ * query: { user: userIdField.defaultQuery("id") },
103
+ * result: { user: { id: userIdField.computeField("id") } },
104
+ * });
105
+ * // id is auto-filled on create, and typed as Id<"usr"> on read
106
+ * await xprisma.user.create({ data: { name: "Alice" } });
107
+ * ```
108
+ */
109
+ defaultQuery(fieldName: string): IdQueryField;
110
+ /**
111
+ * Like {@link computeField} but for nullable columns — `compute` returns
112
+ * `Id<Brand> | null` instead of `Id<Brand>`.
113
+ *
114
+ * @param fieldName - The nullable model field to read from.
115
+ * @returns A {@link NullableIdComputeField} whose `compute` returns `Id<Brand> | null`.
116
+ */
117
+ computeNullableField(fieldName: string): NullableIdComputeField<Brand>;
118
+ };
119
+ /**
120
+ * The read/nullable-read/write surface returned by {@link nullableIdField} —
121
+ * mirrors the nullable methods of {@link IdTransform} but omits `defaultQuery`,
122
+ * `read`, and `computeField` since nullable FK columns neither auto-generate IDs
123
+ * nor require a non-null read path at the top level.
124
+ */
125
+ type NullableIdTransform<Brand extends string> = {
126
+ readNullable(value: unknown): Id<Brand> | null;
127
+ write(value: Id<Brand>): string;
128
+ computeNullableField(fieldName: string): NullableIdComputeField<Brand>;
54
129
  };
55
130
  /**
56
131
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
57
132
  *
58
- * Works with any codec variant exposing `safeParse`.
133
+ * Requires a codec variant that exposes a synchronous `generate()` in addition to `safeParse` — see {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp codec** qualify; Opaque, Signed, Wrapped, and Digest codecs cannot be passed to `idField()`.
59
134
  *
60
135
  * Use `computeField(fieldName)` to produce a typed `$extends` result-component
61
136
  * field definition — the brand is carried through Prisma's type machinery
62
137
  * automatically and no per-call-site cast is required.
63
138
  *
139
+ * For codecs that do not expose a synchronous `generate()` (Opaque Timestamp,
140
+ * Signed Timestamp, Wrapped key, Digest), use {@link idFieldReadOnly} instead —
141
+ * it accepts any {@link IdColumnCodec} and omits `defaultQuery`.
142
+ *
64
143
  * @example
65
144
  * ```ts
66
145
  * import { idField } from "@smonn/ids/prisma";
@@ -77,7 +156,60 @@ type IdTransform<Brand extends string> = {
77
156
  * // xprisma.user.findUnique(…).id is typed as Id<"usr"> — no cast required
78
157
  * ```
79
158
  */
80
- declare function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand>;
159
+ declare function idField<Brand extends string>(codec: IdGeneratingCodec<Brand>): IdTransform<Brand>;
160
+ /**
161
+ * Standalone nullable counterpart of {@link idField} for Prisma adapter symmetry
162
+ * with the other ORM adapters ({@link nullableIdColumn} in Drizzle/Kysely,
163
+ * `nullableIdType` in MikroORM, `nullableIdTransformer` in TypeORM).
164
+ *
165
+ * Accepts any {@link IdColumnCodec} — no synchronous `generate()` required,
166
+ * because nullable FK columns do not auto-generate IDs.
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * import { nullableIdField } from "@smonn/ids/prisma";
171
+ * import { createTimestampId } from "@smonn/ids";
172
+ *
173
+ * const usr = createTimestampId("usr");
174
+ * const authorIdField = nullableIdField(usr);
175
+ *
176
+ * const xprisma = prisma.$extends({
177
+ * result: {
178
+ * post: { authorId: authorIdField.computeNullableField("authorId") },
179
+ * },
180
+ * });
181
+ * // xprisma.post.findUnique(…).authorId is typed as Id<"usr"> | null
182
+ * ```
183
+ */
184
+ declare function nullableIdField<Brand extends string>(codec: IdColumnCodec<Brand>): NullableIdTransform<Brand>;
185
+ /**
186
+ * Read-only sibling of {@link idField} for codec variants that do not expose a
187
+ * synchronous `generate()` — Opaque Timestamp, Signed Timestamp, Wrapped key,
188
+ * and Digest codecs all qualify.
189
+ *
190
+ * Accepts any {@link IdColumnCodec} (the wider constraint that only requires
191
+ * `safeParse`) and returns the full read/transform surface of {@link IdTransform}
192
+ * **minus `defaultQuery`**. Because `defaultQuery` is the only method that calls
193
+ * `generate()`, callers who only need the read path are not forced to provide a
194
+ * synchronous generator.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * import { idFieldReadOnly } from "@smonn/ids/prisma";
199
+ * import { createOpaqueTimestampId } from "@smonn/ids/opaque";
200
+ *
201
+ * const inv = createOpaqueTimestampId("inv", { key });
202
+ * const invoiceIdField = idFieldReadOnly(inv);
203
+ *
204
+ * const xprisma = prisma.$extends({
205
+ * result: {
206
+ * invoice: { id: invoiceIdField.computeField("id") },
207
+ * },
208
+ * });
209
+ * // xprisma.invoice.findUnique(…).id is typed as Id<"inv"> — no cast required
210
+ * ```
211
+ */
212
+ declare function idFieldReadOnly<Brand extends string>(codec: IdColumnCodec<Brand>): Omit<IdTransform<Brand>, "defaultQuery">;
81
213
  //#endregion
82
- export { type IdColumnCodec, IdComputeField, IdTransform, IdsError, type IdsErrorCode, idField, isIdsError };
214
+ export { type IdColumnCodec, IdComputeField, IdGeneratingCodec, IdQueryField, IdTransform, IdsError, type IdsErrorCode, NullableIdComputeField, NullableIdTransform, idField, idFieldReadOnly, isIdsError, nullableIdField };
83
215
  //# sourceMappingURL=prisma.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;AAcA;;;;;;AAAA,KAAY,cAAA;EACV,KAAA,EAAO,MAAA;EACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;AAAA;;;;;KAOtC,WAAA;;;;AAPsC;AAOlD;EAME,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;;;;;EAQzB,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;EAmBiB;;;;;;;;;;;;;;;AAAe;AA4BlD;;EA5BE,YAAA,CAAa,SAAA,WAAoB,cAAA,CAAe,KAAA;AAAA;;;;;;;;;;;;;AA4BsC;;;;;;;;;;;;;iBAAxE,OAAA,uBAA8B,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,WAAA,CAAY,KAAA"}
1
+ {"version":3,"file":"prisma.d.mts","names":[],"sources":["../src/adapters/prisma.ts"],"mappings":";;;;;;AAgBA;;;;;;;AAAA,KAAY,iBAAA,yBAA0C,aAAA,CAAc,KAAA;EAClE,QAAA,IAAY,EAAA,CAAG,KAAA;AAAA;;;;;;;KASL,YAAA;EAAA,CAAsC,SAAA,WAAA,mBAAA;AAAA;;;AAAA;AAQlD;;;KAAY,cAAA;EACV,KAAA,EAAO,MAAA;EACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;AAAA;;;;;;KAQtC,sBAAA;EACV,KAAA,EAAO,MAAA;EACP,OAAA,GAAU,KAAA,EAAO,MAAA,sBAA4B,EAAA,CAAG,KAAA;AAAA;;;AAVA;AAQlD;KASY,WAAA;;;;;;EAMV,IAAA,CAAK,KAAA,YAAiB,EAAA,CAAG,KAAA;EAboB;;;;EAkB7C,YAAA,CAAa,KAAA,YAAiB,EAAA,CAAG,KAAA;;;;;;AAlBe;AAOlD;EAmBE,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;;;;;;;;;;;;;;;;;;;EAmBhB,YAAA,CAAa,SAAA,WAAoB,cAAA,CAAe,KAAA;;;;;;;;;;;;;;;;;;;;;AA8BgB;EARhE,YAAA,CAAa,SAAA,WAAoB,YAAA;EAiBvB;;;;;;;EATV,oBAAA,CAAqB,SAAA,WAAoB,sBAAA,CAAuB,KAAA;AAAA;;;;;;;KAStD,mBAAA;EACV,YAAA,CAAa,KAAA,YAAiB,EAAA,CAAG,KAAA;EACjC,KAAA,CAAM,KAAA,EAAO,EAAA,CAAG,KAAA;EAChB,oBAAA,CAAqB,SAAA,WAAoB,sBAAA,CAAuB,KAAA;AAAA;;;;;AAAA;AAgClE;;;;;;;;;;;;;;;;AAA4F;AA4F5F;;;;;;;iBA5FgB,OAAA,uBAA8B,KAAA,EAAO,iBAAA,CAAkB,KAAA,IAAS,WAAA,CAAY,KAAA;;;;;;;;;AA8FrE;AA6CvB;;;;;;;;;;;;;;;iBA/CgB,eAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,mBAAA,CAAoB,KAAA;;;AA+CH;;;;;;;;;;;;;;;;;;;;;;;;;iBAFJ,eAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,IAAA,CAAK,WAAA,CAAY,KAAA"}
package/dist/prisma.mjs CHANGED
@@ -1,15 +1,19 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
2
+ import { n as readIdColumnNullable, t as readIdColumn } from "./adapter-types-CjzFNDcJ.mjs";
3
3
  //#region src/adapters/prisma.ts
4
4
  /**
5
5
  * Creates a read/write transform pair for use with Prisma's `$extends` extension model.
6
6
  *
7
- * Works with any codec variant exposing `safeParse`.
7
+ * Requires a codec variant that exposes a synchronous `generate()` in addition to `safeParse` — see {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp codec** qualify; Opaque, Signed, Wrapped, and Digest codecs cannot be passed to `idField()`.
8
8
  *
9
9
  * Use `computeField(fieldName)` to produce a typed `$extends` result-component
10
10
  * field definition — the brand is carried through Prisma's type machinery
11
11
  * automatically and no per-call-site cast is required.
12
12
  *
13
+ * For codecs that do not expose a synchronous `generate()` (Opaque Timestamp,
14
+ * Signed Timestamp, Wrapped key, Digest), use {@link idFieldReadOnly} instead —
15
+ * it accepts any {@link IdColumnCodec} and omits `defaultQuery`.
16
+ *
13
17
  * @example
14
18
  * ```ts
15
19
  * import { idField } from "@smonn/ids/prisma";
@@ -27,10 +31,138 @@ import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
27
31
  * ```
28
32
  */
29
33
  function idField(codec) {
34
+ const { generate } = codec;
35
+ return {
36
+ read(value) {
37
+ return readIdColumn(codec, value);
38
+ },
39
+ readNullable(value) {
40
+ return readIdColumnNullable(codec, value);
41
+ },
42
+ write(value) {
43
+ return value;
44
+ },
45
+ computeField(fieldName) {
46
+ return {
47
+ needs: { [fieldName]: true },
48
+ compute: (model) => readIdColumn(codec, model[fieldName])
49
+ };
50
+ },
51
+ defaultQuery(fieldName) {
52
+ function injectIfAbsent(data) {
53
+ if (data[fieldName] == null) return {
54
+ ...data,
55
+ [fieldName]: generate()
56
+ };
57
+ return data;
58
+ }
59
+ return {
60
+ async create({ args, query }) {
61
+ const data = args.data;
62
+ return query(data != null ? {
63
+ ...args,
64
+ data: injectIfAbsent(data)
65
+ } : args);
66
+ },
67
+ async createMany({ args, query }) {
68
+ const data = args.data;
69
+ return query(Array.isArray(data) ? {
70
+ ...args,
71
+ data: data.map((item) => injectIfAbsent(item))
72
+ } : args);
73
+ },
74
+ async upsert({ args, query }) {
75
+ const createData = args.create;
76
+ return query(createData != null ? {
77
+ ...args,
78
+ create: injectIfAbsent(createData)
79
+ } : args);
80
+ }
81
+ };
82
+ },
83
+ computeNullableField(fieldName) {
84
+ return {
85
+ needs: { [fieldName]: true },
86
+ compute: (model) => readIdColumnNullable(codec, model[fieldName])
87
+ };
88
+ }
89
+ };
90
+ }
91
+ /**
92
+ * Standalone nullable counterpart of {@link idField} for Prisma adapter symmetry
93
+ * with the other ORM adapters ({@link nullableIdColumn} in Drizzle/Kysely,
94
+ * `nullableIdType` in MikroORM, `nullableIdTransformer` in TypeORM).
95
+ *
96
+ * Accepts any {@link IdColumnCodec} — no synchronous `generate()` required,
97
+ * because nullable FK columns do not auto-generate IDs.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { nullableIdField } from "@smonn/ids/prisma";
102
+ * import { createTimestampId } from "@smonn/ids";
103
+ *
104
+ * const usr = createTimestampId("usr");
105
+ * const authorIdField = nullableIdField(usr);
106
+ *
107
+ * const xprisma = prisma.$extends({
108
+ * result: {
109
+ * post: { authorId: authorIdField.computeNullableField("authorId") },
110
+ * },
111
+ * });
112
+ * // xprisma.post.findUnique(…).authorId is typed as Id<"usr"> | null
113
+ * ```
114
+ */
115
+ function nullableIdField(codec) {
116
+ return {
117
+ readNullable(value) {
118
+ return readIdColumnNullable(codec, value);
119
+ },
120
+ write(value) {
121
+ return value;
122
+ },
123
+ computeNullableField(fieldName) {
124
+ return {
125
+ needs: { [fieldName]: true },
126
+ compute: (model) => readIdColumnNullable(codec, model[fieldName])
127
+ };
128
+ }
129
+ };
130
+ }
131
+ /**
132
+ * Read-only sibling of {@link idField} for codec variants that do not expose a
133
+ * synchronous `generate()` — Opaque Timestamp, Signed Timestamp, Wrapped key,
134
+ * and Digest codecs all qualify.
135
+ *
136
+ * Accepts any {@link IdColumnCodec} (the wider constraint that only requires
137
+ * `safeParse`) and returns the full read/transform surface of {@link IdTransform}
138
+ * **minus `defaultQuery`**. Because `defaultQuery` is the only method that calls
139
+ * `generate()`, callers who only need the read path are not forced to provide a
140
+ * synchronous generator.
141
+ *
142
+ * @example
143
+ * ```ts
144
+ * import { idFieldReadOnly } from "@smonn/ids/prisma";
145
+ * import { createOpaqueTimestampId } from "@smonn/ids/opaque";
146
+ *
147
+ * const inv = createOpaqueTimestampId("inv", { key });
148
+ * const invoiceIdField = idFieldReadOnly(inv);
149
+ *
150
+ * const xprisma = prisma.$extends({
151
+ * result: {
152
+ * invoice: { id: invoiceIdField.computeField("id") },
153
+ * },
154
+ * });
155
+ * // xprisma.invoice.findUnique(…).id is typed as Id<"inv"> — no cast required
156
+ * ```
157
+ */
158
+ function idFieldReadOnly(codec) {
30
159
  return {
31
160
  read(value) {
32
161
  return readIdColumn(codec, value);
33
162
  },
163
+ readNullable(value) {
164
+ return readIdColumnNullable(codec, value);
165
+ },
34
166
  write(value) {
35
167
  return value;
36
168
  },
@@ -39,10 +171,16 @@ function idField(codec) {
39
171
  needs: { [fieldName]: true },
40
172
  compute: (model) => readIdColumn(codec, model[fieldName])
41
173
  };
174
+ },
175
+ computeNullableField(fieldName) {
176
+ return {
177
+ needs: { [fieldName]: true },
178
+ compute: (model) => readIdColumnNullable(codec, model[fieldName])
179
+ };
42
180
  }
43
181
  };
44
182
  }
45
183
  //#endregion
46
- export { IdsError, idField, isIdsError };
184
+ export { IdsError, idField, idFieldReadOnly, isIdsError, nullableIdField };
47
185
 
48
186
  //# sourceMappingURL=prisma.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Typed `$extends` result-component field definition produced by\n * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`\n * return type is statically `Id<Brand>`, so the extended-client model field is\n * typed correctly without a per-call-site cast.\n */\nexport type IdComputeField<Brand extends string> = {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n};\n\n/**\n * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\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 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 * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns An {@link IdComputeField} whose `compute` return type is statically\n * `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\n computeField(fieldName: string): IdComputeField<Brand>;\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`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition — the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\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: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdColumnCodec<Brand>): IdTransform<Brand> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoFA,SAAgB,QAA8B,OAAiD;CAC7F,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;CACF;AACF"}
1
+ {"version":3,"file":"prisma.mjs","names":[],"sources":["../src/adapters/prisma.ts"],"sourcesContent":["import type { ModelQueryOptionsCb, ModelQueryOptionsCbArgs } from \"@prisma/client/runtime/library\";\nimport { readIdColumn, readIdColumnNullable, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Extension of {@link IdColumnCodec} that also exposes synchronous `generate()`.\n * Required by {@link idField} so that {@link IdTransform.defaultQuery} can produce\n * IDs at write time. Every full codec variant (Timestamp, Reverse Timestamp) satisfies\n * this; async-generate codecs (Opaque, Signed, Wrapped, Digest) do not and are\n * therefore unsupported by `defaultQuery`.\n */\nexport type IdGeneratingCodec<Brand extends string> = IdColumnCodec<Brand> & {\n generate(): Id<Brand>;\n};\n\n/**\n * The per-model object returned by {@link IdTransform.defaultQuery}, suitable for\n * the model-level value inside a Prisma `$extends({ query: { modelName: … } })` block.\n * Structurally identical to `{ [operation: string]: ModelQueryOptionsCb }` from\n * `@prisma/client/runtime/library`.\n */\nexport type IdQueryField = { [operation: string]: ModelQueryOptionsCb };\n\n/**\n * Typed `$extends` result-component field definition produced by\n * {@link IdTransform.computeField} — a `{ needs, compute }` pair whose `compute`\n * return type is statically `Id<Brand>`, so the extended-client model field is\n * typed correctly without a per-call-site cast.\n */\nexport type IdComputeField<Brand extends string> = {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand>;\n};\n\n/**\n * Typed `$extends` result-component field definition produced by\n * {@link IdTransform.computeNullableField} — like {@link IdComputeField} but\n * `compute` returns `Id<Brand> | null` for nullable columns.\n */\nexport type NullableIdComputeField<Brand extends string> = {\n needs: Record<string, boolean>;\n compute: (model: Record<string, unknown>) => Id<Brand> | null;\n};\n\n/**\n * Read/write transform pair and `$extends` result-component factory for\n * integrating `Id<Brand>` with Prisma extensions.\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 read(value: unknown): Id<Brand>;\n /**\n * Nullable read transform: returns `null` when `value` is `null` or `undefined`;\n * otherwise delegates to {@link read}. Use for optional foreign keys.\n */\n readNullable(value: unknown): Id<Brand> | null;\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 * Creates a typed `$extends` result-component field definition that carries\n * `Id<Brand>` through Prisma's type machinery without a per-call-site cast.\n *\n * @param fieldName - The model field to read from (e.g. `\"id\"`).\n * @returns An {@link IdComputeField} whose `compute` return type is statically\n * `Id<Brand>`, so the extended-client model field is typed correctly.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * result: {\n * user: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\n computeField(fieldName: string): IdComputeField<Brand>;\n /**\n * Creates a `$extends` query-component model slice that auto-generates\n * `Id<Brand>` values for `create`, `createMany`, and `upsert` operations\n * when the field is absent, `undefined`, or `null` in `args.data` (or\n * `args.create` for upsert). Explicitly supplied values are always passed\n * through unchanged.\n *\n * @param fieldName - The model field to auto-generate (e.g. `\"id\"`).\n * @returns An {@link IdQueryField} suitable for the model-level value inside\n * a Prisma `$extends({ query: { modelName: … } })` block.\n *\n * @example\n * ```ts\n * const xprisma = prisma.$extends({\n * query: { user: userIdField.defaultQuery(\"id\") },\n * result: { user: { id: userIdField.computeField(\"id\") } },\n * });\n * // id is auto-filled on create, and typed as Id<\"usr\"> on read\n * await xprisma.user.create({ data: { name: \"Alice\" } });\n * ```\n */\n defaultQuery(fieldName: string): IdQueryField;\n /**\n * Like {@link computeField} but for nullable columns — `compute` returns\n * `Id<Brand> | null` instead of `Id<Brand>`.\n *\n * @param fieldName - The nullable model field to read from.\n * @returns A {@link NullableIdComputeField} whose `compute` returns `Id<Brand> | null`.\n */\n computeNullableField(fieldName: string): NullableIdComputeField<Brand>;\n};\n\n/**\n * The read/nullable-read/write surface returned by {@link nullableIdField} —\n * mirrors the nullable methods of {@link IdTransform} but omits `defaultQuery`,\n * `read`, and `computeField` since nullable FK columns neither auto-generate IDs\n * nor require a non-null read path at the top level.\n */\nexport type NullableIdTransform<Brand extends string> = {\n readNullable(value: unknown): Id<Brand> | null;\n write(value: Id<Brand>): string;\n computeNullableField(fieldName: string): NullableIdComputeField<Brand>;\n};\n\n/**\n * Creates a read/write transform pair for use with Prisma's `$extends` extension model.\n *\n * Requires a codec variant that exposes a synchronous `generate()` in addition to `safeParse` — see {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp codec** qualify; Opaque, Signed, Wrapped, and Digest codecs cannot be passed to `idField()`.\n *\n * Use `computeField(fieldName)` to produce a typed `$extends` result-component\n * field definition — the brand is carried through Prisma's type machinery\n * automatically and no per-call-site cast is required.\n *\n * For codecs that do not expose a synchronous `generate()` (Opaque Timestamp,\n * Signed Timestamp, Wrapped key, Digest), use {@link idFieldReadOnly} instead —\n * it accepts any {@link IdColumnCodec} and omits `defaultQuery`.\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: { id: userIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.user.findUnique(…).id is typed as Id<\"usr\"> — no cast required\n * ```\n */\nexport function idField<Brand extends string>(codec: IdGeneratingCodec<Brand>): IdTransform<Brand> {\n const { generate } = codec;\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n readNullable(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n // Prisma's $extends types `compute` as returning `any` in its constraint\n // type (DynamicResultExtensionArgs). Returning a pre-built object with an\n // explicit Id<Brand> return type on `compute` causes TypeScript to infer\n // the brand through the `& R` intersection in $extends — encapsulating\n // the single necessary cast here rather than pushing it to every call site.\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n defaultQuery(fieldName: string): IdQueryField {\n function injectIfAbsent(data: Record<string, unknown>): Record<string, unknown> {\n if (data[fieldName] == null) {\n return { ...data, [fieldName]: generate() };\n }\n return data;\n }\n\n type QueryArg = Parameters<ModelQueryOptionsCbArgs[\"query\"]>[0];\n\n return {\n async create({ args, query }) {\n const data = args.data as Record<string, unknown> | null | undefined;\n const nextArgs =\n data != null ? ({ ...args, data: injectIfAbsent(data) } as unknown as QueryArg) : args;\n return query(nextArgs);\n },\n async createMany({ args, query }) {\n const data = args.data as Array<Record<string, unknown>> | null | undefined;\n const nextArgs = Array.isArray(data)\n ? ({ ...args, data: data.map((item) => injectIfAbsent(item)) } as unknown as QueryArg)\n : args;\n return query(nextArgs);\n },\n async upsert({ args, query }) {\n const createData = args.create as Record<string, unknown> | null | undefined;\n const nextArgs =\n createData != null\n ? ({ ...args, create: injectIfAbsent(createData) } as unknown as QueryArg)\n : args;\n return query(nextArgs);\n },\n };\n },\n computeNullableField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> | null =>\n readIdColumnNullable(codec, model[fieldName]),\n };\n },\n };\n}\n\n/**\n * Standalone nullable counterpart of {@link idField} for Prisma adapter symmetry\n * with the other ORM adapters ({@link nullableIdColumn} in Drizzle/Kysely,\n * `nullableIdType` in MikroORM, `nullableIdTransformer` in TypeORM).\n *\n * Accepts any {@link IdColumnCodec} — no synchronous `generate()` required,\n * because nullable FK columns do not auto-generate IDs.\n *\n * @example\n * ```ts\n * import { nullableIdField } from \"@smonn/ids/prisma\";\n * import { createTimestampId } from \"@smonn/ids\";\n *\n * const usr = createTimestampId(\"usr\");\n * const authorIdField = nullableIdField(usr);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * post: { authorId: authorIdField.computeNullableField(\"authorId\") },\n * },\n * });\n * // xprisma.post.findUnique(…).authorId is typed as Id<\"usr\"> | null\n * ```\n */\nexport function nullableIdField<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): NullableIdTransform<Brand> {\n return {\n readNullable(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeNullableField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> | null =>\n readIdColumnNullable(codec, model[fieldName]),\n };\n },\n };\n}\n\n/**\n * Read-only sibling of {@link idField} for codec variants that do not expose a\n * synchronous `generate()` — Opaque Timestamp, Signed Timestamp, Wrapped key,\n * and Digest codecs all qualify.\n *\n * Accepts any {@link IdColumnCodec} (the wider constraint that only requires\n * `safeParse`) and returns the full read/transform surface of {@link IdTransform}\n * **minus `defaultQuery`**. Because `defaultQuery` is the only method that calls\n * `generate()`, callers who only need the read path are not forced to provide a\n * synchronous generator.\n *\n * @example\n * ```ts\n * import { idFieldReadOnly } from \"@smonn/ids/prisma\";\n * import { createOpaqueTimestampId } from \"@smonn/ids/opaque\";\n *\n * const inv = createOpaqueTimestampId(\"inv\", { key });\n * const invoiceIdField = idFieldReadOnly(inv);\n *\n * const xprisma = prisma.$extends({\n * result: {\n * invoice: { id: invoiceIdField.computeField(\"id\") },\n * },\n * });\n * // xprisma.invoice.findUnique(…).id is typed as Id<\"inv\"> — no cast required\n * ```\n */\nexport function idFieldReadOnly<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): Omit<IdTransform<Brand>, \"defaultQuery\"> {\n return {\n read(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n readNullable(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n write(value: Id<Brand>): string {\n return value;\n },\n computeField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> =>\n readIdColumn(codec, model[fieldName]),\n };\n },\n computeNullableField(fieldName: string) {\n return {\n needs: { [fieldName]: true },\n compute: (model: Record<string, unknown>): Id<Brand> | null =>\n readIdColumnNullable(codec, model[fieldName]),\n };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqKA,SAAgB,QAA8B,OAAqD;CACjG,MAAM,EAAE,aAAa;CACrB,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,aAAa,OAAkC;GAC7C,OAAO,qBAAqB,OAAO,KAAK;EAC1C;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAM3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;EACA,aAAa,WAAiC;GAC5C,SAAS,eAAe,MAAwD;IAC9E,IAAI,KAAK,cAAc,MACrB,OAAO;KAAE,GAAG;MAAO,YAAY,SAAS;IAAE;IAE5C,OAAO;GACT;GAIA,OAAO;IACL,MAAM,OAAO,EAAE,MAAM,SAAS;KAC5B,MAAM,OAAO,KAAK;KAGlB,OAAO,MADL,QAAQ,OAAQ;MAAE,GAAG;MAAM,MAAM,eAAe,IAAI;KAAE,IAA4B,IAC/D;IACvB;IACA,MAAM,WAAW,EAAE,MAAM,SAAS;KAChC,MAAM,OAAO,KAAK;KAIlB,OAAO,MAHU,MAAM,QAAQ,IAAI,IAC9B;MAAE,GAAG;MAAM,MAAM,KAAK,KAAK,SAAS,eAAe,IAAI,CAAC;KAAE,IAC3D,IACiB;IACvB;IACA,MAAM,OAAO,EAAE,MAAM,SAAS;KAC5B,MAAM,aAAa,KAAK;KAKxB,OAAO,MAHL,cAAc,OACT;MAAE,GAAG;MAAM,QAAQ,eAAe,UAAU;KAAE,IAC/C,IACe;IACvB;GACF;EACF;EACA,qBAAqB,WAAmB;GACtC,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,qBAAqB,OAAO,MAAM,UAAU;GAChD;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,SAAgB,gBACd,OAC4B;CAC5B,OAAO;EACL,aAAa,OAAkC;GAC7C,OAAO,qBAAqB,OAAO,KAAK;EAC1C;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,qBAAqB,WAAmB;GACtC,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,qBAAqB,OAAO,MAAM,UAAU;GAChD;EACF;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,gBACd,OAC0C;CAC1C,OAAO;EACL,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;EACA,aAAa,OAAkC;GAC7C,OAAO,qBAAqB,OAAO,KAAK;EAC1C;EACA,MAAM,OAA0B;GAC9B,OAAO;EACT;EACA,aAAa,WAAmB;GAC9B,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,aAAa,OAAO,MAAM,UAAU;GACxC;EACF;EACA,qBAAqB,WAAmB;GACtC,OAAO;IACL,OAAO,GAAG,YAAY,KAAK;IAC3B,UAAU,UACR,qBAAqB,OAAO,MAAM,UAAU;GAChD;EACF;CACF;AACF"}
@@ -1,9 +1,20 @@
1
+ import { t as Id } from "./types-hGBnCpJj.mjs";
1
2
  import { n as IdColumnCodec } from "./adapter-types-Bia_w9sg.mjs";
2
3
  import { n as IdsErrorCode, r as isIdsError, t as IdsError } from "./error-CifcKKOG.mjs";
3
4
  import { ValueTransformer } from "typeorm";
4
5
 
5
6
  //#region src/adapters/typeorm.d.ts
6
7
  /**
8
+ * Extension of {@link IdColumnCodec} that also exposes synchronous `generate()`.
9
+ * Required by {@link beforeInsertHook} so that the hook can produce IDs at insert
10
+ * time. Every full codec variant (Timestamp, Reverse Timestamp) satisfies this;
11
+ * async-generate codecs (Opaque, Signed, Wrapped, Digest) do not and are therefore
12
+ * unsupported by `beforeInsertHook`.
13
+ */
14
+ type IdGeneratingCodec<Brand extends string> = IdColumnCodec<Brand> & {
15
+ generate(): Id<Brand>;
16
+ };
17
+ /**
7
18
  * TypeORM column transformer for `Id<Brand>`.
8
19
  *
9
20
  * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`
@@ -36,6 +47,78 @@ import { ValueTransformer } from "typeorm";
36
47
  * ```
37
48
  */
38
49
  declare function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer;
50
+ /**
51
+ * Returns a function suitable for use inside a TypeORM `@BeforeInsert()` lifecycle
52
+ * hook that auto-generates an `Id<Brand>` for `fieldName` when the field is absent
53
+ * (`null` or `undefined`) on the entity at insert time. If the field already has a
54
+ * value it is left unchanged.
55
+ *
56
+ * Requires a codec variant that exposes a synchronous `generate()` — see
57
+ * {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp
58
+ * codec** qualify; passing an async-generate codec (Opaque, Signed, Wrapped, Digest)
59
+ * is a **compile-time type error**.
60
+ *
61
+ * Pair with {@link idTransformer} on the same column: `idTransformer` handles the
62
+ * read/write path; `beforeInsertHook` handles generation.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * import { idTransformer, beforeInsertHook } from "@smonn/ids/typeorm";
67
+ * import { createTimestampId } from "@smonn/ids";
68
+ * import type { Id } from "@smonn/ids";
69
+ * import { BeforeInsert, Column, Entity } from "typeorm";
70
+ *
71
+ * const usr = createTimestampId("usr");
72
+ * const fillUserId = beforeInsertHook("id", usr);
73
+ *
74
+ * @Entity()
75
+ * class User {
76
+ * @Column({ type: "text", transformer: idTransformer(usr) })
77
+ * id!: Id<"usr">;
78
+ *
79
+ * @BeforeInsert()
80
+ * generateId() {
81
+ * fillUserId(this);
82
+ * }
83
+ * }
84
+ * ```
85
+ *
86
+ * @remarks
87
+ * **Gating rule:** `beforeInsertHook` requires a synchronous `generate()` codec
88
+ * (`IdGeneratingCodec`). Async-generate codecs — Opaque Timestamp, Signed Timestamp,
89
+ * Wrapped key, and Digest — do not satisfy `IdGeneratingCodec` and cannot be passed
90
+ * here. TypeORM `@BeforeInsert` can be async, but synchronous generation keeps the
91
+ * hook ergonomic and matches the full Prisma parity shape.
92
+ *
93
+ * **Read/write path:** Use {@link idTransformer} on the column for database
94
+ * read/write transforms. `beforeInsertHook` only handles the generation step — it
95
+ * does not replace the transformer.
96
+ */
97
+ declare function beforeInsertHook<Brand extends string>(fieldName: string, codec: IdGeneratingCodec<Brand>): (entity: Record<string, unknown>) => void;
98
+ /**
99
+ * TypeORM column transformer for a **nullable** `Id<Brand>` column.
100
+ *
101
+ * Behaves like {@link idTransformer} but `from` returns `null` for `null` /
102
+ * `undefined` database values and `to` passes `null` / `undefined` through
103
+ * unchanged. Use for optional foreign keys.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { nullableIdTransformer } from "@smonn/ids/typeorm";
108
+ * import { createTimestampId } from "@smonn/ids";
109
+ * import type { Id } from "@smonn/ids";
110
+ * import { Column, Entity } from "typeorm";
111
+ *
112
+ * const usr = createTimestampId("usr");
113
+ *
114
+ * @Entity()
115
+ * class Post {
116
+ * @Column({ type: "text", nullable: true, transformer: nullableIdTransformer(usr) })
117
+ * authorId!: Id<"usr"> | null;
118
+ * }
119
+ * ```
120
+ */
121
+ declare function nullableIdTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer;
39
122
  //#endregion
40
- export { type IdColumnCodec, IdsError, type IdsErrorCode, idTransformer, isIdsError };
123
+ export { type IdColumnCodec, IdGeneratingCodec, IdsError, type IdsErrorCode, beforeInsertHook, idTransformer, isIdsError, nullableIdTransformer };
41
124
  //# sourceMappingURL=typeorm.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;AAAkF;;;;;;;;;;;;;;;;;;AAAlF,iBAAgB,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA"}
1
+ {"version":3,"file":"typeorm.d.mts","names":[],"sources":["../src/adapters/typeorm.ts"],"mappings":";;;;;;AAgBA;;;;;;;AAAA,KAAY,iBAAA,yBAA0C,aAAA,CAAc,KAAA;EAClE,QAAA,IAAY,EAAA,CAAG,KAAA;AAAA;;;;;;;AAAA;AAmCjB;;;;;;;;;;;;;;AAAkF;AA0DlF;;;;;;;;;;iBA1DgB,aAAA,uBAAoC,KAAA,EAAO,aAAA,CAAc,KAAA,IAAS,gBAAA;;;;;;AA6DtE;AA+BZ;;;;;;;;;;;;;;AAEG;;;;;;;;;;;;;;;;;;;;;;;;;;;iBApCa,gBAAA,uBACd,SAAA,UACA,KAAA,EAAO,iBAAA,CAAkB,KAAA,KACvB,MAAA,EAAQ,MAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA+BI,qBAAA,uBACd,KAAA,EAAO,aAAA,CAAc,KAAA,IACpB,gBAAA"}
package/dist/typeorm.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { n as isIdsError, t as IdsError } from "./error-Cp5qYZcv.mjs";
2
- import { t as readIdColumn } from "./adapter-types-7wWdELSh.mjs";
2
+ import { n as readIdColumnNullable, t as readIdColumn } from "./adapter-types-CjzFNDcJ.mjs";
3
3
  //#region src/adapters/typeorm.ts
4
4
  /**
5
5
  * TypeORM column transformer for `Id<Brand>`.
@@ -43,7 +43,92 @@ function idTransformer(codec) {
43
43
  }
44
44
  };
45
45
  }
46
+ /**
47
+ * Returns a function suitable for use inside a TypeORM `@BeforeInsert()` lifecycle
48
+ * hook that auto-generates an `Id<Brand>` for `fieldName` when the field is absent
49
+ * (`null` or `undefined`) on the entity at insert time. If the field already has a
50
+ * value it is left unchanged.
51
+ *
52
+ * Requires a codec variant that exposes a synchronous `generate()` — see
53
+ * {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp
54
+ * codec** qualify; passing an async-generate codec (Opaque, Signed, Wrapped, Digest)
55
+ * is a **compile-time type error**.
56
+ *
57
+ * Pair with {@link idTransformer} on the same column: `idTransformer` handles the
58
+ * read/write path; `beforeInsertHook` handles generation.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * import { idTransformer, beforeInsertHook } from "@smonn/ids/typeorm";
63
+ * import { createTimestampId } from "@smonn/ids";
64
+ * import type { Id } from "@smonn/ids";
65
+ * import { BeforeInsert, Column, Entity } from "typeorm";
66
+ *
67
+ * const usr = createTimestampId("usr");
68
+ * const fillUserId = beforeInsertHook("id", usr);
69
+ *
70
+ * @Entity()
71
+ * class User {
72
+ * @Column({ type: "text", transformer: idTransformer(usr) })
73
+ * id!: Id<"usr">;
74
+ *
75
+ * @BeforeInsert()
76
+ * generateId() {
77
+ * fillUserId(this);
78
+ * }
79
+ * }
80
+ * ```
81
+ *
82
+ * @remarks
83
+ * **Gating rule:** `beforeInsertHook` requires a synchronous `generate()` codec
84
+ * (`IdGeneratingCodec`). Async-generate codecs — Opaque Timestamp, Signed Timestamp,
85
+ * Wrapped key, and Digest — do not satisfy `IdGeneratingCodec` and cannot be passed
86
+ * here. TypeORM `@BeforeInsert` can be async, but synchronous generation keeps the
87
+ * hook ergonomic and matches the full Prisma parity shape.
88
+ *
89
+ * **Read/write path:** Use {@link idTransformer} on the column for database
90
+ * read/write transforms. `beforeInsertHook` only handles the generation step — it
91
+ * does not replace the transformer.
92
+ */
93
+ function beforeInsertHook(fieldName, codec) {
94
+ return function(entity) {
95
+ if (entity[fieldName] == null) entity[fieldName] = codec.generate();
96
+ };
97
+ }
98
+ /**
99
+ * TypeORM column transformer for a **nullable** `Id<Brand>` column.
100
+ *
101
+ * Behaves like {@link idTransformer} but `from` returns `null` for `null` /
102
+ * `undefined` database values and `to` passes `null` / `undefined` through
103
+ * unchanged. Use for optional foreign keys.
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * import { nullableIdTransformer } from "@smonn/ids/typeorm";
108
+ * import { createTimestampId } from "@smonn/ids";
109
+ * import type { Id } from "@smonn/ids";
110
+ * import { Column, Entity } from "typeorm";
111
+ *
112
+ * const usr = createTimestampId("usr");
113
+ *
114
+ * @Entity()
115
+ * class Post {
116
+ * @Column({ type: "text", nullable: true, transformer: nullableIdTransformer(usr) })
117
+ * authorId!: Id<"usr"> | null;
118
+ * }
119
+ * ```
120
+ */
121
+ function nullableIdTransformer(codec) {
122
+ return {
123
+ to(value) {
124
+ return value;
125
+ },
126
+ from(value) {
127
+ return readIdColumnNullable(codec, value);
128
+ }
129
+ };
130
+ }
46
131
  //#endregion
47
- export { IdsError, idTransformer, isIdsError };
132
+ export { IdsError, beforeInsertHook, idTransformer, isIdsError, nullableIdTransformer };
48
133
 
49
134
  //# sourceMappingURL=typeorm.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { readIdColumn, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF"}
1
+ {"version":3,"file":"typeorm.mjs","names":[],"sources":["../src/adapters/typeorm.ts"],"sourcesContent":["import type { ValueTransformer } from \"typeorm\";\nimport { readIdColumn, readIdColumnNullable, type IdColumnCodec } from \"./adapter-types.js\";\nimport type { Id } from \"../types.js\";\n\n/** {@link IdsError} class, {@link isIdsError} type guard, and {@link IdsErrorCode} union — re-exported from `\"@smonn/ids\"` for convenience. */\nexport { IdsError, isIdsError, type IdsErrorCode } from \"../error.js\";\n\nexport type { IdColumnCodec };\n\n/**\n * Extension of {@link IdColumnCodec} that also exposes synchronous `generate()`.\n * Required by {@link beforeInsertHook} so that the hook can produce IDs at insert\n * time. Every full codec variant (Timestamp, Reverse Timestamp) satisfies this;\n * async-generate codecs (Opaque, Signed, Wrapped, Digest) do not and are therefore\n * unsupported by `beforeInsertHook`.\n */\nexport type IdGeneratingCodec<Brand extends string> = IdColumnCodec<Brand> & {\n generate(): Id<Brand>;\n};\n\n/**\n * TypeORM column transformer for `Id<Brand>`.\n *\n * Returns a `ValueTransformer` object suitable for use in a TypeORM `@Column`\n * decorator's `transformer` option.\n *\n * **Write path (`to`):** passes the `Id<Brand>` directly to the database — it is\n * already the canonical string form.\n *\n * **Read path (`from`):** normalises the raw database value via `codec.safeParse()`.\n * Throws `IdsError` with code `\"invalid_id\"` if the value does not parse as a valid\n * `Id<Brand>`.\n *\n * **TypeORM branding caveat:** TypeORM cannot brand a generated entity field type at\n * the schema level. Annotate the entity field explicitly: `id!: Id<\"usr\">`.\n *\n * @example\n * ```ts\n * import { idTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n * }\n * ```\n */\nexport function idTransformer<Brand extends string>(codec: IdColumnCodec<Brand>): ValueTransformer {\n return {\n to(value: Id<Brand>): string {\n return value;\n },\n from(value: unknown): Id<Brand> {\n return readIdColumn(codec, value);\n },\n };\n}\n\n/**\n * Returns a function suitable for use inside a TypeORM `@BeforeInsert()` lifecycle\n * hook that auto-generates an `Id<Brand>` for `fieldName` when the field is absent\n * (`null` or `undefined`) on the entity at insert time. If the field already has a\n * value it is left unchanged.\n *\n * Requires a codec variant that exposes a synchronous `generate()` — see\n * {@link IdGeneratingCodec}. Only the **Timestamp codec** and **Reverse Timestamp\n * codec** qualify; passing an async-generate codec (Opaque, Signed, Wrapped, Digest)\n * is a **compile-time type error**.\n *\n * Pair with {@link idTransformer} on the same column: `idTransformer` handles the\n * read/write path; `beforeInsertHook` handles generation.\n *\n * @example\n * ```ts\n * import { idTransformer, beforeInsertHook } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { BeforeInsert, Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n * const fillUserId = beforeInsertHook(\"id\", usr);\n *\n * @Entity()\n * class User {\n * @Column({ type: \"text\", transformer: idTransformer(usr) })\n * id!: Id<\"usr\">;\n *\n * @BeforeInsert()\n * generateId() {\n * fillUserId(this);\n * }\n * }\n * ```\n *\n * @remarks\n * **Gating rule:** `beforeInsertHook` requires a synchronous `generate()` codec\n * (`IdGeneratingCodec`). Async-generate codecs — Opaque Timestamp, Signed Timestamp,\n * Wrapped key, and Digest — do not satisfy `IdGeneratingCodec` and cannot be passed\n * here. TypeORM `@BeforeInsert` can be async, but synchronous generation keeps the\n * hook ergonomic and matches the full Prisma parity shape.\n *\n * **Read/write path:** Use {@link idTransformer} on the column for database\n * read/write transforms. `beforeInsertHook` only handles the generation step — it\n * does not replace the transformer.\n */\nexport function beforeInsertHook<Brand extends string>(\n fieldName: string,\n codec: IdGeneratingCodec<Brand>,\n): (entity: Record<string, unknown>) => void {\n return function (entity: Record<string, unknown>): void {\n if (entity[fieldName] == null) {\n entity[fieldName] = codec.generate();\n }\n };\n}\n\n/**\n * TypeORM column transformer for a **nullable** `Id<Brand>` column.\n *\n * Behaves like {@link idTransformer} but `from` returns `null` for `null` /\n * `undefined` database values and `to` passes `null` / `undefined` through\n * unchanged. Use for optional foreign keys.\n *\n * @example\n * ```ts\n * import { nullableIdTransformer } from \"@smonn/ids/typeorm\";\n * import { createTimestampId } from \"@smonn/ids\";\n * import type { Id } from \"@smonn/ids\";\n * import { Column, Entity } from \"typeorm\";\n *\n * const usr = createTimestampId(\"usr\");\n *\n * @Entity()\n * class Post {\n * @Column({ type: \"text\", nullable: true, transformer: nullableIdTransformer(usr) })\n * authorId!: Id<\"usr\"> | null;\n * }\n * ```\n */\nexport function nullableIdTransformer<Brand extends string>(\n codec: IdColumnCodec<Brand>,\n): ValueTransformer {\n return {\n to(value: Id<Brand> | null | undefined): string | null | undefined {\n return value;\n },\n from(value: unknown): Id<Brand> | null {\n return readIdColumnNullable(codec, value);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAgB,cAAoC,OAA+C;CACjG,OAAO;EACL,GAAG,OAA0B;GAC3B,OAAO;EACT;EACA,KAAK,OAA2B;GAC9B,OAAO,aAAa,OAAO,KAAK;EAClC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDA,SAAgB,iBACd,WACA,OAC2C;CAC3C,OAAO,SAAU,QAAuC;EACtD,IAAI,OAAO,cAAc,MACvB,OAAO,aAAa,MAAM,SAAS;CAEvC;AACF;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,sBACd,OACkB;CAClB,OAAO;EACL,GAAG,OAAgE;GACjE,OAAO;EACT;EACA,KAAK,OAAkC;GACrC,OAAO,qBAAqB,OAAO,KAAK;EAC1C;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smonn/ids",
3
- "version": "0.15.0",
3
+ "version": "1.0.0-rc.1",
4
4
  "license": "MIT",
5
5
  "author": "Simon Ingeson (https://github.com/smonn)",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- {"version":3,"file":"adapter-types-7wWdELSh.mjs","names":[],"sources":["../src/adapters/adapter-types.ts"],"sourcesContent":["import { IdsError } from \"../error.js\";\nimport type { Id, ParseError, ParseResult } from \"../types.js\";\n\n/** Discriminated failure value passed to `onError` and emitted to the framework's error handler. */\nexport type IdParamFailure =\n | { readonly reason: \"brand_mismatch\"; readonly status: number }\n | { readonly reason: \"malformed\"; readonly status: number };\n\n/** Minimum structural type required by web and ORM adapters. Any codec variant satisfies this — all expose `safeParse`. Adapters only ever call `safeParse` — never key-dependent methods like `extractTimestamp`, `wrap`, or `unwrap`. */\nexport type IdCodec<Brand extends string> = {\n safeParse(value: unknown): ParseResult<Brand>;\n};\n\n/** Re-exported from ORM adapter subpaths (`@smonn/ids/drizzle`, `@smonn/ids/prisma`, `@smonn/ids/kysely`) under the public name; structurally identical to {@link IdCodec}. */\nexport type IdColumnCodec<Brand extends string> = IdCodec<Brand>;\n\n/** Parses `value` as `Id<Brand>` via `codec.safeParse`; throws `IdsError(\"invalid_id\")` on failure. Shared read helper for ORM adapters. */\nexport function readIdColumn<Brand extends string>(\n codec: IdCodec<Brand>,\n value: unknown,\n): Id<Brand> {\n const result = codec.safeParse(value);\n if (!result.ok) {\n throw new IdsError(\"invalid_id\", `invalid ID from database: ${result.error}`, {\n cause: result.error,\n });\n }\n return result.id;\n}\n\n/**\n * Maps a `ParseError` to `{ reason, status }` for web adapter failure handling.\n *\n * - `invalid_prefix` → `brand_mismatch` / default 404\n * - anything else → `malformed` / default 400\n * - `options.status[reason]` overrides the default for that reason\n */\nexport function resolveIdParamFailure(\n error: ParseError,\n options?: { status?: { brand_mismatch?: number; malformed?: number } },\n): IdParamFailure {\n const reason = error === \"invalid_prefix\" ? (\"brand_mismatch\" as const) : (\"malformed\" as const);\n const defaultStatus = reason === \"brand_mismatch\" ? 404 : 400;\n const status = options?.status?.[reason] ?? defaultStatus;\n return { reason, status };\n}\n"],"mappings":";;;AAiBA,SAAgB,aACd,OACA,OACW;CACX,MAAM,SAAS,MAAM,UAAU,KAAK;CACpC,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,SAAS,cAAc,6BAA6B,OAAO,SAAS,EAC5E,OAAO,OAAO,MAChB,CAAC;CAEH,OAAO,OAAO;AAChB;;;;;;;;AASA,SAAgB,sBACd,OACA,SACgB;CAChB,MAAM,SAAS,UAAU,mBAAoB,mBAA8B;CAC3E,MAAM,gBAAgB,WAAW,mBAAmB,MAAM;CAE1D,OAAO;EAAE;EAAQ,QADF,SAAS,SAAS,WAAW;CACpB;AAC1B"}