@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.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. 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
+ }
@@ -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
+ }