@jarenjs/linq 0.56.0 → 0.67.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.
- package/ARCHITECTURE.md +10 -0
- package/README.md +93 -2
- package/docs/APP-PEN.md +3 -3
- package/docs/CONTRACT-PEN.md +10 -6
- package/docs/DB-CLIENT.md +98 -19
- package/docs/FLOW-PEN.md +12 -5
- package/docs/FORMS-PEN.md +2 -2
- package/docs/JSLT-PEN.md +4 -4
- package/docs/LINQ-FORMAT.md +42 -34
- package/docs/MIGRATION-PEN.md +2 -2
- package/docs/MODEL-PEN.md +15 -6
- package/docs/QUERY-PEN.md +107 -19
- package/docs/SCHEMA-PEN.md +2 -2
- package/package.json +6 -6
- package/src/app/action.js +4 -8
- package/src/app/define.js +8 -13
- package/src/async.js +58 -10
- package/src/concurrency.js +40 -8
- package/src/contract/define.js +23 -10
- package/src/contract/index.js +5 -5
- package/src/contract/operation.js +10 -14
- package/src/db/handle.js +3 -0
- package/src/db/include.js +40 -5
- package/src/db/index.js +6 -0
- package/src/db/ledger.js +195 -0
- package/src/db/open.js +59 -11
- package/src/db/replication.js +20 -0
- package/src/errors.js +10 -1
- package/src/expression.js +30 -4
- package/src/federate.js +531 -0
- package/src/flow/dag.js +28 -14
- package/src/flow/fsm.js +6 -11
- package/src/index.js +1 -0
- package/src/jslt/rules.js +7 -12
- package/src/migration/define.js +9 -14
- package/src/migration/steps.js +5 -9
- package/src/model/collection.js +102 -0
- package/src/model/index.js +1 -1
- package/types/contract.d.ts +115 -18
- package/types/db.d.ts +189 -11
- package/types/index.d.ts +65 -0
- package/types/model.d.ts +34 -1
package/src/flow/fsm.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* and everything else §5.1 lists.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import { deepFreeze } from '@jarenjs/core/object';
|
|
25
|
+
import { deepFreeze, isJsonObject } from '@jarenjs/core/object';
|
|
26
26
|
|
|
27
27
|
import { LinqBuildError } from '../errors.js';
|
|
28
28
|
import { effectDescriptor, readEffects } from '../effect.js';
|
|
@@ -47,11 +47,6 @@ const ON_MEMBERS = Object.freeze(['payload']);
|
|
|
47
47
|
/** The members `defineFsm()` takes. */
|
|
48
48
|
const FSM_MEMBERS = Object.freeze(['initial', 'states', 'transitions', 'context']);
|
|
49
49
|
|
|
50
|
-
/** @param {any} value */
|
|
51
|
-
function isPlainObject(value) {
|
|
52
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
50
|
/**
|
|
56
51
|
* A member set the pen knows, or `JL0101` naming the one it does not.
|
|
57
52
|
* @param {any} spec
|
|
@@ -111,7 +106,7 @@ export function effect(run, props = undefined) {
|
|
|
111
106
|
export function state(id, options = undefined) {
|
|
112
107
|
const out = { id: readId(id, 'state()') };
|
|
113
108
|
if (options !== undefined) {
|
|
114
|
-
if (!
|
|
109
|
+
if (!isJsonObject(options)) {
|
|
115
110
|
throw new LinqBuildError('JL0101',
|
|
116
111
|
`state() options are { entry?, exit?, final? }, got ${describeValue(options)}`);
|
|
117
112
|
}
|
|
@@ -203,7 +198,7 @@ export function on(from, event = null, options = undefined) {
|
|
|
203
198
|
entry.event = event;
|
|
204
199
|
}
|
|
205
200
|
if (options !== undefined) {
|
|
206
|
-
if (!
|
|
201
|
+
if (!isJsonObject(options)) {
|
|
207
202
|
throw new LinqBuildError('JL0101',
|
|
208
203
|
`on() options are { payload? }, got ${describeValue(options)}`);
|
|
209
204
|
}
|
|
@@ -239,7 +234,7 @@ export function on(from, event = null, options = undefined) {
|
|
|
239
234
|
* compileFsm(machine).step('draft', 'submit').state; // 'review'
|
|
240
235
|
*/
|
|
241
236
|
export function defineFsm(spec) {
|
|
242
|
-
if (!
|
|
237
|
+
if (!isJsonObject(spec)) {
|
|
243
238
|
throw new LinqBuildError('JL0101',
|
|
244
239
|
`defineFsm() takes { initial, states, transitions, context? }, got ${describeValue(spec)}`);
|
|
245
240
|
}
|
|
@@ -266,7 +261,7 @@ export function defineFsm(spec) {
|
|
|
266
261
|
states.push(entry);
|
|
267
262
|
return;
|
|
268
263
|
}
|
|
269
|
-
if (!
|
|
264
|
+
if (!isJsonObject(entry) || entry[STATE] !== true) {
|
|
270
265
|
throw new LinqBuildError('JL0101',
|
|
271
266
|
`defineFsm() states[${i}] is an id or state(id, options?), got ${describeValue(entry)}`,
|
|
272
267
|
`/states/${i}`);
|
|
@@ -305,7 +300,7 @@ export function defineFsm(spec) {
|
|
|
305
300
|
}
|
|
306
301
|
const transitions = spec.transitions.map((declaration, i) => {
|
|
307
302
|
const at = `/transitions/${i}`;
|
|
308
|
-
const entry =
|
|
303
|
+
const entry = isJsonObject(declaration) ? declaration[TRANSITION] : undefined;
|
|
309
304
|
if (entry === undefined) {
|
|
310
305
|
throw new LinqBuildError('JL0101',
|
|
311
306
|
`defineFsm() transitions[${i}] is on(from, event?).to(state), got `
|
package/src/index.js
CHANGED
|
@@ -13,4 +13,5 @@
|
|
|
13
13
|
export { from, fromDocument, Sequence } from './sequence.js';
|
|
14
14
|
export { fromAsync, AsyncSequence } from './async.js';
|
|
15
15
|
export { createPushQueue } from './sources.js';
|
|
16
|
+
export { federate } from './federate.js';
|
|
16
17
|
export { LinqBuildError, LinqRuntimeError, LINQ_CODES } from './errors.js';
|
package/src/jslt/rules.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* body's operators, the schema — is the compiler's.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
|
|
16
|
+
import { deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
|
|
17
17
|
import { LinqBuildError } from '../errors.js';
|
|
18
18
|
import { isSchemaBuilder, schemaOf } from '../schema/brand.js';
|
|
19
19
|
import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
|
|
@@ -24,11 +24,6 @@ const DISPOSITIONS = ['share', 'fresh', 'error'];
|
|
|
24
24
|
/** A JSON value, copied: the document is a value of its own. @param {any} v */
|
|
25
25
|
const copy = (v) => JSON.parse(JSON.stringify(v));
|
|
26
26
|
|
|
27
|
-
/** @param {any} value */
|
|
28
|
-
function isPlainObject(value) {
|
|
29
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
27
|
/**
|
|
33
28
|
* The `match` member (§3.1), or `undefined` for the unconditional rule.
|
|
34
29
|
* @param {any} match
|
|
@@ -37,7 +32,7 @@ function isPlainObject(value) {
|
|
|
37
32
|
function readMatch(match) {
|
|
38
33
|
if (match === undefined || match === null) return undefined;
|
|
39
34
|
if (typeof match === 'string') return match;
|
|
40
|
-
if (!
|
|
35
|
+
if (!isJsonObject(match)) {
|
|
41
36
|
throw new LinqBuildError('JL0101',
|
|
42
37
|
`rule() match is a JSONPath string or { path?, schema? }, got ${describeValue(match)}`,
|
|
43
38
|
'/match');
|
|
@@ -97,7 +92,7 @@ function readBody(value) {
|
|
|
97
92
|
export function rule(match, bodyOrFn, options = undefined) {
|
|
98
93
|
const out = {};
|
|
99
94
|
if (options !== undefined) {
|
|
100
|
-
if (!
|
|
95
|
+
if (!isJsonObject(options)) {
|
|
101
96
|
throw new LinqBuildError('JL0101',
|
|
102
97
|
`rule() options are { mode?, priority? }, got ${describeValue(options)}`);
|
|
103
98
|
}
|
|
@@ -156,7 +151,7 @@ export function stylesheet(rules, options = undefined) {
|
|
|
156
151
|
}
|
|
157
152
|
const out = { $jslt: '0.1' };
|
|
158
153
|
if (options !== undefined) {
|
|
159
|
-
if (!
|
|
154
|
+
if (!isJsonObject(options)) {
|
|
160
155
|
throw new LinqBuildError('JL0101',
|
|
161
156
|
`stylesheet() options are { unmatched?, modes? }, got ${describeValue(options)}`);
|
|
162
157
|
}
|
|
@@ -170,7 +165,7 @@ export function stylesheet(rules, options = undefined) {
|
|
|
170
165
|
out.unmatched = readDisposition(options.unmatched, 'unmatched');
|
|
171
166
|
}
|
|
172
167
|
if (options.modes !== undefined) {
|
|
173
|
-
if (!
|
|
168
|
+
if (!isJsonObject(options.modes)) {
|
|
174
169
|
throw new LinqBuildError('JL0101',
|
|
175
170
|
`stylesheet() modes is { name: { unmatched } }, got ${describeValue(options.modes)}`,
|
|
176
171
|
'/modes');
|
|
@@ -179,7 +174,7 @@ export function stylesheet(rules, options = undefined) {
|
|
|
179
174
|
const modes = {};
|
|
180
175
|
for (const name of Object.keys(options.modes)) {
|
|
181
176
|
const mode = options.modes[name];
|
|
182
|
-
if (!
|
|
177
|
+
if (!isJsonObject(mode) || Object.keys(mode).length !== 1 || mode.unmatched === undefined) {
|
|
183
178
|
throw new LinqBuildError('JL0101',
|
|
184
179
|
`stylesheet() mode '${name}' is { unmatched } and nothing else (JSLT-FORMAT §2.1)`,
|
|
185
180
|
`/modes/${name}`);
|
|
@@ -191,7 +186,7 @@ export function stylesheet(rules, options = undefined) {
|
|
|
191
186
|
}
|
|
192
187
|
}
|
|
193
188
|
out.rules = rules.map((r, i) => {
|
|
194
|
-
if (!
|
|
189
|
+
if (!isJsonObject(r)) {
|
|
195
190
|
throw new LinqBuildError('JL0101',
|
|
196
191
|
`stylesheet() rule ${i} is an object — rule(match, body) — got ${describeValue(r)}`,
|
|
197
192
|
`/rules/${i}`);
|
package/src/migration/define.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { canonicalizeJson } from '@jarenjs/json/canonical';
|
|
18
18
|
import { hashContent } from '@jarenjs/core/string';
|
|
19
|
-
import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
|
|
19
|
+
import { deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
|
|
20
20
|
|
|
21
21
|
import { LinqBuildError } from '../errors.js';
|
|
22
22
|
import { describeValue, requireJson } from '../json-boundary.js';
|
|
@@ -30,11 +30,6 @@ const HEAD_MEMBERS = ['$migration', 'id', 'from', 'to', 'note', 'steps'];
|
|
|
30
30
|
/** A JSON value, copied: the document is a value of its own. @param {any} v */
|
|
31
31
|
const copy = (v) => JSON.parse(JSON.stringify(v));
|
|
32
32
|
|
|
33
|
-
/** @param {any} value */
|
|
34
|
-
function isPlainObject(value) {
|
|
35
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
/**
|
|
39
34
|
* The model without its `x-rename` hints. A hint is a PLANNING
|
|
40
35
|
* instruction, not shape (MIGRATION-FORMAT §3): two models that differ
|
|
@@ -47,11 +42,11 @@ function withoutRenameHints(model) {
|
|
|
47
42
|
for (const key of Object.keys(model)) setObjectMember(out, key, model[key]);
|
|
48
43
|
for (const member of ['collections', 'entities']) {
|
|
49
44
|
const declared = model[member];
|
|
50
|
-
if (!
|
|
45
|
+
if (!isJsonObject(declared)) continue;
|
|
51
46
|
const stripped = {};
|
|
52
47
|
for (const name of Object.keys(declared)) {
|
|
53
48
|
const spec = declared[name];
|
|
54
|
-
if (
|
|
49
|
+
if (isJsonObject(spec) && Object.hasOwn(spec, 'x-rename')) {
|
|
55
50
|
const { 'x-rename': _hint, ...rest } = spec;
|
|
56
51
|
setObjectMember(stripped, name, rest);
|
|
57
52
|
}
|
|
@@ -82,10 +77,10 @@ function shapeHashOf(model) {
|
|
|
82
77
|
*/
|
|
83
78
|
function requireModel(model, what) {
|
|
84
79
|
const doc = requireJson(model, what);
|
|
85
|
-
if (!
|
|
80
|
+
if (!isJsonObject(doc) || doc.$model !== '0.1') {
|
|
86
81
|
throw new LinqBuildError('JL0101',
|
|
87
82
|
`${what} is a $model 0.1 document (defineModel(…), or its JSON), got `
|
|
88
|
-
+ `${
|
|
83
|
+
+ `${isJsonObject(doc) ? 'an object without $model: \'0.1\'' : describeValue(model)}`);
|
|
89
84
|
}
|
|
90
85
|
return doc;
|
|
91
86
|
}
|
|
@@ -98,7 +93,7 @@ function requireModel(model, what) {
|
|
|
98
93
|
function declaredNames(model) {
|
|
99
94
|
const names = [];
|
|
100
95
|
for (const member of ['entities', 'collections']) {
|
|
101
|
-
if (
|
|
96
|
+
if (isJsonObject(model[member])) names.push(...Object.keys(model[member]));
|
|
102
97
|
}
|
|
103
98
|
return names;
|
|
104
99
|
}
|
|
@@ -241,7 +236,7 @@ export class Migration {
|
|
|
241
236
|
* @returns {Migration}
|
|
242
237
|
*/
|
|
243
238
|
export function defineMigration(spec) {
|
|
244
|
-
if (!
|
|
239
|
+
if (!isJsonObject(spec)) {
|
|
245
240
|
throw new LinqBuildError('JL0101', 'defineMigration() takes { id, from, to, note? }');
|
|
246
241
|
}
|
|
247
242
|
for (const key of Object.keys(spec)) {
|
|
@@ -277,7 +272,7 @@ export function defineMigration(spec) {
|
|
|
277
272
|
*/
|
|
278
273
|
export function fromPlanned(document, options = undefined) {
|
|
279
274
|
const doc = copy(requireJson(document, 'fromPlanned() document'));
|
|
280
|
-
if (!
|
|
275
|
+
if (!isJsonObject(doc) || doc.$migration !== MIGRATION_VERSION
|
|
281
276
|
|| typeof doc.id !== 'string' || doc.id === ''
|
|
282
277
|
|| typeof doc.from !== 'string' || typeof doc.to !== 'string' || !Array.isArray(doc.steps)) {
|
|
283
278
|
throw new LinqBuildError('JL0101',
|
|
@@ -293,7 +288,7 @@ export function fromPlanned(document, options = undefined) {
|
|
|
293
288
|
}
|
|
294
289
|
let names = null;
|
|
295
290
|
if (options !== undefined) {
|
|
296
|
-
if (!
|
|
291
|
+
if (!isJsonObject(options)) {
|
|
297
292
|
throw new LinqBuildError('JL0101', `fromPlanned() options are { from?, to? }, got ${describeValue(options)}`);
|
|
298
293
|
}
|
|
299
294
|
for (const key of Object.keys(options)) {
|
package/src/migration/steps.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* `$it` inside the `$for` the format's own example writes.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { isJsonObject } from '@jarenjs/core/object';
|
|
16
17
|
import { captureExpression } from '../expression.js';
|
|
17
18
|
import { body } from '../jslt/body.js';
|
|
18
19
|
import { describeValue, requireJson } from '../json-boundary.js';
|
|
@@ -28,11 +29,6 @@ const NO_PARAMS = new Set();
|
|
|
28
29
|
/** A JSON value, copied: the document is a value of its own. @param {any} v */
|
|
29
30
|
const copy = (v) => JSON.parse(JSON.stringify(v));
|
|
30
31
|
|
|
31
|
-
/** @param {any} value */
|
|
32
|
-
function isPlainObject(value) {
|
|
33
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
32
|
/**
|
|
37
33
|
* A table name: an identifier, as the artifact's pattern admits.
|
|
38
34
|
* @param {any} name
|
|
@@ -117,7 +113,7 @@ export function transformStep(name, spelling) {
|
|
|
117
113
|
else if (Array.isArray(spelling)) {
|
|
118
114
|
stylesheet = copy(requireJson(spelling, 'transform() rules'));
|
|
119
115
|
}
|
|
120
|
-
else if (
|
|
116
|
+
else if (isJsonObject(spelling) && spelling.$jslt === '0.1') {
|
|
121
117
|
for (const key of Object.keys(spelling)) {
|
|
122
118
|
if (key !== '$jslt' && key !== 'rules') {
|
|
123
119
|
throw new LinqBuildError('JL0102',
|
|
@@ -155,7 +151,7 @@ export function assertStep(name, spelling, options = undefined) {
|
|
|
155
151
|
requireName(name, 'assert()');
|
|
156
152
|
let expect;
|
|
157
153
|
if (options !== undefined) {
|
|
158
|
-
if (!
|
|
154
|
+
if (!isJsonObject(options)) {
|
|
159
155
|
throw new LinqBuildError('JL0101', `assert() options are { expect? }, got ${describeValue(options)}`);
|
|
160
156
|
}
|
|
161
157
|
for (const key of Object.keys(options)) {
|
|
@@ -211,10 +207,10 @@ export function deriveStep(name, columns) {
|
|
|
211
207
|
*/
|
|
212
208
|
export function rawStep(step) {
|
|
213
209
|
const raw = copy(requireJson(step, 'step()'));
|
|
214
|
-
if (!
|
|
210
|
+
if (!isJsonObject(raw) || !STEP_KINDS.includes(raw.kind)) {
|
|
215
211
|
throw new LinqBuildError('JL0101',
|
|
216
212
|
`step() takes a migration step with a recognised kind (${STEP_KINDS.join(', ')}), got `
|
|
217
|
-
+ `${
|
|
213
|
+
+ `${isJsonObject(raw) ? `kind ${describeValue(raw.kind)}` : describeValue(step)}`);
|
|
218
214
|
}
|
|
219
215
|
const need = (member, ok) => {
|
|
220
216
|
if (!ok) {
|
package/src/model/collection.js
CHANGED
|
@@ -18,6 +18,10 @@ import { DOCUMENT_SCOPE, refuseStrandedRename } from './entity.js';
|
|
|
18
18
|
/** The index options the grammar names beside `name` and `path`. */
|
|
19
19
|
const INDEX_OPTIONS = new Set(['name', 'unique', 'derive', 'precision', 'dims', 'physical']);
|
|
20
20
|
|
|
21
|
+
/** The options an EXPRESSION index takes: it names the members it reads
|
|
22
|
+
* itself, so nothing that describes a member's storage belongs here. */
|
|
23
|
+
const EXPRESSION_OPTIONS = new Set(['name', 'unique']);
|
|
24
|
+
|
|
21
25
|
/** The marker a collection spec carries so `defineModel` can tell it apart. */
|
|
22
26
|
export const COLLECTION = Symbol.for('@jarenjs/linq/model-collection');
|
|
23
27
|
|
|
@@ -108,6 +112,104 @@ export function index(path, options = {}) {
|
|
|
108
112
|
return Object.freeze(out);
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
/**
|
|
116
|
+
* One node of an index expression, checked here so a pen mistake is a
|
|
117
|
+
* build error rather than a store refusal at open. The vocabulary is the
|
|
118
|
+
* model's, closed: a member path (a lambda or a JSONPath string), a JSON
|
|
119
|
+
* scalar, or a call to a function the HOST declares — this pen resolves
|
|
120
|
+
* no name, because arity and determinism are the store's to check
|
|
121
|
+
* against the declarations it was given.
|
|
122
|
+
* @param {any} node
|
|
123
|
+
* @param {string} what
|
|
124
|
+
* @param {number} depth
|
|
125
|
+
* @returns {any}
|
|
126
|
+
*/
|
|
127
|
+
function expressionNode(node, what, depth) {
|
|
128
|
+
if (depth > 8)
|
|
129
|
+
throw new LinqBuildError('JL0101', `${what} nests deeper than 8`);
|
|
130
|
+
if (typeof node === 'function') return { member: capturePath(node, what) };
|
|
131
|
+
if (typeof node === 'string' && node.charCodeAt(0) === 0x24) return { member: node };
|
|
132
|
+
if (typeof node === 'number' || typeof node === 'boolean') return { value: node };
|
|
133
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) {
|
|
134
|
+
throw new LinqBuildError('JL0101',
|
|
135
|
+
`${what} takes a path lambda, a JSONPath string, a JSON scalar, or `
|
|
136
|
+
+ '{ call, args } — an index expression is never SQL text');
|
|
137
|
+
}
|
|
138
|
+
if (node.member !== undefined) {
|
|
139
|
+
if (typeof node.member === 'function')
|
|
140
|
+
return { member: capturePath(node.member, `${what}.member`) };
|
|
141
|
+
if (typeof node.member !== 'string' || node.member.length === 0)
|
|
142
|
+
throw new LinqBuildError('JL0101', `${what}.member takes a path lambda or a JSONPath string`);
|
|
143
|
+
return { member: node.member };
|
|
144
|
+
}
|
|
145
|
+
if (node.value !== undefined) {
|
|
146
|
+
if (typeof node.value !== 'string' && typeof node.value !== 'number'
|
|
147
|
+
&& typeof node.value !== 'boolean') {
|
|
148
|
+
throw new LinqBuildError('JL0101',
|
|
149
|
+
`${what}.value takes a JSON string, number or boolean`);
|
|
150
|
+
}
|
|
151
|
+
return { value: node.value };
|
|
152
|
+
}
|
|
153
|
+
if (typeof node.call !== 'string' || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(node.call))
|
|
154
|
+
throw new LinqBuildError('JL0101', `${what}.call names a function by identifier`);
|
|
155
|
+
const args = node.args ?? [];
|
|
156
|
+
if (!Array.isArray(args))
|
|
157
|
+
throw new LinqBuildError('JL0101', `${what}.args is an array`);
|
|
158
|
+
return {
|
|
159
|
+
call: node.call,
|
|
160
|
+
args: args.map((argument, i) => expressionNode(argument, `${what}.args[${i}]`, depth + 1)),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* An index over a computed value: a closed expression over declared
|
|
166
|
+
* members, JSON scalars and functions the HOST declares deterministic.
|
|
167
|
+
*
|
|
168
|
+
* The function is resolved where the declarations are — at
|
|
169
|
+
* `openStore({ expressions })` — so a name this pen has never heard of
|
|
170
|
+
* is not an error here; a wrong ARITY and a missing declaration are
|
|
171
|
+
* `JD0004` at open, before any DDL. What this pen decides is the shape.
|
|
172
|
+
* @param {any} expression - a `{ call, args }` node, a path lambda, a
|
|
173
|
+
* JSONPath string, or a JSON scalar
|
|
174
|
+
* @param {{ name?: string, unique?: boolean }} [options]
|
|
175
|
+
* @returns {any} the frozen index document
|
|
176
|
+
*/
|
|
177
|
+
export function expressionIndex(expression, options = {}) {
|
|
178
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options))
|
|
179
|
+
throw new LinqBuildError('JL0101', 'expressionIndex() takes an options object');
|
|
180
|
+
for (const key of Object.keys(options)) {
|
|
181
|
+
if (!EXPRESSION_OPTIONS.has(key)) {
|
|
182
|
+
throw new LinqBuildError('JL0101',
|
|
183
|
+
`expressionIndex() does not take '${key}' — the options are name, unique`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const resolved = expressionNode(expression, 'expressionIndex()', 0);
|
|
187
|
+
const out = {
|
|
188
|
+
name: options.name === undefined
|
|
189
|
+
? expressionName(resolved)
|
|
190
|
+
: requireName(options.name, 'expressionIndex() name'),
|
|
191
|
+
expression: resolved,
|
|
192
|
+
};
|
|
193
|
+
if (options.unique !== undefined)
|
|
194
|
+
out.unique = requireJson(options.unique, 'expressionIndex() unique');
|
|
195
|
+
return Object.freeze(out);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The default name: `by_` + the outermost call and the members it
|
|
199
|
+
* reads, identifier-safe — the same stem the store's column takes.
|
|
200
|
+
* @param {any} node */
|
|
201
|
+
function expressionName(node) {
|
|
202
|
+
const members = [];
|
|
203
|
+
const walk = (current) => {
|
|
204
|
+
if (current.member !== undefined) members.push(current.member);
|
|
205
|
+
else if (current.call !== undefined) current.args.forEach(walk);
|
|
206
|
+
};
|
|
207
|
+
walk(node);
|
|
208
|
+
const parts = [node.call ?? 'x', ...members.map((path) =>
|
|
209
|
+
path.replace(/^\$\.?/, '').replace(/[^A-Za-z0-9]+/g, '_'))];
|
|
210
|
+
return `by_${parts.join('_').replace(/^_+|_+$/g, '')}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
111
213
|
/**
|
|
112
214
|
* The key declaration: a pointer string, a captured member path, or
|
|
113
215
|
* `null` (the store allocates; `identity` says how).
|
package/src/model/index.js
CHANGED
|
@@ -41,7 +41,7 @@ export const {
|
|
|
41
41
|
/** The relation members: `rel.hasMany`, `rel.hasOne`, `rel.belongsToMany`. */
|
|
42
42
|
export const rel = createRelations(EntityBuilder);
|
|
43
43
|
|
|
44
|
-
export { collection, index } from './collection.js';
|
|
44
|
+
export { collection, index, expressionIndex } from './collection.js';
|
|
45
45
|
export { defineModel } from './define.js';
|
|
46
46
|
export { withEntity } from './entity.js';
|
|
47
47
|
export { isSchemaBuilder, schemaOf, SCHEMA_BUILDER } from '../schema/brand.js';
|
package/types/contract.d.ts
CHANGED
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
* the `Infer<>` of its output, the union of its declared error codes,
|
|
8
8
|
* and whether its media makes it opaque. Every one of those is a
|
|
9
9
|
* compile-time reading of the SAME builders the emitted document was
|
|
10
|
-
* written from (D2), so the
|
|
11
|
-
* handler table and an AI toolbox with no
|
|
10
|
+
* written from (D2), so the wrappers below can type a client (an HTTP
|
|
11
|
+
* one with its byte method), a handler table and an AI toolbox with no
|
|
12
|
+
* `generate` step.
|
|
12
13
|
*
|
|
13
14
|
* The agreement is a gate: `test/consumer/linq-contract.ts` proves
|
|
14
15
|
* `ContractOf<>`'s members EQUAL to what
|
|
@@ -215,6 +216,13 @@ export type SubscribableOf<C> = Simplify<{
|
|
|
215
216
|
ContractOf<C>[K]
|
|
216
217
|
}>;
|
|
217
218
|
|
|
219
|
+
/** The opaque operations of a contract: what an HTTP client's `bytes`
|
|
220
|
+
* reaches — exactly the set §12.3's `ByteOperations` declares. */
|
|
221
|
+
export type OpaqueOf<C> = Simplify<{
|
|
222
|
+
[K in keyof ContractOf<C> as ContractOf<C>[K] extends { opaque: true } ? K : never]:
|
|
223
|
+
ContractOf<C>[K]
|
|
224
|
+
}>;
|
|
225
|
+
|
|
218
226
|
// ——— the fixed outcome shapes (§10.1, rendered by §12.3) ———
|
|
219
227
|
|
|
220
228
|
/** The correlation members of every outcome. A member a binding cannot
|
|
@@ -249,36 +257,104 @@ export type Failure = {
|
|
|
249
257
|
retryable: boolean | null;
|
|
250
258
|
};
|
|
251
259
|
|
|
252
|
-
/** The
|
|
253
|
-
export
|
|
260
|
+
/** The binding a handler context comes from. */
|
|
261
|
+
export type CarrierName = 'http' | 'port' | 'local';
|
|
262
|
+
|
|
263
|
+
/** The members every carrier's context shares; `host` is the host
|
|
264
|
+
* lifecycle's acquired value (§7.7), `null` by default. */
|
|
265
|
+
export interface HandlerContextBase<Host = null> {
|
|
254
266
|
op: unknown;
|
|
255
267
|
trace: string;
|
|
256
|
-
|
|
257
|
-
path: string;
|
|
258
|
-
params: Readonly<Record<string, string>>;
|
|
268
|
+
host: Host;
|
|
259
269
|
headers: Readonly<Record<string, string>>;
|
|
260
|
-
body: string | Uint8Array | null;
|
|
261
270
|
signal: AbortSignal | null;
|
|
262
|
-
idempotency: Readonly<{ key: string; scope: string }> | null;
|
|
263
271
|
fail(code: string, params?: Record<string, unknown>, details?: unknown,
|
|
264
272
|
options?: { retryable?: boolean }): Failure;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** The HTTP binding's context: the request line, the raw body of an
|
|
276
|
+
* opaque operation, the idempotency key, and the entity-tag and status
|
|
277
|
+
* arms. */
|
|
278
|
+
export interface HttpHandlerContext<Host = null> extends HandlerContextBase<Host> {
|
|
279
|
+
carrier: 'http';
|
|
280
|
+
method: string;
|
|
281
|
+
path: string;
|
|
282
|
+
params: Readonly<Record<string, string>>;
|
|
283
|
+
body: string | Uint8Array | AsyncIterable<Uint8Array> | null;
|
|
284
|
+
idempotency: Readonly<{ key: string; scope: string }> | null;
|
|
265
285
|
etag(tag: string, options?: { strong?: boolean }): void;
|
|
266
286
|
status(status: number): void;
|
|
267
287
|
}
|
|
268
288
|
|
|
269
|
-
/**
|
|
289
|
+
/** The port and local bindings' context: no request line, no body, no
|
|
290
|
+
* key, and no callable `etag` or `status` — spelled `null`, never
|
|
291
|
+
* omitted (§15, §16). */
|
|
292
|
+
export interface ChannelHandlerContext<Host = null, Carrier extends 'port' | 'local' = 'port' | 'local'>
|
|
293
|
+
extends HandlerContextBase<Host> {
|
|
294
|
+
carrier: Carrier;
|
|
295
|
+
method: null;
|
|
296
|
+
path: null;
|
|
297
|
+
params: null;
|
|
298
|
+
body: null;
|
|
299
|
+
idempotency: null;
|
|
300
|
+
etag: null;
|
|
301
|
+
status: null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The per-request context a server binding hands a handler, selected
|
|
305
|
+
* by carrier: the HTTP context by default (`HandlerContext` is the
|
|
306
|
+
* shape it always was, plus `carrier` and `host`); a carrier union is a
|
|
307
|
+
* discriminated union — narrow on `carrier` before an HTTP-only
|
|
308
|
+
* member. */
|
|
309
|
+
export type HandlerContext<Host = null, Carrier extends CarrierName = 'http'> =
|
|
310
|
+
Extract<HttpHandlerContext<Host> | ChannelHandlerContext<Host, 'port'> | ChannelHandlerContext<Host, 'local'>, { carrier: Carrier }>;
|
|
311
|
+
|
|
312
|
+
/** Per-call options of `bytes` (§10.6): the members of `InvokeContext`
|
|
313
|
+
* that apply to an opaque call, plus the request body to send. */
|
|
314
|
+
export interface ByteContext {
|
|
315
|
+
signal?: AbortSignal; attempt?: unknown; headers?: Record<string, string>;
|
|
316
|
+
ifNoneMatch?: string; ifMatch?: string;
|
|
317
|
+
body?: string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array> | null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The success value of `bytes`: the status, the lowercase response
|
|
321
|
+
* headers, the response media and the LIVE body — a stream the caller
|
|
322
|
+
* reads; `null` when the response carries none. */
|
|
323
|
+
export type ByteResponse = {
|
|
324
|
+
status: number; headers: Record<string, string>; media: string | null;
|
|
325
|
+
body: ReadableStream<Uint8Array> | null;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/** The info beside every snapshot (§19): the event's seq, whether the
|
|
329
|
+
* stream resumed, whether the snapshot re-seeds a consumer whose cursor
|
|
330
|
+
* fell behind the server's retention (`reset`), and the server log's
|
|
331
|
+
* watermarks when it reported them. */
|
|
332
|
+
export interface SnapshotInfo {
|
|
333
|
+
seq: number; resumed: boolean; reset: boolean;
|
|
334
|
+
earliestAvailable: number | null; highWatermark: number | null;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** What `client.subscribe` takes (§19); every callback is optional.
|
|
338
|
+
* `reconnect` opts the HTTP client into re-establishing the stream
|
|
339
|
+
* after a network loss, up to `max` further attempts from the last
|
|
340
|
+
* delivered seq; the budget spent, `onError` gets one `JC2097`. The
|
|
341
|
+
* port client has no network loss to reconnect from and ignores it. */
|
|
270
342
|
export interface SubscribeHandlers<T> {
|
|
271
|
-
onSnapshot?(value: T, info:
|
|
343
|
+
onSnapshot?(value: T, info: SnapshotInfo): void;
|
|
272
344
|
onPatch?(emission: { patch: readonly Json[]; seq: number }): void;
|
|
273
345
|
onError?(outcome: Outcome<never>): void;
|
|
274
346
|
onEnd?(info: { reason: string }): void;
|
|
275
347
|
signal?: AbortSignal;
|
|
276
348
|
lastSeq?: number;
|
|
349
|
+
reconnect?: { max: number };
|
|
277
350
|
}
|
|
278
351
|
|
|
279
|
-
/** A live subscription: `stop()` releases it
|
|
352
|
+
/** A live subscription: `stop()` releases it; `lastSeq` is the last
|
|
353
|
+
* delivered seq (`null` before the first, the passed `lastSeq` until an
|
|
354
|
+
* event moves it) — what a re-entered `subscribe` passes (§19). */
|
|
280
355
|
export interface Subscription {
|
|
281
356
|
stop(): void;
|
|
357
|
+
readonly lastSeq: number | null;
|
|
282
358
|
}
|
|
283
359
|
|
|
284
360
|
// ——— the three identity wrappers ———
|
|
@@ -303,11 +379,25 @@ export interface TypedClient<C> {
|
|
|
303
379
|
close(): void;
|
|
304
380
|
}
|
|
305
381
|
|
|
306
|
-
/**
|
|
307
|
-
|
|
382
|
+
/** An HTTP client typed by one contract's operations: `TypedClient` plus
|
|
383
|
+
* `bytes` over the opaque ones, whose success owns a live stream. */
|
|
384
|
+
export interface TypedHttpClient<C> extends TypedClient<C> {
|
|
385
|
+
bytes<K extends keyof OpaqueOf<C>>(
|
|
386
|
+
op: K,
|
|
387
|
+
input: OpaqueOf<C>[K] extends { input: infer I } ? I : never,
|
|
388
|
+
ctx?: ByteContext,
|
|
389
|
+
): Promise<Outcome<ByteResponse>>;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** The handler table of a server binding, one handler per invokable
|
|
393
|
+
* operation. `Host` is what the host lifecycle's `acquire` hands the
|
|
394
|
+
* handler as `ctx.host`; `Carrier` selects the context — `'http'` by
|
|
395
|
+
* default, a union for a table several bindings share (narrow on
|
|
396
|
+
* `ctx.carrier` before an HTTP-only member). */
|
|
397
|
+
export type TypedHandlerTable<C, Host = null, Carrier extends CarrierName = 'http'> = {
|
|
308
398
|
[K in keyof InvokableOf<C>]: (
|
|
309
399
|
input: InvokableOf<C>[K] extends { input: infer I } ? I : never,
|
|
310
|
-
ctx: HandlerContext,
|
|
400
|
+
ctx: HandlerContext<Host, Carrier>,
|
|
311
401
|
) => (InvokableOf<C>[K] extends { output: infer O } ? O : never)
|
|
312
402
|
| Failure
|
|
313
403
|
| Promise<(InvokableOf<C>[K] extends { output: infer O } ? O : never) | Failure>;
|
|
@@ -360,10 +450,17 @@ export function defineContract<O extends Record<string, AnyOperation | ({
|
|
|
360
450
|
/** Bind a contract client to the contract that types it. Identity at runtime. */
|
|
361
451
|
export function typedClient<C extends Contract<any>>(client: unknown, contract: C): TypedClient<C>;
|
|
362
452
|
|
|
453
|
+
/** Bind an HTTP client (`openHttpClient`) to the contract that types it:
|
|
454
|
+
* `typedClient` plus `bytes` over the opaque operations. Identity at
|
|
455
|
+
* runtime; a local or port client has no `bytes` and takes `typedClient`. */
|
|
456
|
+
export function typedHttpClient<C extends Contract<any>>(client: unknown, contract: C): TypedHttpClient<C>;
|
|
457
|
+
|
|
363
458
|
/** Bind a handler table to the contract it serves. Identity at runtime;
|
|
364
|
-
* a missing or misspelled operation is a type error.
|
|
365
|
-
|
|
366
|
-
|
|
459
|
+
* a missing or misspelled operation is a type error. Name `Host` and
|
|
460
|
+
* `Carrier` explicitly for a table whose context is not the HTTP default:
|
|
461
|
+
* `typedHandlers<typeof Shop, { db: Client }, 'http' | 'port'>(Shop, …)`. */
|
|
462
|
+
export function typedHandlers<C extends Contract<any>, Host = null, Carrier extends CarrierName = 'http'>(
|
|
463
|
+
contract: C, handlers: TypedHandlerTable<C, Host, Carrier>): TypedHandlerTable<C, Host, Carrier>;
|
|
367
464
|
|
|
368
465
|
/** Bind `contractTools`' output to the contract that types it. Identity
|
|
369
466
|
* at runtime. */
|