@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.
- package/README.md +11 -34
- package/dist/arktype-json-codec-Cykol-li.mjs +120 -0
- package/dist/arktype-json-codec-Cykol-li.mjs.map +1 -0
- package/dist/arktype-json-codec-DBfCWQkt.d.mts +58 -0
- package/dist/arktype-json-codec-DBfCWQkt.d.mts.map +1 -0
- package/dist/{codec-types-CH37Yc0C.d.mts → codec-types-5Jy4t54u.d.mts} +1 -1
- package/dist/codec-types-5Jy4t54u.d.mts.map +1 -0
- package/dist/codec-types.d.mts +1 -1
- package/dist/codecs.d.mts +14 -2
- package/dist/codecs.d.mts.map +1 -0
- package/dist/codecs.mjs +3 -2
- package/dist/column-types.d.mts +2 -2
- package/dist/column-types.mjs +2 -2
- package/dist/control.mjs +3 -2
- package/dist/control.mjs.map +1 -1
- package/dist/{pack-meta-DycetSQB.mjs → pack-meta-BaJhoZfD.mjs} +5 -6
- package/dist/pack-meta-BaJhoZfD.mjs.map +1 -0
- package/dist/pack.d.mts +3 -5
- package/dist/pack.d.mts.map +1 -1
- package/dist/pack.mjs +3 -1
- package/dist/registry-DN6MqSGJ.mjs +14 -0
- package/dist/registry-DN6MqSGJ.mjs.map +1 -0
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +4 -22
- package/dist/runtime.mjs.map +1 -1
- package/package.json +9 -8
- package/src/core/arktype-json-codec.ts +143 -328
- package/src/core/pack-meta.ts +6 -24
- package/src/core/registry.ts +11 -0
- package/src/exports/codecs.ts +7 -2
- package/src/exports/column-types.ts +1 -1
- package/src/exports/runtime.ts +4 -19
- package/dist/arktype-json-codec-BbiBmtTK.mjs +0 -224
- package/dist/arktype-json-codec-BbiBmtTK.mjs.map +0 -1
- package/dist/arktype-json-codec-yNv1hzBm.d.mts +0 -92
- package/dist/arktype-json-codec-yNv1hzBm.d.mts.map +0 -1
- package/dist/codec-types-CH37Yc0C.d.mts.map +0 -1
- package/dist/pack-meta-DycetSQB.mjs.map +0 -1
|
@@ -1,395 +1,167 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Arktype-json codec (TML-2357).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 {
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
>
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
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];
|
package/src/core/pack-meta.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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);
|
package/src/exports/codecs.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
export type {
|
|
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
|
-
|
|
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';
|