@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,316 @@
1
+ //@ts-check
2
+ /**
3
+ * @file The graph builder: `include(pick, spec)` and the root clauses
4
+ * accumulate a `load` specification (MODEL-FORMAT §10.4, §10.5) — the
5
+ * client EMITS the spec the store already runs and runs nothing itself.
6
+ * The relation member is captured to its NAME; every callback is
7
+ * captured to a query expression over `$it` through the chain's own
8
+ * recording proxy — with no parameters, since a load clause binds no
9
+ * externals — and `toArray()` is `load(spec)`, `explain()` is
10
+ * `explainLoad(spec)`: the one-statement guarantee is the store's. A
11
+ * spec is plain JSON in a fixed member order (`where, orderBy, take,
12
+ * skip, after, maxDepth, include` at the root; `where, orderBy, take,
13
+ * skip, count, include` in an include — a keyset cursor paginates the
14
+ * root alone), so one graph is one document; `toJSON()` is that
15
+ * document, as a pen's is.
16
+ */
17
+
18
+ import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
19
+
20
+ import { captureExpression } from '../expression.js';
21
+ import { LinqBuildError } from '../errors.js';
22
+ import { requireJson, describeValue } from '../json-boundary.js';
23
+
24
+ const NO_PARAMS = new Set();
25
+ const ROOT_KEYS = ['where', 'orderBy', 'take', 'skip', 'after', 'maxDepth', 'include'];
26
+ const INCLUDE_KEYS = ['where', 'orderBy', 'take', 'skip', 'count', 'include'];
27
+ /** A member path over the row: the shorthand or the bracketed spelling. */
28
+ const MEMBER_PATH = /^\$it(?:\.([A-Za-z_$][\w$]*)|\['((?:[^'\\]|\\.)*)'\])$/;
29
+
30
+ /**
31
+ * Capture one callback over the row bound as `it`.
32
+ * @param {any} fn
33
+ * @param {string} what - for the message
34
+ * @returns {any} the query expression
35
+ */
36
+ function captureOver(fn, what) {
37
+ if (typeof fn !== 'function') {
38
+ throw new LinqBuildError('JL0101', `${what} takes a callback over the row, got ${describeValue(fn)}`);
39
+ }
40
+ return captureExpression(fn, ['it'], NO_PARAMS);
41
+ }
42
+
43
+ /**
44
+ * The relation entry a member names, or the refusal naming the table.
45
+ * @param {Record<string, any>} relations
46
+ * @param {string} entityName
47
+ * @param {string} member
48
+ * @param {string} what
49
+ * @returns {{ member: string, entry: any }}
50
+ */
51
+ function requireRelation(relations, entityName, member, what) {
52
+ const entry = relations[member];
53
+ if (entry === undefined) {
54
+ const declared = Object.keys(relations);
55
+ throw new LinqBuildError('JL0107',
56
+ `'${member}' is not a relation member of '${entityName}' — ${what} loads a declared relation`
57
+ + (declared.length === 0 ? `, and '${entityName}' declares none`
58
+ : ` (${declared.map((name) => `'${name}'`).join(', ')})`));
59
+ }
60
+ return { member, entry };
61
+ }
62
+
63
+ /**
64
+ * The relation member a pick names: `(u) => u.posts` — or `u.get('posts')`
65
+ * for a name that collides with a method — captured to `$it.posts` and
66
+ * looked up in the relation table.
67
+ * @param {any} pick
68
+ * @param {Record<string, any>} relations
69
+ * @param {string} entityName
70
+ * @returns {{ member: string, entry: any }}
71
+ */
72
+ function pickMember(pick, relations, entityName) {
73
+ const captured = captureOver(pick, 'include()');
74
+ const match = typeof captured === 'string' ? MEMBER_PATH.exec(captured) : null;
75
+ if (match === null) {
76
+ throw new LinqBuildError('JL0107',
77
+ `include() picks one relation member of '${entityName}' by name ((u) => u.posts); got `
78
+ + (typeof captured === 'string' ? captured : 'an operator result'));
79
+ }
80
+ const member = match[1] ?? match[2].replace(/\\(.)/g, '$1');
81
+ return requireRelation(relations, entityName, member, 'include()');
82
+ }
83
+
84
+ /**
85
+ * One `$orderby` term: a bare key for a plain ascending order, else the
86
+ * `{ $key, $dir, $empty, $collation }` spec — as the chain spells it.
87
+ * @param {any} key - the key callback
88
+ * @param {boolean} desc
89
+ * @param {{ empty?: string, collation?: string } | undefined} options
90
+ * @param {string} what
91
+ * @returns {any}
92
+ */
93
+ function orderTerm(key, desc, options, what) {
94
+ const expression = captureOver(key, what);
95
+ const spec = { $key: expression };
96
+ if (desc) spec.$dir = 'desc';
97
+ if (options !== undefined && options !== null) {
98
+ if (options.empty !== undefined) spec.$empty = requireJson(options.empty, `${what} empty`);
99
+ if (options.collation !== undefined) spec.$collation = requireJson(options.collation, `${what} collation`);
100
+ }
101
+ return Object.keys(spec).length === 1 ? expression : spec;
102
+ }
103
+
104
+ /**
105
+ * An include spec's `orderBy`: a key callback, `{ key, desc?, empty?,
106
+ * collation? }`, or an array of either.
107
+ * @param {any} orderBy
108
+ * @param {string} what
109
+ * @returns {any}
110
+ */
111
+ function lowerOrder(orderBy, what) {
112
+ const one = (term, at) => {
113
+ if (typeof term === 'function') return orderTerm(term, false, undefined, at);
114
+ if (term !== null && typeof term === 'object' && !Array.isArray(term) && typeof term.key === 'function') {
115
+ return orderTerm(term.key, term.desc === true, term, at);
116
+ }
117
+ throw new LinqBuildError('JL0101',
118
+ `${at} takes a key callback ((p) => p.stars) or { key, desc?, empty?, collation? }, got ${describeValue(term)}`);
119
+ };
120
+ return Array.isArray(orderBy) ? orderBy.map((term, i) => one(term, `${what}[${i}]`)) : one(orderBy, what);
121
+ }
122
+
123
+ /**
124
+ * Lower one include: `true` (or nothing) includes the rows; an object
125
+ * carries the clauses the store's `load` reads, each callback captured,
126
+ * nested includes resolved against the TARGET's relation table.
127
+ * @param {any} spec
128
+ * @param {string} entityName - the target entity
129
+ * @param {(name: string) => Record<string, any> | undefined} relationsOf
130
+ * @param {string[]} path
131
+ * @returns {any}
132
+ */
133
+ function lowerInclude(spec, entityName, relationsOf, path) {
134
+ if (spec === undefined || spec === true) return true;
135
+ const at = path.join('.');
136
+ if (spec === null || typeof spec !== 'object' || Array.isArray(spec)) {
137
+ // the member list is DERIVED from the same constant the check below
138
+ // reads: typed out, it named `after` — a member that check refuses —
139
+ // and a reader who believed the first message met the second
140
+ throw new LinqBuildError('JL0101',
141
+ `the include spec at ${at} is true or `
142
+ + `{ ${INCLUDE_KEYS.map((key) => `${key}?`).join(', ')} }, got ${describeValue(spec)}`);
143
+ }
144
+ for (const key of Object.keys(spec)) {
145
+ if (!INCLUDE_KEYS.includes(key)) {
146
+ throw new LinqBuildError('JL0101',
147
+ `the include spec at ${at} does not take '${key}' — the members are ${INCLUDE_KEYS.join(', ')}`
148
+ + (key === 'after' ? '; a keyset cursor paginates the root: after() on the graph' : ''));
149
+ }
150
+ }
151
+ const out = {};
152
+ if (spec.where !== undefined) out.where = captureOver(spec.where, `${at} where`);
153
+ if (spec.orderBy !== undefined) out.orderBy = lowerOrder(spec.orderBy, `${at} orderBy`);
154
+ for (const key of ['take', 'skip']) {
155
+ if (spec[key] !== undefined) out[key] = requireJson(spec[key], `${at} ${key}`);
156
+ }
157
+ if (spec.count !== undefined) {
158
+ if (spec.count !== true) throw new LinqBuildError('JL0101', `the include spec at ${at}: count takes true`);
159
+ out.count = true;
160
+ }
161
+ if (spec.include !== undefined) out.include = lowerIncludes(spec.include, entityName, relationsOf, path);
162
+ return out;
163
+ }
164
+
165
+ /**
166
+ * A nested include record — member → `true` | spec — over the target's
167
+ * relation table.
168
+ * @param {any} includes
169
+ * @param {string} entityName
170
+ * @param {(name: string) => Record<string, any> | undefined} relationsOf
171
+ * @param {string[]} path
172
+ * @returns {Record<string, any>}
173
+ */
174
+ function lowerIncludes(includes, entityName, relationsOf, path) {
175
+ if (includes === null || typeof includes !== 'object' || Array.isArray(includes)) {
176
+ throw new LinqBuildError('JL0101',
177
+ `include at ${path.join('.')} is a record of relation members, got ${describeValue(includes)}`);
178
+ }
179
+ const relations = relationsOf(entityName) ?? {};
180
+ const out = {};
181
+ for (const member of Object.keys(includes)) {
182
+ const { entry } = requireRelation(relations, entityName, member, 'a nested include');
183
+ setObjectMember(out, member, lowerInclude(includes[member], entry.to, relationsOf, [...path, member]));
184
+ }
185
+ return out;
186
+ }
187
+
188
+ /** The graph over one entity set: an immutable builder of a `load` spec. */
189
+ export class Graph {
190
+ #set;
191
+ #entity;
192
+ #spec;
193
+ #tracking;
194
+
195
+ /**
196
+ * @param {any} set - the store's entity set
197
+ * @param {string} entity - its name
198
+ * @param {Record<string, any>} [spec] - the spec so far (`orderBy` as a list)
199
+ * @param {boolean} [tracking] - whether `toArray()` registers snapshots
200
+ */
201
+ constructor(set, entity, spec = {}, tracking = true) {
202
+ this.#set = set;
203
+ this.#entity = entity;
204
+ this.#spec = spec;
205
+ this.#tracking = tracking;
206
+ }
207
+
208
+ /** @param {Record<string, any>} patch */
209
+ #with(patch) {
210
+ return new Graph(this.#set, this.#entity, { ...this.#spec, ...patch }, this.#tracking);
211
+ }
212
+
213
+ /** The other roots' tables, where a nested include finds its target's. */
214
+ #relationsOf() {
215
+ const scoped = this.#set.scope?.relations;
216
+ return (name) => (scoped !== null && typeof scoped === 'object' ? scoped[name] : undefined);
217
+ }
218
+
219
+ /**
220
+ * Include one relation member, with an optional spec over its rows.
221
+ * @param {(u: any) => any} pick - `(u) => u.posts`
222
+ * @param {any} [spec] - `{ where?, orderBy?, take?, skip?, after?, count?, include? }`
223
+ * @returns {Graph}
224
+ */
225
+ include(pick, spec) {
226
+ const { member, entry } = pickMember(pick, this.#set.relations, this.#entity);
227
+ const lowered = lowerInclude(spec, entry.to, this.#relationsOf(), [member]);
228
+ const include = { ...(this.#spec.include ?? {}) };
229
+ setObjectMember(include, member, lowered);
230
+ return this.#with({ include });
231
+ }
232
+
233
+ /** Filter the root rows; consecutive calls conjoin. @param {(it: any) => any} predicate */
234
+ where(predicate) {
235
+ const expression = captureOver(predicate, 'where()');
236
+ const previous = this.#spec.where;
237
+ if (previous === undefined) return this.#with({ where: expression });
238
+ const conjoined = previous !== null && typeof previous === 'object'
239
+ && Object.keys(previous).length === 1 && Array.isArray(previous.$and)
240
+ ? [...previous.$and, expression] : [previous, expression];
241
+ return this.#with({ where: { $and: conjoined } });
242
+ }
243
+
244
+ /** @param {(it: any) => any} key @param {{ empty?: string, collation?: string }} [options] */
245
+ orderBy(key, options) {
246
+ return this.#with({ orderBy: [orderTerm(key, false, options, 'orderBy()')] });
247
+ }
248
+
249
+ /** @param {(it: any) => any} key @param {{ empty?: string, collation?: string }} [options] */
250
+ orderByDescending(key, options) {
251
+ return this.#with({ orderBy: [orderTerm(key, true, options, 'orderByDescending()')] });
252
+ }
253
+
254
+ /** @param {(it: any) => any} key @param {{ empty?: string, collation?: string }} [options] */
255
+ thenBy(key, options) {
256
+ return this.#then(orderTerm(key, false, options, 'thenBy()'), 'thenBy');
257
+ }
258
+
259
+ /** @param {(it: any) => any} key @param {{ empty?: string, collation?: string }} [options] */
260
+ thenByDescending(key, options) {
261
+ return this.#then(orderTerm(key, true, options, 'thenByDescending()'), 'thenByDescending');
262
+ }
263
+
264
+ /** @param {any} term @param {string} what */
265
+ #then(term, what) {
266
+ if (this.#spec.orderBy === undefined) {
267
+ throw new LinqBuildError('JL0005',
268
+ `${what}() extends an orderBy()/orderByDescending() — none precedes it`);
269
+ }
270
+ return this.#with({ orderBy: [...this.#spec.orderBy, term] });
271
+ }
272
+
273
+ /** @param {number} count */
274
+ take(count) { return this.#with({ take: requireJson(count, 'take()') }); }
275
+
276
+ /** @param {number} count */
277
+ skip(count) { return this.#with({ skip: requireJson(count, 'skip()') }); }
278
+
279
+ /** The keyset cursor (§10.5). @param {string | number} cursor */
280
+ after(cursor) { return this.#with({ after: requireJson(cursor, 'after()') }); }
281
+
282
+ /** The include depth bound (§10.4). @param {number} depth */
283
+ maxDepth(depth) { return this.#with({ maxDepth: requireJson(depth, 'maxDepth()') }); }
284
+
285
+ /** The same graph, loaded without registering snapshots. */
286
+ asNoTracking() {
287
+ return new Graph(this.#set, this.#entity, this.#spec, false);
288
+ }
289
+
290
+ /** The document, as every pen answers it: the emitted spec. */
291
+ toJSON() {
292
+ return this.toSpec();
293
+ }
294
+
295
+ /** The emitted `load` spec: plain frozen JSON, a snapshot. */
296
+ toSpec() {
297
+ const out = {};
298
+ for (const key of ROOT_KEYS) {
299
+ const value = this.#spec[key];
300
+ if (value === undefined) continue;
301
+ out[key] = key === 'orderBy' && value.length === 1 ? value[0] : value;
302
+ }
303
+ return deepFreeze(structuredClone(out));
304
+ }
305
+
306
+ /** `load(spec)` — the store's one statement. */
307
+ toArray() {
308
+ const spec = this.toSpec();
309
+ return this.#tracking ? this.#set.load(spec) : this.#set.asNoTracking().load(spec);
310
+ }
311
+
312
+ /** `explainLoad(spec)` — the SQL, the includes, the pagination strategy. */
313
+ explain() {
314
+ return this.#set.explainLoad(this.toSpec());
315
+ }
316
+ }
@@ -0,0 +1,19 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `@jarenjs/linq/db` — the store's front door. `open(model,
4
+ * options)` returns a client typed from the model pen whose entity
5
+ * handles chain (`client.entities.Post.where(…)` is `fromAsync(handle)
6
+ * .where(…)`, so every read is the chain's document and pushes down),
7
+ * load graphs from an EMITTED `load` spec (`include`), track and save
8
+ * changes, link and unlink many-to-many members and maintain live
9
+ * results — over exactly one runtime edge: this subpath imports
10
+ * `@jarenjs/db`, `@jarenjs/validate` and `@jarenjs/formats`, declared as
11
+ * optional peers; no other subpath of the package does, and the store
12
+ * never imports this package (the edge suite in the tests holds both).
13
+ * The store stays the engine: the client adds no storage semantics and
14
+ * duplicates no algorithm — `include` emits the spec the store runs,
15
+ * membership is the store's own `link`/`unlink`, `live` is the store's
16
+ * registration — and every read is one an `explain()` can name.
17
+ */
18
+
19
+ export { open, defaultValidator } from './open.js';
package/src/db/live.js ADDED
@@ -0,0 +1,43 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `live(source, options)`: a chain's document and bound externals —
4
+ * or a hand-written document — handed to the store's own registration
5
+ * (`store.live` for an entity-root chain, `collection.live` for a
6
+ * collection's), so the mode, the reason and the maintenance are the
7
+ * store's (LIVE-FORMAT §7): an entity document re-runs on invalidation,
8
+ * declared, and a store opened without capture refuses (`JD0050`)
9
+ * exactly as it does for a document. The chain's `params()` bindings are
10
+ * the externals, fixed at registration; `options.externals` adds to them.
11
+ */
12
+
13
+ /** Whether a source is a chain: a document and an explanation to give. */
14
+ const isChain = (source) => source !== null && typeof source === 'object'
15
+ && typeof source.toDocument === 'function' && typeof source.explain === 'function';
16
+
17
+ /**
18
+ * The document a source stands for, with the values it bound.
19
+ * @param {any} source - a chain (either surface) or a query document
20
+ * @returns {{ document: any, bindings: Record<string, unknown> }}
21
+ */
22
+ function documentOf(source) {
23
+ if (!isChain(source)) return { document: source, bindings: {} };
24
+ const explained = source.explain();
25
+ // a chain split by a host callback has no document; its own refusal
26
+ // (`JL0005`) names why
27
+ if (explained.document === undefined) source.toDocument();
28
+ return { document: explained.document, bindings: explained.bindings };
29
+ }
30
+
31
+ /**
32
+ * Register a live query through the store's own registration.
33
+ * @param {(document: any, options: any) => Promise<any>} register - the
34
+ * store's `live` (entity roots) or a collection's
35
+ * @param {any} source - a chain or a document
36
+ * @param {any} [options] - LIVE-FORMAT §7 options; `externals` merge over
37
+ * the chain's bindings
38
+ * @returns {Promise<any>} the store's live query
39
+ */
40
+ export function registerLive(register, source, options = {}) {
41
+ const { document, bindings } = documentOf(source);
42
+ return register(document, { ...options, externals: { ...bindings, ...(options.externals ?? {}) } });
43
+ }
@@ -0,0 +1,37 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `link`/`unlink` on a handle: the relation table decides. The
4
+ * member must be a many-to-many relation of the entity — `JL0107`
5
+ * otherwise, naming the kind it is or the members that are declared —
6
+ * and then the store's own `link`/`unlink` record the change
7
+ * (MODEL-FORMAT §11.7) for `saveChanges()` to write. The store would
8
+ * refuse the same members itself (`JD2003`); the client sees it earlier
9
+ * from the table it already reads, mirrored, never invented.
10
+ */
11
+
12
+ import { LinqBuildError } from '../errors.js';
13
+
14
+ /**
15
+ * The many-to-many relation entry a membership operation names.
16
+ * @param {Record<string, any>} relations - the handle's relation table
17
+ * @param {string} entityName
18
+ * @param {string} member
19
+ * @param {string} verb - `link` or `unlink`, for the message
20
+ * @returns {any} the relation entry
21
+ */
22
+ export function requireMembership(relations, entityName, member, verb) {
23
+ const entry = relations[member];
24
+ if (entry === undefined) {
25
+ const declared = Object.keys(relations).filter((name) => relations[name].kind === 'manyToMany');
26
+ throw new LinqBuildError('JL0107',
27
+ `'${member}' is not a relation member of '${entityName}' — ${verb}() attaches a many-to-many `
28
+ + `membership${declared.length === 0 ? `, and '${entityName}' declares none`
29
+ : ` (${declared.map((name) => `'${name}'`).join(', ')})`}`);
30
+ }
31
+ if (entry.kind !== 'manyToMany') {
32
+ throw new LinqBuildError('JL0107',
33
+ `'${member}' is a ${entry.kind} relation of '${entityName}' — ${verb}() attaches many-to-many `
34
+ + "memberships only; write the related entity's foreign key instead");
35
+ }
36
+ return entry;
37
+ }
package/src/db/open.js ADDED
@@ -0,0 +1,82 @@
1
+ //@ts-check
2
+ /**
3
+ * @file `open(model, options)`: `openStore` with every option forwarded
4
+ * verbatim, plus `validator` — a `JarenValidator` wired as the store's
5
+ * `compileSchema`. The default validator reproduces the configuration
6
+ * MIGRATING-FROM-ZOD's recipe uses (`collectErrors`, the string and
7
+ * date-time formats registered), so `s.string().email()` asserts out
8
+ * of the box; a host that already has a `compileSchema` passes it and
9
+ * wins; `validator: null` opens the store unvalidated — the store's own
10
+ * declared downgrade (`capabilities.validated === false`), chosen by
11
+ * name, never by omission. The client is a frozen record of handles built ONCE at open
12
+ * from the names the store declares — no Proxy anywhere: an unknown
13
+ * name is `undefined`, and for a pen model a compile error.
14
+ */
15
+
16
+ import { openStore } from '@jarenjs/db';
17
+ import { JarenValidator } from '@jarenjs/validate';
18
+ import { stringFormats, dateTimeFormats } from '@jarenjs/formats';
19
+ import { setObjectMember } from '@jarenjs/core/object';
20
+
21
+ import { createEntityHandle, createCollectionHandle } from './handle.js';
22
+ import { registerLive } from './live.js';
23
+
24
+ /**
25
+ * The validator the client compiles entity and collection schemas with
26
+ * when none is given: every issue collected, formats asserting.
27
+ * @returns {JarenValidator}
28
+ */
29
+ export function defaultValidator() {
30
+ return new JarenValidator({ collectErrors: true })
31
+ .addFormats(stringFormats)
32
+ .addFormats(dateTimeFormats);
33
+ }
34
+
35
+ /**
36
+ * Open a store and front it.
37
+ * @param {any} model - a `$model` document: the model pen's, or JSON
38
+ * @param {any} options - `openStore`'s options (`driver` required), plus
39
+ * `validator?` (a `JarenValidator`, wired as `compileSchema` unless an
40
+ * explicit `compileSchema` is given; `null` for an unvalidated store)
41
+ * @returns {Promise<any>} the client
42
+ */
43
+ export async function open(model, options) {
44
+ if (options === null || typeof options !== 'object') {
45
+ throw new TypeError('open needs { driver } from @jarenjs/db/node, /bun or /wasm');
46
+ }
47
+ const { validator, ...storeOptions } = options;
48
+ if (validator !== undefined && validator !== null
49
+ && (typeof validator !== 'object' || typeof validator.compile !== 'function')) {
50
+ throw new TypeError('open: validator must be a JarenValidator (an object with compile(schema)), '
51
+ + 'or null for an unvalidated store');
52
+ }
53
+ if (storeOptions.compileSchema === undefined && validator !== null) {
54
+ const jaren = validator ?? defaultValidator();
55
+ storeOptions.compileSchema = (schema) => jaren.compile(schema);
56
+ }
57
+ const store = await openStore(model, storeOptions);
58
+ const entities = {};
59
+ for (const name of store.roots ?? []) {
60
+ setObjectMember(entities, name, createEntityHandle(store, name));
61
+ }
62
+ const collections = {};
63
+ for (const name of Object.keys(model.collections ?? {})) {
64
+ setObjectMember(collections, name, createCollectionHandle(store, name));
65
+ }
66
+ /** @type {Record<string, any>} */
67
+ const client = {
68
+ store,
69
+ capabilities: store.capabilities,
70
+ entities: Object.freeze(entities),
71
+ collections: Object.freeze(collections),
72
+ transaction: (fn) => store.transaction(fn),
73
+ close: (closeOptions) => store.close(closeOptions),
74
+ };
75
+ // the unit of work and entity live queries exist exactly when the
76
+ // model declares entities — as on the store
77
+ if (store.saveChanges !== undefined) {
78
+ client.saveChanges = () => store.saveChanges();
79
+ client.live = (source, liveOptions) => registerLive(store.live, source, liveOptions);
80
+ }
81
+ return Object.freeze(client);
82
+ }