@jarenjs/linq 0.49.2 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -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 +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
@@ -0,0 +1,526 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The schema builders — one small immutable class per kind of
4
+ * JSON Schema node. A builder holds frozen STATE (its kind, its
5
+ * children, the constraint keywords it collected, the annotations it
6
+ * carries); every method answers a new builder; `.schema` assembles the
7
+ * document once and memoizes it. Nothing here evaluates a schema: a
8
+ * builder refuses only what it cannot spell (`JL0101`, `JL0102`) or
9
+ * what a document could not carry faithfully (`JL0103`, `JL0104`), and
10
+ * leaves every question of meaning to the validator's compiler.
11
+ *
12
+ * The base class is what a subclass extends — the model pen adds its
13
+ * vocabulary by subclassing through `with()`, never by patching a
14
+ * prototype it imported. The named factory functions (`string()`,
15
+ * `object()`, …) live in `factories.js`, built once per class set, so
16
+ * every subpath constructs its own classes through one implementation.
17
+ */
18
+
19
+ import { LinqBuildError } from '../errors.js';
20
+ import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
21
+ import { SCHEMA_BUILDER, isSchemaBuilder } from './brand.js';
22
+ import { assemble } from './emit.js';
23
+ import { captureCheck } from './check.js';
24
+
25
+ /** Kinds that carry a single scalar `type` — the only nodes `coerceToType` runs on. */
26
+ const SCALARS = new Set(['string', 'number', 'integer', 'boolean', 'null']);
27
+
28
+ /** A `$defs` name that spells as a `$ref` fragment without encoding. */
29
+ const NAME_RE = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
30
+
31
+ /**
32
+ * The keywords the pen writes itself, or that would change what a
33
+ * document asserts: `meta()` refuses them so an annotation is never a
34
+ * back door around a builder.
35
+ *
36
+ * The two kinds are not the same size. 44 of these have a builder method
37
+ * that emits them; the other 25 — `not`, the unevaluated pair,
38
+ * `dependentSchemas`/`dependencies`, the `contains` bounds, the content
39
+ * family, the four format bounds, the identification and dynamic-
40
+ * reference families, `definitions`, `additionalItems` and `$data` — are
41
+ * owned only by the second clause, and are written with `keyword()` or
42
+ * `from()`. SCHEMA-PEN.md §6.2 lists them and `test/linq/schema-pen.test.js`
43
+ * holds that list equal to this set.
44
+ */
45
+ const OWNED = new Set([
46
+ '$schema', '$id', '$ref', '$defs', 'definitions', '$anchor', '$dynamicRef',
47
+ '$dynamicAnchor', '$recursiveRef', '$recursiveAnchor', '$vocabulary', '$query',
48
+ '$data', 'data',
49
+ 'type', 'nullable', 'const', 'enum',
50
+ 'properties', 'required', 'additionalProperties', 'patternProperties',
51
+ 'propertyNames', 'minProperties', 'maxProperties', 'dependentRequired',
52
+ 'dependentSchemas', 'dependencies', 'unevaluatedProperties',
53
+ 'items', 'prefixItems', 'additionalItems', 'contains', 'minContains',
54
+ 'maxContains', 'minItems', 'maxItems', 'uniqueItems', 'unevaluatedItems',
55
+ 'minLength', 'maxLength', 'pattern', 'format',
56
+ 'contentEncoding', 'contentMediaType', 'contentSchema',
57
+ 'minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf',
58
+ 'formatMinimum', 'formatMaximum', 'formatExclusiveMinimum', 'formatExclusiveMaximum',
59
+ 'anyOf', 'oneOf', 'allOf', 'not', 'if', 'then', 'else',
60
+ 'default', 'title', 'description', 'examples', 'errorMessage',
61
+ 'x-coerce', 'x-trim',
62
+ ]);
63
+
64
+ /** The JSON boundary (`JL0101`), shared with every pen. */
65
+ export { describeValue, requireJson } from '../json-boundary.js';
66
+
67
+ /** @param {any} value @param {string} what */
68
+ export function requireBuilder(value, what) {
69
+ if (isSchemaBuilder(value)) return value;
70
+ throw new LinqBuildError('JL0101',
71
+ `${what} takes a schema builder, got ${describeValue(value)} — wrap a hand-written `
72
+ + 'JSON Schema with from()');
73
+ }
74
+
75
+ /** @param {any} value @param {string} what */
76
+ export function requireString(value, what) {
77
+ if (typeof value === 'string') return value;
78
+ throw new LinqBuildError('JL0101', `${what} takes a string, got ${describeValue(value)}`);
79
+ }
80
+
81
+ /** @param {any} value @param {string} what */
82
+ export function requireCount(value, what) {
83
+ if (Number.isInteger(value) && value >= 0) return value;
84
+ throw new LinqBuildError('JL0101',
85
+ `${what} takes a non-negative integer, got ${describeValue(value)}`);
86
+ }
87
+
88
+ /** @param {any} value @param {string} what */
89
+ export function requireNumber(value, what) {
90
+ if (typeof value === 'number' && Number.isFinite(value) && !Object.is(value, -0)) return value;
91
+ throw new LinqBuildError('JL0101', `${what} takes a finite number, got ${describeValue(value)}`);
92
+ }
93
+
94
+ /** @param {any} value @param {string} what */
95
+ export function requireName(value, what) {
96
+ if (typeof value === 'string' && NAME_RE.test(value)) return value;
97
+ throw new LinqBuildError('JL0101',
98
+ `${what} takes a definition name (letters, digits, '_', '.', '-', not starting `
99
+ + `with a digit), got ${describeValue(value)}`);
100
+ }
101
+
102
+ /** @param {any} value @param {string} what @returns {[string, any][]} */
103
+ export function requireBuilderMap(value, what) {
104
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
105
+ throw new LinqBuildError('JL0101',
106
+ `${what} takes a plain object of builders, got ${describeValue(value)}`);
107
+ }
108
+ requireNameMap(value, what);
109
+ return Object.keys(value).map((key) => [key, requireBuilder(value[key], `${what}.${key}`)]);
110
+ }
111
+
112
+ /**
113
+ * The `enum` keyword on a TYPED scalar: every value must be of the
114
+ * builder's own JSON type, or the enum could never be satisfied.
115
+ * @param {any} builder
116
+ * @param {readonly any[]} values
117
+ * @returns {any}
118
+ */
119
+ function typedEnum(builder, values) {
120
+ if (!Array.isArray(values) || values.length === 0) {
121
+ throw new LinqBuildError('JL0101', 'enumOf() takes a non-empty array of values');
122
+ }
123
+ const kind = builder.state.kind;
124
+ for (const value of values) {
125
+ requireJson(value, 'enumOf()');
126
+ const fits = kind === 'string' ? typeof value === 'string'
127
+ : kind === 'integer' ? Number.isInteger(value)
128
+ : typeof value === 'number';
129
+ if (!fits) {
130
+ throw new LinqBuildError('JL0101',
131
+ `enumOf() on a ${kind} takes ${kind} values; ${JSON.stringify(value)} could never `
132
+ + 'satisfy the enum');
133
+ }
134
+ }
135
+ return builder.keyword('enum', Object.freeze(values.slice()));
136
+ }
137
+
138
+ /**
139
+ * Replace or append one annotation, keeping the order annotations were
140
+ * first set in.
141
+ * @param {readonly (readonly [string, any])[]} annotations
142
+ * @param {string} key
143
+ * @param {any} value
144
+ * @returns {readonly (readonly [string, any])[]}
145
+ */
146
+ function annotate(annotations, key, value) {
147
+ const index = annotations.findIndex(([k]) => k === key);
148
+ const next = annotations.slice();
149
+ if (index === -1) next.push(Object.freeze([key, value]));
150
+ else next[index] = Object.freeze([key, value]);
151
+ return Object.freeze(next);
152
+ }
153
+
154
+ /** The state every kind shares. @param {string} kind @param {object} own */
155
+ export function initial(kind, own) {
156
+ return Object.freeze({
157
+ kind,
158
+ keywords: Object.freeze({}),
159
+ annotations: Object.freeze([]),
160
+ checks: Object.freeze([]),
161
+ nullable: false,
162
+ optional: false,
163
+ ...own,
164
+ });
165
+ }
166
+
167
+ /** The immutable base every builder shares. */
168
+ export class SchemaBuilder {
169
+ #state;
170
+ #document;
171
+
172
+ /** @param {any} state - frozen builder state */
173
+ constructor(state) {
174
+ this.#state = state;
175
+ }
176
+
177
+ /** The brand: `true` on every builder, absent on a document. */
178
+ get [SCHEMA_BUILDER]() { return true; }
179
+
180
+ /** The frozen state (kind, children, keywords, annotations). */
181
+ get state() { return this.#state; }
182
+
183
+ /**
184
+ * A new builder of the same class with part of the state replaced —
185
+ * the one way state changes, for this class and for a subclass.
186
+ * @param {object} patch
187
+ * @returns {this}
188
+ */
189
+ with(patch) {
190
+ const Kind = /** @type {any} */ (this.constructor);
191
+ return new Kind(Object.freeze({ ...this.#state, ...patch }));
192
+ }
193
+
194
+ /** The document: assembled once, deep-frozen, `$defs` hoisted. */
195
+ get schema() {
196
+ if (this.#document === undefined) this.#document = assemble(this);
197
+ return this.#document;
198
+ }
199
+
200
+ /** `JSON.stringify(builder)` is the document. */
201
+ toJSON() { return this.schema; }
202
+
203
+ /** As an object member: left out of `required`. */
204
+ optional() { return this.with({ optional: true }); }
205
+
206
+ /** Admit `null`: folded into `type` where there is one, an `anyOf` otherwise. */
207
+ nullable() {
208
+ if (this.#hasAnnotation('x-coerce')) {
209
+ throw new LinqBuildError('JL0102',
210
+ 'a coerced value cannot be nullable — the normalizer coerces only a single-typed '
211
+ + 'scalar, so a nullable coercion would never run; drop coerce() or nullable()');
212
+ }
213
+ return this.with({ nullable: true });
214
+ }
215
+
216
+ /** `default`: materialized by the normalizer's `useDefaults`. @param {any} value */
217
+ default(value) { return this.annotate('default', requireJson(value, 'default()')); }
218
+
219
+ /** `description`. @param {string} text */
220
+ describe(text) { return this.annotate('description', requireString(text, 'describe()')); }
221
+
222
+ /** `title`. @param {string} text */
223
+ title(text) { return this.annotate('title', requireString(text, 'title()')); }
224
+
225
+ /** One more entry of `examples`. @param {any} value */
226
+ example(value) {
227
+ const current = this.annotation('examples') ?? [];
228
+ return this.annotate('examples', [...current, requireJson(value, 'example()')]);
229
+ }
230
+
231
+ /**
232
+ * Annotations written verbatim; a keyword the pen owns is refused.
233
+ * @param {Record<string, any>} annotations
234
+ */
235
+ meta(annotations) {
236
+ if (annotations === null || typeof annotations !== 'object' || Array.isArray(annotations)) {
237
+ throw new LinqBuildError('JL0101',
238
+ `meta() takes a plain object of annotations, got ${describeValue(annotations)}`);
239
+ }
240
+ let next = this;
241
+ for (const key of Object.keys(annotations)) {
242
+ if (OWNED.has(key)) {
243
+ throw new LinqBuildError('JL0104',
244
+ `meta() cannot write '${key}' — the pen owns that keyword; spell it through the `
245
+ + `builder method that emits it, keyword('${key}', value) where no method does, `
246
+ + 'or wrap a hand-written schema with from()');
247
+ }
248
+ next = next.annotate(key, requireJson(annotations[key], `meta().${key}`));
249
+ }
250
+ return next;
251
+ }
252
+
253
+ /** `errorMessage`: the validator's author-supplied message spec. @param {any} spec */
254
+ message(spec) { return this.annotate('errorMessage', requireJson(spec, 'message()')); }
255
+
256
+ /**
257
+ * A cross-field rule as `$query`: a callback captured through the
258
+ * chain's recording proxy (the value at `$`, and `{ root, path }`),
259
+ * or a hand-written query document embedded verbatim.
260
+ * @param {((value: any, externals: any) => any) | object} rule
261
+ */
262
+ check(rule) {
263
+ const query = typeof rule === 'function'
264
+ ? captureCheck(rule)
265
+ : requireJson(rule, 'check()');
266
+ return this.with({ checks: Object.freeze([...this.#state.checks, query]) });
267
+ }
268
+
269
+ /** `x-coerce: true` — the normalizer's per-field coercion predicate. */
270
+ coerce() {
271
+ if (!SCALARS.has(this.#state.kind)) {
272
+ throw new LinqBuildError('JL0102',
273
+ `coerce() applies to a scalar (string, number, integer, boolean, nil); a `
274
+ + `${this.#state.kind} has no single type the normalizer could coerce to`);
275
+ }
276
+ if (this.#state.nullable) {
277
+ throw new LinqBuildError('JL0102',
278
+ 'a nullable value cannot be coerced — the normalizer coerces only a single-typed '
279
+ + 'scalar, so the coercion would never run; drop nullable() or coerce()');
280
+ }
281
+ return this.annotate('x-coerce', true);
282
+ }
283
+
284
+ /** `x-trim: true` — the normalizer's per-field trim predicate. */
285
+ trim() {
286
+ if (this.#state.kind !== 'string') {
287
+ throw new LinqBuildError('JL0102',
288
+ `trim() applies to a string; a ${this.#state.kind} carries no whitespace to trim`);
289
+ }
290
+ return this.annotate('x-trim', true);
291
+ }
292
+
293
+ /**
294
+ * Set one annotation keyword.
295
+ * @param {string} key
296
+ * @param {any} value
297
+ * @returns {this}
298
+ */
299
+ annotate(key, value) {
300
+ return this.with({ annotations: annotate(this.#state.annotations, key, value) });
301
+ }
302
+
303
+ /**
304
+ * One annotation's value, or `undefined` — what a subclass reads
305
+ * before it merges into a keyword it owns.
306
+ * @param {string} key
307
+ * @returns {any}
308
+ */
309
+ annotation(key) {
310
+ const found = this.#state.annotations.find(([k]) => k === key);
311
+ return found === undefined ? undefined : found[1];
312
+ }
313
+
314
+ /** @param {string} key */
315
+ #hasAnnotation(key) {
316
+ return this.#state.annotations.some(([k]) => k === key);
317
+ }
318
+
319
+ /**
320
+ * One constraint keyword, in the order first set.
321
+ * @param {string} key
322
+ * @param {any} value
323
+ * @returns {this}
324
+ */
325
+ keyword(key, value) {
326
+ return this.with({ keywords: Object.freeze({ ...this.#state.keywords, [key]: value }) });
327
+ }
328
+ }
329
+
330
+ /** `{ type: 'string' }` and the string constraints. */
331
+ export class StringBuilder extends SchemaBuilder {
332
+ /** `minLength`. @param {number} n */
333
+ min(n) { return this.keyword('minLength', requireCount(n, 'min()')); }
334
+ /** `maxLength`. @param {number} n */
335
+ max(n) { return this.keyword('maxLength', requireCount(n, 'max()')); }
336
+ /** `minLength` and `maxLength` together. @param {number} n */
337
+ length(n) { return this.min(n).max(n); }
338
+ /**
339
+ * `pattern`: a regular expression source (a `RegExp` without flags is
340
+ * taken by its source — the keyword has no flags to carry).
341
+ * @param {string | RegExp} source
342
+ */
343
+ pattern(source) {
344
+ if (source instanceof RegExp) {
345
+ if (source.flags !== '') {
346
+ throw new LinqBuildError('JL0102',
347
+ `pattern() cannot carry the flags '${source.flags}' — a JSON Schema pattern is a `
348
+ + 'bare regular expression source; spell the flag inside the expression, or drop it');
349
+ }
350
+ return this.keyword('pattern', source.source);
351
+ }
352
+ return this.keyword('pattern', requireString(source, 'pattern()'));
353
+ }
354
+ /** `format`. @param {string} name */
355
+ format(name) { return this.keyword('format', requireString(name, 'format()')); }
356
+ /** `format: 'email'`. */
357
+ email() { return this.format('email'); }
358
+ /** `format: 'uuid'`. */
359
+ uuid() { return this.format('uuid'); }
360
+ /** `format: 'uri'`. */
361
+ uri() { return this.format('uri'); }
362
+ /** `enum` beside `type: 'string'` — a typed enum (a store maps it to a column). @param {readonly string[]} values */
363
+ enumOf(values) { return typedEnum(this, values); }
364
+ }
365
+
366
+ /** `{ type: 'number' | 'integer' }` and the numeric constraints. */
367
+ export class NumberBuilder extends SchemaBuilder {
368
+ /** `minimum`. @param {number} n */
369
+ min(n) { return this.keyword('minimum', requireNumber(n, 'min()')); }
370
+ /** `maximum`. @param {number} n */
371
+ max(n) { return this.keyword('maximum', requireNumber(n, 'max()')); }
372
+ /** `exclusiveMinimum`. @param {number} n */
373
+ gt(n) { return this.keyword('exclusiveMinimum', requireNumber(n, 'gt()')); }
374
+ /** `exclusiveMaximum`. @param {number} n */
375
+ lt(n) { return this.keyword('exclusiveMaximum', requireNumber(n, 'lt()')); }
376
+ /** `multipleOf` (strictly positive). @param {number} n */
377
+ multipleOf(n) {
378
+ if (!(requireNumber(n, 'multipleOf()') > 0)) {
379
+ throw new LinqBuildError('JL0101', `multipleOf() takes a positive number, got ${n}`);
380
+ }
381
+ return this.keyword('multipleOf', n);
382
+ }
383
+ /** `type: 'integer'`. */
384
+ int() { return this.with({ kind: 'integer' }); }
385
+ /** `enum` beside the numeric type — a typed enum. @param {readonly number[]} values */
386
+ enumOf(values) { return typedEnum(this, values); }
387
+ }
388
+
389
+ /** `{ type: 'array', items }` and the array constraints. */
390
+ export class ArrayBuilder extends SchemaBuilder {
391
+ /** `minItems`. @param {number} n */
392
+ min(n) { return this.keyword('minItems', requireCount(n, 'min()')); }
393
+ /** `maxItems`. @param {number} n */
394
+ max(n) { return this.keyword('maxItems', requireCount(n, 'max()')); }
395
+ /** `minItems` and `maxItems` together. @param {number} n */
396
+ length(n) { return this.min(n).max(n); }
397
+ /** `uniqueItems: true`. */
398
+ unique() { return this.keyword('uniqueItems', true); }
399
+ /** `contains`. @param {any} builder */
400
+ contains(builder) { return this.with({ contains: requireBuilder(builder, 'contains()') }); }
401
+ }
402
+
403
+ /** `{ type: 'array', prefixItems }` — every position required, the rest open. */
404
+ export class TupleBuilder extends SchemaBuilder {
405
+ /** `items`: what may follow the positions (`never()` closes the tuple). @param {any} builder */
406
+ rest(builder) { return this.with({ rest: requireBuilder(builder, 'rest()') }); }
407
+ }
408
+
409
+ /** `{ type: 'object', properties, required, additionalProperties: false }`. */
410
+ export class ObjectBuilder extends SchemaBuilder {
411
+ /** Drop `additionalProperties: false`: the object admits other members. */
412
+ open() { return this.with({ open: true }); }
413
+ /** `minProperties`. @param {number} n */
414
+ minProperties(n) { return this.keyword('minProperties', requireCount(n, 'minProperties()')); }
415
+ /** `maxProperties`. @param {number} n */
416
+ maxProperties(n) { return this.keyword('maxProperties', requireCount(n, 'maxProperties()')); }
417
+ /** `patternProperties`. @param {Record<string, any>} map pattern → builder */
418
+ patternProperties(map) {
419
+ return this.with({ patterns: Object.freeze(requireBuilderMap(map, 'patternProperties()')) });
420
+ }
421
+ /** `propertyNames`. @param {any} builder */
422
+ propertyNames(builder) { return this.with({ names: requireBuilder(builder, 'propertyNames()') }); }
423
+ /** `dependentRequired`. @param {Record<string, string[]>} map */
424
+ dependentRequired(map) {
425
+ requireJson(map, 'dependentRequired()');
426
+ if (map === null || typeof map !== 'object' || Array.isArray(map)
427
+ || Object.keys(map).some((key) => !Array.isArray(map[key])
428
+ || map[key].some((name) => typeof name !== 'string'))) {
429
+ throw new LinqBuildError('JL0101',
430
+ 'dependentRequired() takes a plain object mapping a member name to an array of member names');
431
+ }
432
+ return this.with({ dependent: map });
433
+ }
434
+ /** More members (a later spelling of a name replaces the earlier one). @param {Record<string, any>} props */
435
+ extend(props) {
436
+ const added = requireBuilderMap(props, 'extend()');
437
+ const kept = this.state.props.filter(([key]) => !added.some(([k]) => k === key));
438
+ return this.with({ props: Object.freeze([...kept, ...added]) });
439
+ }
440
+ /** Only these members. @param {readonly string[]} keys */
441
+ pick(keys) {
442
+ const names = requireKeys(keys, 'pick()', this.state.props);
443
+ return this.with({ props: Object.freeze(this.state.props.filter(([key]) => names.includes(key))) });
444
+ }
445
+ /** All but these members. @param {readonly string[]} keys */
446
+ omit(keys) {
447
+ const names = requireKeys(keys, 'omit()', this.state.props);
448
+ return this.with({ props: Object.freeze(this.state.props.filter(([key]) => !names.includes(key))) });
449
+ }
450
+ /** Every member optional. */
451
+ partial() {
452
+ return this.with({
453
+ props: Object.freeze(this.state.props.map(([key, member]) => [key, member.optional()])),
454
+ });
455
+ }
456
+ /** These members required again (every member, when no keys are given). @param {readonly string[]} [keys] */
457
+ required(keys) {
458
+ const names = keys === undefined
459
+ ? this.state.props.map(([key]) => key)
460
+ : requireKeys(keys, 'required()', this.state.props);
461
+ return this.with({
462
+ props: Object.freeze(this.state.props.map(([key, member]) =>
463
+ (names.includes(key) ? [key, member.with({ optional: false })] : [key, member]))),
464
+ });
465
+ }
466
+ }
467
+
468
+ /** @param {any} keys @param {string} what @param {readonly any[]} props */
469
+ function requireKeys(keys, what, props) {
470
+ if (!Array.isArray(keys) || keys.some((key) => typeof key !== 'string')) {
471
+ throw new LinqBuildError('JL0101', `${what} takes an array of member names`);
472
+ }
473
+ for (const key of keys) {
474
+ if (!props.some(([name]) => name === key)) {
475
+ throw new LinqBuildError('JL0101', `${what}: '${key}' is not a member of this object`);
476
+ }
477
+ }
478
+ return keys;
479
+ }
480
+
481
+ /** `{ if, then, else }` — a conditional; typed `unknown`, as emit reads it. */
482
+ export class WhenBuilder extends SchemaBuilder {
483
+ /**
484
+ * The `then` branch. (A builder carrying this method is thenable-shaped:
485
+ * an awaited or promise-resolved `when()` reaches here with a function,
486
+ * which is refused by name rather than resolved as a schema.)
487
+ * @param {any} builder
488
+ */
489
+ then(builder) {
490
+ if (typeof builder === 'function') {
491
+ throw new LinqBuildError('JL0101',
492
+ 'a when() builder is not a promise — it was awaited or handed to a promise '
493
+ + 'resolution; keep builders out of async return positions');
494
+ }
495
+ return this.with({ then: requireBuilder(builder, 'then()') });
496
+ }
497
+ /** The `else` branch. @param {any} builder */
498
+ else(builder) { return this.with({ else: requireBuilder(builder, 'else()') }); }
499
+ }
500
+
501
+ /**
502
+ * `false` — the schema nothing satisfies; it carries no keywords.
503
+ *
504
+ * The refusals below are about that boolean document, not about this
505
+ * class: once `nullable()` has been applied the node emitted is
506
+ * `{ anyOf: [false, { type: 'null' }] }`, an object that carries
507
+ * keywords like any other, and `null` reaches a check on it. So both
508
+ * overrides step aside there — which is what makes the second remedy
509
+ * each message names actually work.
510
+ */
511
+ export class NeverBuilder extends SchemaBuilder {
512
+ /** @param {string} key @param {any} value */
513
+ annotate(key, value) {
514
+ if (this.state.nullable) return super.annotate(key, value);
515
+ throw new LinqBuildError('JL0102',
516
+ `never() is the boolean schema false, which carries no '${key}' — annotate the member `
517
+ + 'that holds it, or nullable() it first');
518
+ }
519
+ /** @param {any} rule */
520
+ check(rule) {
521
+ if (this.state.nullable) return super.check(rule);
522
+ throw new LinqBuildError('JL0102',
523
+ 'never() is the boolean schema false; nothing reaches a check on it — check the member '
524
+ + 'that holds it, or nullable() it first');
525
+ }
526
+ }
@@ -0,0 +1,29 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `check()` — a cross-field rule captured into the validator's
4
+ * `$query` keyword through the chain's own recording proxy, over the
5
+ * shared root capture (`src/capture-root.js`): the value proxy is
6
+ * rooted at `$` (the instance the keyword is evaluated at), and the
7
+ * externals proxy answers exactly the two names `compileQuerySchema`
8
+ * binds — `root` and `path` — refusing any other at build time
9
+ * (`JL0104`), earlier than the validator's own compile error and with
10
+ * the same meaning. The same capture, with a different externals list,
11
+ * serves the model pen's `compute()` and the JSLT pen's `body()`.
12
+ */
13
+
14
+ import { captureQuery } from '../capture-root.js';
15
+
16
+ /** The names the validator binds on every `$query` evaluation. */
17
+ const EXTERNALS = ['root', 'path'];
18
+
19
+ export { captureQuery };
20
+
21
+ /**
22
+ * Capture one `check()` rule into a `$query` document: the value proxy
23
+ * (rooted at `$`) and the externals proxy `{ root, path }`.
24
+ * @param {(value: any, externals: any) => any} rule
25
+ * @returns {any} the captured query expression (plain JSON)
26
+ */
27
+ export function captureCheck(rule) {
28
+ return captureQuery('check()', EXTERNALS, rule);
29
+ }