@jarenjs/linq 0.67.0 → 0.72.2

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/src/federate.js CHANGED
@@ -45,6 +45,7 @@
45
45
 
46
46
  import { LinqBuildError, LinqRuntimeError } from './errors.js';
47
47
  import { compileDocument, isProviderSource, providerRoot } from './provider.js';
48
+ import { memberSegment } from './expression.js';
48
49
 
49
50
  /** The strategies this boundary knows. One, for now, and it says so. */
50
51
  const STRATEGIES = new Set(['hash']);
@@ -123,18 +124,6 @@ function byteSize(row) {
123
124
  return encoder.encode(text).length;
124
125
  }
125
126
 
126
- /**
127
- * The root NAME a root expression carries: `'$.Post[*]'` → `'Post'`.
128
- * A federated source's root is always this shape, because the
129
- * federation names it.
130
- * @param {string} root
131
- * @returns {string}
132
- */
133
- function rootName(root) {
134
- const match = /^\$\.([^.[\]]+)\[\*\]$/.exec(root);
135
- return match === null ? '' : match[1];
136
- }
137
-
138
127
  /**
139
128
  * The FLWOR a terminal wrapped, and how to put a rewritten one back.
140
129
  *
@@ -412,7 +401,7 @@ export function federate(spec) {
412
401
  // the federated root, which is what the chain binds and what the
413
402
  // resident document ranges over; the source's OWN root is what its
414
403
  // child document uses, and `childDocument` rewrites between them
415
- const root = `$.${name}[*]`;
404
+ const root = `$${memberSegment(name)}[*]`;
416
405
  members.set(root, { name, root, provider, estimatedRows });
417
406
  }
418
407
 
@@ -448,8 +437,8 @@ export function federate(spec) {
448
437
  }, open);
449
438
 
450
439
  const input = {
451
- [rootName(plan.build.root)]: built.rows,
452
- [rootName(plan.probe.root)]: probed.rows,
440
+ [plan.build.member.name]: built.rows,
441
+ [plan.probe.member.name]: probed.rows,
453
442
  };
454
443
  const compiled = compileDocument(plan.resident,
455
444
  { ...options, externals: options?.externalNames ?? [] });
@@ -462,7 +451,7 @@ export function federate(spec) {
462
451
  catch (error) {
463
452
  // the call's own failure is the one the caller gets: a cursor
464
453
  // that also fails to close must not replace the budget's refusal
465
- await closeAll(open);
454
+ await closeAll(open).catch(() => {});
466
455
  throw error;
467
456
  }
468
457
  };
@@ -513,7 +502,7 @@ export function federate(spec) {
513
502
  const handleFor = (name) => {
514
503
  let handle = handles.get(name);
515
504
  if (handle === undefined) {
516
- const root = `$.${name}[*]`;
505
+ const root = `$${memberSegment(name)}[*]`;
517
506
  if (!members.has(root)) {
518
507
  throw new LinqBuildError('JL0005',
519
508
  `'${name}' is not one of this federation's sources (${names.join(', ')})`);
@@ -0,0 +1,97 @@
1
+ //@ts-check
2
+ /** JTLT's public rules and text segments, with query capture but no renderer. */
3
+ import { DocumentBuilder, optionsOf, snapshot } from '../authored.js';
4
+ import { captureQuery } from '../capture-root.js';
5
+ import { LinqBuildError } from '../errors.js';
6
+
7
+ const RULE_KEYS = ['match', 'mode', 'priority'];
8
+ const OUTPUTS = ['text', 'xml'];
9
+
10
+ function list(value, what) {
11
+ if (!Array.isArray(value)) throw new LinqBuildError('JL0101', `${what} takes an array`);
12
+ return value;
13
+ }
14
+
15
+ function rulesOf(values) {
16
+ return list(values, 'rules()').map((value) => value instanceof RuleBuilder ? value.schema : snapshot(value));
17
+ }
18
+
19
+ function queryDocument(value, options) {
20
+ const opts = optionsOf(options ?? {}, ['externals'], 'query options');
21
+ const names = opts.externals ?? [];
22
+ if (!Array.isArray(names) || names.some((n) => typeof n !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(n))
23
+ || new Set(names).size !== names.length || names.includes('root') || names.includes('path'))
24
+ throw new LinqBuildError('JL0101', 'externals must be unique parameter names; root and path are already bound');
25
+ // An undeclared callback external is the shared capture's JL0104 refusal.
26
+ return snapshot(typeof value === 'function'
27
+ ? captureQuery('JTLT expression', ['root', 'path', ...names], value, { fold: false })
28
+ : value);
29
+ }
30
+
31
+ /** A literal segment; escape a leading dollar so it cannot become a query. @param {string} value */
32
+ export function text(value) {
33
+ if (typeof value !== 'string') throw new LinqBuildError('JL0101', 'text() takes a string');
34
+ return value.startsWith('$') ? '$' + value : value;
35
+ }
36
+
37
+ /** An interpolated query string/object, optionally captured from a callback. */
38
+ export function query(value, options = {}) {
39
+ const doc = queryDocument(value, options);
40
+ if (typeof doc !== 'string' && (doc === null || Array.isArray(doc) || typeof doc !== 'object'
41
+ || !Object.keys(doc).some((key) => key.startsWith('$'))))
42
+ throw new LinqBuildError('JL0101', 'query() needs a string or query object segment; use json() for a literal JSON value');
43
+ return doc;
44
+ }
45
+
46
+ /** Interpolate a query without output-method escaping. */
47
+ export function raw(value, options = {}) { return snapshot({ $raw: queryDocument(value, options) }); }
48
+ /** Serialize query results as JSON text, with output-method escaping. */
49
+ export function json(value, options = {}) { return snapshot({ $json: queryDocument(value, options) }); }
50
+ /** Splice template dispatch, optionally selecting a literal mode. */
51
+ export function apply(value, mode, options = {}) {
52
+ if (mode !== undefined && typeof mode !== 'string') throw new LinqBuildError('JL0101', 'apply() mode must be a string');
53
+ const doc = queryDocument(value, options);
54
+ return snapshot({ $apply: mode === undefined ? doc : [doc, mode] });
55
+ }
56
+
57
+ /** One immutable template rule. */
58
+ export class RuleBuilder extends DocumentBuilder {
59
+ /** Replace the segment list. @param {readonly any[]} value */
60
+ body(value) { return this.with({ body: list(value, 'body()') }); }
61
+ /** Replace the JSLT match specification. @param {any} value */
62
+ match(value) { return this.with({ match: value }); }
63
+ /** Replace the mode. @param {string} value */
64
+ mode(value) { return this.with({ mode: value }); }
65
+ /** Replace priority; the compiler owns the reserved priority band. @param {number} value */
66
+ priority(value) { return this.with({ priority: value }); }
67
+ }
68
+
69
+ /** A public rule; omit match for a catch-all in its mode. */
70
+ export function rule(body = [], options = {}) {
71
+ return new RuleBuilder({ ...optionsOf(options, RULE_KEYS, 'rule()'), body: list(body, 'rule() body') });
72
+ }
73
+
74
+ /** A bare rule list or versioned JTLT envelope. */
75
+ export class TemplateBuilder extends DocumentBuilder {
76
+ /** Replace rules while preserving the bare/envelope distinction. @param {readonly any[]} values */
77
+ rules(values) {
78
+ const rules = rulesOf(values);
79
+ return Array.isArray(this.schema) ? new TemplateBuilder(rules) : this.with({ rules });
80
+ }
81
+ /** Append a rule without changing existing rules. @param {any} value */
82
+ rule(value) { return this.rules([...(Array.isArray(this.schema) ? this.schema : this.schema.rules), value]); }
83
+ /** Set output; a bare list becomes an envelope when an output is declared. @param {'text' | 'xml'} value */
84
+ output(value) {
85
+ if (!OUTPUTS.includes(value)) throw new LinqBuildError('JL0101', 'output() takes text or xml');
86
+ return Array.isArray(this.schema) ? stylesheet(this.schema, { output: value }) : this.with({ output: value });
87
+ }
88
+ }
89
+
90
+ /** The versioned public template envelope. */
91
+ export function stylesheet(rules = [], options = {}) {
92
+ return new TemplateBuilder({ $jtlt: '0.1', ...optionsOf(options, ['output'], 'stylesheet()'), rules: rulesOf(rules) });
93
+ }
94
+ /** The public bare-rule-array shorthand, preserved on serialization. @param {readonly any[]} rules */
95
+ export function bare(rules = []) { return new TemplateBuilder(rulesOf(rules)); }
96
+ /** Preserve a raw public template exactly, including envelope member order. @param {any} document */
97
+ export function from(document) { return new TemplateBuilder(document); }
@@ -0,0 +1,96 @@
1
+ //@ts-check
2
+ /** JSON catalogs and existing MessageSpec values, checked against English vocabulary. */
3
+ import { compileMessageTemplate } from '@jarenjs/core/message';
4
+ import { snapshot, optionsOf } from '../authored.js';
5
+ import { requireNameMap } from '../json-boundary.js';
6
+ import { LinqBuildError } from '../errors.js';
7
+ import { CATALOGS } from './vocabulary.js';
8
+ export { CATALOGS } from './vocabulary.js';
9
+
10
+ const ALL = Object.freeze(Object.assign({}, ...Object.values(CATALOGS)));
11
+ function sourceOf(source) {
12
+ if (source === 'all') return ALL;
13
+ if (!Object.hasOwn(CATALOGS, source)) throw new LinqBuildError('JL0101', `Unknown message catalog '${source}'`);
14
+ return CATALOGS[source];
15
+ }
16
+ function mapOf(value) {
17
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
18
+ throw new LinqBuildError('JL0101', 'A catalog takes a plain message-id map');
19
+ return requireNameMap(value, 'catalog entries');
20
+ }
21
+ function checkTemplate(id, text, reference, locale, exact = true) {
22
+ if (!Object.hasOwn(reference, id)) throw new LinqBuildError('JL0101', `${locale}/${id}: unknown message id`);
23
+ if (typeof text !== 'string') throw new LinqBuildError('JL0101', `${locale}/${id}: JSON catalog entries are template strings`);
24
+ const names = compileMessageTemplate(text).parameters;
25
+ const missing = exact ? reference[id].filter((name) => !names.includes(name)) : [];
26
+ const extra = names.filter((name) => !reference[id].includes(name));
27
+ if (missing.length || extra.length)
28
+ throw new LinqBuildError('JL0101', `${locale}/${id}: placeholder mismatch; missing [${missing.join(', ')}], extra [${extra.join(', ')}]`);
29
+ }
30
+
31
+ /** An immutable catalog draft. Serialize only after complete() or explicit partial(). */
32
+ export class CatalogBuilder {
33
+ #document;
34
+ #source;
35
+ #locale;
36
+ /** @param {object} document @param {string} source @param {string} locale */
37
+ constructor(document, source, locale) {
38
+ sourceOf(source);
39
+ if (typeof locale !== 'string' || !locale) throw new LinqBuildError('JL0101', 'A catalog needs a nonempty locale name');
40
+ this.#document = snapshot(mapOf(document));
41
+ this.#source = source;
42
+ this.#locale = locale;
43
+ }
44
+ /** Add or replace a template, checking its placeholders immediately. */
45
+ entry(id, value) {
46
+ checkTemplate(id, value, sourceOf(this.#source), this.#locale);
47
+ return new CatalogBuilder({ ...this.#document, [id]: value }, this.#source, this.#locale);
48
+ }
49
+ /** Add several templates atomically; the prior draft is unchanged on refusal. @param {object} values */
50
+ entries(values) {
51
+ const reference = sourceOf(this.#source);
52
+ for (const [id, value] of Object.entries(mapOf(values))) checkTemplate(id, value, reference, this.#locale);
53
+ return new CatalogBuilder({ ...this.#document, ...values }, this.#source, this.#locale);
54
+ }
55
+ /** Emit an explicitly partial catalog; all present entries still need valid names/placeholders. */
56
+ partial() {
57
+ const reference = sourceOf(this.#source);
58
+ for (const [id, value] of Object.entries(this.#document)) checkTemplate(id, value, reference, this.#locale);
59
+ return this.#document;
60
+ }
61
+ /** Emit a complete catalog, refusing missing or extra ids before publication. */
62
+ complete() {
63
+ const document = this.partial();
64
+ const missing = Object.keys(sourceOf(this.#source)).filter((id) => !Object.hasOwn(document, id));
65
+ if (missing.length) throw new LinqBuildError('JL0101', `${this.#locale}: incomplete ${this.#source} catalog; missing [${missing.join(', ')}]`);
66
+ return document;
67
+ }
68
+ /** Serializing an unfinished draft is a completeness check, never an implicit partial. */
69
+ toJSON() { return this.complete(); }
70
+ }
71
+
72
+ /** Start a catalog for an English-owned key space and a diagnostic locale name. */
73
+ export function catalog(source = 'all', locale = 'en') { return new CatalogBuilder({}, source, locale); }
74
+ /** A raw JSON catalog draft; both exits validate its keys and placeholders. */
75
+ export function from(document, options = {}) {
76
+ const opts = optionsOf(options, ['source', 'locale'], 'from()');
77
+ return new CatalogBuilder(document, opts.source ?? 'all', opts.locale ?? 'en');
78
+ }
79
+ /** An inline MessageSpec string, parsed by the same compiler as a catalog entry. @param {string} value */
80
+ export function inline(value) {
81
+ if (typeof value !== 'string') throw new LinqBuildError('JL0101', 'inline() takes a template string');
82
+ compileMessageTemplate(value);
83
+ return value;
84
+ }
85
+ /** The existing structured MessageSpec; it is a message reference, not a catalog entry. */
86
+ export function message(id, options = {}) {
87
+ if (!Object.hasOwn(ALL, id)) throw new LinqBuildError('JL0101', `Unknown message id '${id}'`);
88
+ const opts = optionsOf(options, ['params', 'message'], 'message()');
89
+ if (Object.hasOwn(opts, 'params')) {
90
+ for (const name of Object.keys(mapOf(opts.params))) {
91
+ if (!ALL[id].includes(name)) throw new LinqBuildError('JL0101', `${id}: unknown parameter '${name}'`);
92
+ }
93
+ }
94
+ if (Object.hasOwn(opts, 'message')) checkTemplate(id, opts.message, ALL, 'fallback', false);
95
+ return snapshot({ $msgid: id, ...opts });
96
+ }
@@ -0,0 +1,259 @@
1
+ // Derived from the English catalogs by scripts/generate-message-pen.js.
2
+ import { deepFreeze } from '@jarenjs/core/object';
3
+ /** Canonical parameter names, grouped by their owning English catalog. */
4
+ export const CATALOGS = deepFreeze({
5
+ "validate": {
6
+ "type": [
7
+ "type",
8
+ "types"
9
+ ],
10
+ "required": [
11
+ "missingProperty"
12
+ ],
13
+ "minimum": [
14
+ "comparison",
15
+ "limit"
16
+ ],
17
+ "maximum": [
18
+ "comparison",
19
+ "limit"
20
+ ],
21
+ "exclusiveMinimum": [
22
+ "comparison",
23
+ "limit"
24
+ ],
25
+ "exclusiveMaximum": [
26
+ "comparison",
27
+ "limit"
28
+ ],
29
+ "multipleOf": [
30
+ "multipleOf"
31
+ ],
32
+ "minLength": [
33
+ "limit"
34
+ ],
35
+ "maxLength": [
36
+ "limit"
37
+ ],
38
+ "pattern": [
39
+ "pattern"
40
+ ],
41
+ "additionalProperties": [
42
+ "additionalProperty"
43
+ ],
44
+ "minProperties": [
45
+ "limit"
46
+ ],
47
+ "maxProperties": [
48
+ "limit"
49
+ ],
50
+ "minItems": [
51
+ "limit"
52
+ ],
53
+ "maxItems": [
54
+ "limit"
55
+ ],
56
+ "uniqueItems": [],
57
+ "contains": [],
58
+ "items": [],
59
+ "allOf": [],
60
+ "anyOf": [],
61
+ "oneOf": [],
62
+ "not": [],
63
+ "format": [
64
+ "format"
65
+ ],
66
+ "if": [],
67
+ "then": [],
68
+ "else": [],
69
+ "false schema": [],
70
+ "$query": [
71
+ "code",
72
+ "docPath"
73
+ ],
74
+ "JQ2001": [
75
+ "code",
76
+ "docPath"
77
+ ],
78
+ "JQ2003": [
79
+ "code",
80
+ "docPath"
81
+ ]
82
+ },
83
+ "forms": {
84
+ "form/required": [],
85
+ "form/type": [
86
+ "type"
87
+ ],
88
+ "form/const": [
89
+ "constValue"
90
+ ],
91
+ "form/enum": [
92
+ "enumValues"
93
+ ],
94
+ "form/minLength": [
95
+ "len",
96
+ "limit"
97
+ ],
98
+ "form/maxLength": [
99
+ "len",
100
+ "limit"
101
+ ],
102
+ "form/pattern": [
103
+ "pattern"
104
+ ],
105
+ "form/format": [
106
+ "format"
107
+ ],
108
+ "form/minimum": [
109
+ "limit"
110
+ ],
111
+ "form/maximum": [
112
+ "limit"
113
+ ],
114
+ "form/exclusiveMinimum": [
115
+ "limit"
116
+ ],
117
+ "form/exclusiveMaximum": [
118
+ "limit"
119
+ ],
120
+ "form/multipleOf": [
121
+ "multipleOf"
122
+ ],
123
+ "form/minItems": [
124
+ "limit"
125
+ ],
126
+ "form/maxItems": [
127
+ "limit"
128
+ ],
129
+ "form/uniqueItems": [],
130
+ "form/minProperties": [
131
+ "limit"
132
+ ],
133
+ "form/maxProperties": [
134
+ "limit"
135
+ ],
136
+ "x-form/assert": [],
137
+ "form/addItem": [],
138
+ "form/removeItem": []
139
+ },
140
+ "contract": {
141
+ "contract/not-found": [],
142
+ "contract/method-not-allowed": [
143
+ "allow"
144
+ ],
145
+ "contract/body-too-large": [
146
+ "limit",
147
+ "op"
148
+ ],
149
+ "contract/unsupported-media": [
150
+ "media",
151
+ "op"
152
+ ],
153
+ "contract/malformed-json": [
154
+ "op"
155
+ ],
156
+ "contract/invalid-input": [
157
+ "op"
158
+ ],
159
+ "contract/idempotency-key-required": [
160
+ "op"
161
+ ],
162
+ "contract/handler-failed": [
163
+ "op"
164
+ ],
165
+ "contract/idempotency-conflict": [
166
+ "kind",
167
+ "op"
168
+ ],
169
+ "contract/invalid-output": [
170
+ "op"
171
+ ],
172
+ "contract/malformed-path": [],
173
+ "contract/malformed-query": [],
174
+ "contract/not-implemented": [
175
+ "op"
176
+ ],
177
+ "contract/precondition-failed": [
178
+ "op"
179
+ ],
180
+ "contract/invalid-header": [
181
+ "header",
182
+ "op"
183
+ ],
184
+ "contract/handler-error": [
185
+ "code",
186
+ "op"
187
+ ],
188
+ "contract/client-invalid-input": [
189
+ "op"
190
+ ],
191
+ "contract/network": [
192
+ "name",
193
+ "op"
194
+ ],
195
+ "contract/cancelled": [
196
+ "op"
197
+ ],
198
+ "contract/invalid-response": [
199
+ "op"
200
+ ],
201
+ "contract/key-storage-failed": [
202
+ "op"
203
+ ],
204
+ "contract/undeclared-response": [
205
+ "op",
206
+ "status"
207
+ ],
208
+ "contract/not-a-contract": [
209
+ "id"
210
+ ],
211
+ "contract/incompatible": [
212
+ "client",
213
+ "id",
214
+ "server"
215
+ ],
216
+ "contract/host-failed": [
217
+ "op"
218
+ ],
219
+ "contract/local-handler-failed": [
220
+ "op"
221
+ ],
222
+ "contract/unknown-operation": [],
223
+ "contract/port-timeout": [
224
+ "ms",
225
+ "op"
226
+ ],
227
+ "contract/malformed-frame": [
228
+ "op"
229
+ ],
230
+ "contract/channel-closed": [
231
+ "op"
232
+ ],
233
+ "contract/not-a-stream": [
234
+ "op"
235
+ ],
236
+ "contract/invalid-snapshot": [
237
+ "op"
238
+ ],
239
+ "contract/seq-regression": [
240
+ "op"
241
+ ],
242
+ "contract/stream-error": [
243
+ "code",
244
+ "op"
245
+ ],
246
+ "contract/heartbeat-missed": [
247
+ "ms",
248
+ "op"
249
+ ],
250
+ "contract/slow-consumer": [
251
+ "op"
252
+ ],
253
+ "contract/reconnect-exhausted": [
254
+ "attempts",
255
+ "lastCode",
256
+ "op"
257
+ ]
258
+ }
259
+ });
@@ -0,0 +1,54 @@
1
+ //@ts-check
2
+ /** Typed files and project envelopes; Studio owns parsing and normalization. */
3
+ import { DocumentBuilder, optionsOf, snapshot } from '../authored.js';
4
+ import { LinqBuildError } from '../errors.js';
5
+
6
+ /** The public project file vocabulary, held equal to Studio's schema by tests. */
7
+ export const FILE_KINDS = Object.freeze(['app', 'jslt', 'query', 'state', 'data', 'schema', 'fsm', 'dag', 'model', 'contract']);
8
+ const LAYOUT_KEYS = ['mode', 'ratio', 'autorun'];
9
+
10
+ /** One file, preserving the caller's text byte-for-byte. */
11
+ export function file(name, kind, text) {
12
+ if (typeof name !== 'string' || name.length === 0 || !FILE_KINDS.includes(kind) || typeof text !== 'string')
13
+ throw new LinqBuildError('JL0101', 'file() requires a nonempty name, a declared kind and text');
14
+ return snapshot({ name, kind, text });
15
+ }
16
+
17
+ /** One file holding a public JSON value; pass another pen's `.schema` explicitly. */
18
+ export function jsonFile(name, kind, document) { return file(name, kind, JSON.stringify(snapshot(document))); }
19
+
20
+ /** Validate the authoring shape, preserving duplicate names for Studio to judge. */
21
+ function filesOf(files) {
22
+ if (!Array.isArray(files)) throw new LinqBuildError('JL0101', 'files() takes an array of project files');
23
+ return files.map((value) => {
24
+ const f = optionsOf(value, ['name', 'kind', 'text'], 'file');
25
+ return file(f.name, f.kind, f.text);
26
+ });
27
+ }
28
+
29
+ /** Immutable public project document. */
30
+ export class ProjectBuilder extends DocumentBuilder {
31
+ /** Replace the whole file list. @param {readonly object[]} values */
32
+ files(values) { return this.with({ files: filesOf(values) }); }
33
+ /** Append one file, without inventing a duplicate-name policy. @param {object} value */
34
+ file(value) { return this.files([...this.schema.files, value]); }
35
+ /** The requested active file; Studio falls back if it cannot resolve it. @param {string} name */
36
+ active(name) {
37
+ if (typeof name !== 'string') throw new LinqBuildError('JL0101', 'active() takes a file name');
38
+ return this.with({ active: name });
39
+ }
40
+ /** Replace layout metadata; Studio supplies defaults. @param {object} value */
41
+ layout(value) { return this.with({ layout: optionsOf(value, LAYOUT_KEYS, 'layout()') }); }
42
+ }
43
+
44
+ /** Start a project with files and optional active/layout metadata. */
45
+ export function defineProject(files = [], options = {}) {
46
+ const opts = optionsOf(options, ['active', 'layout'], 'defineProject()');
47
+ let built = new ProjectBuilder({ project: '0.1', files: filesOf(files) });
48
+ if (Object.hasOwn(opts, 'active')) built = built.active(opts.active);
49
+ if (Object.hasOwn(opts, 'layout')) built = built.layout(opts.layout);
50
+ return built;
51
+ }
52
+
53
+ /** A public envelope supplied verbatim; the Studio parser remains authoritative. @param {any} document */
54
+ export function from(document) { return new ProjectBuilder(document); }