@jarenjs/linq 0.49.2 → 0.66.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 (79) hide show
  1. package/ARCHITECTURE.md +227 -0
  2. package/README.md +650 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1221 -0
  5. package/docs/DB-CLIENT.md +882 -0
  6. package/docs/FLOW-PEN.md +1033 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +778 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1092 -0
  12. package/docs/QUERY-PEN.md +1724 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +251 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +255 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +377 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +48 -11
  24. package/src/contract/define.js +282 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +338 -0
  28. package/src/db/handle.js +89 -0
  29. package/src/db/include.js +351 -0
  30. package/src/db/index.js +24 -0
  31. package/src/db/ledger.js +195 -0
  32. package/src/db/live.js +43 -0
  33. package/src/db/membership.js +37 -0
  34. package/src/db/open.js +130 -0
  35. package/src/document.js +143 -13
  36. package/src/effect.js +65 -0
  37. package/src/errors.js +78 -6
  38. package/src/expression.js +463 -36
  39. package/src/federate.js +531 -0
  40. package/src/flow/capture.js +33 -0
  41. package/src/flow/dag.js +316 -0
  42. package/src/flow/fsm.js +323 -0
  43. package/src/flow/index.js +22 -0
  44. package/src/forms/index.js +43 -0
  45. package/src/forms/rules.js +170 -0
  46. package/src/forms/submit.js +177 -0
  47. package/src/index.js +5 -2
  48. package/src/jslt/body.js +226 -0
  49. package/src/jslt/index.js +18 -0
  50. package/src/jslt/rules.js +202 -0
  51. package/src/json-boundary.js +90 -0
  52. package/src/migration/define.js +318 -0
  53. package/src/migration/index.js +15 -0
  54. package/src/migration/steps.js +244 -0
  55. package/src/model/collection.js +273 -0
  56. package/src/model/define.js +125 -0
  57. package/src/model/entity.js +307 -0
  58. package/src/model/index.js +47 -0
  59. package/src/model/relation.js +85 -0
  60. package/src/provider.js +137 -20
  61. package/src/schema/brand.js +31 -0
  62. package/src/schema/builders.js +526 -0
  63. package/src/schema/check.js +29 -0
  64. package/src/schema/emit.js +394 -0
  65. package/src/schema/factories.js +239 -0
  66. package/src/schema/index.js +37 -0
  67. package/src/schema-of.js +24 -0
  68. package/src/sequence.js +233 -103
  69. package/src/sources.js +10 -3
  70. package/types/app.d.ts +293 -0
  71. package/types/contract.d.ts +468 -0
  72. package/types/db.d.ts +359 -0
  73. package/types/flow.d.ts +285 -0
  74. package/types/forms.d.ts +253 -0
  75. package/types/index.d.ts +296 -26
  76. package/types/jslt.d.ts +193 -0
  77. package/types/migration.d.ts +201 -0
  78. package/types/model.d.ts +526 -0
  79. package/types/schema.d.ts +494 -0
