@prisma-next/extension-arktype-json 0.5.0-dev.59 → 0.5.0-dev.61

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 (38) hide show
  1. package/README.md +11 -34
  2. package/dist/arktype-json-codec-Cykol-li.mjs +120 -0
  3. package/dist/arktype-json-codec-Cykol-li.mjs.map +1 -0
  4. package/dist/arktype-json-codec-DBfCWQkt.d.mts +58 -0
  5. package/dist/arktype-json-codec-DBfCWQkt.d.mts.map +1 -0
  6. package/dist/{codec-types-CH37Yc0C.d.mts → codec-types-5Jy4t54u.d.mts} +1 -1
  7. package/dist/codec-types-5Jy4t54u.d.mts.map +1 -0
  8. package/dist/codec-types.d.mts +1 -1
  9. package/dist/codecs.d.mts +14 -2
  10. package/dist/codecs.d.mts.map +1 -0
  11. package/dist/codecs.mjs +3 -2
  12. package/dist/column-types.d.mts +2 -2
  13. package/dist/column-types.mjs +2 -2
  14. package/dist/control.mjs +3 -2
  15. package/dist/control.mjs.map +1 -1
  16. package/dist/{pack-meta-DycetSQB.mjs → pack-meta-BaJhoZfD.mjs} +5 -6
  17. package/dist/pack-meta-BaJhoZfD.mjs.map +1 -0
  18. package/dist/pack.d.mts +3 -5
  19. package/dist/pack.d.mts.map +1 -1
  20. package/dist/pack.mjs +3 -1
  21. package/dist/registry-DN6MqSGJ.mjs +14 -0
  22. package/dist/registry-DN6MqSGJ.mjs.map +1 -0
  23. package/dist/runtime.d.mts.map +1 -1
  24. package/dist/runtime.mjs +4 -22
  25. package/dist/runtime.mjs.map +1 -1
  26. package/package.json +9 -8
  27. package/src/core/arktype-json-codec.ts +143 -328
  28. package/src/core/pack-meta.ts +6 -24
  29. package/src/core/registry.ts +11 -0
  30. package/src/exports/codecs.ts +7 -2
  31. package/src/exports/column-types.ts +1 -1
  32. package/src/exports/runtime.ts +4 -19
  33. package/dist/arktype-json-codec-BbiBmtTK.mjs +0 -224
  34. package/dist/arktype-json-codec-BbiBmtTK.mjs.map +0 -1
  35. package/dist/arktype-json-codec-yNv1hzBm.d.mts +0 -92
  36. package/dist/arktype-json-codec-yNv1hzBm.d.mts.map +0 -1
  37. package/dist/codec-types-CH37Yc0C.d.mts.map +0 -1
  38. package/dist/pack-meta-DycetSQB.mjs.map +0 -1
@@ -1,395 +1,167 @@
1
1
  /**
2
- * Single source of truth for the arktype-json `arktype/json@1` codec.
2
+ * Arktype-json codec (TML-2357).
3
3
  *
4
- * Ships the per-library JSON-with-schema column factory (`arktypeJson`) and
5
- * the framework-registration descriptor (`arktypeJsonCodec`). The two
6
- * surfaces share one serialize/rehydrate pipeline keyed on arktype's
7
- * internal IR.
4
+ * Spec § Case 3: method-level generic over `S extends Type<unknown>`. The schema's TypeScript-level inferred type `S['infer']` is only available at the column-author site (where the user passes their typed schema), not at the descriptor's factory site (where only the serialized IR is available). This drives the shape:
8
5
  *
9
- * **Serialization** (column-author site, eager):
6
+ * 1. {@link ArktypeJsonCodecClass} extends {@link CodecImpl} and is generic over `TInferred` — the application-level JS type the schema validates to. The constructor takes both the descriptor (for `id` proxy) and the rehydrated arktype `Type` (closure-captured so encode/decode/encodeJson/decodeJson can validate through it). 2. {@link ArktypeJsonDescriptor} extends {@link CodecDescriptorImpl} over {@link
7
+ * ArktypeJsonTypeParams}. Factory rehydrates the schema from `params.jsonIr` and returns `(ctx) => new ArktypeJsonCodecClass<unknown>(this, schema)` — `S` is erased to `unknown` because the descriptor only sees IR. The runtime path through `descriptor.factory(params)` always exists (e.g. for `validateContract` re-materialization); it just loses the typed inferred shape. 3. {@link arktypeJsonColumn} is the column-author
8
+ * surface with the method-level generic over `S extends Type<unknown>`. It bypasses `descriptor.factory` because `S` is only available here, instead constructing the typed codec directly so `S['infer']` flows through `codecFactory`'s return into the column site's resolved output type. Eager serialization at this call site captures `expression` (for the emit-path renderer) and `jsonIr` (for runtime rehydration).
10
9
  *
11
- * - `expression`: `schema.expression` arktype's TypeScript-source-like
12
- * rendering used by the emit-path `renderOutputType` to produce the
13
- * column's TS type in `contract.d.ts`.
14
- * - `jsonIr`: `schema.json` — arktype's internal IR. Lossless; the
15
- * rehydration source consumed by `ark.schema(jsonIr)` at runtime.
16
- *
17
- * The pair is sufficient: `expression` round-trips with the rehydrated
18
- * schema (`ark.schema(jsonIr).expression === expression`) so the emit-path
19
- * output is stable across serialize/rehydrate.
20
- *
21
- * **Rehydration** (runtime, on factory invocation): `ark.schema(typeParams.jsonIr)`
22
- * returns a callable `Type`-like with `~standard`. The returned codec's
23
- * `decode` body validates wire payloads through the rehydrated schema and
24
- * throws `RUNTIME.JSON_SCHEMA_VALIDATION_FAILED` on rejection — no separate
25
- * validator-registry consultation.
26
- *
27
- * See the codec-registry-unification spec § Case J (JSON-with-schema).
10
+ * `satisfies ColumnHelperFor<ArktypeJsonDescriptor>` (coarse) is applied — the typeParams shape is verified. `ColumnHelperForStrict` is intentionally skipped: the descriptor's factory return is `ArktypeJsonCodecClass<unknown>` while the helper produces `ArktypeJsonCodecClass<S['infer']>`, and `Codec`'s `TInput` is invariant (used contravariantly in `encode`, covariantly in `decode`/`encodeJson`/`decodeJson`). Strict
11
+ * assignment fails by design; the explicit `expectTypeOf` tests in `test/arktype-json-codec.types.test-d.ts` cover the literal-preservation property the strict variant would otherwise enforce.
28
12
  */
