@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,277 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The six RFC 6902 operations of a transition's `patch`
4
+ * (APP-FORMAT §3.2), written by code. A `path` is a captured lambda over
5
+ * the state — `(st) => st.todos` is `/todos`, `st.todos.at(2)` is
6
+ * `/todos/2` — so a pointer is DERIVED from the state shape and a typo
7
+ * is a capture error rather than a silent `JA2004` at dispatch; a
8
+ * pointer string is accepted verbatim for the locations a shape cannot
9
+ * spell.
10
+ *
11
+ * The one place this pen computes a pointer is a NON-literal index: an
12
+ * `at(x.payload.i)` cannot be written as pointer text at build time, so
13
+ * the pen emits the pointer as a string expression — `{ "$concat":
14
+ * ["/todos/", "$payload.i", "/done"] }` — which is exactly what §3.2
15
+ * means by "op members like `value` and `path` are themselves query
16
+ * expressions". A path that is not a chain of member reads and
17
+ * subscripts at all is `JL0102`: the pen will not guess where an
18
+ * arbitrary expression lands in the state.
19
+ *
20
+ * A `value` is left exactly as the caller wrote it — an expression from
21
+ * the enclosing action capture, or a literal — because the action
22
+ * capture is what spells the whole transition.
23
+ */
24
+
25
+ import { LinqBuildError } from '../errors.js';
26
+ import { liftExpression } from '../expression.js';
27
+ import { describeValue } from '../json-boundary.js';
28
+ import { captureAction } from './capture.js';
29
+
30
+ /** RFC 6901: `~` and `/` escape inside a pointer segment. @param {string} segment */
31
+ const escapeSegment = (segment) => segment.replaceAll('~', '~0').replaceAll('/', '~1');
32
+
33
+ /**
34
+ * `JL0102` for a path expression the pen cannot lower to a pointer.
35
+ * @param {any} doc - what the capture produced
36
+ * @returns {never}
37
+ */
38
+ function unlowerable(doc) {
39
+ throw new LinqBuildError('JL0102',
40
+ 'a patch path is a JSON Pointer, and this one cannot be written as one: '
41
+ + `${JSON.stringify(doc)} — a path lambda reads members and subscripts off the state `
42
+ + '(st.todos.at(2).done, st.todos.at(x.payload.i)); anything else, pass the pointer as '
43
+ + 'a string', '/path');
44
+ }
45
+
46
+ /**
47
+ * Lower one RFC 9535 path string — the shape the capture writes — to a
48
+ * JSON Pointer.
49
+ * @param {string} path
50
+ * @returns {string}
51
+ */
52
+ function pointerOf(path) {
53
+ if (path[0] !== '$') unlowerable(path);
54
+ let i = 1;
55
+ let out = '';
56
+ while (i < path.length) {
57
+ if (path[i] === '.') {
58
+ let j = i + 1;
59
+ while (j < path.length && path[j] !== '.' && path[j] !== '[') j++;
60
+ if (j === i + 1) unlowerable(path);
61
+ out += `/${escapeSegment(path.slice(i + 1, j))}`;
62
+ i = j;
63
+ continue;
64
+ }
65
+ if (path[i] !== '[') unlowerable(path);
66
+ if (path[i + 1] === "'") {
67
+ let name = '';
68
+ let j = i + 2;
69
+ for (;;) {
70
+ if (j >= path.length) unlowerable(path);
71
+ const ch = path[j];
72
+ if (ch === "'") break;
73
+ if (ch !== '\\') { name += ch; j++; continue; }
74
+ const esc = path[j + 1];
75
+ if (esc === 'u') {
76
+ name += String.fromCodePoint(Number.parseInt(path.slice(j + 2, j + 6), 16));
77
+ j += 6;
78
+ }
79
+ else { name += esc; j += 2; }
80
+ }
81
+ if (path[j + 1] !== ']') unlowerable(path);
82
+ out += `/${escapeSegment(name)}`;
83
+ i = j + 2;
84
+ continue;
85
+ }
86
+ const close = path.indexOf(']', i);
87
+ const index = path.slice(i + 1, close);
88
+ if (close === -1 || !/^\d+$/.test(index)) unlowerable(path);
89
+ out += `/${index}`;
90
+ i = close + 1;
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * The pieces of a pointer, left to right: pointer TEXT for every segment
97
+ * the capture wrote literally, and one query expression per computed
98
+ * one. A `$get` chain is how the capture spells a subscript it could not
99
+ * fold into the path string, so unwinding it is what turns
100
+ * `st.todos.at(x.payload.i).done` into `/todos/`, the index, `/done`.
101
+ * @param {any} doc - a path string, or a `$get` over one
102
+ * @returns {any[] | null} the pieces, or `null` when this is not a path
103
+ */
104
+ function pointerPieces(doc) {
105
+ if (typeof doc === 'string') return [{ text: pointerOf(doc) }];
106
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)
107
+ || !Array.isArray(doc.$get) || doc.$get.length !== 2) {
108
+ return null;
109
+ }
110
+ const base = pointerPieces(doc.$get[0]);
111
+ if (base === null) return null;
112
+ const key = doc.$get[1];
113
+ // a bare string operand is a literal member name; one starting with
114
+ // `$` is a path, which is a computed segment like any other operator
115
+ if (typeof key === 'string' && key[0] !== '$') return [...base, { text: `/${escapeSegment(key)}` }];
116
+ if (Number.isInteger(key) && key >= 0) return [...base, { text: `/${key}` }];
117
+ return [...base, { text: '/' }, { expr: key }];
118
+ }
119
+
120
+ /**
121
+ * The `path` member of one operation: a pointer string verbatim, or a
122
+ * captured lambda lowered to one — as text where every segment is
123
+ * literal, as a `$concat` expression where one is computed.
124
+ * @param {any} path - `(st, x) => …`, or a JSON Pointer string
125
+ * @param {string} [suffix] - a literal tail, `'/-'` for an array append
126
+ * @returns {any} the pointer, or the expression that builds it
127
+ */
128
+ export function pathMember(path, suffix = '') {
129
+ if (typeof path === 'string') {
130
+ if (path !== '' && path[0] !== '/') {
131
+ throw new LinqBuildError('JL0102',
132
+ `a patch path given as a string is an RFC 6901 JSON Pointer — '${path}' does not `
133
+ + "start with '/'; write a lambda over the state instead, (st) => st.todos", '/path');
134
+ }
135
+ return path + suffix;
136
+ }
137
+ if (typeof path !== 'function') {
138
+ throw new LinqBuildError('JL0101',
139
+ 'a patch path is a lambda over the state — (st) => st.todos — or a JSON Pointer '
140
+ + `string, got ${describeValue(path)}`, '/path');
141
+ }
142
+ const doc = captureAction('a patch path', path);
143
+ const lowered = pointerPieces(doc);
144
+ if (lowered === null) return unlowerable(doc);
145
+ const pieces = suffix === '' ? lowered : [...lowered, { text: suffix }];
146
+ // adjacent literal text is one operand; all-literal is one pointer
147
+ /** @type {any[]} */
148
+ const merged = [];
149
+ /** @type {string | null} */
150
+ let text = null;
151
+ let literal = true;
152
+ for (const piece of pieces) {
153
+ if (piece.expr === undefined) {
154
+ text = text === null ? piece.text : text + piece.text;
155
+ continue;
156
+ }
157
+ if (text !== null) { merged.push(text); text = null; }
158
+ merged.push(piece.expr);
159
+ literal = false;
160
+ }
161
+ if (text !== null) merged.push(text);
162
+ // lifted, not written as data: a `$`-keyed object in a captured tree
163
+ // is a CONSTRUCTOR (the `$map` escape), and this one is an operator
164
+ return literal ? merged[0] : liftExpression({ $concat: merged });
165
+ }
166
+
167
+ /**
168
+ * One operation object, in RFC 6902's member order (`op`, `from`,
169
+ * `path`, `value` — only what applies).
170
+ * @param {string} op
171
+ * @param {any} path
172
+ * @param {{ from?: any, value?: any, hasValue?: boolean, suffix?: string }} parts
173
+ * @returns {any}
174
+ */
175
+ function operation(op, path, parts) {
176
+ const out = { op };
177
+ if (parts.from !== undefined) out.from = pathMember(parts.from);
178
+ out.path = pathMember(path, parts.suffix);
179
+ if (parts.hasValue === true) out.value = parts.value;
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * `{ "op": "add", "path", "value" }` — inserts into an array, or sets a
185
+ * member.
186
+ * @param {any} path - `(st) => st.todos`, or a JSON Pointer string
187
+ * @param {any} value - an expression from the action's scope, or a literal
188
+ * @returns {any}
189
+ * @example
190
+ * add((st) => st.todos, x.payload);
191
+ */
192
+ export function add(path, value) { return operation('add', path, { value, hasValue: true }); }
193
+
194
+ /**
195
+ * `{ "op": "add", "path": "<path>/-", "value" }` — RFC 6902's array
196
+ * APPEND, which is `add` at the array's `-` position and not at the
197
+ * array itself: `add((st) => st.todos, item)` sets `/todos` TO the item
198
+ * and drops the list, because a pointer that names a member replaces
199
+ * that member. The distinction is the RFC's, and it costs a state
200
+ * schema's own `validateState` to find out at dispatch time, so the
201
+ * append has its own name here.
202
+ * @param {any} path - the ARRAY's path
203
+ * @param {any} value
204
+ * @returns {any}
205
+ * @example
206
+ * append((st) => st.todos, { text: x.payload.text, done: false });
207
+ */
208
+ export function append(path, value) {
209
+ return operation('add', path, { value, hasValue: true, suffix: '/-' });
210
+ }
211
+
212
+ /**
213
+ * `{ "op": "replace", "path", "value" }` — the op an app transition
214
+ * writes most, and the one an array ELEMENT must use (`add` inserts).
215
+ * @param {any} path
216
+ * @param {any} value
217
+ * @returns {any}
218
+ * @example
219
+ * replace((st) => st.count, s.count.add(1));
220
+ */
221
+ export function replace(path, value) {
222
+ return operation('replace', path, { value, hasValue: true });
223
+ }
224
+
225
+ /**
226
+ * `{ "op": "remove", "path" }`.
227
+ * @param {any} path
228
+ * @returns {any}
229
+ */
230
+ export function remove(path) { return operation('remove', path, {}); }
231
+
232
+ /**
233
+ * `{ "op": "move", "from", "path" }`.
234
+ * @param {any} from
235
+ * @param {any} path
236
+ * @returns {any}
237
+ */
238
+ export function move(from, path) { return operation('move', path, { from }); }
239
+
240
+ /**
241
+ * `{ "op": "copy", "from", "path" }`.
242
+ * @param {any} from
243
+ * @param {any} path
244
+ * @returns {any}
245
+ */
246
+ export function copy(from, path) { return operation('copy', path, { from }); }
247
+
248
+ /**
249
+ * `{ "op": "test", "path", "value" }` — a failing test aborts the whole
250
+ * transition (`JA2004`), which is the format's own way to write a
251
+ * precondition.
252
+ * @param {any} path
253
+ * @param {any} value
254
+ * @returns {any}
255
+ */
256
+ export function test(path, value) { return operation('test', path, { value, hasValue: true }); }
257
+
258
+ /**
259
+ * A patch list, as the transition carries it.
260
+ * @param {any} list
261
+ * @returns {any[]}
262
+ */
263
+ export function readPatch(list) {
264
+ if (!Array.isArray(list)) {
265
+ throw new LinqBuildError('JL0101',
266
+ 'transition() patch is an array of add/replace/remove/move/copy/test operations, got '
267
+ + describeValue(list));
268
+ }
269
+ return list.map((op, i) => {
270
+ if (op === null || typeof op !== 'object' || Array.isArray(op) || typeof op.op !== 'string') {
271
+ throw new LinqBuildError('JL0101',
272
+ `transition() patch[${i}] is one of add/replace/remove/move/copy/test, got `
273
+ + describeValue(op), `/patch/${i}`);
274
+ }
275
+ return op;
276
+ });
277
+ }
package/src/app/sub.js ADDED
@@ -0,0 +1,106 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `sub()` — one subscription entry (APP-FORMAT §5.3):
4
+ * `{ run, with?, when?, withQuery?, key?, for? }`, in the member order
5
+ * the format writes them.
6
+ *
7
+ * The distinction the format makes its whole restart rule out of is the
8
+ * one the pen makes at the door: `with` is VERBATIM data and never
9
+ * evaluated — a static entry never restarts — while `withQuery`, `key`
10
+ * and `for` are callbacks captured as queries, which is what makes an
11
+ * entry dynamic. A member that is sometimes data and sometimes
12
+ * executable is how a document becomes accidentally executable, so the
13
+ * pen refuses a callback under `with` before the runtime would pass it
14
+ * to a handler as props.
15
+ *
16
+ * The combinations §5.3 calls `JA0008` — `with` beside `withQuery`,
17
+ * `with` beside `for`, `key` without either — are refused here, naming
18
+ * the same reason, because the pen can see all three members at once.
19
+ */
20
+
21
+ import { LinqBuildError } from '../errors.js';
22
+ import { describeValue, requireJson } from '../json-boundary.js';
23
+ import { captureSub } from './capture.js';
24
+
25
+ /** The subscription brand: how `defineApp` tells one apart. */
26
+ export const SUB = Symbol.for('@jarenjs/linq/app-sub');
27
+
28
+ /** The members `sub()` takes beside its `run` name. */
29
+ const SUB_MEMBERS = Object.freeze(['with', 'when', 'withQuery', 'key', 'for']);
30
+
31
+ /** The order §5.3 writes an entry's members in. */
32
+ const ENTRY_ORDER = Object.freeze(['run', 'with', 'when', 'withQuery', 'key', 'for']);
33
+
34
+ /**
35
+ * One subscription entry (§5.3). `run` names a registered handler
36
+ * (`options.subs[run]`); `when` decides liveness by effective boolean
37
+ * value after boot and after every state change.
38
+ *
39
+ * @param {string} run - the registered handler name
40
+ * @param {{ with?: any, when?: any, withQuery?: any, key?: any, for?: any }} [options]
41
+ * @returns {any} the subscription declaration
42
+ * @throws {LinqBuildError} `JL0101` an empty `run`, a member the pen does
43
+ * not know, or a callback under `with`; `JL0102` a combination §5.3
44
+ * refuses; `JL0104` a name the closed world does not bind
45
+ * @example
46
+ * sub('interval', { with: { ms: 1000, tick: 'tick' }, when: (s) => s.running });
47
+ * sub('room', { for: (s) => s.rooms.all(), withQuery: (s, x) => ({ id: x.item.id }) });
48
+ */
49
+ export function sub(run, options = undefined) {
50
+ if (typeof run !== 'string' || run === '') {
51
+ throw new LinqBuildError('JL0101',
52
+ `sub() takes the handler name as a non-empty string, got ${describeValue(run)}`, '/run');
53
+ }
54
+ const entry = { run };
55
+ if (options !== undefined) {
56
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) {
57
+ throw new LinqBuildError('JL0101',
58
+ `sub() options are { with?, when?, withQuery?, key?, for? }, got ${describeValue(options)}`);
59
+ }
60
+ for (const key of Object.keys(options)) {
61
+ if (!SUB_MEMBERS.includes(key)) {
62
+ throw new LinqBuildError('JL0101',
63
+ `sub() does not take '${key}' — it takes ${SUB_MEMBERS.join(', ')} (APP-FORMAT §5.3)`,
64
+ `/${key}`);
65
+ }
66
+ }
67
+ if (options.with !== undefined && options.withQuery !== undefined) {
68
+ throw new LinqBuildError('JL0102',
69
+ "sub() carries both 'with' and 'withQuery' — one entry, one props source: 'with' is "
70
+ + "verbatim data and never restarts, 'withQuery' derives the props from the state "
71
+ + 'and makes the entry dynamic (APP-FORMAT §5.3, the runtime\'s JA0008)');
72
+ }
73
+ if (options.with !== undefined && options.for !== undefined) {
74
+ throw new LinqBuildError('JL0102',
75
+ "sub() carries 'with' beside 'for' — a fan-out's props come from 'withQuery', or "
76
+ + 'default to the item itself (APP-FORMAT §5.3, the runtime\'s JA0008)');
77
+ }
78
+ if (options.key !== undefined && options.withQuery === undefined && options.for === undefined) {
79
+ throw new LinqBuildError('JL0102',
80
+ "sub() carries 'key' without 'withQuery' or 'for' — a static subscription never "
81
+ + 'restarts, so it has no restart key (APP-FORMAT §5.3, the runtime\'s JA0008)',
82
+ '/key');
83
+ }
84
+ if (options.with !== undefined) {
85
+ if (typeof options.with === 'function') {
86
+ throw new LinqBuildError('JL0101',
87
+ "sub() 'with' is VERBATIM data handed to the handler, never evaluated — for props "
88
+ + "derived from the state write 'withQuery' instead (APP-FORMAT §5.3)", '/with');
89
+ }
90
+ entry.with = JSON.parse(JSON.stringify(requireJson(options.with, "sub() 'with'")));
91
+ }
92
+ if (options.when !== undefined) entry.when = captureSub('when', [], options.when);
93
+ const instance = options.for !== undefined ? Object.freeze(['item']) : Object.freeze([]);
94
+ if (options.withQuery !== undefined) {
95
+ entry.withQuery = captureSub('withQuery', instance, options.withQuery);
96
+ }
97
+ if (options.key !== undefined) entry.key = captureSub('key', instance, options.key);
98
+ if (options.for !== undefined) entry.for = captureSub('for', [], options.for);
99
+ }
100
+ const out = {};
101
+ for (const member of ENTRY_ORDER) {
102
+ if (entry[member] !== undefined) out[member] = entry[member];
103
+ }
104
+ Object.defineProperty(out, SUB, { value: true, enumerable: false });
105
+ return Object.freeze(out);
106
+ }