@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/async.js
CHANGED
|
@@ -26,10 +26,13 @@
|
|
|
26
26
|
* refuses (`JL0005`) and `explain()` reports the split.
|
|
27
27
|
* - a PROVIDER origin (`fromAsync(store.entity('Post'))`) runs nothing
|
|
28
28
|
* here: everything up to the first `mapAsync` is ONE document the
|
|
29
|
-
* provider executes
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `
|
|
29
|
+
* provider executes — a terminal's wrapper included, exactly as the
|
|
30
|
+
* synchronous surface pushes it, `execute` answering a promise if it
|
|
31
|
+
* must (D8). Iteration hands that document to the provider's CURSOR
|
|
32
|
+
* when it offers one, so a `for await` pulls one row at a time from
|
|
33
|
+
* an open statement and a `break` releases it; the residual after
|
|
34
|
+
* the split streams locally, and a `join` exists on this surface
|
|
35
|
+
* only inside that pushed document.
|
|
33
36
|
*
|
|
34
37
|
* Early termination CLOSES the source: every consumer is a
|
|
35
38
|
* `for await … break` chain, and async generators propagate `return()`
|
|
@@ -437,10 +440,11 @@ export class AsyncSequence {
|
|
|
437
440
|
}
|
|
438
441
|
|
|
439
442
|
/** Barriers, the split, the relation hops the callbacks navigated,
|
|
440
|
-
* and — when representable — the document
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* of
|
|
443
|
+
* and — when representable — the document; plus `streaming` and
|
|
444
|
+
* `barrier`, what this surface's own execution does. Stages are named
|
|
445
|
+
* by the OPERATOR the caller wrote (`selectMany`, `orderByDescending`),
|
|
446
|
+
* and a `thenBy` is part of the `$orderby` barrier it extends, not a
|
|
447
|
+
* barrier of its own. */
|
|
444
448
|
explain() {
|
|
445
449
|
const firstMap = this.#stages.findIndex((s) => s.kind === 'mapAsync');
|
|
446
450
|
// over a provider nothing before the split materialises HERE — the
|
|
@@ -455,7 +459,19 @@ export class AsyncSequence {
|
|
|
455
459
|
barriers.push({ operator: stage.name ?? stage.kind, reason: BARRIERS[stage.kind] });
|
|
456
460
|
}
|
|
457
461
|
}
|
|
458
|
-
|
|
462
|
+
// what THIS surface does with the item stream: one item at a time,
|
|
463
|
+
// or a buffer at the first local barrier. Over a provider the pushed
|
|
464
|
+
// document's own class — a set residual, an unbindable external — is
|
|
465
|
+
// the provider's to report: `explain(document, { externals: bindings })`
|
|
466
|
+
// on the source answers it, and its cursor carries the same answer
|
|
467
|
+
const first = barriers[0];
|
|
468
|
+
const out = {
|
|
469
|
+
barriers,
|
|
470
|
+
streaming: first === undefined ? 'row' : 'buffered',
|
|
471
|
+
barrier: first === undefined ? null : { construct: first.operator, reason: first.reason },
|
|
472
|
+
hops: this.#hops(),
|
|
473
|
+
bindings: this.#externals().values,
|
|
474
|
+
};
|
|
459
475
|
if (firstMap < 0) {
|
|
460
476
|
out.document = this.toDocument();
|
|
461
477
|
}
|
|
@@ -487,7 +503,7 @@ export class AsyncSequence {
|
|
|
487
503
|
// runs whole; the residual continues locally over its rows
|
|
488
504
|
const firstMap = stages.findIndex((s) => s.kind === 'mapAsync');
|
|
489
505
|
i = firstMap < 0 ? stages.length : firstMap;
|
|
490
|
-
stream =
|
|
506
|
+
stream = this.#providerStream(stages.slice(0, i), values);
|
|
491
507
|
}
|
|
492
508
|
else {
|
|
493
509
|
stream = this.#origin.iterate();
|
|
@@ -522,6 +538,33 @@ export class AsyncSequence {
|
|
|
522
538
|
yield* iterateAndClose(stream);
|
|
523
539
|
}
|
|
524
540
|
|
|
541
|
+
/**
|
|
542
|
+
* The pushed window as a STREAM. A provider that offers the cursor
|
|
543
|
+
* protocol (`cursor(document, options)` — the store's entity sets and
|
|
544
|
+
* collections do) is handed the unwrapped document and answers one
|
|
545
|
+
* item per pull from an open statement; the generator's `for await`
|
|
546
|
+
* releases that statement on break, throw and exhaustion alike,
|
|
547
|
+
* exactly once. A provider without one keeps the element window it
|
|
548
|
+
* always had: the `toArray`-wrapped document run whole, its array
|
|
549
|
+
* iterated — a materialisation, which is what such a provider can do.
|
|
550
|
+
* @param {readonly any[]} stages - the stages up to the split
|
|
551
|
+
* @param {Record<string, any>} values - the bound externals
|
|
552
|
+
* @returns {AsyncIterator<any>}
|
|
553
|
+
*/
|
|
554
|
+
#providerStream(stages, values) {
|
|
555
|
+
const source = this.#origin.source;
|
|
556
|
+
if (typeof source.cursor === 'function') {
|
|
557
|
+
return source.cursor(this.#documentOf(stages), { externals: values });
|
|
558
|
+
}
|
|
559
|
+
return this.#materializedWindow(stages);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/** The legacy element window, iterated: one `toArray` push, run whole.
|
|
563
|
+
* @param {readonly any[]} stages */
|
|
564
|
+
async* #materializedWindow(stages) {
|
|
565
|
+
yield* await this.#pushWindow('toArray', undefined, stages);
|
|
566
|
+
}
|
|
567
|
+
|
|
525
568
|
/** @param {AsyncIterator<any>} stream @param {any} stage @param {any} values */
|
|
526
569
|
#applyStreamStage(stream, stage, values) {
|
|
527
570
|
const self = this;
|
|
@@ -614,7 +657,12 @@ export class AsyncSequence {
|
|
|
614
657
|
}
|
|
615
658
|
}
|
|
616
659
|
|
|
660
|
+
/** The whole result as one array. Over a provider with no `mapAsync`
|
|
661
|
+
* this is ONE pushed window — the terminal asked for the array, so the
|
|
662
|
+
* store answers it in one statement rather than one row at a time;
|
|
663
|
+
* everywhere else it drains the item stream. */
|
|
617
664
|
async toArray() {
|
|
665
|
+
if (this.#pushable()) return this.#pushWindow('toArray');
|
|
618
666
|
return collect(this[Symbol.asyncIterator]());
|
|
619
667
|
}
|
|
620
668
|
|
package/src/concurrency.js
CHANGED
|
@@ -54,6 +54,18 @@ export async function* applyMapAsync(items, fn, opts) {
|
|
|
54
54
|
/** The pipeline's own failure, so source cleanup cannot displace it.
|
|
55
55
|
* @type {{ reason: any } | null} */
|
|
56
56
|
let failure = null;
|
|
57
|
+
/** A callback failure may close while a pull is pending; finally awaits
|
|
58
|
+
* that same close, so cancellation reaches the producer exactly once.
|
|
59
|
+
* @type {Promise<any> | null} */
|
|
60
|
+
let closing = null;
|
|
61
|
+
const closeSource = () => {
|
|
62
|
+
if (closing === null) {
|
|
63
|
+
closing = Promise.resolve().then(() => typeof items.return === 'function'
|
|
64
|
+
? items.return(undefined) : undefined);
|
|
65
|
+
closing.catch(() => {}); // the finally block reports cleanup failures
|
|
66
|
+
}
|
|
67
|
+
return closing;
|
|
68
|
+
};
|
|
57
69
|
|
|
58
70
|
try {
|
|
59
71
|
if (opts.mode === 'concat' || (opts.mode === 'parallel' && opts.concurrency === 1)) {
|
|
@@ -122,14 +134,29 @@ export async function* applyMapAsync(items, fn, opts) {
|
|
|
122
134
|
// and stops the pull, at once — not when it reaches the head of the
|
|
123
135
|
// ordered window: the rejection itself still surfaces in order
|
|
124
136
|
let rejected = false;
|
|
137
|
+
let stopPull;
|
|
138
|
+
/** @type {Promise<IteratorResult<any>>} */
|
|
139
|
+
const stoppedPull = new Promise((resolve) => {
|
|
140
|
+
stopPull = () => resolve({ done: true, value: undefined });
|
|
141
|
+
});
|
|
142
|
+
const taskFailed = () => {
|
|
143
|
+
if (rejected) return;
|
|
144
|
+
rejected = true;
|
|
145
|
+
stopPull();
|
|
146
|
+
controller.abort();
|
|
147
|
+
closeSource();
|
|
148
|
+
};
|
|
149
|
+
// An idle producer must not hide a callback's failure. The race also
|
|
150
|
+
// observes a pending next() that rejects after cancellation won.
|
|
151
|
+
const next = () => Promise.race([items.next(), stoppedPull]);
|
|
125
152
|
const pull = async () => {
|
|
126
|
-
const step = await
|
|
153
|
+
const step = await next();
|
|
127
154
|
if (step.done) { sourceDone = true; return null; }
|
|
128
155
|
if (rejected) return null; // a sibling failed while this pull awaited
|
|
129
156
|
const promise = Promise.resolve(fn(step.value, signal));
|
|
130
157
|
// a rejection must wait its turn in the ordered window without
|
|
131
158
|
// firing unhandledRejection while an earlier task is in flight
|
|
132
|
-
promise.catch(
|
|
159
|
+
promise.catch(taskFailed);
|
|
133
160
|
return { promise };
|
|
134
161
|
};
|
|
135
162
|
if (opts.ordered) {
|
|
@@ -153,19 +180,24 @@ export async function* applyMapAsync(items, fn, opts) {
|
|
|
153
180
|
let nextId = 0;
|
|
154
181
|
const inflight = new Map();
|
|
155
182
|
const start = async () => {
|
|
156
|
-
const step = await
|
|
183
|
+
const step = await next();
|
|
157
184
|
if (step.done) { sourceDone = true; return; }
|
|
185
|
+
if (rejected) return; // a task failed while this source pull awaited
|
|
158
186
|
const id = nextId++;
|
|
159
|
-
|
|
187
|
+
const promise = Promise.resolve(fn(step.value, signal)).then(
|
|
160
188
|
(value) => ({ id, value }),
|
|
161
189
|
// the task's identity rides in an internal ENVELOPE, never on the
|
|
162
190
|
// rejection value. Stamping the value mutated whatever the handler
|
|
163
191
|
// threw: a frozen error became a different TypeError, a thrown
|
|
164
192
|
// string came back boxed, an ordinary error grew a private
|
|
165
193
|
// property, and a hostile proxy could break normalization outright.
|
|
166
|
-
(error) => { throw new TaskFailure(id, error); })
|
|
194
|
+
(error) => { throw new TaskFailure(id, error); });
|
|
195
|
+
// Observe failures immediately, including while the next source
|
|
196
|
+
// pull is pending or downstream has stopped consuming the window.
|
|
197
|
+
promise.catch(taskFailed);
|
|
198
|
+
inflight.set(id, promise);
|
|
167
199
|
};
|
|
168
|
-
while (!sourceDone && inflight.size < opts.concurrency) await start();
|
|
200
|
+
while (!sourceDone && !rejected && inflight.size < opts.concurrency) await start();
|
|
169
201
|
while (inflight.size > 0) {
|
|
170
202
|
let settled;
|
|
171
203
|
try {
|
|
@@ -179,7 +211,7 @@ export async function* applyMapAsync(items, fn, opts) {
|
|
|
179
211
|
throw err;
|
|
180
212
|
}
|
|
181
213
|
inflight.delete(settled.id);
|
|
182
|
-
if (!sourceDone) await start();
|
|
214
|
+
if (!sourceDone && !rejected) await start();
|
|
183
215
|
yield settled.value;
|
|
184
216
|
}
|
|
185
217
|
}
|
|
@@ -196,7 +228,7 @@ export async function* applyMapAsync(items, fn, opts) {
|
|
|
196
228
|
// itself succeeded. Composed as a rejection the `await` adopts,
|
|
197
229
|
// because a `throw` here would be the very substitution this
|
|
198
230
|
// avoids: it discards whatever completion the block was carrying.
|
|
199
|
-
await
|
|
231
|
+
await closeSource().then(undefined,
|
|
200
232
|
(cleanupError) => Promise.reject(failure === null
|
|
201
233
|
? cleanupError
|
|
202
234
|
: new AggregateError([failure.reason, cleanupError],
|
package/src/contract/define.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* document means.
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { cloneJson, deepFreeze, setObjectMember } from '@jarenjs/core/object';
|
|
24
|
+
import { cloneJson, deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
|
|
25
25
|
|
|
26
26
|
import { LinqBuildError } from '../errors.js';
|
|
27
27
|
import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
|
|
@@ -36,11 +36,6 @@ const HEAD_MEMBERS = ['id', 'version', 'compat'];
|
|
|
36
36
|
/** `[A-Za-z_][A-Za-z0-9_-]*` — a contract id (§2.1). */
|
|
37
37
|
const ID = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
38
38
|
|
|
39
|
-
/** @param {any} value */
|
|
40
|
-
function isPlainObject(value) {
|
|
41
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
39
|
/**
|
|
45
40
|
* One schema position — `input`, `output`, an error's `schema` — as the
|
|
46
41
|
* document carries it: a builder emitted into the shared `$defs`
|
|
@@ -53,7 +48,7 @@ function isPlainObject(value) {
|
|
|
53
48
|
function schemaAt(value, ctx, at) {
|
|
54
49
|
if (isSchemaBuilder(value)) return emitInto(value, ctx, at);
|
|
55
50
|
const json = requireJson(value, `the schema at ${at}`);
|
|
56
|
-
if (typeof json !== 'boolean' && !
|
|
51
|
+
if (typeof json !== 'boolean' && !isJsonObject(json)) {
|
|
57
52
|
throw new LinqBuildError('JL0101',
|
|
58
53
|
`a schema is an object, true or false — got ${describeValue(value)}`, at);
|
|
59
54
|
}
|
|
@@ -78,7 +73,7 @@ function schemaAt(value, ctx, at) {
|
|
|
78
73
|
*/
|
|
79
74
|
function declarationOf(declared, at) {
|
|
80
75
|
if (isOperation(declared)) return declared;
|
|
81
|
-
if (!
|
|
76
|
+
if (!isJsonObject(declared)) {
|
|
82
77
|
throw new LinqBuildError('JL0101',
|
|
83
78
|
`an operation is read(), command() or subscribe() — got ${describeValue(declared)}`, at);
|
|
84
79
|
}
|
|
@@ -166,7 +161,7 @@ export class Contract {
|
|
|
166
161
|
* compileContract(shop.document).ids; // ['catalog.load']
|
|
167
162
|
*/
|
|
168
163
|
export function defineContract(meta, operations) {
|
|
169
|
-
if (!
|
|
164
|
+
if (!isJsonObject(meta)) {
|
|
170
165
|
throw new LinqBuildError('JL0101',
|
|
171
166
|
`defineContract() takes ({ id?, version?, compat? }, operations), got `
|
|
172
167
|
+ `${describeValue(meta)} as its first argument`);
|
|
@@ -191,7 +186,7 @@ export function defineContract(meta, operations) {
|
|
|
191
186
|
throw new LinqBuildError('JL0101',
|
|
192
187
|
'defineContract() compat is an array of peer version strings', '/compat');
|
|
193
188
|
}
|
|
194
|
-
if (!
|
|
189
|
+
if (!isJsonObject(operations)) {
|
|
195
190
|
throw new LinqBuildError('JL0101',
|
|
196
191
|
`defineContract() operations is a plain object of id → operation, got `
|
|
197
192
|
+ `${describeValue(operations)}`, '/operations');
|
|
@@ -235,6 +230,24 @@ export function typedClient(client, contract) {
|
|
|
235
230
|
return client;
|
|
236
231
|
}
|
|
237
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Bind an HTTP client to the contract that types it: `typedClient` plus
|
|
235
|
+
* `bytes` over the opaque operations. Identity at runtime — a local or
|
|
236
|
+
* port client carries no `bytes` and takes `typedClient`.
|
|
237
|
+
* @template C
|
|
238
|
+
* @template T
|
|
239
|
+
* @param {T} client - an `openHttpClient` client
|
|
240
|
+
* @param {C} contract - the pen's contract; the type argument only
|
|
241
|
+
* @returns {T}
|
|
242
|
+
* @example
|
|
243
|
+
* const api = typedHttpClient(openHttpClient(compileContract(shop.document), { baseUrl }), shop);
|
|
244
|
+
* const outcome = await api.bytes('image.bytes', { id: 3 }); // ok: { status, headers, media, body: ReadableStream }
|
|
245
|
+
*/
|
|
246
|
+
export function typedHttpClient(client, contract) {
|
|
247
|
+
void contract;
|
|
248
|
+
return client;
|
|
249
|
+
}
|
|
250
|
+
|
|
238
251
|
/**
|
|
239
252
|
* Bind a handler table to the contract it serves. Identity at runtime:
|
|
240
253
|
* the table is checked against the operation ids and each handler's
|
package/src/contract/index.js
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
* earlier than the compiler's `JC0008`), `error()` one entry of an
|
|
12
12
|
* operation's `errors`.
|
|
13
13
|
*
|
|
14
|
-
* The
|
|
15
|
-
* `typedTools` — carry the inferred `Operations` type
|
|
16
|
-
*
|
|
17
|
-
* projection. The document is the deliverable; nothing here imports
|
|
14
|
+
* The identity wrappers — `typedClient`, `typedHttpClient`,
|
|
15
|
+
* `typedHandlers`, `typedTools` — carry the inferred `Operations` type
|
|
16
|
+
* onto a client (an HTTP one with its byte method), a handler table and
|
|
17
|
+
* an AI toolbox without running the TypeScript projection. The document is the deliverable; nothing here imports
|
|
18
18
|
* `@jarenjs/contract` or an engine.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
-
export { defineContract, typedClient, typedHandlers, typedTools, Contract } from './define.js';
|
|
21
|
+
export { defineContract, typedClient, typedHttpClient, typedHandlers, typedTools, Contract } from './define.js';
|
|
22
22
|
export { read, command, subscribe, error } from './operation.js';
|
|
23
23
|
export { http } from './http.js';
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* nothing.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
import { isJsonObject } from '@jarenjs/core/object';
|
|
23
24
|
import { LinqBuildError } from '../errors.js';
|
|
24
25
|
import { describeValue } from '../json-boundary.js';
|
|
25
26
|
|
|
@@ -57,11 +58,6 @@ export const POLICY_VALUES = Object.freeze({
|
|
|
57
58
|
/** An error code: `^[a-z][a-z0-9-]*$` (§3's table). */
|
|
58
59
|
const CODE = /^[a-z][a-z0-9-]*$/;
|
|
59
60
|
|
|
60
|
-
/** @param {any} value */
|
|
61
|
-
function isPlainObject(value) {
|
|
62
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
61
|
/**
|
|
66
62
|
* A member set the pen knows, or `JL0101` naming the one it does not.
|
|
67
63
|
* @param {any} spec
|
|
@@ -104,7 +100,7 @@ function policyMember(member, value, at) {
|
|
|
104
100
|
return value;
|
|
105
101
|
}
|
|
106
102
|
if (member === 'limits') {
|
|
107
|
-
if (!
|
|
103
|
+
if (!isJsonObject(value)) {
|
|
108
104
|
throw new LinqBuildError('JL0101', 'policy.limits is { maxBodyBytes }', at);
|
|
109
105
|
}
|
|
110
106
|
closedTo(value, ['maxBodyBytes'], 'policy.limits', at);
|
|
@@ -116,7 +112,7 @@ function policyMember(member, value, at) {
|
|
|
116
112
|
return { maxBodyBytes: value.maxBodyBytes };
|
|
117
113
|
}
|
|
118
114
|
if (member === 'errors') {
|
|
119
|
-
if (!
|
|
115
|
+
if (!isJsonObject(value)) {
|
|
120
116
|
throw new LinqBuildError('JL0101', 'policy.errors is { details }', at);
|
|
121
117
|
}
|
|
122
118
|
closedTo(value, ['details'], 'policy.errors', at);
|
|
@@ -128,7 +124,7 @@ function policyMember(member, value, at) {
|
|
|
128
124
|
return { details: value.details };
|
|
129
125
|
}
|
|
130
126
|
if (member === 'retry') {
|
|
131
|
-
if (!
|
|
127
|
+
if (!isJsonObject(value)) {
|
|
132
128
|
throw new LinqBuildError('JL0101', 'policy.retry is { max, on }', at);
|
|
133
129
|
}
|
|
134
130
|
closedTo(value, ['max', 'on'], 'policy.retry', at);
|
|
@@ -144,7 +140,7 @@ function policyMember(member, value, at) {
|
|
|
144
140
|
return { max: value.max, on: value.on.slice() };
|
|
145
141
|
}
|
|
146
142
|
// stream
|
|
147
|
-
if (!
|
|
143
|
+
if (!isJsonObject(value)) {
|
|
148
144
|
throw new LinqBuildError('JL0101', 'policy.stream is { resume?, heartbeatMs?, maxPatchBytes? }', at);
|
|
149
145
|
}
|
|
150
146
|
closedTo(value, ['resume', 'heartbeatMs', 'maxPatchBytes'], 'policy.stream', at);
|
|
@@ -184,7 +180,7 @@ function policyMember(member, value, at) {
|
|
|
184
180
|
* @returns {any}
|
|
185
181
|
*/
|
|
186
182
|
export function emitPolicy(policy, at) {
|
|
187
|
-
if (!
|
|
183
|
+
if (!isJsonObject(policy)) {
|
|
188
184
|
throw new LinqBuildError('JL0101',
|
|
189
185
|
`policy is a plain object of the members CONTRACT-FORMAT §3.1 declares, got `
|
|
190
186
|
+ `${describeValue(policy)}`, at);
|
|
@@ -210,7 +206,7 @@ export function emitPolicy(policy, at) {
|
|
|
210
206
|
* error({ status: 409, schema: Conflict });
|
|
211
207
|
*/
|
|
212
208
|
export function error(spec = {}) {
|
|
213
|
-
if (!
|
|
209
|
+
if (!isJsonObject(spec)) {
|
|
214
210
|
throw new LinqBuildError('JL0101',
|
|
215
211
|
`error() takes { status?, schema? }, got ${describeValue(spec)}`);
|
|
216
212
|
}
|
|
@@ -235,7 +231,7 @@ export function error(spec = {}) {
|
|
|
235
231
|
* @returns {[string, any][]} code → `{ status?, schema? }`, in declaration order
|
|
236
232
|
*/
|
|
237
233
|
export function readErrors(errors, at) {
|
|
238
|
-
if (!
|
|
234
|
+
if (!isJsonObject(errors)) {
|
|
239
235
|
throw new LinqBuildError('JL0101',
|
|
240
236
|
`errors is a plain object of code → error(), got ${describeValue(errors)}`, at);
|
|
241
237
|
}
|
|
@@ -245,7 +241,7 @@ export function readErrors(errors, at) {
|
|
|
245
241
|
`an error code matches ^[a-z][a-z0-9-]*$, got '${code}'`, `${at}/${code}`);
|
|
246
242
|
}
|
|
247
243
|
const declared = errors[code];
|
|
248
|
-
if (!
|
|
244
|
+
if (!isJsonObject(declared)) {
|
|
249
245
|
throw new LinqBuildError('JL0101',
|
|
250
246
|
`errors.${code} is error({ status?, schema? }), got ${describeValue(declared)}`,
|
|
251
247
|
`${at}/${code}`);
|
|
@@ -270,7 +266,7 @@ export function readErrors(errors, at) {
|
|
|
270
266
|
*/
|
|
271
267
|
export function checkOperation(kind, spec, at) {
|
|
272
268
|
const base = at ?? '';
|
|
273
|
-
if (!
|
|
269
|
+
if (!isJsonObject(spec)) {
|
|
274
270
|
throw new LinqBuildError('JL0101',
|
|
275
271
|
`${kind}() takes { input?, output, errors?, policy?, http?, doc? }, got `
|
|
276
272
|
+ `${describeValue(spec)}`, at);
|
package/src/db/handle.js
CHANGED
|
@@ -56,6 +56,9 @@ export function createEntityHandle(store, name) {
|
|
|
56
56
|
const members = { ...set };
|
|
57
57
|
chainStart(set, members);
|
|
58
58
|
members.include = (pick, spec) => new Graph(set, name).include(pick, spec);
|
|
59
|
+
// the graph with nothing included: the root clauses, the keyset and
|
|
60
|
+
// the page over the rows alone
|
|
61
|
+
members.graph = () => new Graph(set, name);
|
|
59
62
|
members.link = (own, member, target) => {
|
|
60
63
|
requireMembership(set.relations, name, member, 'link');
|
|
61
64
|
set.link(own, member, target);
|
package/src/db/include.js
CHANGED
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* `explainLoad(spec)`: the one-statement guarantee is the store's. A
|
|
11
11
|
* spec is plain JSON in a fixed member order (`where, orderBy, take,
|
|
12
12
|
* skip, after, maxDepth, include` at the root; `where, orderBy, take,
|
|
13
|
-
* skip, count, include` in an include — a keyset
|
|
14
|
-
* root alone), so one graph is one document;
|
|
15
|
-
* document, as a pen's is.
|
|
13
|
+
* skip, count, maxRows, maxBytes, include` in an include — a keyset
|
|
14
|
+
* cursor paginates the root alone), so one graph is one document;
|
|
15
|
+
* `toJSON()` is that document, as a pen's is. `cursor()` is
|
|
16
|
+
* `loadCursor(spec)`: one root graph per pull.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
|
|
@@ -23,7 +24,7 @@ import { requireJson, describeValue } from '../json-boundary.js';
|
|
|
23
24
|
|
|
24
25
|
const NO_PARAMS = new Set();
|
|
25
26
|
const ROOT_KEYS = ['where', 'orderBy', 'take', 'skip', 'after', 'maxDepth', 'include'];
|
|
26
|
-
const INCLUDE_KEYS = ['where', 'orderBy', 'take', 'skip', 'count', 'include'];
|
|
27
|
+
const INCLUDE_KEYS = ['where', 'orderBy', 'take', 'skip', 'count', 'maxRows', 'maxBytes', 'include'];
|
|
27
28
|
/** A member path over the row: the shorthand or the bracketed spelling. */
|
|
28
29
|
const MEMBER_PATH = /^\$it(?:\.([A-Za-z_$][\w$]*)|\['((?:[^'\\]|\\.)*)'\])$/;
|
|
29
30
|
|
|
@@ -158,6 +159,12 @@ function lowerInclude(spec, entityName, relationsOf, path) {
|
|
|
158
159
|
if (spec.count !== true) throw new LinqBuildError('JL0101', `the include spec at ${at}: count takes true`);
|
|
159
160
|
out.count = true;
|
|
160
161
|
}
|
|
162
|
+
for (const key of ['maxRows', 'maxBytes']) {
|
|
163
|
+
// the per-root bound (MODEL-FORMAT §10.4); `Infinity` is the spelled
|
|
164
|
+
// unbounded case, and its JSON form is `null`
|
|
165
|
+
if (spec[key] === undefined) continue;
|
|
166
|
+
out[key] = spec[key] === Infinity ? null : requireJson(spec[key], `${at} ${key}`);
|
|
167
|
+
}
|
|
161
168
|
if (spec.include !== undefined) out.include = lowerIncludes(spec.include, entityName, relationsOf, path);
|
|
162
169
|
return out;
|
|
163
170
|
}
|
|
@@ -276,7 +283,9 @@ export class Graph {
|
|
|
276
283
|
/** @param {number} count */
|
|
277
284
|
skip(count) { return this.#with({ skip: requireJson(count, 'skip()') }); }
|
|
278
285
|
|
|
279
|
-
/** The keyset
|
|
286
|
+
/** The keyset continuation (§10.5): the `{ order, keys, key }` value a
|
|
287
|
+
* page emitted — a bare unique-column value is the single-column form.
|
|
288
|
+
* @param {any} cursor */
|
|
280
289
|
after(cursor) { return this.#with({ after: requireJson(cursor, 'after()') }); }
|
|
281
290
|
|
|
282
291
|
/** The include depth bound (§10.4). @param {number} depth */
|
|
@@ -309,6 +318,32 @@ export class Graph {
|
|
|
309
318
|
return this.#tracking ? this.#set.load(spec) : this.#set.asNoTracking().load(spec);
|
|
310
319
|
}
|
|
311
320
|
|
|
321
|
+
/**
|
|
322
|
+
* `loadCursor(spec, options)` — the store's graph cursor: one root
|
|
323
|
+
* graph per pull, its includes attached and bounded, from the same one
|
|
324
|
+
* statement; `return()` releases it. Untracked unless `tracking: true`
|
|
325
|
+
* is spelled per call — a snapshot per yielded root is a unit of work
|
|
326
|
+
* that grows with the result.
|
|
327
|
+
* @param {{ signal?: AbortSignal, tracking?: boolean }} [options]
|
|
328
|
+
*/
|
|
329
|
+
cursor(options) {
|
|
330
|
+
return this.#set.loadCursor(this.toSpec(), options);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* `page(spec, options)` — the store's bounded page over the composite
|
|
335
|
+
* keyset (§10.5): `{ items, continuation, hasMore, snapshot }`, never
|
|
336
|
+
* more than `limit` roots or `maxBytes` serialised bytes, the
|
|
337
|
+
* continuation unsigned and structural. Untracked unless `tracking:
|
|
338
|
+
* true`.
|
|
339
|
+
* @param {{ limit?: number, after?: any, maxBytes?: number,
|
|
340
|
+
* consistency?: 'live' | 'snapshot', signal?: AbortSignal,
|
|
341
|
+
* tracking?: boolean }} [options]
|
|
342
|
+
*/
|
|
343
|
+
page(options) {
|
|
344
|
+
return this.#set.page(this.toSpec(), options);
|
|
345
|
+
}
|
|
346
|
+
|
|
312
347
|
/** `explainLoad(spec)` — the SQL, the includes, the pagination strategy. */
|
|
313
348
|
explain() {
|
|
314
349
|
return this.#set.explainLoad(this.toSpec());
|
package/src/db/index.js
CHANGED
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* duplicates no algorithm — `include` emits the spec the store runs,
|
|
15
15
|
* membership is the store's own `link`/`unlink`, `live` is the store's
|
|
16
16
|
* registration — and every read is one an `explain()` can name.
|
|
17
|
+
* `createDbLedger` is the contract ledger over a declared collection of
|
|
18
|
+
* the client's store, through that same surface: the durable idempotency
|
|
19
|
+
* ledger a host was left to write, with no edge from `@jarenjs/contract`
|
|
20
|
+
* to a store (DB-CLIENT.md §2.6).
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
export { open, defaultValidator } from './open.js';
|
|
24
|
+
export { createDbLedger } from './ledger.js';
|
|
25
|
+
export { defineReplication } from './replication.js';
|