@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.
- package/ARCHITECTURE.md +227 -0
- package/README.md +650 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1221 -0
- package/docs/DB-CLIENT.md +882 -0
- package/docs/FLOW-PEN.md +1033 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +778 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1092 -0
- package/docs/QUERY-PEN.md +1724 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +251 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +255 -0
- package/src/app/index.js +20 -0
- package/src/app/patch.js +277 -0
- package/src/app/sub.js +106 -0
- package/src/async.js +377 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +48 -11
- package/src/contract/define.js +282 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +338 -0
- package/src/db/handle.js +89 -0
- package/src/db/include.js +351 -0
- package/src/db/index.js +24 -0
- package/src/db/ledger.js +195 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +130 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +78 -6
- package/src/expression.js +463 -36
- package/src/federate.js +531 -0
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +316 -0
- package/src/flow/fsm.js +323 -0
- package/src/flow/index.js +22 -0
- package/src/forms/index.js +43 -0
- package/src/forms/rules.js +170 -0
- package/src/forms/submit.js +177 -0
- package/src/index.js +5 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +202 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +318 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +244 -0
- package/src/model/collection.js +273 -0
- package/src/model/define.js +125 -0
- package/src/model/entity.js +307 -0
- package/src/model/index.js +47 -0
- package/src/model/relation.js +85 -0
- package/src/provider.js +137 -20
- package/src/schema/brand.js +31 -0
- package/src/schema/builders.js +526 -0
- package/src/schema/check.js +29 -0
- package/src/schema/emit.js +394 -0
- package/src/schema/factories.js +239 -0
- package/src/schema/index.js +37 -0
- package/src/schema-of.js +24 -0
- package/src/sequence.js +233 -103
- package/src/sources.js +10 -3
- package/types/app.d.ts +293 -0
- package/types/contract.d.ts +468 -0
- package/types/db.d.ts +359 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +296 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +526 -0
- package/types/schema.d.ts +494 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `@jarenjs/linq/forms` — the form by code. The schema pen's every
|
|
4
|
+
* name, rebuilt from SUBCLASSES that carry the `x-form` vocabulary
|
|
5
|
+
* (`form({ visible, enabled, assert, computed, message })`), plus
|
|
6
|
+
* `assertOnSubmit()`, the one call that answers the same rules' Layer-3
|
|
7
|
+
* `$query` twin. A rule is an ANNOTATION: the document a form pen writes
|
|
8
|
+
* validates exactly as the schema pen's does, and `buildFormModel`,
|
|
9
|
+
* `compileFormRules` and `evaluateFormRules` read the rules off it.
|
|
10
|
+
* Nothing here imports `@jarenjs/forms`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
SchemaBuilder, StringBuilder, NumberBuilder, ArrayBuilder, TupleBuilder,
|
|
15
|
+
ObjectBuilder, WhenBuilder, NeverBuilder,
|
|
16
|
+
} from '../schema/builders.js';
|
|
17
|
+
import { createFactories } from '../schema/factories.js';
|
|
18
|
+
import { withForm } from './rules.js';
|
|
19
|
+
|
|
20
|
+
/** The rule-aware classes: new classes, one mixin, no patched prototype. */
|
|
21
|
+
export const FormBuilder = withForm(SchemaBuilder);
|
|
22
|
+
export const FormStringBuilder = withForm(StringBuilder);
|
|
23
|
+
export const FormNumberBuilder = withForm(NumberBuilder);
|
|
24
|
+
export const FormArrayBuilder = withForm(ArrayBuilder);
|
|
25
|
+
export const FormTupleBuilder = withForm(TupleBuilder);
|
|
26
|
+
export const FormObjectBuilder = withForm(ObjectBuilder);
|
|
27
|
+
export const FormWhenBuilder = withForm(WhenBuilder);
|
|
28
|
+
export const FormNeverBuilder = withForm(NeverBuilder);
|
|
29
|
+
|
|
30
|
+
export const {
|
|
31
|
+
string, number, integer, boolean, nil, literal, enumOf,
|
|
32
|
+
object, array, tuple, record, union, discriminated, intersection,
|
|
33
|
+
named, ref, lazy, any, never, when, from, document,
|
|
34
|
+
datetime, date, time, duration,
|
|
35
|
+
} = /** @type {any} */ (createFactories({
|
|
36
|
+
Base: FormBuilder, String: FormStringBuilder, Number: FormNumberBuilder,
|
|
37
|
+
Array: FormArrayBuilder, Tuple: FormTupleBuilder, Object: FormObjectBuilder,
|
|
38
|
+
When: FormWhenBuilder, Never: FormNeverBuilder,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
export { withForm } from './rules.js';
|
|
42
|
+
export { assertOnSubmit } from './submit.js';
|
|
43
|
+
export { isSchemaBuilder, schemaOf, SCHEMA_BUILDER } from '../schema/brand.js';
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The `x-form` vocabulary as one builder method: `withForm(Base)`,
|
|
4
|
+
* a mixin applied to every schema-pen class at module scope, so
|
|
5
|
+
* `./forms` is a set of NEW classes (never a patched prototype) on which
|
|
6
|
+
* any member can carry a cross-field rule.
|
|
7
|
+
*
|
|
8
|
+
* `form({ visible, enabled, assert, computed, message })` writes the one
|
|
9
|
+
* `x-form` annotation the forms README's layer 2 specifies. The three
|
|
10
|
+
* predicates and `computed` are CALLBACKS captured over the rule
|
|
11
|
+
* context, never paths typed as strings: `$` is the whole form document
|
|
12
|
+
* (cross-field is the point), and `value`/`pointer` are the two
|
|
13
|
+
* externals `compileFormRules` binds per evaluation — so the callback
|
|
14
|
+
* receives one context object spelling all three, `c.root`, `c.value`
|
|
15
|
+
* and `c.pointer`, and any other name is `JL0104` here rather than a
|
|
16
|
+
* compile error from forms naming the same two.
|
|
17
|
+
*
|
|
18
|
+
* What the pen refuses is what the reader would refuse and the builder
|
|
19
|
+
* can already see: a member `x-form` does not define, and `preview` —
|
|
20
|
+
* which is not an authored member at all but the format registry's own
|
|
21
|
+
* hint (`getFormatInfo(format).preview`), so spelling it here would
|
|
22
|
+
* write a keyword nothing reads.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { LinqBuildError } from '../errors.js';
|
|
26
|
+
import { captureQuery } from '../capture-root.js';
|
|
27
|
+
import { describeValue, requireJson } from '../json-boundary.js';
|
|
28
|
+
|
|
29
|
+
/** The keyword every rule method writes. */
|
|
30
|
+
export const KEYWORD = 'x-form';
|
|
31
|
+
|
|
32
|
+
/** The members `x-form` defines, in the order the README's table lists them. */
|
|
33
|
+
const RULE_MEMBERS = Object.freeze(['visible', 'enabled', 'assert', 'computed', 'message']);
|
|
34
|
+
|
|
35
|
+
/** The members captured as queries over the rule context. */
|
|
36
|
+
const QUERY_MEMBERS = Object.freeze(['visible', 'enabled', 'assert', 'computed']);
|
|
37
|
+
|
|
38
|
+
/** The two externals `compileFormRules` binds, beside the document at `$`. */
|
|
39
|
+
const EXTERNALS = Object.freeze(['value', 'pointer']);
|
|
40
|
+
|
|
41
|
+
/** Where an unbound name could have come from, appended to `JL0104`. */
|
|
42
|
+
const ADVICE = () => ' — the document being edited is the context\'s root (c.root), the '
|
|
43
|
+
+ 'field\'s own value c.value and its pointer c.pointer';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The rule context: one object over the capture's document root and its
|
|
47
|
+
* two externals. `root` is the value proxy (`$`), so `c.root.company`
|
|
48
|
+
* writes `"$.company"`; anything the evaluator does not bind falls
|
|
49
|
+
* through to the externals proxy, which refuses it by name.
|
|
50
|
+
* @param {any} doc - the `$` proxy
|
|
51
|
+
* @param {any} externals - the externals proxy (`value`, `pointer`)
|
|
52
|
+
* @returns {any}
|
|
53
|
+
*/
|
|
54
|
+
function contextProxy(doc, externals) {
|
|
55
|
+
return new Proxy(Object.freeze({}), {
|
|
56
|
+
get(_target, prop) {
|
|
57
|
+
if (prop === 'root') return doc;
|
|
58
|
+
if (typeof prop === 'symbol') return undefined;
|
|
59
|
+
return externals[prop];
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* One query-valued rule member: a callback captured over the context, or
|
|
66
|
+
* a query document written by hand, copied.
|
|
67
|
+
* @param {string} member - `visible`, `enabled`, `assert` or `computed`
|
|
68
|
+
* @param {any} value
|
|
69
|
+
* @returns {any} the query document (plain JSON)
|
|
70
|
+
*/
|
|
71
|
+
function ruleMember(member, value) {
|
|
72
|
+
if (typeof value !== 'function') {
|
|
73
|
+
return JSON.parse(JSON.stringify(requireJson(value, `form() ${member}`)));
|
|
74
|
+
}
|
|
75
|
+
return captureQuery(`form() ${member}`, EXTERNALS,
|
|
76
|
+
(doc, externals) => value(contextProxy(doc, externals)),
|
|
77
|
+
{ advice: ADVICE, fold: false });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The `message` member: a plain string (an inline template) or a
|
|
82
|
+
* MessageSpec object, verbatim.
|
|
83
|
+
* @param {any} value
|
|
84
|
+
* @returns {any}
|
|
85
|
+
*/
|
|
86
|
+
function readMessage(value) {
|
|
87
|
+
if (typeof value === 'string') return value;
|
|
88
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
89
|
+
throw new LinqBuildError('JL0101',
|
|
90
|
+
'form() message is an inline string or a MessageSpec { $msgid?, message?, params? }, '
|
|
91
|
+
+ `got ${describeValue(value)}`, '/message');
|
|
92
|
+
}
|
|
93
|
+
if (value.$msgid === undefined && value.message === undefined) {
|
|
94
|
+
throw new LinqBuildError('JL0101',
|
|
95
|
+
"form() message as a MessageSpec needs '$msgid' and/or 'message'", '/message');
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(JSON.stringify(requireJson(value, 'form() message')));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The mixin: a subclass of `Base` carrying `form()`.
|
|
102
|
+
* @template {new (state: any) => any} B
|
|
103
|
+
* @param {B} Base - a schema-pen builder class
|
|
104
|
+
* @returns {B} a new class, `Base` plus the vocabulary
|
|
105
|
+
*/
|
|
106
|
+
export function withForm(Base) {
|
|
107
|
+
return class extends Base {
|
|
108
|
+
/**
|
|
109
|
+
* One `x-form` annotation (the forms README, layer 2): cross-field
|
|
110
|
+
* visibility, enablement, a preemptive assertion, a derived value
|
|
111
|
+
* and the message an assertion failure renders. A rule is an
|
|
112
|
+
* ANNOTATION — it never changes what the schema validates, so
|
|
113
|
+
* `additionalProperties` and every other keyword stay exactly what
|
|
114
|
+
* the schema pen wrote.
|
|
115
|
+
*
|
|
116
|
+
* @param {{ visible?: any, enabled?: any, assert?: any, computed?: any, message?: any }} spec
|
|
117
|
+
* @returns {this}
|
|
118
|
+
* @throws {LinqBuildError} `JL0101` a member `x-form` does not define,
|
|
119
|
+
* or a message that is neither a string nor a MessageSpec;
|
|
120
|
+
* `JL0102` `preview`, which the format registry derives from the
|
|
121
|
+
* field's own `format`; `JL0104` a name the rule context does not bind
|
|
122
|
+
* @example
|
|
123
|
+
* s.string().form({ visible: (c) => c.root.company.ne('') });
|
|
124
|
+
* s.number().form({ computed: (c) => c.root.lines.all().amount.sum() });
|
|
125
|
+
*/
|
|
126
|
+
form(spec) {
|
|
127
|
+
if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
128
|
+
throw new LinqBuildError('JL0101',
|
|
129
|
+
'form() takes { visible?, enabled?, assert?, computed?, message? }, got '
|
|
130
|
+
+ describeValue(spec));
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(spec)) {
|
|
133
|
+
if (key === 'preview') {
|
|
134
|
+
throw new LinqBuildError('JL0102',
|
|
135
|
+
"form() cannot write 'preview' — a field's preview hint is DERIVED from its "
|
|
136
|
+
+ 'format by the registry (getFormatInfo(format).preview), never authored, and '
|
|
137
|
+
+ 'buildFormModel reads it from there; spell the format instead, and a host '
|
|
138
|
+
+ 'that understands the hint draws it beside the control', '/preview');
|
|
139
|
+
}
|
|
140
|
+
if (!RULE_MEMBERS.includes(key)) {
|
|
141
|
+
throw new LinqBuildError('JL0101',
|
|
142
|
+
`form() does not take '${key}' — x-form defines ${RULE_MEMBERS.join(', ')}`,
|
|
143
|
+
`/${key}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const current = this.annotation(KEYWORD) ?? {};
|
|
147
|
+
const rules = { ...current };
|
|
148
|
+
for (const member of QUERY_MEMBERS) {
|
|
149
|
+
if (spec[member] !== undefined) rules[member] = ruleMember(member, spec[member]);
|
|
150
|
+
}
|
|
151
|
+
if (spec.message !== undefined) rules.message = readMessage(spec.message);
|
|
152
|
+
// the README's own member order, whatever order the caller wrote
|
|
153
|
+
const ordered = {};
|
|
154
|
+
for (const member of RULE_MEMBERS) {
|
|
155
|
+
if (rules[member] !== undefined) ordered[member] = rules[member];
|
|
156
|
+
}
|
|
157
|
+
return this.annotate(KEYWORD, Object.freeze(ordered));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** As in the schema pen, but `x-form` is owned here. @param {Record<string, any>} annotations */
|
|
161
|
+
meta(annotations) {
|
|
162
|
+
if (annotations !== null && typeof annotations === 'object' && KEYWORD in annotations) {
|
|
163
|
+
throw new LinqBuildError('JL0104',
|
|
164
|
+
`meta() cannot write '${KEYWORD}' — the forms pen owns that keyword; spell it `
|
|
165
|
+
+ 'through form()');
|
|
166
|
+
}
|
|
167
|
+
return super.meta(annotations);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `assertOnSubmit()` — the second spelling of a rule written once.
|
|
4
|
+
* An `x-form.assert` is evaluated per keystroke by `evaluateFormRules`;
|
|
5
|
+
* the same constraint is authoritative at submit through the validator's
|
|
6
|
+
* `$query` keyword, and the forms README's layer 3 is the transform that
|
|
7
|
+
* copies one into the other. The pen offers it as ONE call over the
|
|
8
|
+
* document it wrote, so an author spells the rule once and asks for both
|
|
9
|
+
* documents.
|
|
10
|
+
*
|
|
11
|
+
* The rules the copy keeps are the README's, and each is a place a naive
|
|
12
|
+
* copy went wrong: an absent field binds `null` (through `$default`), an
|
|
13
|
+
* item-template assert quantifies over the ELEMENTS rather than the
|
|
14
|
+
* selected leaves, and an assert on a field that also declares `visible`
|
|
15
|
+
* is guarded by it so it holds vacuously while the field is hidden. The
|
|
16
|
+
* branches land on the ROOT, where `$` is the instance root the rule
|
|
17
|
+
* context expects; the message travels with them so submit renders the
|
|
18
|
+
* same text the keystroke path does.
|
|
19
|
+
*
|
|
20
|
+
* Nothing here imports `@jarenjs/forms`: the transform reads the emitted
|
|
21
|
+
* document, and the pen test pins it equal to
|
|
22
|
+
* `formRulesToQueryAssertions` over the corpus.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { deepFreeze } from '@jarenjs/core/object';
|
|
26
|
+
|
|
27
|
+
import { LinqBuildError } from '../errors.js';
|
|
28
|
+
import { isSchemaBuilder, schemaOf } from '../schema/brand.js';
|
|
29
|
+
import { describeValue } from '../json-boundary.js';
|
|
30
|
+
import { KEYWORD } from './rules.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The loop-variable prefix for element quantification — the same one the
|
|
34
|
+
* forms transform uses, so the two documents are byte-equal.
|
|
35
|
+
*/
|
|
36
|
+
const ITEM_VAR = '_item';
|
|
37
|
+
|
|
38
|
+
/** RFC 6901: `~` and `/` escape inside a pointer segment. @param {string} key */
|
|
39
|
+
const escapePointer = (key) => key.replaceAll('~', '~0').replaceAll('/', '~1');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The RFC 9535 name selector `['…']` for one member name.
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @returns {string}
|
|
45
|
+
*/
|
|
46
|
+
function nameSelector(key) {
|
|
47
|
+
let out = "['";
|
|
48
|
+
for (const ch of key) {
|
|
49
|
+
const code = /** @type {number} */ (ch.codePointAt(0));
|
|
50
|
+
if (ch === '\\' || ch === "'") out += `\\${ch}`;
|
|
51
|
+
else if (code < 0x20) out += `\\u${code.toString(16).padStart(4, '0')}`;
|
|
52
|
+
else out += ch;
|
|
53
|
+
}
|
|
54
|
+
return `${out}']`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A new chunk list with `selector` appended to its last chunk. */
|
|
58
|
+
function extendChunks(chunks, selector) {
|
|
59
|
+
const next = chunks.slice();
|
|
60
|
+
next[next.length - 1] += selector;
|
|
61
|
+
return next;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The `$query` document for one assert, from its location expressed as
|
|
66
|
+
* path chunks split at each array expansion.
|
|
67
|
+
* @param {readonly string[]} chunks
|
|
68
|
+
* @param {any} assert - the authored rule document
|
|
69
|
+
* @param {string} pointer - the field's data pointer
|
|
70
|
+
* @param {any} [visible] - the field's `visible` rule, when it has one
|
|
71
|
+
* @returns {any}
|
|
72
|
+
*/
|
|
73
|
+
function assertQuery(chunks, assert, pointer, visible) {
|
|
74
|
+
const depth = chunks.length - 1;
|
|
75
|
+
const at = (k) => (k === 0 ? chunks[0] : `$${ITEM_VAR}${k - 1}${chunks[k]}`);
|
|
76
|
+
const body = visible === undefined ? assert : { $or: [{ $not: visible }, assert] };
|
|
77
|
+
let query = {
|
|
78
|
+
$let: { value: { $default: [at(depth), { $const: null }] }, pointer: { $const: pointer } },
|
|
79
|
+
$return: body,
|
|
80
|
+
};
|
|
81
|
+
for (let k = depth - 1; k >= 0; k--) {
|
|
82
|
+
query = { $every: { [`${ITEM_VAR}${k}`]: at(k) }, $satisfies: query };
|
|
83
|
+
}
|
|
84
|
+
return query;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The `errorMessage.$query` spec one assert carries: the rule's message
|
|
89
|
+
* with `params` merged over `{ pointer }`, or the catalog default.
|
|
90
|
+
* @param {any} message
|
|
91
|
+
* @param {string} pointer
|
|
92
|
+
* @returns {any}
|
|
93
|
+
*/
|
|
94
|
+
function messageSpec(message, pointer) {
|
|
95
|
+
if (typeof message === 'string') return { message, params: { pointer } };
|
|
96
|
+
if (message !== null && typeof message === 'object' && !Array.isArray(message)) {
|
|
97
|
+
return { ...message, params: { pointer, ...(message.params || {}) } };
|
|
98
|
+
}
|
|
99
|
+
return { $msgid: 'x-form/assert', params: { pointer } };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Walk the structural spine `buildFormModel` walks — `properties`,
|
|
104
|
+
* `prefixItems`, `items`, `allOf` — collecting every `x-form.assert`
|
|
105
|
+
* with its data location. `$ref`s are NOT resolved: a definition's data
|
|
106
|
+
* location depends on its use site.
|
|
107
|
+
* @param {any} schema
|
|
108
|
+
* @param {string} pointer
|
|
109
|
+
* @param {readonly string[]} chunks
|
|
110
|
+
* @param {any[]} out
|
|
111
|
+
*/
|
|
112
|
+
function collect(schema, pointer, chunks, out) {
|
|
113
|
+
if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) return;
|
|
114
|
+
|
|
115
|
+
const rules = schema[KEYWORD];
|
|
116
|
+
if (rules !== null && typeof rules === 'object' && !Array.isArray(rules)
|
|
117
|
+
&& rules.assert !== undefined) {
|
|
118
|
+
out.push({
|
|
119
|
+
query: assertQuery(chunks, rules.assert, pointer, rules.visible),
|
|
120
|
+
pointer,
|
|
121
|
+
message: rules.message,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (schema.properties !== null && typeof schema.properties === 'object') {
|
|
125
|
+
for (const [key, sub] of Object.entries(schema.properties)) {
|
|
126
|
+
collect(sub, `${pointer}/${escapePointer(key)}`, extendChunks(chunks, nameSelector(key)), out);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(schema.prefixItems)) {
|
|
130
|
+
for (let i = 0; i < schema.prefixItems.length; i++) {
|
|
131
|
+
collect(schema.prefixItems[i], `${pointer}/${i}`, extendChunks(chunks, `[${i}]`), out);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (schema.items !== null && typeof schema.items === 'object' && !Array.isArray(schema.items)) {
|
|
135
|
+
collect(schema.items, `${pointer}/-`, [...extendChunks(chunks, '[*]'), ''], out);
|
|
136
|
+
}
|
|
137
|
+
if (Array.isArray(schema.allOf)) {
|
|
138
|
+
for (const branch of schema.allOf) collect(branch, pointer, chunks, out);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The submit twin of a document's `x-form.assert` rules: every assert
|
|
144
|
+
* copied onto the ROOT as its own `allOf` branch
|
|
145
|
+
* `{ $query, errorMessage }`, so the rule an author wrote once for
|
|
146
|
+
* per-keystroke feedback is also what the compiled validator enforces.
|
|
147
|
+
*
|
|
148
|
+
* A document with no assert answers the document itself — there is
|
|
149
|
+
* nothing to copy, and a needless `allOf` would be a second spelling of
|
|
150
|
+
* the same schema.
|
|
151
|
+
*
|
|
152
|
+
* @param {any} root - the root builder, or a document
|
|
153
|
+
* @returns {any} the deep-frozen submit document
|
|
154
|
+
* @throws {LinqBuildError} `JL0101` a value that is not a builder or an
|
|
155
|
+
* object schema
|
|
156
|
+
* @example
|
|
157
|
+
* const schema = s.object({ vatId: s.string().form({ assert: …, message: … }) });
|
|
158
|
+
* new JarenValidator().compile(assertOnSubmit(schema)); // app-side
|
|
159
|
+
*/
|
|
160
|
+
export function assertOnSubmit(root) {
|
|
161
|
+
const schema = isSchemaBuilder(root) ? schemaOf(root) : root;
|
|
162
|
+
if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
|
|
163
|
+
throw new LinqBuildError('JL0101',
|
|
164
|
+
'assertOnSubmit() takes the document\'s root builder or its schema object, got '
|
|
165
|
+
+ describeValue(root));
|
|
166
|
+
}
|
|
167
|
+
/** @type {any[]} */
|
|
168
|
+
const asserts = [];
|
|
169
|
+
collect(schema, '', ['$'], asserts);
|
|
170
|
+
if (asserts.length === 0) return deepFreeze(JSON.parse(JSON.stringify(schema)));
|
|
171
|
+
const branches = asserts.map((entry) => ({
|
|
172
|
+
$query: entry.query,
|
|
173
|
+
errorMessage: { $query: messageSpec(entry.message, entry.pointer) },
|
|
174
|
+
}));
|
|
175
|
+
const allOf = Array.isArray(schema.allOf) ? schema.allOf : [];
|
|
176
|
+
return deepFreeze(JSON.parse(JSON.stringify({ ...schema, allOf: [...allOf, ...branches] })));
|
|
177
|
+
}
|
package/src/index.js
CHANGED
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
* expressions as plain Jaren query documents (QUERY-FORMAT.md),
|
|
5
5
|
* executes deferred over any iterable, and hands the SAME document
|
|
6
6
|
* whole to any provider exposing `execute(document, options)` (D2:
|
|
7
|
-
* contract-level coupling
|
|
8
|
-
*
|
|
7
|
+
* contract-level coupling — the chain imports no provider; the client
|
|
8
|
+
* subpath `./db` is the package's one declared edge, toward the
|
|
9
|
+
* store). The normative surface, mapping table and error codes live
|
|
10
|
+
* in docs/QUERY-PEN.md.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
13
|
export { from, fromDocument, Sequence } from './sequence.js';
|
|
12
14
|
export { fromAsync, AsyncSequence } from './async.js';
|
|
13
15
|
export { createPushQueue } from './sources.js';
|
|
16
|
+
export { federate } from './federate.js';
|
|
14
17
|
export { LinqBuildError, LinqRuntimeError, LINQ_CODES } from './errors.js';
|
package/src/jslt/body.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `body()` — a JSLT rule body captured through the chain's own
|
|
4
|
+
* recording proxy over the shared root capture: `fn(value, x)` with
|
|
5
|
+
* `value` rooted at `$` (the matched value) and `x` the externals —
|
|
6
|
+
* `root` and `path`, which the engine binds on every dispatch
|
|
7
|
+
* (JSLT-FORMAT §8.2), plus the parameters the body declares (§8.1); an
|
|
8
|
+
* undeclared name is `JL0104` at build time, where the fix can be
|
|
9
|
+
* named. `apply()` spells the one body-local operator (§6) and `op()`
|
|
10
|
+
* any registered one (§13): both lift a hand-spelled operator into the
|
|
11
|
+
* capture, and the engine's compiler stays the only judge of what it
|
|
12
|
+
* means. A returned literal is spelled as the format's own constructor
|
|
13
|
+
* (`{ "level": "unknown" }`), never folded into `$const` as the chain
|
|
14
|
+
* spells a constant; a string starting `$` is escaped `$$`. The one
|
|
15
|
+
* rule the pen checks itself is the `[]` idiom of §6.3 —
|
|
16
|
+
* an `apply` as a bare object member is refused HERE because the
|
|
17
|
+
* engine would only refuse it at run time, on the second child.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { liftExpression, toExpression } from '../expression.js';
|
|
21
|
+
import { captureQuery } from '../capture-root.js';
|
|
22
|
+
import { describeValue } from '../json-boundary.js';
|
|
23
|
+
import { LinqBuildError } from '../errors.js';
|
|
24
|
+
import { deepFreeze } from '@jarenjs/core/object';
|
|
25
|
+
|
|
26
|
+
/** The two names the engine binds on every dispatch (§8.2). */
|
|
27
|
+
const RESERVED = ['root', 'path'];
|
|
28
|
+
const NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
29
|
+
|
|
30
|
+
/** The `{ $apply: … }` NODES `apply()` built, by identity. The node, not
|
|
31
|
+
* the proxy over it: a proxy is consumed the moment its value is lowered
|
|
32
|
+
* (into a member, an operand, an argument), while the node it carries is
|
|
33
|
+
* the very object that lands in the emitted document. */
|
|
34
|
+
const APPLY_NODES = new WeakSet();
|
|
35
|
+
/** How many `body()` captures are in progress (captures nest). */
|
|
36
|
+
let bodies = 0;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The declared parameter names, or `JL0101`/`JL0104` naming the problem.
|
|
40
|
+
* @param {any} options
|
|
41
|
+
* @returns {string[]}
|
|
42
|
+
*/
|
|
43
|
+
function readExternals(options) {
|
|
44
|
+
if (options === undefined) return [];
|
|
45
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
46
|
+
throw new LinqBuildError('JL0101',
|
|
47
|
+
`body() options are { externals?: string[] }, got ${describeValue(options)}`);
|
|
48
|
+
}
|
|
49
|
+
for (const key of Object.keys(options)) {
|
|
50
|
+
if (key !== 'externals') throw new LinqBuildError('JL0101', `body() does not take '${key}'`);
|
|
51
|
+
}
|
|
52
|
+
const list = options.externals;
|
|
53
|
+
if (list === undefined) return [];
|
|
54
|
+
if (!Array.isArray(list)) {
|
|
55
|
+
throw new LinqBuildError('JL0101',
|
|
56
|
+
`body() externals is an array of parameter names, got ${describeValue(list)}`);
|
|
57
|
+
}
|
|
58
|
+
/** @type {string[]} */
|
|
59
|
+
const names = [];
|
|
60
|
+
for (const name of list) {
|
|
61
|
+
if (typeof name !== 'string' || !NAME_RE.test(name)) {
|
|
62
|
+
throw new LinqBuildError('JL0101',
|
|
63
|
+
`body() externals are identifiers ('rate'), got ${describeValue(name)}`);
|
|
64
|
+
}
|
|
65
|
+
if (RESERVED.includes(name)) {
|
|
66
|
+
throw new LinqBuildError('JL0104',
|
|
67
|
+
`'${name}' is engine-bound on every dispatch (JSLT-FORMAT §8.2) — it needs no `
|
|
68
|
+
+ 'declaration and is always present on the externals argument');
|
|
69
|
+
}
|
|
70
|
+
if (!names.includes(name)) names.push(name);
|
|
71
|
+
}
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* `an object member takes exactly one value` — the `[]` idiom's refusal.
|
|
77
|
+
* @param {string} key @param {string} at
|
|
78
|
+
*/
|
|
79
|
+
function bareApply(key, at) {
|
|
80
|
+
return new LinqBuildError('JL0102',
|
|
81
|
+
`an object member takes exactly one value — '${key}' holds an apply(), which yields `
|
|
82
|
+
+ 'a SEQUENCE and fails at run time on the second child (JQ2001); wrap the apply in '
|
|
83
|
+
+ `[] (JSLT-FORMAT §6.3: ${key}: [apply(…)])`, at);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The `[]` idiom (§6.3): an object member holds exactly one value, and
|
|
88
|
+
* an `$apply` yields a sequence — so an `apply()` in a member position
|
|
89
|
+
* is refused. Inside an array constructor it is the idiom itself (the
|
|
90
|
+
* brackets splice the sequence); as an operator's operand it is an
|
|
91
|
+
* ordinary expression (`apply(…).count()`); at the top of the body it is
|
|
92
|
+
* the body's own sequence.
|
|
93
|
+
*
|
|
94
|
+
* The walk runs over the CAPTURED DOCUMENT rather than over the value
|
|
95
|
+
* the callback returned, because a callback may hand an `apply()` to an
|
|
96
|
+
* operator (`op('$if', [f, { children: apply(…) }, null])`) and the
|
|
97
|
+
* object literal is lowered before the callback ever returns — the
|
|
98
|
+
* marker is gone, and only the node it left in the document is still
|
|
99
|
+
* there to find. The document is plain JSON at this point and the nodes
|
|
100
|
+
* are the same objects `apply()` built, so identity is the whole test.
|
|
101
|
+
*
|
|
102
|
+
* Member position is read the way `toExpression` writes it, three lines
|
|
103
|
+
* earlier: a map CONSTRUCTOR has no `$`-prefixed key (a data object that
|
|
104
|
+
* does is spelled `$map`, as pairs), and everything else with one is an
|
|
105
|
+
* operator or FLWOR phrase whose operands are expressions.
|
|
106
|
+
* @param {any} node - a node of the captured document
|
|
107
|
+
* @param {string} at - its JSON pointer, for `docPath`
|
|
108
|
+
*/
|
|
109
|
+
function refuseBareApply(node, at) {
|
|
110
|
+
if (node === null || typeof node !== 'object') return;
|
|
111
|
+
if (Array.isArray(node)) {
|
|
112
|
+
for (let i = 0; i < node.length; i++) refuseBareApply(node[i], `${at}/${i}`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const keys = Object.keys(node);
|
|
116
|
+
if (!keys.some((k) => k.charCodeAt(0) === 0x24)) {
|
|
117
|
+
for (const key of keys) {
|
|
118
|
+
if (APPLY_NODES.has(node[key])) throw bareApply(key, `${at}/${key}`);
|
|
119
|
+
refuseBareApply(node[key], `${at}/${key}`);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// `$map` is the one phrase whose operands ARE members: it spells a data
|
|
124
|
+
// object with `$`-prefixed keys as [key, value] pairs (`toExpression`).
|
|
125
|
+
if (keys.length === 1 && keys[0] === '$map' && Array.isArray(node.$map)) {
|
|
126
|
+
node.$map.forEach((pair, i) => {
|
|
127
|
+
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
128
|
+
refuseBareApply(pair, `${at}/$map/${i}`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (APPLY_NODES.has(pair[1])) {
|
|
132
|
+
throw bareApply(typeof pair[0] === 'string' ? pair[0] : String(i), `${at}/$map/${i}/1`);
|
|
133
|
+
}
|
|
134
|
+
refuseBareApply(pair[1], `${at}/$map/${i}/1`);
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
for (const key of keys) refuseBareApply(node[key], `${at}/${key}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* `{ $apply: selector }` or `{ $apply: [selector, mode] }` — the
|
|
143
|
+
* apply-templates operator (§6), inside a `body()` callback. The
|
|
144
|
+
* selector is an expression (`v.chapters.all()`), a JSONPath string
|
|
145
|
+
* taken verbatim (`'$.chapters[*]'`), or plain data (`[1, 2]` embeds as
|
|
146
|
+
* `$const`); the mode is a literal string naming the target mode (a
|
|
147
|
+
* mode is not an expression, §6.2). The result is an expression: it
|
|
148
|
+
* composes as one (`apply(…).count()`), and as an array element it is
|
|
149
|
+
* the `[]` idiom — as a bare object member it is refused (`JL0102`).
|
|
150
|
+
* @param {any} selector
|
|
151
|
+
* @param {string} [mode]
|
|
152
|
+
* @returns {any} an expression over the `$apply` document
|
|
153
|
+
*/
|
|
154
|
+
export function apply(selector, mode = undefined) {
|
|
155
|
+
if (bodies === 0) {
|
|
156
|
+
throw new LinqBuildError('JL0102',
|
|
157
|
+
'apply() spells $apply, which exists only inside a rule body (JSLT-FORMAT §6.1) — '
|
|
158
|
+
+ 'call it inside body()');
|
|
159
|
+
}
|
|
160
|
+
if (selector === undefined) {
|
|
161
|
+
throw new LinqBuildError('JL0101',
|
|
162
|
+
"apply() takes a selector: a path (v.chapters.all(), '$.chapters[*]') or an expression");
|
|
163
|
+
}
|
|
164
|
+
const doc = typeof selector === 'string' ? selector : toExpression(selector);
|
|
165
|
+
let node;
|
|
166
|
+
if (mode === undefined) node = { $apply: doc };
|
|
167
|
+
else {
|
|
168
|
+
if (typeof mode !== 'string') {
|
|
169
|
+
throw new LinqBuildError('JL0101',
|
|
170
|
+
`apply() takes the target mode as a literal string (a mode is not an expression, `
|
|
171
|
+
+ `JSLT-FORMAT §6.2), got ${describeValue(mode)}`);
|
|
172
|
+
}
|
|
173
|
+
node = { $apply: [doc, mode] };
|
|
174
|
+
}
|
|
175
|
+
APPLY_NODES.add(node);
|
|
176
|
+
return liftExpression(node);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* `{ [name]: operands }` — a registered operator (§13), spelled without
|
|
181
|
+
* judging it: the pen writes the name it is given and the engine's
|
|
182
|
+
* compiler decides (`JQ0002` for a name no registry answers). Works in
|
|
183
|
+
* any capture in progress — a body, or a chain callback over an
|
|
184
|
+
* in-memory source compiled with the same registry.
|
|
185
|
+
* @param {string} name - the operator name, `$`-prefixed (`'$npv'`)
|
|
186
|
+
* @param {any} [operands] - one operand, or an array of them
|
|
187
|
+
* @returns {any} an expression over the operator document
|
|
188
|
+
*/
|
|
189
|
+
export function op(name, operands = []) {
|
|
190
|
+
if (typeof name !== 'string' || name.length < 2 || name.charCodeAt(0) !== 0x24) {
|
|
191
|
+
throw new LinqBuildError('JL0101',
|
|
192
|
+
`op() takes an operator name starting with '$' ('$npv'), got ${describeValue(name)}`);
|
|
193
|
+
}
|
|
194
|
+
const value = Array.isArray(operands) ? operands.map(toExpression) : toExpression(operands);
|
|
195
|
+
return liftExpression({ [name]: value });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Capture one rule body: `fn(value, x)` over the matched value at `$`,
|
|
200
|
+
* with `x.root`, `x.path` and every declared parameter; the result is
|
|
201
|
+
* the body's query document, plain and deep-frozen.
|
|
202
|
+
* @param {(value: any, externals: any) => any} fn
|
|
203
|
+
* @param {{ externals?: readonly string[] }} [options]
|
|
204
|
+
* @returns {any} the body document (plain JSON)
|
|
205
|
+
*/
|
|
206
|
+
export function body(fn, options = undefined) {
|
|
207
|
+
if (typeof fn !== 'function') {
|
|
208
|
+
throw new LinqBuildError('JL0101',
|
|
209
|
+
`body() takes a callback (value, x) => …, got ${describeValue(fn)}`);
|
|
210
|
+
}
|
|
211
|
+
const declared = readExternals(options);
|
|
212
|
+
const advice = (/** @type {string} */ name) =>
|
|
213
|
+
` — a stylesheet parameter is declared first: body(fn, { externals: ['${name}'] })`;
|
|
214
|
+
bodies++;
|
|
215
|
+
try {
|
|
216
|
+
const doc = captureQuery('body()', RESERVED.concat(declared), fn, { advice, fold: false });
|
|
217
|
+
refuseBareApply(doc, '');
|
|
218
|
+
// a body spells a literal as the format's own constructor (never a
|
|
219
|
+
// folded `$const`); the tree may still share a caller's array or
|
|
220
|
+
// object, so it is copied before it is frozen
|
|
221
|
+
return deepFreeze(JSON.parse(JSON.stringify(doc)));
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
bodies--;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file `@jarenjs/linq/jslt` — `$jslt` 0.1 stylesheets by code. Rule
|
|
4
|
+
* bodies are callbacks captured over `$` through the chain's own
|
|
5
|
+
* recording proxy, with `root`/`path` and the declared parameters as
|
|
6
|
+
* typed externals; `apply()` spells the apply-templates operator and
|
|
7
|
+
* `op()` any registered one; `rule()` and `stylesheet()` write the
|
|
8
|
+
* rule object and the envelope of JSLT-FORMAT §2. The document is the
|
|
9
|
+
* deliverable: plain, deep-frozen JSON that `compileJsltStylesheet`
|
|
10
|
+
* takes unchanged; nothing here imports an engine. `body()` is the
|
|
11
|
+
* migration pen's body-capture entry point too (a `jslt` step's body
|
|
12
|
+
* binds the same `root`/`path`); the flow and app pens capture through
|
|
13
|
+
* `captureQuery` directly, because their evaluators bind nothing and a
|
|
14
|
+
* body's two reserved names would be a promise neither engine keeps.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { body, apply, op } from './body.js';
|
|
18
|
+
export { rule, stylesheet } from './rules.js';
|