29
13
 
30
14
  import type { JsonValue } from '@prisma-next/contract/types';
31
- import type { ColumnTypeDescriptor } from '@prisma-next/contract-authoring';
32
- import type {
33
- Codec,
34
- CodecDescriptor,
35
- CodecInstanceContext,
15
+ import {
16
+ type AnyCodecDescriptor,
17
+ type CodecCallContext,
18
+ CodecDescriptorImpl,
19
+ CodecImpl,
20
+ type CodecInstanceContext,
21
+ type ColumnHelperFor,
22
+ type ColumnSpec,
23
+ column,
36
24
  } from '@prisma-next/framework-components/codec';
37
25
  import { runtimeError } from '@prisma-next/framework-components/runtime';
38
- import { codec } from '@prisma-next/sql-relational-core/ast';
26
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
39
27
  import { ArkErrors, ark, type Type, type } from 'arktype';
40
28
 
41
- // ── Constants ────────────────────────────────────────────────────────────
42
-
43
29
  /** Codec id for arktype-backed JSON columns. Library-bound, not target-bound. */
44
30
  export const ARKTYPE_JSON_CODEC_ID = 'arktype/json@1' as const;
45
31
 
46
32
  /** Native storage type backing the codec. JSONB on Postgres; binary, indexable. */
47
33
  export const ARKTYPE_JSON_NATIVE_TYPE = 'jsonb' as const;
48
34
 
49
- // ── typeParams shape ─────────────────────────────────────────────────────
50
-
51
35
  /**
52
- * Eagerly serialized typeParams for the arktype-json column. Carried in
53
- * the contract IR; the runtime descriptor's factory rehydrates `jsonIr`
54
- * and the emitter consumes `expression`.
36
+ * Eagerly serialized typeParams for the arktype-json column. Carried in the contract IR; the runtime descriptor's factory rehydrates `jsonIr` and the emitter consumes `expression`.
55
37
  */
56
38
  export type ArktypeJsonTypeParams = {
57
39
  /**
58
- * Arktype's TypeScript-source-like rendering of the schema. Read by
59
- * `renderOutputType` to emit the column's TS type into `contract.d.ts`.
60
- * Stable across the serialize/rehydrate cycle: the rehydrated schema's
61
- * `expression` matches the source schema's.
40
+ * Arktype's TypeScript-source-like rendering of the schema. Read by `renderOutputType` to emit the column's TS type into `contract.d.ts`. Stable across the serialize/rehydrate cycle: the rehydrated schema's `expression` matches the source schema's.
62
41
  */
63
42
  readonly expression: string;
64
43
  /**
65
- * Arktype's internal IR for the schema. Lossless; the rehydration
66
- * source. Schema-shape — `ark.schema(jsonIr)` reconstructs a callable
67
- * `Type`-like structurally identical to the original `type(definition)`
68
- * output.
44
+ * Arktype's internal IR for the schema. Lossless; the rehydration source. Schema-shape — `ark.schema(jsonIr)` reconstructs a callable `Type`-like structurally identical to the original `type(definition)` output.
69
45
  */
70
46
  readonly jsonIr: object;
71
47
  };
72
48
 
73
- // ── Curried higher-order codec factory ───────────────────────────────────
74
-
75
- /**
76
- * Codec instance returned by `arktypeJson(schema)(ctx)` and by
77
- * `arktypeJsonCodec.factory(typeParams)(ctx)`. The `TInferred` slot
78
- * carries the arktype schema's inferred output type.
79
- */
80
- export type ArktypeJsonCodec<TInferred> = Codec<
81
- typeof ARKTYPE_JSON_CODEC_ID,
82
- readonly ['equality'],
83
- string,
84
- TInferred
85
- >;
86
-
87
- /**
88
- * Structural narrow of arktype's `Type` — the surface our codec depends
89
- * on: a callable validator that returns `inferOut | ArkErrors`, plus the
90
- * `expression` string for emit-path rendering.
91
- *
92
- * Avoids depending on the precise generics of arktype's `Type<t, $>` so
93
- * schemas built in any scope (the default `Ark` from `type(...)` AND the
94
- * minimal scope from `ark.schema(...)`) satisfy the same contract.
95
- */
96
49
  type ArktypeSchemaLike = ((value: unknown) => unknown) & {
97
50
  readonly expression: string;
98
51
  };
99
52
 
100
- /**
101
- * Type predicate for `ArktypeSchemaLike`. Lets the column-author
102
- * factory narrow `unknown` schemas to the structural shape the codec
103
- * depends on after the explicit field guards run, so the descriptor
104
- * builder doesn't fall back to a `as unknown as` cast.
105
- */
106
53
  function isArktypeSchemaLike(value: unknown): value is ArktypeSchemaLike {
107
54
  if (typeof value !== 'function') return false;
108
55
  const expression = (value as { readonly expression?: unknown }).expression;
109
56
  return typeof expression === 'string';
110
57
  }
111
58
 
112
- /**
113
- * Build the curried factory for a rehydrated arktype schema. The factory's
114
- * returned codec carries the schema in its closure; `decode` validates
115
- * wire payloads via `schema(parsed)`, throwing
116
- * `RUNTIME.JSON_SCHEMA_VALIDATION_FAILED` on rejection.
117
- *
118
- * Encode is `JSON.stringify` — the schema validates the input shape only
119
- * at the read boundary (decode), matching the JSON-validator philosophy:
120
- * the payload may have been written by any source (this writer, a
121
- * previous version of the schema, a manual SQL `INSERT`); validate when
122
- * reading, not when writing.
123
- *
124
- * Author bodies are sync; main's `codec({...})` factory promise-lifts
125
- * `encode`/`decode` into the framework-required `Promise<…>` boundary
126
- * shape (per ADR 204).
127
- */
128
- function arktypeJsonCodecForSchema<TInferred>(
129
- schema: ArktypeSchemaLike,
130
- ): (ctx: CodecInstanceContext) => ArktypeJsonCodec<TInferred> {
131
- // Shared schema check used by both `decode` (wire → JS) and
132
- // `decodeJson` (JsonValue → JS). Either entry point must reject
133
- // payloads that don't match the schema; without the shared validator,
134
- // any caller that hands parsed JSON straight to the codec would bypass
135
- // schema enforcement and return unchecked data.
136
- function validateSchema(value: unknown): TInferred {
137
- const result = schema(value);
138
- if (result instanceof ArkErrors) {
139
- throw runtimeError(
140
- 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
141
- `arktype-json schema validation failed (decode): ${result.summary}`,
142
- { codecId: ARKTYPE_JSON_CODEC_ID, issues: result.summary },
143
- );
144
- }
145
- // arktype's call-result is `inferOut | ArkErrors`; the ArkErrors
146
- // branch is excluded above. The cast threads the caller-supplied
147
- // generic onto the structurally-typed validation output.
148
- return result as TInferred;
149
- }
150
-
151
- // Derive both `encode` (wire string) and `encodeJson` (JsonValue)
152
- // outputs from the same `JSON.stringify` → `JSON.parse` round-trip,
153
- // then validate the normalized payload through the schema. Without
154
- // this normalization, a non-JSON-safe runtime value (e.g. a class
155
- // instance, a function field on a narrowed type) could slip through
156
- // `encodeJson` unchanged while `encode` silently dropped or
157
- // transformed it — producing wire payloads the codec's own decode
158
- // path would later reject. The serialize/parse round-trip also
159
- // produces the JSON-safe shape required by the contract IR's
160
- // `JsonValue` surface, so `encodeJson` no longer needs a blind cast.
161
- function serializeToJsonSafe(value: TInferred): { wire: string; json: JsonValue } {
162
- // `JSON.stringify` returns `string | undefined` — `undefined`
163
- // happens when the input is `undefined` itself or contains only
164
- // unserializable values (functions, symbols). Reject explicitly so
165
- // the caller sees the schema-failure code rather than a downstream
166
- // `JSON.parse(undefined)` SyntaxError.
167
- const wire: string | undefined = JSON.stringify(value);
168
- if (typeof wire !== 'string') {
169
- throw runtimeError(
170
- 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
171
- `arktype-json value is not representable as JSON (codecId: ${ARKTYPE_JSON_CODEC_ID})`,
172
- { codecId: ARKTYPE_JSON_CODEC_ID },
173
- );
174
- }
175
- const json = JSON.parse(wire) as JsonValue;
176
- // Validate the normalized payload — the round-trip strips
177
- // class-prototype shape and arktype-narrowed fields, and the
178
- // schema must still accept the result. Run validation and discard
179
- // its return value (we keep `json` as the JsonValue, not the
180
- // schema's `inferOut` which already matches `TInferred`).
181
- validateSchema(json);
182
- return { wire, json };
59
+ function validateSchema<TInferred>(schema: ArktypeSchemaLike, value: unknown): TInferred {
60
+ const result = schema(value);
61
+ if (result instanceof ArkErrors) {
62
+ throw runtimeError(
63
+ 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
64
+ `arktype-json schema validation failed (decode): ${result.summary}`,
65
+ { codecId: ARKTYPE_JSON_CODEC_ID, issues: result.summary },
66
+ );
183
67
  }
184
-
185
- return (_ctx) =>
186
- codec<typeof ARKTYPE_JSON_CODEC_ID, readonly ['equality'], string, TInferred>({
187
- typeId: ARKTYPE_JSON_CODEC_ID,
188
- targetTypes: [ARKTYPE_JSON_NATIVE_TYPE],
189
- traits: ['equality'] as const,
190
- encode: (value: TInferred): string => serializeToJsonSafe(value).wire,
191
- decode: (wire: string): TInferred => validateSchema(JSON.parse(wire)),
192
- encodeJson: (value: TInferred): JsonValue => serializeToJsonSafe(value).json,
193
- decodeJson: (json: JsonValue) => validateSchema(json),
194
- }) as ArktypeJsonCodec<TInferred>;
68
+ return result as TInferred;
195
69
  }
196
70
 
197
- // ── Column-author surface ────────────────────────────────────────────────
198
-
199
- /**
200
- * Curried column-author factory for arktype-validated JSON columns.
201
- *
202
- * Usage:
203
- *
204
- * ```ts
205
- * import { type } from 'arktype';
206
- * import { arktypeJson } from '@prisma-next/extension-arktype-json/column-types';
207
- *
208
- * const ProductSchema = type({ name: 'string', price: 'number' });
209
- *
210
- * const Product = {
211
- * columns: {
212
- * id: textCodec,
213
- * settings: arktypeJson(ProductSchema),
214
- * // ^? ColumnTypeDescriptor with type :: (ctx) => Codec<…, { name: string; price: number }>
215
- * },
216
- * };
217
- * ```
218
- *
219
- * The schema's inferred output flows through `S['infer']` so the no-emit
220
- * `FieldOutputType` resolver produces the precise TS type at the column
221
- * site. Eager serialization at this call site captures `expression` (for
222
- * the emit-path renderer) and `jsonIr` (for runtime rehydration).
223
- *
224
- * @throws {Error} if the schema doesn't expose `expression` and `json`
225
- * fields (i.e. is not an arktype `Type`). The factory validates the
226
- * schema shape at the call site so configuration errors surface during
227
- * contract authoring, not at runtime.
228
- */
229
- export function arktypeJson<S extends Type<unknown>>(
230
- schema: S,
231
- ): ColumnTypeDescriptor & {
232
- readonly codecId: typeof ARKTYPE_JSON_CODEC_ID;
233
- readonly nativeType: typeof ARKTYPE_JSON_NATIVE_TYPE;
234
- readonly typeParams: ArktypeJsonTypeParams;
235
- readonly type: (ctx: CodecInstanceContext) => ArktypeJsonCodec<S['infer']>;
236
- } {
237
- // Reject non-callable / non-arktype-shaped lookalikes before any
238
- // property reads. An object shaped like `{ expression, json }` would
239
- // otherwise pass the field checks and only explode on the first
240
- // `decode`/`decodeJson` call, defeating the early authoring-time
241
- // guard this factory provides. The `isArktypeSchemaLike` predicate
242
- // narrows `schema` so the descriptor builder hands the typed shape
243
- // straight to the curried factory — no `as unknown as` cast.
244
- if (!isArktypeSchemaLike(schema)) {
245
- throw new Error(
246
- typeof schema !== 'function'
247
- ? 'arktypeJson(schema) expects a callable arktype Type.'
248
- : 'arktypeJson(schema) expects an arktype Type (missing `expression: string`).',
71
+ function serializeToJsonSafe<TInferred>(
72
+ schema: ArktypeSchemaLike,
73
+ value: TInferred,
74
+ ): { wire: string; json: JsonValue } {
75
+ const wire: string | undefined = JSON.stringify(value);
76
+ if (typeof wire !== 'string') {
77
+ throw runtimeError(
78
+ 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
79
+ `arktype-json value is not representable as JSON (codecId: ${ARKTYPE_JSON_CODEC_ID})`,
80
+ { codecId: ARKTYPE_JSON_CODEC_ID },
249
81
  );
250
82
  }
251
- const jsonIr: unknown = (schema as { readonly json?: unknown }).json;
252
- if (jsonIr === null || typeof jsonIr !== 'object') {
253
- throw new Error('arktypeJson(schema) expects an arktype Type (missing `json` IR).');
254
- }
255
- return {
256
- codecId: ARKTYPE_JSON_CODEC_ID,
257
- nativeType: ARKTYPE_JSON_NATIVE_TYPE,
258
- typeParams: { expression: schema.expression, jsonIr },
259
- type: arktypeJsonCodecForSchema<S['infer']>(schema),
260
- } as const;
83
+ const json = JSON.parse(wire) as JsonValue;
84
+ validateSchema(schema, json);
85
+ return { wire, json };
261
86
  }
262
87
 
263
- // ── Framework-registration descriptor ────────────────────────────────────
264
-
265
- /**
266
- * Standard Schema validator for the descriptor's typeParams. Asserts the
267
- * shape `{ expression: string; jsonIr: object }` at the contract IR
268
- * boundary; deeper IR-shape validation happens implicitly when
269
- * `ark.schema(jsonIr)` reparses (corrupt IR throws there).
270
- *
271
- * Eats its own dog food: the validator is itself an arktype schema.
272
- */
273
- const arktypeJsonParamsSchema = type({
274
- expression: 'string',
275
- jsonIr: 'object',
276
- });
277
-
278
- /**
279
- * Rehydrate an arktype schema from the serialized IR. Throws a clean
280
- * error if the IR is corrupt — the "corruption-of-contract.json" case.
281
- */
282
88
  function rehydrateSchema(jsonIr: object): ArktypeSchemaLike {
89
+ let rehydrated: unknown;
283
90
  try {
284
- return ark.schema(jsonIr) as unknown as ArktypeSchemaLike;
91
+ rehydrated = ark.schema(jsonIr);
285
92
  } catch (error) {
286
93
  throw runtimeError(
287
94
  'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
95
+ /* c8 ignore next — the `String(error)` fallback covers throws of non-Error values; arktype only throws Error subclasses, so this branch is defensive only. */
288
96
  `Failed to rehydrate arktype schema from contract IR: ${error instanceof Error ? error.message : String(error)}`,
289
97
  { codecId: ARKTYPE_JSON_CODEC_ID, jsonIr },
290
98
  );
291
99
  }
100
+ /* c8 ignore start — defensive: ark.schema either throws (handled above) or returns a callable Type with `expression: string`. The structural guard is kept so a future ark internal change can't silently slip a non-callable past us. */
101
+ if (!isArktypeSchemaLike(rehydrated)) {
102
+ throw runtimeError(
103
+ 'RUNTIME.JSON_SCHEMA_VALIDATION_FAILED',
104
+ `Rehydrated arktype schema does not have the expected callable + 'expression: string' shape (codecId: ${ARKTYPE_JSON_CODEC_ID})`,
105
+ { codecId: ARKTYPE_JSON_CODEC_ID, jsonIr },
106
+ );
107
+ }
108
+ /* c8 ignore stop */
109
+ return rehydrated;
292
110
  }
293
111
 
294
- /**
295
- * Render the emit-path TS type for an arktype-json column. Reads the
296
- * eagerly-extracted `expression` directly — the round-trip stability
297
- * guarantee (rehydrated schema's `expression` matches the source's)
298
- * means the rendered output is consistent across serialize/rehydrate.
299
- */
300
112
  function renderArktypeJsonOutputType(params: ArktypeJsonTypeParams): string {
301
113
  const expression = params.expression.trim();
302
114
  return expression.length > 0 ? expression : 'unknown';
303
115
  }
304
116
 
305
- /**
306
- * Build a permissive `renderOutputType` that accepts the framework's
307
- * generic typeParams shape and dispatches to the type-narrow renderer
308
- * once the input is structurally an `ArktypeJsonTypeParams`.
309
- */
310
- function renderArktypeJsonOutputTypeFromUnknownParams(
311
- typeParams: Record<string, unknown>,
312
- ): string | undefined {
313
- const expression = typeParams['expression'];
314
- const jsonIr = typeParams['jsonIr'];
315
- if (typeof expression !== 'string' || jsonIr === null || typeof jsonIr !== 'object') {
316
- return undefined;
317
- }
318
- return renderArktypeJsonOutputType({ expression, jsonIr });
319
- }
320
-
321
- /**
322
- * Emit-only `Codec` instance for `arktype/json@1`. Threaded through the
323
- * pack-meta's `codecInstances` array so the emitter's `CodecLookup` can
324
- * find a `renderOutputType` for the codec id (the emitter consults the
325
- * codec-id-keyed `CodecLookup` at the framework boundary; the unified
326
- * descriptor's `renderOutputType` is the long-term home for the renderer
327
- * but the emit-path glue still routes through `CodecLookup`).
328
- *
329
- * All conversion methods are sentinels that throw if invoked — runtime
330
- * materialization always goes through `arktypeJsonCodec.factory`'s
331
- * curried `(params) => (ctx) => Codec`, never through this instance.
332
- * `encodeJson`/`decodeJson` throw alongside `encode`/`decode` so a
333
- * mistaken contract-load that resolved to this stub fails fast at the
334
- * JSON boundary instead of silently returning unvalidated payloads. A
335
- * future cleanup could route the emit path through the descriptor map
336
- * directly and retire this shim.
337
- */
338
- const ARKTYPE_JSON_RUNTIME_DISPATCH_ERROR =
339
- 'arktype-json codec instances must be materialized via the descriptor factory; this is an emit-only stub';
340
-
341
- export const arktypeJsonEmitCodec: Codec<
117
+ export class ArktypeJsonCodecClass<TInferred> extends CodecImpl<
342
118
  typeof ARKTYPE_JSON_CODEC_ID,
343
119
  readonly ['equality'],
344
120
  string,
345
- unknown
346
- > = {
347
- id: ARKTYPE_JSON_CODEC_ID,
348
- targetTypes: [ARKTYPE_JSON_NATIVE_TYPE],
349
- traits: ['equality'] as const,
350
- encode: () => Promise.reject(new Error(ARKTYPE_JSON_RUNTIME_DISPATCH_ERROR)),
351
- decode: () => Promise.reject(new Error(ARKTYPE_JSON_RUNTIME_DISPATCH_ERROR)),
352
- encodeJson: () => {
353
- throw new Error(ARKTYPE_JSON_RUNTIME_DISPATCH_ERROR);
354
- },
355
- decodeJson: () => {
356
- throw new Error(ARKTYPE_JSON_RUNTIME_DISPATCH_ERROR);
357
- },
358
- renderOutputType: renderArktypeJsonOutputTypeFromUnknownParams,
359
- };
121
+ TInferred
122
+ > {
123
+ constructor(
124
+ descriptor: ArktypeJsonDescriptor,
125
+ private readonly schema: ArktypeSchemaLike,
126
+ ) {
127
+ super(descriptor);
128
+ }
360
129
 
361
- /**
362
- * Framework-registration descriptor for the arktype-json codec. Registered
363
- * through the SQL runtime's `parameterizedCodecs:` slot. `sql-runtime`'s
364
- * `initializeTypeHelpers` (and per-column walk in
365
- * `buildContractCodecRegistry`) calls `arktypeJsonCodec.factory(typeParams)
366
- * (ctx)` once per `storage.types` instance (or once per inline-typeParams
367
- * column) to materialize the resolved codec carrying the rehydrated
368
- * schema.
369
- *
370
- * Per Phase B of codec-registry-unification, `descriptorFor('arktype/json@1')`
371
- * returns this descriptor and its `traits`/`targetTypes` are the codec-id-
372
- * keyed source of truth — no parallel placeholder on the legacy `codecs:`
373
- * slot is needed (the runtime descriptor ships `codecs: () => createCodecRegistry()`
374
- * — empty).
375
- */
376
- export const arktypeJsonCodec: CodecDescriptor<ArktypeJsonTypeParams> = {
377
- codecId: ARKTYPE_JSON_CODEC_ID,
378
- traits: ['equality'] as const,
379
- targetTypes: [ARKTYPE_JSON_NATIVE_TYPE] as const,
380
- paramsSchema: arktypeJsonParamsSchema,
381
- renderOutputType: renderArktypeJsonOutputType,
382
- factory: (params) => {
130
+ async encode(value: TInferred, _ctx: CodecCallContext): Promise<string> {
131
+ return serializeToJsonSafe(this.schema, value).wire;
132
+ }
133
+
134
+ async decode(wire: string, _ctx: CodecCallContext): Promise<TInferred> {
135
+ return validateSchema<TInferred>(this.schema, JSON.parse(wire));
136
+ }
137
+
138
+ encodeJson(value: TInferred): JsonValue {
139
+ return serializeToJsonSafe(this.schema, value).json;
140
+ }
141
+
142
+ decodeJson(json: JsonValue): TInferred {
143
+ return validateSchema<TInferred>(this.schema, json);
144
+ }
145
+ }
146
+
147
+ const arktypeJsonParamsSchema = type({
148
+ expression: 'string',
149
+ jsonIr: 'object',
150
+ }) satisfies StandardSchemaV1<ArktypeJsonTypeParams>;
151
+
152
+ export class ArktypeJsonDescriptor extends CodecDescriptorImpl<ArktypeJsonTypeParams> {
153
+ override readonly codecId = ARKTYPE_JSON_CODEC_ID;
154
+ override readonly traits = ['equality'] as const;
155
+ override readonly targetTypes = [ARKTYPE_JSON_NATIVE_TYPE] as const;
156
+ override readonly paramsSchema: StandardSchemaV1<ArktypeJsonTypeParams> = arktypeJsonParamsSchema;
157
+ override renderOutputType(params: ArktypeJsonTypeParams): string {
158
+ return renderArktypeJsonOutputType(params);
159
+ }
160
+ override factory(
161
+ params: ArktypeJsonTypeParams,
162
+ ): (ctx: CodecInstanceContext) => ArktypeJsonCodecClass<unknown> {
383
163
  const schema = rehydrateSchema(params.jsonIr);
384
164
  /* c8 ignore start — defensive parity check; not exercised by typical contracts */
385
- // The rehydrated schema's `expression` should match the serialized
386
- // one; diverging means contract.json was hand-edited out from under
387
- // the emit-path renderer. Surface as a soft warning at materialization
388
- // time so the caller knows their emit output may not match the
389
- // runtime schema. The runtime keeps using the schema rehydrated from
390
- // `jsonIr` — that's the lossless source — so the worst case is an
391
- // emit-vs-runtime divergence at a single column, not a runtime
392
- // failure.
393
165
  const rehydratedExpression = (schema as { readonly expression?: unknown }).expression;
394
166
  if (typeof rehydratedExpression === 'string' && rehydratedExpression !== params.expression) {
395
167
  console.warn(
@@ -397,6 +169,49 @@ export const arktypeJsonCodec: CodecDescriptor<ArktypeJsonTypeParams> = {
397
169
  );
398
170
  }
399
171
  /* c8 ignore stop */
400
- return arktypeJsonCodecForSchema<unknown>(schema);
401
- },
402
- };
172
+ return () => new ArktypeJsonCodecClass<unknown>(this, schema);
173
+ }
174
+ }
175
+
176
+ export const arktypeJsonDescriptor = new ArktypeJsonDescriptor();
177
+
178
+ /**
179
+ * Per-codec column helper for `arktype/json@1`. Method-level generic over `S extends Type<unknown>` so the column site preserves the schema's inferred TS type in the resolved codec (`ArktypeJsonCodecClass<S['infer']>`). Bypasses `descriptor.factory` because `S` is only available at the column-author site; constructs the typed codec directly with the closure-captured schema.
180
+ *
181
+ * Eager serialization at this call site captures `expression` (for the emit-path renderer) and `jsonIr` (for runtime rehydration via the descriptor's factory).
182
+ *
183
+ * @throws {Error} if the schema doesn't expose `expression` and `json` fields (i.e. is not an arktype `Type`). Validates the schema shape at the call site so configuration errors surface during contract authoring, not at runtime.
184
+ */
185
+ export function arktypeJsonColumn<S extends Type<unknown>>(
186
+ schema: S,
187
+ ): ColumnSpec<ArktypeJsonCodecClass<S['infer']>, ArktypeJsonTypeParams> {
188
+ if (!isArktypeSchemaLike(schema)) {
189
+ throw new Error(
190
+ typeof schema !== 'function'
191
+ ? 'arktypeJsonColumn(schema) expects a callable arktype Type.'
192
+ : 'arktypeJsonColumn(schema) expects an arktype Type (missing `expression: string`).',
193
+ );
194
+ }
195
+ const jsonIr: unknown = (schema as { readonly json?: unknown }).json;
196
+ if (jsonIr === null || typeof jsonIr !== 'object') {
197
+ throw new Error('arktypeJsonColumn(schema) expects an arktype Type (missing `json` IR).');
198
+ }
199
+ const params: ArktypeJsonTypeParams = { expression: schema.expression, jsonIr };
200
+ return column(
201
+ (_ctx: CodecInstanceContext) =>
202
+ new ArktypeJsonCodecClass<S['infer']>(arktypeJsonDescriptor, schema),
203
+ arktypeJsonDescriptor.codecId,
204
+ params,
205
+ ARKTYPE_JSON_NATIVE_TYPE,
206
+ );
207
+ }
208
+
209
+ arktypeJsonColumn satisfies ColumnHelperFor<ArktypeJsonDescriptor>;
210
+ // Note: `ColumnHelperForStrict` is intentionally not applied — `Codec` is invariant in `TInput` (encode contravariant, decode covariant), so `ArktypeJsonCodecClass<S['infer']>` is not assignable to `ArktypeJsonCodecClass<unknown>` (the descriptor.factory return). `expectTypeOf` tests cover the literal-preservation property strict satisfies would otherwise enforce.
211
+
212
+ /**
213
+ * Codec instance returned by `arktypeJsonColumn(schema).codecFactory(ctx)` and by `arktypeJsonDescriptor.factory(typeParams)(ctx)`. The `TInferred` slot carries the arktype schema's inferred output type at the column-author site; descriptor-side factories erase to `unknown`.
214
+ */
215
+ export type ArktypeJsonCodec<TInferred> = ArktypeJsonCodecClass<TInferred>;
216
+
217
+ export const codecDescriptors: readonly AnyCodecDescriptor[] = [arktypeJsonDescriptor];
@@ -1,24 +1,14 @@
1
1
  /**
2
2
  * arktype-json pack metadata.
3
3
  *
4
- * The pack metadata is the framework-composition entry point: control-
5
- * stack assembly reads `types.codecTypes.import` to thread the type-side
6
- * imports into emitted `contract.d.ts`, and `types.storage` declares the
7
- * codec id's storage backing (`jsonb` on Postgres).
4
+ * The pack metadata is the framework-composition entry point: control-stack assembly reads `types.codecTypes.import` to thread the type-side imports into emitted `contract.d.ts`, and `types.storage` declares the codec id's storage backing (`jsonb` on Postgres).
8
5
  *
9
- * Per Phase B of codec-registry-unification, runtime materialization
10
- * flows through the unified descriptor map (`arktypeJsonCodec`
11
- * parameterized descriptor), not through the legacy runtime codec
12
- * lookup. This metadata still carries an emit-only `Codec` instance
13
- * (`arktypeJsonEmitCodec`) under `codecInstances` so the framework
14
- * emitter's codec-id-keyed `CodecLookup` can resolve `renderOutputType`
15
- * at emit time — that shim retires when the emit path consults the
16
- * descriptor map directly (TML-2357). Control-stack consumers read
17
- * codec metadata from `descriptorFor('arktype/json@1')`.
6
+ * Per TML-2357 runtime materialization flows through the unified descriptor map (`arktypeJsonDescriptor`) and the emit path consults `descriptorFor('arktype/json@1').renderOutputType` directly — no per-library "emit-only Codec" stub.
18
7
  */
19
8
 
20
9
  import type { CodecTypes } from '../types/codec-types';
21
- import { ARKTYPE_JSON_CODEC_ID, arktypeJsonEmitCodec } from './arktype-json-codec';
10
+ import { ARKTYPE_JSON_CODEC_ID } from './arktype-json-codec';
11
+ import { arktypeJsonCodecRegistry } from './registry';
22
12
 
23
13
  const arktypeJsonPackMetaBase = {
24
14
  kind: 'extension',
@@ -29,13 +19,7 @@ const arktypeJsonPackMetaBase = {
29
19
  capabilities: {},
30
20
  types: {
31
21
  codecTypes: {
32
- // The emitter's `CodecLookup` is the codec-id-keyed source of
33
- // truth for `renderOutputType` at the framework emit-path
34
- // boundary. We thread an emit-only `Codec` instance carrying the
35
- // `renderOutputType` here so the lookup resolves; runtime
36
- // materialization goes through the unified descriptor's
37
- // `factory: (P) => (CodecInstanceContext) => Codec`, never through this shim.
38
- codecInstances: [arktypeJsonEmitCodec],
22
+ codecDescriptors: Array.from(arktypeJsonCodecRegistry.values()),
39
23
  import: {
40
24
  package: '@prisma-next/extension-arktype-json/codec-types',
41
25
  named: 'CodecTypes',
@@ -54,9 +38,7 @@ const arktypeJsonPackMetaBase = {
54
38
  } as const;
55
39
 
56
40
  /**
57
- * Public pack metadata. The phantom `__codecTypes` field threads the
58
- * codec-types map's literal type into the pack ref so contract-builder
59
- * generics can pick it up; it is never accessed at runtime.
41
+ * Public pack metadata. The phantom `__codecTypes` field threads the codec-types map's literal type into the pack ref so contract-builder generics can pick it up; it is never accessed at runtime.
60
42
  */
61
43
  export const arktypeJsonPackMeta: typeof arktypeJsonPackMetaBase & {
62
44
  readonly __codecTypes?: CodecTypes;
@@ -0,0 +1,11 @@
1
+ import { buildCodecDescriptorRegistry } from '@prisma-next/sql-relational-core/codec-descriptor-registry';
2
+ import type { CodecDescriptorRegistry } from '@prisma-next/sql-relational-core/query-lane-context';
3
+ import { codecDescriptors } from './arktype-json-codec';
4
+
5
+ /**
6
+ * Registry of every codec descriptor shipped by `@prisma-next/extension-arktype-json`.
7
+ *
8
+ * Public consumer surface for the arktype-json codec set. Currently a single entry (`arktype/json@1`); the registry shape stays consistent with the other codec-shipping packages so consumers don't need to special-case extensions. See ADR 208.
9
+ */
10
+ export const arktypeJsonCodecRegistry: CodecDescriptorRegistry =
11
+ buildCodecDescriptorRegistry(codecDescriptors);
@@ -1,6 +1,11 @@
1
- export type { ArktypeJsonCodec, ArktypeJsonTypeParams } from '../core/arktype-json-codec';
1
+ export type {
2
+ ArktypeJsonCodec,
3
+ ArktypeJsonDescriptor,
4
+ ArktypeJsonTypeParams,
5
+ } from '../core/arktype-json-codec';
2
6
  export {
3
7
  ARKTYPE_JSON_CODEC_ID,
4
8
  ARKTYPE_JSON_NATIVE_TYPE,
5
- arktypeJsonCodec,
9
+ arktypeJsonColumn,
6
10
  } from '../core/arktype-json-codec';
11
+ export { arktypeJsonCodecRegistry } from '../core/registry';
@@ -1,2 +1,2 @@
1
1
  export type { ArktypeJsonCodec, ArktypeJsonTypeParams } from '../core/arktype-json-codec';
2
- export { arktypeJson } from '../core/arktype-json-codec';
2
+ export { arktypeJsonColumn as arktypeJson } from '../core/arktype-json-codec';