@@ -0,0 +1,394 @@
1
+ //@ts-check
2
+ /**
3
+ * @file Document assembly: a builder tree in, one standard JSON Schema
4
+ * document out — deep-frozen, `$defs` hoisted, every `$ref` resolved
5
+ * against the definitions reachable from the root. This module reads
6
+ * builder STATE and writes JSON; it imports no engine and constructs no
7
+ * builder, so the pen never learns what a keyword means — the
8
+ * validator's compiler stays the only judge of semantics. It refuses
9
+ * exactly two things a document cannot carry faithfully: a definition
10
+ * spelled twice or referenced but never defined (`JL0103`), and a
11
+ * construct whose emitted form would mean something else (`JL0102`) —
12
+ * closed objects under `allOf`, and a default or coercion in a branch
13
+ * the normalizer never descends.
14
+ */
15
+
16
+ import { cloneJson, deepFreeze, setObjectMember } from '@jarenjs/core/object';
17
+
18
+ import { LinqBuildError } from '../errors.js';
19
+ import { isSchemaBuilder } from './brand.js';
20
+
21
+ /** The `type` keyword a kind carries, where it carries one. */
22
+ const TYPED = Object.freeze({
23
+ __proto__: null,
24
+ string: 'string', number: 'number', integer: 'integer', boolean: 'boolean',
25
+ null: 'null', object: 'object', record: 'object', array: 'array', tuple: 'array',
26
+ });
27
+
28
+ /** The three keywords `compileNormalizer` acts on. */
29
+ const NORMALIZER_KEYS = ['default', 'x-coerce', 'x-trim'];
30
+
31
+ /** A definition whose body is still being emitted (a cycle in progress). */
32
+ const PENDING = Symbol('pending');
33
+
34
+ /** @param {string} key a JSON pointer token, escaped per RFC 6901 */
35
+ const token = (key) => key.replaceAll('~', '~0').replaceAll('/', '~1');
36
+
37
+ /**
38
+ * A hoisting context one or more roots share: every `named()` builder
39
+ * they reach becomes one entry of a single `$defs` block, in
40
+ * first-reference order. A schema document has one root; a contract
41
+ * document's roots are its operations' `input`, `output` and error
42
+ * schemas, so the same walk hoists a whole contract's definitions to
43
+ * its own root.
44
+ * @returns {any}
45
+ */
46
+ export function createHoist() {
47
+ return {
48
+ /** @type {Map<string, any>} definition name → body, in discovery order */
49
+ defs: new Map(),
50
+ /** @type {Map<string, any>} definition name → the builder that owns it */
51
+ owners: new Map(),
52
+ /** @type {Map<string, string>} names demanded by `ref()` → where */
53
+ demanded: new Map(),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Emit one builder into a shared hoisting context (`emitInto(builder,
59
+ * ctx, at)`): the same walk `assemble` runs, with the definitions
60
+ * landing in the caller's context instead of a private one.
61
+ */
62
+ export { emitNode as emitInto };
63
+
64
+ /**
65
+ * The `$defs` block a context collected, or `null` when it collected
66
+ * none; every name a `ref()` demanded must be answered by then.
67
+ * @param {any} ctx
68
+ * @returns {any}
69
+ */
70
+ export function hoistedDefs(ctx) {
71
+ for (const [name, at] of ctx.demanded) {
72
+ if (!ctx.defs.has(name)) {
73
+ throw new LinqBuildError('JL0103',
74
+ `ref('${name}') names no definition in this document — a name is defined by `
75
+ + `named('${name}', …) somewhere the root can reach`, at);
76
+ }
77
+ }
78
+ if (ctx.defs.size === 0) return null;
79
+ const defs = {};
80
+ for (const [name, body] of ctx.defs) setObjectMember(defs, name, body);
81
+ return defs;
82
+ }
83
+
84
+ /**
85
+ * Assemble the document of one builder.
86
+ * @param {any} root - the builder whose document this is
87
+ * @returns {any} the deep-frozen JSON Schema document
88
+ */
89
+ export function assemble(root) {
90
+ const ctx = createHoist();
91
+ const doc = emitNode(root, ctx, '');
92
+ const defs = hoistedDefs(ctx);
93
+ if (defs === null) return deepFreeze(doc);
94
+ const out = {};
95
+ setObjectMember(out, '$defs', defs);
96
+ if (typeof doc === 'boolean') {
97
+ // a boolean root with definitions: nothing references them, but a
98
+ // false root cannot carry them either
99
+ throw new LinqBuildError('JL0102',
100
+ 'a boolean schema cannot carry $defs; name the root instead');
101
+ }
102
+ for (const key of Object.keys(doc)) setObjectMember(out, key, doc[key]);
103
+ return deepFreeze(out);
104
+ }
105
+
106
+ /**
107
+ * Whether a builder, or anything it reaches, carries a keyword the
108
+ * normalizer acts on. Walked over the builder graph rather than over
109
+ * emitted documents so a cycle in progress is no obstacle.
110
+ * @param {any} builder
111
+ * @param {Set<any>} seen
112
+ * @returns {boolean}
113
+ */
114
+ export function reachesNormalizer(builder, seen = new Set()) {
115
+ if (!isSchemaBuilder(builder) || seen.has(builder)) return false;
116
+ seen.add(builder);
117
+ const st = builder.state;
118
+ for (const [key] of st.annotations) {
119
+ if (NORMALIZER_KEYS.includes(key)) return true;
120
+ }
121
+ return children(st).some((child) => reachesNormalizer(child, seen));
122
+ }
123
+
124
+ /** Every builder one state holds directly (lazy thunks resolved). */
125
+ function children(st) {
126
+ switch (st.kind) {
127
+ case 'object':
128
+ return [...st.props.map(([, b]) => b), ...st.patterns.map(([, b]) => b),
129
+ ...(st.names === null ? [] : [st.names])];
130
+ case 'array':
131
+ return st.contains === null ? [st.items] : [st.items, st.contains];
132
+ case 'tuple':
133
+ return st.rest === null ? st.items : [...st.items, st.rest];
134
+ case 'record':
135
+ return [st.values];
136
+ case 'union': case 'discriminated': case 'intersection':
137
+ return st.options;
138
+ case 'named':
139
+ return [st.target];
140
+ case 'lazy':
141
+ return [resolveLazy(st, '')];
142
+ case 'when':
143
+ return [st.cond, st.then, st.else].filter((b) => b !== null);
144
+ default:
145
+ return [];
146
+ }
147
+ }
148
+
149
+ /**
150
+ * The builder a `lazy()` stands for — always a named one, because an
151
+ * anonymous recursion has no `$ref` to spell.
152
+ * @param {any} st
153
+ * @param {string} at
154
+ */
155
+ function resolveLazy(st, at) {
156
+ const target = st.thunk();
157
+ if (!isSchemaBuilder(target)) {
158
+ throw new LinqBuildError('JL0103',
159
+ 'lazy() must return a builder', at);
160
+ }
161
+ if (target.state.kind !== 'named') {
162
+ throw new LinqBuildError('JL0103',
163
+ 'lazy() must return a NAMED builder — a recursion is spelled as a $ref, and a '
164
+ + "$ref needs a definition to point at: lazy(() => Node) where Node = named('Node', …)",
165
+ at);
166
+ }
167
+ return target;
168
+ }
169
+
170
+ /**
171
+ * The kind a builder resolves to through `named`/`lazy` wrappers, or
172
+ * `null` when it cannot be known here (a `ref` by name).
173
+ * @param {any} builder
174
+ * @param {string} at
175
+ * @returns {any} the resolved builder, or null
176
+ */
177
+ function resolve(builder, at) {
178
+ let current = builder;
179
+ for (let hops = 0; hops < 64; hops++) {
180
+ const st = current.state;
181
+ if (st.kind === 'named') current = st.target;
182
+ else if (st.kind === 'lazy') current = resolveLazy(st, at);
183
+ else return current;
184
+ }
185
+ return null;
186
+ }
187
+
188
+ /**
189
+ * Refuse a normalizer keyword under a branch the normalizer never
190
+ * descends (`anyOf`/`oneOf` options, `if`/`then`/`else`, `contains`,
191
+ * `propertyNames`): a default there is never materialized and a
192
+ * coercion never runs, so a document carrying one would promise a
193
+ * normalization that does not happen.
194
+ * @param {any} builder
195
+ * @param {string} where - the keyword, for the message
196
+ * @param {string} at
197
+ */
198
+ function refuseNormalizerUnder(builder, where, at) {
199
+ if (!reachesNormalizer(builder)) return;
200
+ throw new LinqBuildError('JL0102',
201
+ `a default(), coerce() or trim() under ${where} never runs — the normalizer does not `
202
+ + 'descend that branch, so the document would promise a normalization that does not '
203
+ + 'happen; move it to the member that holds the branch, or drop it', at);
204
+ }
205
+
206
+ /**
207
+ * Emit one builder as a schema node, registering definitions as they
208
+ * are discovered.
209
+ * @param {any} builder
210
+ * @param {any} ctx
211
+ * @param {string} at - JSON pointer of this node in the document
212
+ * @returns {any} a plain schema node (object or boolean)
213
+ */
214
+ function emitNode(builder, ctx, at) {
215
+ const st = builder.state;
216
+ let node = emitCore(builder, st, ctx, at);
217
+ if (st.nullable) node = nullableOf(node, st);
218
+ if (st.checks.length > 0) {
219
+ node.$query = st.checks.length === 1 ? st.checks[0] : { $and: st.checks };
220
+ }
221
+ for (const [key, value] of st.annotations) setObjectMember(node, key, value);
222
+ return node;
223
+ }
224
+
225
+ /** Fold `null` into a typed node; wrap an untyped one in `anyOf`. */
226
+ function nullableOf(node, st) {
227
+ if (TYPED[st.kind] !== undefined) {
228
+ node.type = [TYPED[st.kind], 'null'];
229
+ // a typed enum admits null only when the enum lists it
230
+ if (Array.isArray(node.enum) && !node.enum.includes(null)) node.enum = [...node.enum, null];
231
+ return node;
232
+ }
233
+ if (st.kind === 'enum') {
234
+ if (!st.values.includes(null)) node.enum = [...st.values, null];
235
+ return node;
236
+ }
237
+ if (st.kind === 'literal') return { enum: [st.value, null] };
238
+ return { anyOf: [node, { type: 'null' }] };
239
+ }
240
+
241
+ /** The constraint keywords a builder collected, in the order set. */
242
+ function withKeywords(node, st) {
243
+ for (const key of Object.keys(st.keywords)) node[key] = st.keywords[key];
244
+ return node;
245
+ }
246
+
247
+ /**
248
+ * The kind-specific core of a node: `type` and the structural
249
+ * keywords, with the collected constraints after them.
250
+ * @param {any} builder
251
+ * @param {any} st
252
+ * @param {any} ctx
253
+ * @param {string} at
254
+ * @returns {any}
255
+ */
256
+ function emitCore(builder, st, ctx, at) {
257
+ switch (st.kind) {
258
+ case 'string': case 'number': case 'integer': case 'boolean': case 'null':
259
+ return withKeywords({ type: st.kind }, st);
260
+ case 'literal':
261
+ return { const: st.value };
262
+ case 'enum':
263
+ return { enum: st.values };
264
+ case 'any':
265
+ return {};
266
+ case 'never':
267
+ return false;
268
+ case 'raw':
269
+ return cloneJson(st.json);
270
+ case 'object': {
271
+ const node = { type: 'object' };
272
+ if (st.props.length > 0) {
273
+ const properties = {};
274
+ const required = [];
275
+ for (const [key, member] of st.props) {
276
+ setObjectMember(properties, key, emitNode(member, ctx, `${at}/properties/${token(key)}`));
277
+ if (!member.state.optional) required.push(key);
278
+ }
279
+ node.properties = properties;
280
+ if (required.length > 0) node.required = required;
281
+ }
282
+ if (!st.open) node.additionalProperties = false;
283
+ if (st.patterns.length > 0) {
284
+ const patternProperties = {};
285
+ for (const [pattern, member] of st.patterns) {
286
+ setObjectMember(patternProperties, pattern,
287
+ emitNode(member, ctx, `${at}/patternProperties/${token(pattern)}`));
288
+ }
289
+ node.patternProperties = patternProperties;
290
+ }
291
+ if (st.names !== null) {
292
+ refuseNormalizerUnder(st.names, 'propertyNames()', `${at}/propertyNames`);
293
+ node.propertyNames = emitNode(st.names, ctx, `${at}/propertyNames`);
294
+ }
295
+ if (st.dependent !== null) node.dependentRequired = cloneJson(st.dependent);
296
+ return withKeywords(node, st);
297
+ }
298
+ case 'record':
299
+ return withKeywords({
300
+ type: 'object',
301
+ additionalProperties: emitNode(st.values, ctx, `${at}/additionalProperties`),
302
+ }, st);
303
+ case 'array': {
304
+ const node = { type: 'array', items: emitNode(st.items, ctx, `${at}/items`) };
305
+ if (st.contains !== null) {
306
+ refuseNormalizerUnder(st.contains, 'contains()', `${at}/contains`);
307
+ node.contains = emitNode(st.contains, ctx, `${at}/contains`);
308
+ }
309
+ return withKeywords(node, st);
310
+ }
311
+ case 'tuple': {
312
+ const node = {
313
+ type: 'array',
314
+ prefixItems: st.items.map((item, i) => emitNode(item, ctx, `${at}/prefixItems/${i}`)),
315
+ };
316
+ if (st.rest !== null) node.items = emitNode(st.rest, ctx, `${at}/items`);
317
+ node.minItems = st.items.length;
318
+ return withKeywords(node, st);
319
+ }
320
+ case 'union': case 'discriminated': {
321
+ const keyword = st.kind === 'union' ? 'anyOf' : 'oneOf';
322
+ const options = st.options.map((option, i) => {
323
+ refuseNormalizerUnder(option, `${st.kind}()`, `${at}/${keyword}/${i}`);
324
+ return emitNode(option, ctx, `${at}/${keyword}/${i}`);
325
+ });
326
+ return { [keyword]: options };
327
+ }
328
+ case 'intersection': {
329
+ const parts = st.options.map((part, i) => {
330
+ const resolved = resolve(part, `${at}/allOf/${i}`);
331
+ if (resolved !== null && resolved.state.kind === 'object' && !resolved.state.open) {
332
+ throw new LinqBuildError('JL0102',
333
+ 'closed objects do not intersect — under allOf each part rejects the other\'s '
334
+ + 'members, so the document would accept neither; open() the parts, or merge '
335
+ + 'them with extend()', `${at}/allOf/${i}`);
336
+ }
337
+ return emitNode(part, ctx, `${at}/allOf/${i}`);
338
+ });
339
+ return { allOf: parts };
340
+ }
341
+ case 'when': {
342
+ const node = {};
343
+ refuseNormalizerUnder(st.cond, 'when()', `${at}/if`);
344
+ node.if = emitNode(st.cond, ctx, `${at}/if`);
345
+ if (st.then !== null) {
346
+ refuseNormalizerUnder(st.then, 'then()', `${at}/then`);
347
+ node.then = emitNode(st.then, ctx, `${at}/then`);
348
+ }
349
+ if (st.else !== null) {
350
+ refuseNormalizerUnder(st.else, 'else()', `${at}/else`);
351
+ node.else = emitNode(st.else, ctx, `${at}/else`);
352
+ }
353
+ return node;
354
+ }
355
+ case 'named':
356
+ return define(st.name, st.target, ctx, at);
357
+ case 'lazy': {
358
+ const named = resolveLazy(st, at);
359
+ return define(named.state.name, named.state.target, ctx, at);
360
+ }
361
+ case 'ref':
362
+ if (!ctx.demanded.has(st.name)) ctx.demanded.set(st.name, at);
363
+ return { $ref: `#/$defs/${st.name}` };
364
+ /* c8 ignore next 2 -- states are produced by the builders alone */
365
+ default:
366
+ throw new LinqBuildError('JL0102', `unknown builder kind '${st.kind}'`, at);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Register a definition on first discovery (its body emitted in place,
372
+ * so a cycle back to it meets the pending entry and stops) and answer
373
+ * the reference to it. The same target under one name is one
374
+ * definition, however many times it is reached; a different target is
375
+ * a collision.
376
+ * @param {string} name
377
+ * @param {any} target
378
+ * @param {any} ctx
379
+ * @param {string} at
380
+ */
381
+ function define(name, target, ctx, at) {
382
+ const owner = ctx.owners.get(name);
383
+ if (owner === undefined) {
384
+ ctx.owners.set(name, target);
385
+ ctx.defs.set(name, PENDING);
386
+ ctx.defs.set(name, emitNode(target, ctx, `/$defs/${token(name)}`));
387
+ }
388
+ else if (owner !== target) {
389
+ throw new LinqBuildError('JL0103',
390
+ `two distinct builders are named '${name}' in one document — a $defs entry `
391
+ + 'can hold one definition; rename one of them', at);
392
+ }
393
+ return { $ref: `#/$defs/${name}` };
394
+ }
@@ -0,0 +1,239 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The named factory functions of a pen — `string()`, `object()`,
4
+ * `union()`, `document()`, … — built ONCE per set of builder classes.
5
+ * `@jarenjs/linq/schema` calls this with the base classes; a pen that
6
+ * extends the schema pen (`./model`) calls it with its subclasses, so
7
+ * the wiring exists exactly once and no subpath patches another's
8
+ * prototype. Every factory answers an instance of the class it was
9
+ * handed, and `with()` keeps that class through every method.
10
+ */
11
+
12
+ import { LinqBuildError } from '../errors.js';
13
+ import { isSchemaBuilder } from './brand.js';
14
+ import {
15
+ initial, requireJson, requireBuilder, requireString, requireName,
16
+ requireBuilderMap, describeValue,
17
+ } from './builders.js';
18
+
19
+ /**
20
+ * @typedef {object} BuilderClasses
21
+ * @property {any} Base - the class every untyped kind is built from
22
+ * @property {any} String
23
+ * @property {any} Number
24
+ * @property {any} Array
25
+ * @property {any} Tuple
26
+ * @property {any} Object
27
+ * @property {any} When
28
+ * @property {any} Never
29
+ */
30
+
31
+ /**
32
+ * Build the factory functions for one set of builder classes.
33
+ * @param {BuilderClasses} classes
34
+ * @returns {Record<string, Function>} the named factories, frozen
35
+ */
36
+ export function createFactories(classes) {
37
+ const {
38
+ Base: BaseBuilder, String: StringBuilder, Number: NumberBuilder,
39
+ Array: ArrayBuilder, Tuple: TupleBuilder, Object: ObjectBuilder,
40
+ When: WhenBuilder, Never: NeverBuilder,
41
+ } = classes;
42
+
43
+ /** A builder, or a hand-written JSON Schema wrapped as one. @param {any} value @param {string} what */
44
+ function builderOrJson(value, what) {
45
+ if (isSchemaBuilder(value)) return value;
46
+ if (typeof value === 'boolean' || (value !== null && typeof value === 'object' && !Array.isArray(value))) {
47
+ return from(value);
48
+ }
49
+ return requireBuilder(value, what);
50
+ }
51
+
52
+
53
+ /** `{ type: 'string' }`. */
54
+ function string() { return new StringBuilder(initial('string', {})); }
55
+ /** `{ type: 'number' }`. */
56
+ function number() { return new NumberBuilder(initial('number', {})); }
57
+ /** `{ type: 'integer' }`. */
58
+ function integer() { return new NumberBuilder(initial('integer', {})); }
59
+ /** `{ type: 'boolean' }`. */
60
+ function boolean() { return new BaseBuilder(initial('boolean', {})); }
61
+ /** `{ type: 'null' }`. */
62
+ function nil() { return new BaseBuilder(initial('null', {})); }
63
+ /** `{}` — anything. */
64
+ function any() { return new BaseBuilder(initial('any', {})); }
65
+ /** `false` — nothing. */
66
+ function never() { return new NeverBuilder(initial('never', {})); }
67
+
68
+ /** `{ const: value }`. @param {any} value */
69
+ function literal(value) {
70
+ return new BaseBuilder(initial('literal', { value: requireJson(value, 'literal()') }));
71
+ }
72
+
73
+ /** `{ enum: values }`. @param {readonly any[]} values */
74
+ function enumOf(values) {
75
+ if (!Array.isArray(values) || values.length === 0) {
76
+ throw new LinqBuildError('JL0101', 'enumOf() takes a non-empty array of JSON values');
77
+ }
78
+ return new BaseBuilder(initial('enum', {
79
+ values: Object.freeze(values.map((value) => requireJson(value, 'enumOf()'))),
80
+ }));
81
+ }
82
+
83
+ /** A closed object (`additionalProperties: false`) of named members. @param {Record<string, any>} props */
84
+ function object(props) {
85
+ return new ObjectBuilder(initial('object', {
86
+ props: Object.freeze(requireBuilderMap(props, 'object()')),
87
+ open: false,
88
+ patterns: Object.freeze([]),
89
+ names: null,
90
+ dependent: null,
91
+ }));
92
+ }
93
+
94
+ /** `{ type: 'array', items }`. @param {any} items */
95
+ function array(items) {
96
+ return new ArrayBuilder(initial('array', {
97
+ items: requireBuilder(items, 'array()'), contains: null,
98
+ }));
99
+ }
100
+
101
+ /** `{ type: 'array', prefixItems, minItems }`. @param {readonly any[]} items */
102
+ function tuple(items) {
103
+ if (!Array.isArray(items)) {
104
+ throw new LinqBuildError('JL0101', 'tuple() takes an array of builders');
105
+ }
106
+ return new TupleBuilder(initial('tuple', {
107
+ items: Object.freeze(items.map((item, i) => requireBuilder(item, `tuple()[${i}]`))),
108
+ rest: null,
109
+ }));
110
+ }
111
+
112
+ /** `{ type: 'object', additionalProperties: values }`. @param {any} values */
113
+ function record(values) {
114
+ return new BaseBuilder(initial('record', { values: requireBuilder(values, 'record()') }));
115
+ }
116
+
117
+ /** @param {any} options @param {string} what */
118
+ function requireOptions(options, what) {
119
+ if (!Array.isArray(options) || options.length === 0) {
120
+ throw new LinqBuildError('JL0101', `${what} takes a non-empty array of builders or schemas`);
121
+ }
122
+ return Object.freeze(options.map((option, i) => builderOrJson(option, `${what}[${i}]`)));
123
+ }
124
+
125
+ /** `{ anyOf: options }`. @param {readonly any[]} options */
126
+ function union(options) {
127
+ return new BaseBuilder(initial('union', { options: requireOptions(options, 'union()') }));
128
+ }
129
+
130
+ /**
131
+ * `{ oneOf: options }` where every option is an object declaring the
132
+ * discriminator as a `literal()` or `enumOf()` member.
133
+ * @param {string} key
134
+ * @param {readonly any[]} options
135
+ */
136
+ function discriminated(key, options) {
137
+ requireString(key, 'discriminated()');
138
+ const parts = requireOptions(options, 'discriminated()');
139
+ parts.forEach((option, i) => {
140
+ const st = option.state;
141
+ const member = st.kind === 'object' ? st.props.find(([name]) => name === key) : undefined;
142
+ const tag = member === undefined ? null : member[1].state.kind;
143
+ if (tag !== 'literal' && tag !== 'enum') {
144
+ throw new LinqBuildError('JL0102',
145
+ `discriminated('${key}') option ${i} does not declare '${key}' as a literal() or `
146
+ + 'enumOf() member — without the tag on every option the oneOf is not a '
147
+ + 'discriminated union; use union() for an untagged one');
148
+ }
149
+ });
150
+ return new BaseBuilder(initial('discriminated', { key, options: parts }));
151
+ }
152
+
153
+ /** `{ allOf: parts }` — parts that are objects must be `open()`. @param {readonly any[]} parts */
154
+ function intersection(parts) {
155
+ return new BaseBuilder(initial('intersection', { options: requireOptions(parts, 'intersection()') }));
156
+ }
157
+
158
+ /**
159
+ * A definition: hoisted to `$defs` and referenced wherever it is used.
160
+ * @param {string} name
161
+ * @param {any} builder
162
+ */
163
+ function named(name, builder) {
164
+ return new BaseBuilder(initial('named', {
165
+ name: requireName(name, 'named()'), target: requireBuilder(builder, 'named()'),
166
+ }));
167
+ }
168
+
169
+ /** A reference to a definition by name. @param {string} name */
170
+ function ref(name) {
171
+ return new BaseBuilder(initial('ref', { name: requireName(name, 'ref()') }));
172
+ }
173
+
174
+ /** A deferred reference to a NAMED builder — the recursion spelling. @param {() => any} thunk */
175
+ function lazy(thunk) {
176
+ if (typeof thunk !== 'function') {
177
+ throw new LinqBuildError('JL0101', 'lazy() takes a function returning a named builder');
178
+ }
179
+ return new BaseBuilder(initial('lazy', { thunk }));
180
+ }
181
+
182
+ /** `{ if: cond }`, extended by `.then()`/`.else()`. @param {any} cond */
183
+ function when(cond) {
184
+ return new WhenBuilder(initial('when', {
185
+ cond: requireBuilder(cond, 'when()'), then: null, else: null,
186
+ }));
187
+ }
188
+
189
+ /** A hand-written JSON Schema, embedded verbatim. @param {object | boolean} json */
190
+ function from(json) {
191
+ if (typeof json !== 'boolean'
192
+ && (json === null || typeof json !== 'object' || Array.isArray(json))) {
193
+ throw new LinqBuildError('JL0101',
194
+ `from() takes a JSON Schema object or boolean, got ${describeValue(json)}`);
195
+ }
196
+ return new BaseBuilder(initial('raw', { json: requireJson(json, 'from()') }));
197
+ }
198
+
199
+ /** `{ type: 'string', format: 'date-time' }`. */
200
+ function datetime() { return string().format('date-time'); }
201
+ /** `{ type: 'string', format: 'date' }`. */
202
+ function date() { return string().format('date'); }
203
+ /** `{ type: 'string', format: 'time' }`. */
204
+ function time() { return string().format('time'); }
205
+ /** `{ type: 'string', format: 'duration' }`. */
206
+ function duration() { return string().format('duration'); }
207
+
208
+ /** The dialect a standalone document may declare. */
209
+ const DRAFTS = Object.freeze({ '2020-12': 'https://json-schema.org/draft/2020-12/schema' });
210
+
211
+ /**
212
+ * A standalone document: the builder's schema, with `$schema` first when
213
+ * a draft is named. The pen writes the 2020-12 vocabulary and no other.
214
+ * @param {any} root
215
+ * @param {{ draft?: '2020-12' }} [options]
216
+ * @returns {any} the deep-frozen document
217
+ */
218
+ function document(root, options = {}) {
219
+ const schema = requireBuilder(root, 'document()').schema;
220
+ if (options.draft === undefined) return schema;
221
+ const uri = DRAFTS[options.draft];
222
+ if (uri === undefined) {
223
+ throw new LinqBuildError('JL0102',
224
+ `document() writes the 2020-12 vocabulary only; '${options.draft}' is not a draft it `
225
+ + 'can declare');
226
+ }
227
+ if (typeof schema === 'boolean') {
228
+ throw new LinqBuildError('JL0102', 'a boolean schema cannot declare a $schema');
229
+ }
230
+ return Object.freeze({ $schema: uri, ...schema });
231
+ }
232
+
233
+ return Object.freeze({
234
+ string, number, integer, boolean, nil, literal, enumOf,
235
+ object, array, tuple, record, union, discriminated, intersection,
236
+ named, ref, lazy, any, never, when, from, document,
237
+ datetime, date, time, duration,
238
+ });
239
+ }
@@ -0,0 +1,37 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/linq/schema` — JSON Schema by code. Named builders
4
+ * write standard 2020-12 documents (the structural keywords, the
5
+ * constraints and the annotations, each with a method of its own;
6
+ * `$query` through the chain's own capture; `$defs`/`$ref` recursion;
7
+ * the normalizer's predicates — SCHEMA-PEN.md §6.2 lists the
8
+ * twenty-five owned keywords that have no method and are written with
9
+ * `keyword()` or `from()` instead), and the hand-authored
10
+ * declarations beside them carry `Infer<>`/`Input<>`, proven against
11
+ * emit's generated types and the validator's verdicts over one corpus.
12
+ * The document is the deliverable; nothing here imports an engine.
13
+ */
14
+
15
+ import {
16
+ SchemaBuilder, StringBuilder, NumberBuilder, ArrayBuilder, TupleBuilder,
17
+ ObjectBuilder, WhenBuilder, NeverBuilder,
18
+ } from './builders.js';
19
+ import { createFactories } from './factories.js';
20
+
21
+ export const {
22
+ string, number, integer, boolean, nil, literal, enumOf,
23
+ object, array, tuple, record, union, discriminated, intersection,
24
+ named, ref, lazy, any, never, when, from, document,
25
+ datetime, date, time, duration,
26
+ } = /** @type {any} */ (createFactories({
27
+ Base: SchemaBuilder, String: StringBuilder, Number: NumberBuilder,
28
+ Array: ArrayBuilder, Tuple: TupleBuilder, Object: ObjectBuilder,
29
+ When: WhenBuilder, Never: NeverBuilder,
30
+ }));
31
+
32
+ export {
33
+ SchemaBuilder, StringBuilder, NumberBuilder, ArrayBuilder, TupleBuilder,
34
+ ObjectBuilder, WhenBuilder, NeverBuilder, requireJson,
35
+ } from './builders.js';
36
+ export { createFactories } from './factories.js';
37
+ export { isSchemaBuilder, schemaOf, SCHEMA_BUILDER } from './brand.js';
@@ -0,0 +1,24 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The chain's side of the schema-builder brand. `ofType`/`cast`
4
+ * accept a builder from `@jarenjs/linq/schema` where a document was,
5
+ * and take its document — recognised by the registry symbol the pen
6
+ * brands its builders with, never by `toJSON` duck-typing (a data
7
+ * object with a `toJSON` member is a schema, not a builder). The
8
+ * symbol is looked up by key so the chain imports nothing from the
9
+ * pen's directory and a chain-only bundle carries none of it.
10
+ */
11
+
12
+ /** The brand key `@jarenjs/linq/schema` answers `true` under. */
13
+ const SCHEMA_BUILDER = Symbol.for('@jarenjs/linq/schema-builder');
14
+
15
+ /**
16
+ * A builder's document, or the value as given.
17
+ * @param {any} value
18
+ * @returns {any} the JSON Schema document
19
+ */
20
+ export function schemaOf(value) {
21
+ return value !== null && typeof value === 'object' && value[SCHEMA_BUILDER] === true
22
+ ? value.schema
23
+ : value;
24
+ }