@jarenjs/linq 0.49.2 → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +217 -0
- package/README.md +559 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1217 -0
- package/docs/DB-CLIENT.md +814 -0
- package/docs/FLOW-PEN.md +1026 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +771 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1083 -0
- package/docs/QUERY-PEN.md +1636 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +255 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +260 -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 +329 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +9 -4
- package/src/contract/define.js +269 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +342 -0
- package/src/db/handle.js +86 -0
- package/src/db/include.js +316 -0
- package/src/db/index.js +19 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +82 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +69 -6
- package/src/expression.js +437 -36
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +302 -0
- package/src/flow/fsm.js +328 -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 +4 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +207 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +323 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +248 -0
- package/src/model/collection.js +171 -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 +371 -0
- package/types/db.d.ts +188 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +231 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +493 -0
- package/types/schema.d.ts +494 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The operation declarations of the contract pen — `read()`,
|
|
4
|
+
* `command()`, `subscribe()` (CONTRACT-FORMAT §3's three kinds) and
|
|
5
|
+
* `error()` (an entry of an operation's `errors` map) — plus the policy
|
|
6
|
+
* vocabulary §3.1 fixes.
|
|
7
|
+
*
|
|
8
|
+
* These write no document: they carry a checked spec that
|
|
9
|
+
* `defineContract` lowers, because the schemas an operation names are
|
|
10
|
+
* hoisted into the contract's own `$defs` and only the whole document
|
|
11
|
+
* knows them. The pen refuses its own surface (a member it does not
|
|
12
|
+
* know, a policy value outside its declared set) and leaves the
|
|
13
|
+
* format's cross-member rules — a read that declares idempotency, a
|
|
14
|
+
* body-located member on a GET, two operations sharing a route shape —
|
|
15
|
+
* to `compileContract`, which is the only judge of them.
|
|
16
|
+
*
|
|
17
|
+
* No default is ever written: §3.1's defaults are materialized by the
|
|
18
|
+
* compiler and marked inferred by `describe()`. A pen that wrote them
|
|
19
|
+
* would turn every default into a declaration and move the revision for
|
|
20
|
+
* nothing.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { LinqBuildError } from '../errors.js';
|
|
24
|
+
import { describeValue } from '../json-boundary.js';
|
|
25
|
+
|
|
26
|
+
/** The operation brand: how `defineContract` tells a declaration apart. */
|
|
27
|
+
export const OPERATION = Symbol.for('@jarenjs/linq/contract-operation');
|
|
28
|
+
|
|
29
|
+
/** The error-declaration brand. */
|
|
30
|
+
export const ERROR_DECLARATION = Symbol.for('@jarenjs/linq/contract-error');
|
|
31
|
+
|
|
32
|
+
/** The three kinds §3 declares. */
|
|
33
|
+
export const KINDS = Object.freeze(['read', 'command', 'subscribe']);
|
|
34
|
+
|
|
35
|
+
/** The members an operation spec accepts, in the order §12.1 fixes. */
|
|
36
|
+
export const OPERATION_MEMBERS = Object.freeze(['input', 'output', 'errors', 'policy', 'http', 'doc']);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The policy members, in the order the pen writes them: §12.1's public
|
|
40
|
+
* order with the two server-side knobs (`limits`, `errors`) in their
|
|
41
|
+
* §3.1 positions — the projection drops those two, so their place is
|
|
42
|
+
* the source document's own.
|
|
43
|
+
*/
|
|
44
|
+
export const POLICY_MEMBERS = Object.freeze([
|
|
45
|
+
'task', 'idempotency', 'revision', 'cache', 'limits', 'errors', 'retry', 'stream', 'audience',
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
/** The value sets §3.1's table declares, by member. */
|
|
49
|
+
export const POLICY_VALUES = Object.freeze({
|
|
50
|
+
__proto__: null,
|
|
51
|
+
task: ['switch', 'exhaust', 'concat', 'parallel'],
|
|
52
|
+
idempotency: ['none', 'optional', 'required'],
|
|
53
|
+
cache: ['none', 'revision'],
|
|
54
|
+
audience: ['public', 'server'],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** An error code: `^[a-z][a-z0-9-]*$` (§3's table). */
|
|
58
|
+
const CODE = /^[a-z][a-z0-9-]*$/;
|
|
59
|
+
|
|
60
|
+
/** @param {any} value */
|
|
61
|
+
function isPlainObject(value) {
|
|
62
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A member set the pen knows, or `JL0101` naming the one it does not.
|
|
67
|
+
* @param {any} spec
|
|
68
|
+
* @param {readonly string[]} members
|
|
69
|
+
* @param {string} what
|
|
70
|
+
* @param {string} at
|
|
71
|
+
*/
|
|
72
|
+
function closedTo(spec, members, what, at) {
|
|
73
|
+
for (const key of Object.keys(spec)) {
|
|
74
|
+
if (!members.includes(key)) {
|
|
75
|
+
throw new LinqBuildError('JL0101',
|
|
76
|
+
`${what} does not take '${key}' — it takes ${members.join(', ')}`, `${at}/${key}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One entry of `policy.retry`/`policy.stream`/`policy.limits`/
|
|
83
|
+
* `policy.errors`, checked against §3.1's table.
|
|
84
|
+
* @param {string} member
|
|
85
|
+
* @param {any} value
|
|
86
|
+
* @param {string} at
|
|
87
|
+
* @returns {any} the member's emitted value
|
|
88
|
+
*/
|
|
89
|
+
function policyMember(member, value, at) {
|
|
90
|
+
const set = POLICY_VALUES[member];
|
|
91
|
+
if (set !== undefined) {
|
|
92
|
+
if (!set.includes(value)) {
|
|
93
|
+
throw new LinqBuildError('JL0101',
|
|
94
|
+
`policy.${member} is one of ${set.join(', ')}, got ${describeValue(value)}`, at);
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
}
|
|
98
|
+
if (member === 'revision') {
|
|
99
|
+
if (typeof value !== 'string' || !value.startsWith('input:')) {
|
|
100
|
+
throw new LinqBuildError('JL0101',
|
|
101
|
+
'policy.revision is "input:<json-pointer>" — where in the input the revision a '
|
|
102
|
+
+ `command asserts lives, got ${describeValue(value)}`, at);
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
if (member === 'limits') {
|
|
107
|
+
if (!isPlainObject(value)) {
|
|
108
|
+
throw new LinqBuildError('JL0101', 'policy.limits is { maxBodyBytes }', at);
|
|
109
|
+
}
|
|
110
|
+
closedTo(value, ['maxBodyBytes'], 'policy.limits', at);
|
|
111
|
+
if (!Number.isInteger(value.maxBodyBytes) || value.maxBodyBytes <= 0) {
|
|
112
|
+
throw new LinqBuildError('JL0101',
|
|
113
|
+
`policy.limits.maxBodyBytes is a positive integer, got ${describeValue(value.maxBodyBytes)}`,
|
|
114
|
+
`${at}/maxBodyBytes`);
|
|
115
|
+
}
|
|
116
|
+
return { maxBodyBytes: value.maxBodyBytes };
|
|
117
|
+
}
|
|
118
|
+
if (member === 'errors') {
|
|
119
|
+
if (!isPlainObject(value)) {
|
|
120
|
+
throw new LinqBuildError('JL0101', 'policy.errors is { details }', at);
|
|
121
|
+
}
|
|
122
|
+
closedTo(value, ['details'], 'policy.errors', at);
|
|
123
|
+
if (!['none', 'paths', 'full'].includes(value.details)) {
|
|
124
|
+
throw new LinqBuildError('JL0101',
|
|
125
|
+
`policy.errors.details is one of none, paths, full, got ${describeValue(value.details)}`,
|
|
126
|
+
`${at}/details`);
|
|
127
|
+
}
|
|
128
|
+
return { details: value.details };
|
|
129
|
+
}
|
|
130
|
+
if (member === 'retry') {
|
|
131
|
+
if (!isPlainObject(value)) {
|
|
132
|
+
throw new LinqBuildError('JL0101', 'policy.retry is { max, on }', at);
|
|
133
|
+
}
|
|
134
|
+
closedTo(value, ['max', 'on'], 'policy.retry', at);
|
|
135
|
+
if (!Number.isInteger(value.max) || value.max < 0) {
|
|
136
|
+
throw new LinqBuildError('JL0101',
|
|
137
|
+
`policy.retry.max is an integer ≥ 0, got ${describeValue(value.max)}`, `${at}/max`);
|
|
138
|
+
}
|
|
139
|
+
if (!Array.isArray(value.on) || value.on.some((code) => typeof code !== 'string')) {
|
|
140
|
+
throw new LinqBuildError('JL0101',
|
|
141
|
+
'policy.retry.on is an array of error codes (declared codes, or JC2xxx taxonomy codes)',
|
|
142
|
+
`${at}/on`);
|
|
143
|
+
}
|
|
144
|
+
return { max: value.max, on: value.on.slice() };
|
|
145
|
+
}
|
|
146
|
+
// stream
|
|
147
|
+
if (!isPlainObject(value)) {
|
|
148
|
+
throw new LinqBuildError('JL0101', 'policy.stream is { resume?, heartbeatMs?, maxPatchBytes? }', at);
|
|
149
|
+
}
|
|
150
|
+
closedTo(value, ['resume', 'heartbeatMs', 'maxPatchBytes'], 'policy.stream', at);
|
|
151
|
+
const stream = {};
|
|
152
|
+
if (value.resume !== undefined) {
|
|
153
|
+
if (value.resume !== 'snapshot' && value.resume !== 'replay') {
|
|
154
|
+
throw new LinqBuildError('JL0101',
|
|
155
|
+
`policy.stream.resume is snapshot or replay, got ${describeValue(value.resume)}`,
|
|
156
|
+
`${at}/resume`);
|
|
157
|
+
}
|
|
158
|
+
stream.resume = value.resume;
|
|
159
|
+
}
|
|
160
|
+
if (value.heartbeatMs !== undefined) {
|
|
161
|
+
if (!Number.isInteger(value.heartbeatMs) || value.heartbeatMs < 1000) {
|
|
162
|
+
throw new LinqBuildError('JL0101',
|
|
163
|
+
`policy.stream.heartbeatMs is an integer ≥ 1000, got ${describeValue(value.heartbeatMs)}`,
|
|
164
|
+
`${at}/heartbeatMs`);
|
|
165
|
+
}
|
|
166
|
+
stream.heartbeatMs = value.heartbeatMs;
|
|
167
|
+
}
|
|
168
|
+
if (value.maxPatchBytes !== undefined) {
|
|
169
|
+
if (!Number.isInteger(value.maxPatchBytes) || value.maxPatchBytes <= 0) {
|
|
170
|
+
throw new LinqBuildError('JL0101',
|
|
171
|
+
'policy.stream.maxPatchBytes is a positive integer, got '
|
|
172
|
+
+ `${describeValue(value.maxPatchBytes)}`, `${at}/maxPatchBytes`);
|
|
173
|
+
}
|
|
174
|
+
stream.maxPatchBytes = value.maxPatchBytes;
|
|
175
|
+
}
|
|
176
|
+
return stream;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* One operation's `policy`, in the pen's member order, declared members
|
|
181
|
+
* only.
|
|
182
|
+
* @param {any} policy
|
|
183
|
+
* @param {string} at
|
|
184
|
+
* @returns {any}
|
|
185
|
+
*/
|
|
186
|
+
export function emitPolicy(policy, at) {
|
|
187
|
+
if (!isPlainObject(policy)) {
|
|
188
|
+
throw new LinqBuildError('JL0101',
|
|
189
|
+
`policy is a plain object of the members CONTRACT-FORMAT §3.1 declares, got `
|
|
190
|
+
+ `${describeValue(policy)}`, at);
|
|
191
|
+
}
|
|
192
|
+
closedTo(policy, POLICY_MEMBERS, 'policy', at);
|
|
193
|
+
const out = {};
|
|
194
|
+
for (const member of POLICY_MEMBERS) {
|
|
195
|
+
if (policy[member] === undefined) continue;
|
|
196
|
+
out[member] = policyMember(member, policy[member], `${at}/${member}`);
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* One entry of an operation's `errors` map — `{ status?, schema? }`.
|
|
203
|
+
* The `schema` may be a schema-pen builder (hoisted into the contract's
|
|
204
|
+
* `$defs` like any other) or a JSON Schema written by hand.
|
|
205
|
+
*
|
|
206
|
+
* @param {any} [spec] - `{ status?, schema? }`
|
|
207
|
+
* @returns {any} the declaration, frozen and branded
|
|
208
|
+
* @throws {LinqBuildError} `JL0101` a member the declaration does not take
|
|
209
|
+
* @example
|
|
210
|
+
* error({ status: 409, schema: Conflict });
|
|
211
|
+
*/
|
|
212
|
+
export function error(spec = {}) {
|
|
213
|
+
if (!isPlainObject(spec)) {
|
|
214
|
+
throw new LinqBuildError('JL0101',
|
|
215
|
+
`error() takes { status?, schema? }, got ${describeValue(spec)}`);
|
|
216
|
+
}
|
|
217
|
+
closedTo(spec, ['status', 'schema'], 'error()', '');
|
|
218
|
+
if (spec.status !== undefined
|
|
219
|
+
&& (!Number.isInteger(spec.status) || spec.status < 100 || spec.status > 599)) {
|
|
220
|
+
throw new LinqBuildError('JL0101',
|
|
221
|
+
`error() status is an integer in 100–599, got ${describeValue(spec.status)}`, '/status');
|
|
222
|
+
}
|
|
223
|
+
const out = {};
|
|
224
|
+
if (spec.status !== undefined) out.status = spec.status;
|
|
225
|
+
if (spec.schema !== undefined) out.schema = spec.schema;
|
|
226
|
+
Object.defineProperty(out, ERROR_DECLARATION, { value: true, enumerable: false });
|
|
227
|
+
return Object.freeze(out);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The `errors` map of an operation, checked: codes match §3's pattern,
|
|
232
|
+
* every entry is an `error()` declaration or the same members by hand.
|
|
233
|
+
* @param {any} errors
|
|
234
|
+
* @param {string} at
|
|
235
|
+
* @returns {[string, any][]} code → `{ status?, schema? }`, in declaration order
|
|
236
|
+
*/
|
|
237
|
+
export function readErrors(errors, at) {
|
|
238
|
+
if (!isPlainObject(errors)) {
|
|
239
|
+
throw new LinqBuildError('JL0101',
|
|
240
|
+
`errors is a plain object of code → error(), got ${describeValue(errors)}`, at);
|
|
241
|
+
}
|
|
242
|
+
return Object.keys(errors).map((code) => {
|
|
243
|
+
if (!CODE.test(code)) {
|
|
244
|
+
throw new LinqBuildError('JL0101',
|
|
245
|
+
`an error code matches ^[a-z][a-z0-9-]*$, got '${code}'`, `${at}/${code}`);
|
|
246
|
+
}
|
|
247
|
+
const declared = errors[code];
|
|
248
|
+
if (!isPlainObject(declared)) {
|
|
249
|
+
throw new LinqBuildError('JL0101',
|
|
250
|
+
`errors.${code} is error({ status?, schema? }), got ${describeValue(declared)}`,
|
|
251
|
+
`${at}/${code}`);
|
|
252
|
+
}
|
|
253
|
+
return [code, declared[ERROR_DECLARATION] === true ? declared : error(declared)];
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* One operation spec, checked against §3's member set. There are two
|
|
259
|
+
* doors into `defineContract`'s emitter and both run this: a
|
|
260
|
+
* `read()`/`command()`/`subscribe()` declaration, which has no position
|
|
261
|
+
* in the document yet, and an operation written by hand as `{ kind,
|
|
262
|
+
* …members }`, which does. A member this pen does not know must not
|
|
263
|
+
* reach the document whichever door it came through — the pen emits only
|
|
264
|
+
* what it was given, so an unchecked member is either dropped in silence
|
|
265
|
+
* or written into a document the grammar refuses.
|
|
266
|
+
* @param {string} kind
|
|
267
|
+
* @param {any} spec
|
|
268
|
+
* @param {string} [at] - the docPath of the operation being assembled;
|
|
269
|
+
* absent at declaration, where the operation has no id yet
|
|
270
|
+
*/
|
|
271
|
+
export function checkOperation(kind, spec, at) {
|
|
272
|
+
const base = at ?? '';
|
|
273
|
+
if (!isPlainObject(spec)) {
|
|
274
|
+
throw new LinqBuildError('JL0101',
|
|
275
|
+
`${kind}() takes { input?, output, errors?, policy?, http?, doc? }, got `
|
|
276
|
+
+ `${describeValue(spec)}`, at);
|
|
277
|
+
}
|
|
278
|
+
closedTo(spec, OPERATION_MEMBERS, `${kind}()`, base);
|
|
279
|
+
if (spec.output === undefined) {
|
|
280
|
+
throw new LinqBuildError('JL0101',
|
|
281
|
+
`${kind}() needs an output — every operation declares one (true for "any value")`,
|
|
282
|
+
`${base}/output`);
|
|
283
|
+
}
|
|
284
|
+
if (spec.doc !== undefined && typeof spec.doc !== 'string') {
|
|
285
|
+
throw new LinqBuildError('JL0101',
|
|
286
|
+
`${kind}() doc is a string, got ${describeValue(spec.doc)}`, `${base}/doc`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* One operation declaration of `kind`.
|
|
292
|
+
* @param {string} kind
|
|
293
|
+
* @param {any} spec
|
|
294
|
+
* @returns {any}
|
|
295
|
+
*/
|
|
296
|
+
function operation(kind, spec) {
|
|
297
|
+
checkOperation(kind, spec);
|
|
298
|
+
const out = { kind, spec };
|
|
299
|
+
Object.defineProperty(out, OPERATION, { value: true, enumerable: false });
|
|
300
|
+
return Object.freeze(out);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* A `read` operation: a query whose result may be cached and whose
|
|
305
|
+
* input members default to the query string.
|
|
306
|
+
* @param {any} spec - `{ input?, output, errors?, policy?, http?, doc? }`
|
|
307
|
+
* @returns {any}
|
|
308
|
+
* @example
|
|
309
|
+
* read({ output: Catalog, http: http({ method: 'GET', path: '/api/catalog' }) });
|
|
310
|
+
*/
|
|
311
|
+
export function read(spec) { return operation('read', spec); }
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* A `command` operation: a state change whose input members default to
|
|
315
|
+
* the request body.
|
|
316
|
+
* @param {any} spec - `{ input?, output, errors?, policy?, http?, doc? }`
|
|
317
|
+
* @returns {any}
|
|
318
|
+
* @example
|
|
319
|
+
* command({ input: SaveInput, output: Product, errors: { conflict: error({ status: 409 }) } });
|
|
320
|
+
*/
|
|
321
|
+
export function command(spec) { return operation('command', spec); }
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* A `subscribe` operation (§17): its `output` is the snapshot schema
|
|
325
|
+
* and its emissions travel the stream binding. The compiler enforces
|
|
326
|
+
* the shape the wire requires (`GET`, `task: switch`, `idempotency:
|
|
327
|
+
* none`, the forced media); the pen writes what is declared.
|
|
328
|
+
* @param {any} spec - `{ input?, output, errors?, policy?, http?, doc? }`
|
|
329
|
+
* @returns {any}
|
|
330
|
+
* @example
|
|
331
|
+
* subscribe({ output: Board, policy: { stream: { resume: 'replay' } } });
|
|
332
|
+
*/
|
|
333
|
+
export function subscribe(spec) { return operation('subscribe', spec); }
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Whether a value is an operation declaration.
|
|
337
|
+
* @param {any} value
|
|
338
|
+
* @returns {boolean}
|
|
339
|
+
*/
|
|
340
|
+
export function isOperation(value) {
|
|
341
|
+
return value !== null && typeof value === 'object' && value[OPERATION] === true;
|
|
342
|
+
}
|
package/src/db/handle.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The handles the client fronts the store with. An entity handle
|
|
4
|
+
* is the store's entity set — every member of it, the provider members
|
|
5
|
+
* included, so `fromAsync(handle)` still binds through its root and two
|
|
6
|
+
* handles of one client still share a scope — plus the chain start
|
|
7
|
+
* (every `AsyncSequence` operator and terminal, delegated to
|
|
8
|
+
* `fromAsync(set)`: nothing is duplicated, every read is the chain and
|
|
9
|
+
* pushes down), `include` (the graph builder that EMITS the store's
|
|
10
|
+
* `load` spec), `link`/`unlink` (checked against the relation table
|
|
11
|
+
* before the store records them) and `live`. A collection handle is the
|
|
12
|
+
* collection with the same chain start and `live`. `explain()` without
|
|
13
|
+
* a document explains the empty chain; with one it is the store's
|
|
14
|
+
* explanation of that document.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { fromAsync, AsyncSequence } from '../async.js';
|
|
18
|
+
import { Graph } from './include.js';
|
|
19
|
+
import { requireMembership } from './membership.js';
|
|
20
|
+
import { registerLive } from './live.js';
|
|
21
|
+
|
|
22
|
+
/** The chain surface, read once from the class: every public operator
|
|
23
|
+
* and terminal, `explain` set aside for its overload. */
|
|
24
|
+
const CHAIN_MEMBERS = Object.getOwnPropertyNames(AsyncSequence.prototype)
|
|
25
|
+
.filter((name) => name !== 'constructor' && name !== 'explain');
|
|
26
|
+
const CHAIN_SYMBOLS = Object.getOwnPropertySymbols(AsyncSequence.prototype);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Give a handle the chain start over a provider: each member is
|
|
30
|
+
* `fromAsync(provider)`'s, so the handle and the chain are one surface.
|
|
31
|
+
* @param {any} provider - the store's handle (a collection or an entity set)
|
|
32
|
+
* @param {Record<string | symbol, any>} members - the handle being built
|
|
33
|
+
*/
|
|
34
|
+
function chainStart(provider, members) {
|
|
35
|
+
for (const name of CHAIN_MEMBERS) {
|
|
36
|
+
members[name] = (...args) => fromAsync(provider)[name](...args);
|
|
37
|
+
}
|
|
38
|
+
for (const symbol of CHAIN_SYMBOLS) {
|
|
39
|
+
members[symbol] = () => fromAsync(provider)[symbol]();
|
|
40
|
+
}
|
|
41
|
+
const explainDocument = provider.explain;
|
|
42
|
+
members.explain = (document, options) => (document === undefined
|
|
43
|
+
? fromAsync(provider).explain()
|
|
44
|
+
: explainDocument(document, options));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The handle over one entity set.
|
|
49
|
+
* @param {any} store - the opened store
|
|
50
|
+
* @param {string} name - the entity
|
|
51
|
+
* @returns {any}
|
|
52
|
+
*/
|
|
53
|
+
export function createEntityHandle(store, name) {
|
|
54
|
+
const set = store.entity(name);
|
|
55
|
+
/** @type {Record<string | symbol, any>} */
|
|
56
|
+
const members = { ...set };
|
|
57
|
+
chainStart(set, members);
|
|
58
|
+
members.include = (pick, spec) => new Graph(set, name).include(pick, spec);
|
|
59
|
+
members.link = (own, member, target) => {
|
|
60
|
+
requireMembership(set.relations, name, member, 'link');
|
|
61
|
+
set.link(own, member, target);
|
|
62
|
+
};
|
|
63
|
+
members.unlink = (own, member, target) => {
|
|
64
|
+
requireMembership(set.relations, name, member, 'unlink');
|
|
65
|
+
set.unlink(own, member, target);
|
|
66
|
+
};
|
|
67
|
+
members.live = (source = fromAsync(set), options = undefined) =>
|
|
68
|
+
registerLive(store.live, source, options);
|
|
69
|
+
return Object.freeze(members);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The handle over one collection.
|
|
74
|
+
* @param {any} store - the opened store
|
|
75
|
+
* @param {string} name - the collection
|
|
76
|
+
* @returns {any}
|
|
77
|
+
*/
|
|
78
|
+
export function createCollectionHandle(store, name) {
|
|
79
|
+
const collection = store.collection(name);
|
|
80
|
+
/** @type {Record<string | symbol, any>} */
|
|
81
|
+
const members = { ...collection };
|
|
82
|
+
chainStart(collection, members);
|
|
83
|
+
members.live = (source = fromAsync(collection), options = undefined) =>
|
|
84
|
+
registerLive(collection.live, source, options);
|
|
85
|
+
return Object.freeze(members);
|
|
86
|
+
}
|