@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/docs/LINQ-FORMAT.md
CHANGED
|
@@ -35,17 +35,17 @@ is the index of the ten, and it is how a reader reaches any of them.
|
|
|
35
35
|
<!--fact:pens.index-->
|
|
36
36
|
| Document | Lines | What it writes, and when to open it |
|
|
37
37
|
|---|---:|---|
|
|
38
|
-
| [LINQ-FORMAT.md](LINQ-FORMAT.md) |
|
|
39
|
-
| [QUERY-PEN.md](QUERY-PEN.md) | 1,
|
|
38
|
+
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | 820 | this file, the binder and the family's **normative reference**: what a pen is, the rules all of them keep, the shared `JL01xx` table, and the cross-pen views derived from the ten guides it indexes. **Read it when** you want a rule that is true of every pen, an index of the eleven documents, or one place to look up a method without knowing which pen owns it |
|
|
39
|
+
| [QUERY-PEN.md](QUERY-PEN.md) | 1,724 | the chain, `.` — query documents (`jaren-query`) and the provider seam. **Read it when** you are querying data, or implementing a provider that answers a query document |
|
|
40
40
|
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | 1,218 | `./schema` — JSON Schema 2020-12: the structural keywords, the constraints and the annotations, each with a method of its own, plus `$query`, `$defs`/`$ref` recursion and the normalizer's per-field predicates. **Read it when** you are describing the shape of data — for validation, for a form, or as the base of an entity |
|
|
41
|
-
| [MODEL-PEN.md](MODEL-PEN.md) | 1,
|
|
41
|
+
| [MODEL-PEN.md](MODEL-PEN.md) | 1,092 | `./model` — the `x-entity` vocabulary on JSON Schema, and the `$model` 0.1 document `openStore` accepts unchanged. **Read it when** you are declaring a store's entities, their keys and their relations |
|
|
42
42
|
| [JSLT-PEN.md](JSLT-PEN.md) | 955 | `./jslt` — `$jslt` 0.1 stylesheets: the envelope and its rules, whose bodies are captured over the matched value. **Read it when** you are transforming one document into another |
|
|
43
43
|
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | 781 | `./migration` — `$migration` 0.1 documents: the two shape hashes and the ordered steps the runner takes. **Read it when** you are moving a store from one model to the next |
|
|
44
|
-
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,
|
|
45
|
-
| [FLOW-PEN.md](FLOW-PEN.md) | 1,
|
|
44
|
+
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | 1,221 | `./contract` — `$contract` 0.1 documents: the operations, their schemas, their declared behavior and their REST binding. **Read it when** you are declaring an API and want its client, its server and its tools typed from one document |
|
|
45
|
+
| [FLOW-PEN.md](FLOW-PEN.md) | 1,033 | `./flow` — `jaren-fsm` 0.1 machines and `jaren-dag` 0.1 dataflows, every query-valued member captured. **Read it when** you are declaring a state machine or a dependency graph of tasks |
|
|
46
46
|
| [APP-PEN.md](APP-PEN.md) | 1,143 | `./app` — the `jaren-app` 0.1 document `createApp` runs, and the JSON Schema of its state beside it. **Read it when** you are declaring a whole application: state, view, actions, effects |
|
|
47
47
|
| [FORMS-PEN.md](FORMS-PEN.md) | 940 | `./forms` — the `x-form` vocabulary on JSON Schema, and `assertOnSubmit()`, the same rules' layer-3 `$query` twin. **Read it when** you are turning a schema into a form |
|
|
48
|
-
| [DB-CLIENT.md](DB-CLIENT.md) |
|
|
48
|
+
| [DB-CLIENT.md](DB-CLIENT.md) | 893 | `./db` — the client: the store's typed front door, not a pen, and the package's one runtime edge. **Read it when** you are reading or writing rows: `load`, `include`, `link`/`unlink`, `live` |
|
|
49
49
|
<!--/fact-->
|
|
50
50
|
|
|
51
51
|
Every row of that table is derived, and none of it is written here: the
|
|
@@ -53,7 +53,7 @@ sentence is the document's own opening line, the length is the file's,
|
|
|
53
53
|
and `npm run docs:derive` writes the table out of the eleven documents
|
|
54
54
|
beside this one. The line counts are not decoration — they are what tells
|
|
55
55
|
a reader whether the document they are about to open is a ten-minute read
|
|
56
|
-
or an afternoon — and `test/docs/
|
|
56
|
+
or an afternoon — and `test/docs/format-citations.test.js` holds each one
|
|
57
57
|
equal to the file it names, independently of the derivation.
|
|
58
58
|
|
|
59
59
|
A document missing from that table is a document a reader cannot reach:
|
|
@@ -182,7 +182,7 @@ mirrored in QUERY-PEN §9 (one table, held equal by a test):
|
|
|
182
182
|
| `JL0102` | a pen was asked for a construct the format cannot carry: a function `refine`/`transform` (cross-field rules are `check()`; transforms are application code), a coercion the normalizer would never run, closed objects under `allOf`, an annotation on `never()`, a draft the pen does not write; in the JSLT pen an `apply()` as a bare object member (the `[]` idiom, JSLT-FORMAT §6.3 — the engine would fail at run time on the second child), a `match` of `{}` (the compiler's `JT0003`, earlier), an `apply()` outside a body; in the contract pen a path template form CONTRACT-FORMAT §4.2 reserves (the compiler's `JC0008`, earlier, naming the same form), a member mapped to `path` the template does not declare, or a hand-written operation `kind` outside `read`/`command`/`subscribe`; in the flow pen a guard given as a plain STRING (FLOW-FORMAT §3 makes a non-`$` literal vacuously true, so a projected display annotation must not decide execution), or a state or node id no declaration carries (the compiler's `JF0004`/`JF0006`/`JF0013`, earlier, naming the id); in the app pen a patch path that is not a chain of member reads and subscripts (a JSON Pointer cannot be written for it), an `$event` field APP-FORMAT §3.1 excludes by construction (`target`, `files`, a touch list — `$event` must survive `JSON.stringify`), an initial state no `default()` describes, a subscription-member combination §5.3 calls `JA0008`, or a view binding an action `actions` does not declare (the loop's `JA2001`, earlier, naming the declared ones); in the forms pen `preview`, which the format registry DERIVES from the field's own `format` |
|
|
183
183
|
| `JL0103` | a `$defs` name collision (two distinct builders under one name), a `ref()` no definition answers, or a `lazy()` that does not return a named builder |
|
|
184
184
|
| `JL0104` | a pen-owned keyword written through `meta()`, or an external a captured rule did not declare: a `check()` external other than `root`/`path`, a `compute()` external at all, a `body()` external other than `root`/`path` and its declared parameters — or `root`/`path` declared as one, since the engine binds them; a flow guard, effect `with`, node query or edge `select` external at all, since both flow engines evaluate with one `$` and nothing else; an app action naming anything but `$event` and `$payload` (APP-FORMAT §3.1's whole ambient vocabulary), a subscription member naming anything but `$item`, and that only under `for` (§5.3's closed world); a form rule naming anything but the context's `root`, `value` and `pointer` |
|
|
185
|
-
| `JL0105` | a relation hop on the chain — the query pen (QUERY-PEN §4, relation navigation) — cannot lower: the
|
|
185
|
+
| `JL0105` | a relation hop on the chain — the query pen (QUERY-PEN §4, relation navigation) — cannot lower: the relation's key column or the key it references is composite or undeclared; a many-to-many entry does not name its join row's columns (`{ joinTable, ownColumn, ownKey, targetColumn, targetKey }`), so there is no join root to lower through; the relation kind is not one this surface lowers; or the provider's relation table holds something that is not a relation record |
|
|
186
186
|
| `JL0106` | a migration step names an entity or collection the target model does not declare (`transform`, `assert`, `derive`); or a `transform` over a planned document finds no draft to replace, or two drafts for one name |
|
|
187
187
|
| `JL0107` | the client (`@jarenjs/linq/db`, [DB-CLIENT.md](DB-CLIENT.md)) was handed a member that is not the relation kind the operation needs: `include()` picks a declared relation member — a scalar member, or a name the model does not declare, is refused naming the declared ones; `link()`/`unlink()` attach many-to-many memberships only — a to-one or to-many relation is refused naming its kind |
|
|
188
188
|
|
|
@@ -201,18 +201,18 @@ and the bundle is the byte count the tree-shaking probe builds.
|
|
|
201
201
|
<!--fact:pens.census-->
|
|
202
202
|
| Document | Subpath | Lines | Mapping rows | Worked examples | Refusals | Bundle |
|
|
203
203
|
|---|---|---:|---:|---:|---:|---:|
|
|
204
|
-
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — |
|
|
205
|
-
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,
|
|
206
|
-
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,218 | 66 | 10 | 4 |
|
|
207
|
-
| [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,
|
|
208
|
-
| [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,
|
|
209
|
-
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 |
|
|
210
|
-
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,
|
|
211
|
-
| [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,
|
|
212
|
-
| [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 |
|
|
213
|
-
| [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 |
|
|
214
|
-
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` |
|
|
215
|
-
| **eleven documents** | | **11,
|
|
204
|
+
| [LINQ-FORMAT.md](LINQ-FORMAT.md) | — | 820 | — | — | — | — |
|
|
205
|
+
| [QUERY-PEN.md](QUERY-PEN.md) | `.` | 1,724 | 34 | 8 | 15 | 174,264 B |
|
|
206
|
+
| [SCHEMA-PEN.md](SCHEMA-PEN.md) | `./schema` | 1,218 | 66 | 10 | 4 | 33,156 B |
|
|
207
|
+
| [MODEL-PEN.md](MODEL-PEN.md) | `./model` | 1,092 | 28 | 6 | 3 | 41,582 B |
|
|
208
|
+
| [JSLT-PEN.md](JSLT-PEN.md) | `./jslt` | 955 | 17 | 8 | 3 | 19,856 B |
|
|
209
|
+
| [MIGRATION-PEN.md](MIGRATION-PEN.md) | `./migration` | 781 | 11 | 5 | 4 | 24,259 B |
|
|
210
|
+
| [CONTRACT-PEN.md](CONTRACT-PEN.md) | `./contract` | 1,221 | 38 | 6 | 3 | 45,298 B |
|
|
211
|
+
| [FLOW-PEN.md](FLOW-PEN.md) | `./flow` | 1,033 | 16 | 7 | 3 | 19,910 B |
|
|
212
|
+
| [APP-PEN.md](APP-PEN.md) | `./app` | 1,143 | 22 | 7 | 3 | 47,444 B |
|
|
213
|
+
| [FORMS-PEN.md](FORMS-PEN.md) | `./forms` | 940 | 18 | 6 | 3 | 37,312 B |
|
|
214
|
+
| [DB-CLIENT.md](DB-CLIENT.md) | `./db` | 893 | 41 | 4 | 2 | 623,994 B |
|
|
215
|
+
| **eleven documents** | | **11,820** | **291** | **67** | | |
|
|
216
216
|
<!--/fact-->
|
|
217
217
|
|
|
218
218
|
A pen whose mapping rows are far below its worked examples is a pen
|
|
@@ -254,16 +254,16 @@ it and each document publishes it. The rounded column is what
|
|
|
254
254
|
<!--fact:pens.cost-->
|
|
255
255
|
| Subpath | Document | Bundle | Rounded |
|
|
256
256
|
|---|---|---:|---:|
|
|
257
|
-
| `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) |
|
|
258
|
-
| `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) |
|
|
259
|
-
| `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) |
|
|
260
|
-
| `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,
|
|
261
|
-
| `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) |
|
|
262
|
-
| `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) |
|
|
263
|
-
| `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 19,
|
|
264
|
-
| `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) |
|
|
265
|
-
| `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) |
|
|
266
|
-
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) |
|
|
257
|
+
| `@jarenjs/linq` | [QUERY-PEN.md](QUERY-PEN.md) | 174,264 B | 174 kB |
|
|
258
|
+
| `@jarenjs/linq/schema` | [SCHEMA-PEN.md](SCHEMA-PEN.md) | 33,156 B | 33 kB |
|
|
259
|
+
| `@jarenjs/linq/model` | [MODEL-PEN.md](MODEL-PEN.md) | 41,582 B | 42 kB |
|
|
260
|
+
| `@jarenjs/linq/jslt` | [JSLT-PEN.md](JSLT-PEN.md) | 19,856 B | 20 kB |
|
|
261
|
+
| `@jarenjs/linq/migration` | [MIGRATION-PEN.md](MIGRATION-PEN.md) | 24,259 B | 24 kB |
|
|
262
|
+
| `@jarenjs/linq/contract` | [CONTRACT-PEN.md](CONTRACT-PEN.md) | 45,298 B | 45 kB |
|
|
263
|
+
| `@jarenjs/linq/flow` | [FLOW-PEN.md](FLOW-PEN.md) | 19,910 B | 20 kB |
|
|
264
|
+
| `@jarenjs/linq/app` | [APP-PEN.md](APP-PEN.md) | 47,444 B | 47 kB |
|
|
265
|
+
| `@jarenjs/linq/forms` | [FORMS-PEN.md](FORMS-PEN.md) | 37,312 B | 37 kB |
|
|
266
|
+
| `@jarenjs/linq/db` | [DB-CLIENT.md](DB-CLIENT.md) | 623,994 B | 624 kB |
|
|
267
267
|
<!--/fact-->
|
|
268
268
|
|
|
269
269
|
Read these as prices, not as scores. `./db` is the largest by an order of
|
|
@@ -504,6 +504,8 @@ it says.
|
|
|
504
504
|
| `collection(schema, { key?, identity?, indexes?, renamedFrom? })` | `{ schema, key?, identity?, indexes?, 'x-rename'? }` — `key` is an RFC 6901 pointer, a captured member path (`(d) => d.id` → `/id`) or `null` (the store allocates, `identity` says how); the other options ride verbatim | `CollectionSpec<Infer<B>>`, carrying the document shape its paths are checked against | native; an option outside the four, a key that is neither pointer nor lambda nor `null`, an `indexes` that is not an array of `index()` entries, all `JL0101` |
|
|
505
505
|
| `index(path, options?)` | `{ name, path, unique?, derive?, precision?, dims?, physical? }` — `path` is a captured lambda (`(p) => p.cell` → `$.cell`), a non-empty array of them (a composite), or a JSONPath string; `name` defaults to `by_<segments>`; the rest ride verbatim for the store's model walk to judge (§2.1) | `IndexPath<D>` over the collection's shape: a member the shape lacks does not compile | native; an option outside the six is `JL0101`; a lambda that answers an operator result or a surface method is `JL0102` |
|
|
506
506
|
|
|
507
|
+
| `expressionIndex(expression, options?)` | `{ name, expression, unique? }` — an index over a COMPUTED value: a `{ call, args }` node whose arguments are member lambdas, JSONPath strings, JSON scalars or further calls; `name` defaults to `by_<call>_<members>` | the expression's member lambdas are checked against the collection's shape | native; a node outside the vocabulary, an option outside the two, and anything that looks like SQL text are all `JL0101` |
|
|
508
|
+
|
|
507
509
|
**The model document**
|
|
508
510
|
|
|
509
511
|
| Method | Emits | Type reading | Status |
|
|
@@ -627,7 +629,8 @@ it says.
|
|
|
627
629
|
| Method | Emits | Type reading | Status |
|
|
628
630
|
|---|---|---|---|
|
|
629
631
|
| `typedClient(client, contract)` | — (identity) | `TypedClient<C>`: `invoke` over the invokable operations, `subscribe` over the subscribe ones, `url` over all of them | native |
|
|
630
|
-
| `
|
|
632
|
+
| `typedHttpClient(client, contract)` | — (identity) | `TypedHttpClient<C>`: `TypedClient<C>` plus `bytes` over `OpaqueOf<C>` — the opaque operations, whose success is a `ByteResponse` (a live stream) rather than the output type; for an `openHttpClient` client only, a local or port client has no `bytes` | native |
|
|
633
|
+
| `typedHandlers(contract, handlers)` | — (identity) | `TypedHandlerTable<C, Host = null, Carrier = 'http'>`: one handler per invokable operation, `(input, ctx) => output \| Failure`; `ctx` is `HandlerContext<Host, Carrier>` — the HTTP context by default, `Host` the host lifecycle's `ctx.host`, a carrier union a discriminated union to narrow on `ctx.carrier` (CONTRACT-FORMAT §7.7) | native; a missing or misspelled operation does not compile, and an HTTP-only member on a port/local context does not either |
|
|
631
634
|
| `typedTools(tools, contract)` | — (identity) | `TypedTool<C>[]`: `name` is the id with `.` → `_`, `execute` takes the operation's ACCEPTED input | native |
|
|
632
635
|
|
|
633
636
|
**What the pen does not judge**
|
|
@@ -764,12 +767,14 @@ it says.
|
|
|
764
767
|
| `client.collections.<name>` | the store's collection | the same chain start and `live`, typed from the pen's collection schema (§2.5) |
|
|
765
768
|
| `saveChanges()`, `transaction(fn)`, `close()`, `capabilities`, `store` | the store's | pass-throughs; `saveChanges` and `live` exist exactly when the model declares entities, as on the store; `store` is the escape hatch, typed `TypedStore` |
|
|
766
769
|
|
|
767
|
-
**The
|
|
770
|
+
**The exported names**
|
|
768
771
|
|
|
769
772
|
| Name | Answers | Type reading |
|
|
770
773
|
|---|---|---|
|
|
771
774
|
| `open(model, options)` | a promise of the frozen client — `store`, `capabilities`, `entities`, `collections`, `transaction`, `close`, and `saveChanges`/`live` when the model declares entities | `Client<InferMeta<typeof model>>` for a pen model; `Client<E>` for `open<E>(json, …)`; the wide map for a bare JSON model |
|
|
775
|
+
| `defineReplication(header)` | a logical replication document builder — §2.7 | `ReplicationPen` |
|
|
772
776
|
| `defaultValidator()` | `new JarenValidator({ collectErrors: true })` with `stringFormats` and `dateTimeFormats` registered | `JarenValidator` |
|
|
777
|
+
| `createDbLedger(client, options?)` | the contract idempotency ledger (`claim`/`commit`/`fail`/`lookup`/`sweep`) over a declared collection of the client's store — §2.6 | `DbLedger`; structurally `@jarenjs/contract`'s `Ledger` |
|
|
773
778
|
|
|
774
779
|
**The entity handle**
|
|
775
780
|
|
|
@@ -792,12 +797,14 @@ it says.
|
|
|
792
797
|
| `orderBy(key, options?)`, `orderByDescending(key, options?)` | `orderBy` | replaces; `options` is `{ empty?, collation? }` |
|
|
793
798
|
| `thenBy(key, options?)`, `thenByDescending(key, options?)` | appends to `orderBy` | `JL0005` when no `orderBy` precedes it |
|
|
794
799
|
| `take(n)`, `skip(n)` | `take`, `skip` | the offset window |
|
|
795
|
-
| `after(cursor)` | `after` | the keyset
|
|
800
|
+
| `after(cursor)` | `after` | the keyset continuation (§10.5) a `page()` over the same ordering emitted — typed by the declared ordering, so a bare key does not compile; the ROOT only |
|
|
796
801
|
| `maxDepth(n)` | `maxDepth` | the include depth bound (§10.4) |
|
|
797
802
|
| `asNoTracking()` | — | changes the load, never the document |
|
|
798
803
|
| `toSpec()`, `toJSON()` | the spec | plain deep-frozen JSON, a snapshot: mutating it changes nothing, and two builds are one document |
|
|
799
804
|
| `toArray()` | — | `load(spec)`: the store's one statement |
|
|
800
|
-
| `
|
|
805
|
+
| `cursor(options?)` | — | `loadCursor(spec, options)`: one root graph per pull from that same statement, its includes attached and bounded per root; `return()` releases it; `{ signal?, tracking? }` — untracked unless `tracking: true` |
|
|
806
|
+
| `page(options?)` | — | `page(spec, options)`: one bounded page over the composite keyset — `{ items, continuation, hasMore, snapshot }`, never more than `limit` roots or `maxBytes` bytes; `{ limit?, after?, maxBytes?, consistency?, signal?, tracking? }`; a `take`/`skip` on the graph beside it is the store's `JD0032` |
|
|
807
|
+
| `explain()` | — | `explainLoad(spec)`: the SQL, the includes, the pagination strategy, the per-root bounds |
|
|
801
808
|
|
|
802
809
|
| Spec member | Emitted | Note |
|
|
803
810
|
|---|---|---|
|
|
@@ -807,6 +814,7 @@ it says.
|
|
|
807
814
|
| `orderBy: (p) => p.pid` | `orderBy: "$it.pid"` | a bare key, ascending |
|
|
808
815
|
| `orderBy: { key, desc?, empty?, collation? }` | `orderBy: { $key, $dir, $empty, $collation }` | as the chain spells `$orderby`; an array of either is an array |
|
|
809
816
|
| `take`, `skip` | `take`, `skip` | the window inside the subquery (a non-integer is the store's `JD0032`) |
|
|
817
|
+
| `maxRows`, `maxBytes` | `maxRows`, `maxBytes` | the per-root bounds (MODEL-FORMAT §10.4): rows of the relation per parent and serialised bytes per parent; crossing one is the store's `JD2073`, never a truncated graph. Defaults 1000 rows / 1 MiB (a `take` is the row bound of the include it windows); `Infinity` spells the unbounded case and emits as `null` |
|
|
810
818
|
| `include: { comments: spec }` | `include: { comments: <lowered> }` | over the TARGET's relation table (the scope carries every root's) |
|
|
811
819
|
| anything else | `JL0101` | the vocabulary is closed; `after` paginates the root, never an include |
|
|
812
820
|
<!--/fact-->
|
package/docs/MIGRATION-PEN.md
CHANGED
|
@@ -745,7 +745,7 @@ from a drop plus a create, and guessing risks silent data loss.
|
|
|
745
745
|
|
|
746
746
|
## 7. Cost
|
|
747
747
|
|
|
748
|
-
`@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->
|
|
748
|
+
`@jarenjs/linq/migration` builds to **<!--fact:bundle.migration-->24,259<!--/fact--> bytes** as a minified,
|
|
749
749
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
750
750
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
751
751
|
(<!--fact:bundle.migration.kb-->24<!--/fact--> kB) beside the other nine subpath prices in
|
|
@@ -778,4 +778,4 @@ proxy behind it, and the canonicalizer — and the two model documents
|
|
|
778
778
|
themselves arrive as data, deep-frozen JSON that the consumer's own model
|
|
779
779
|
module built. So a consumer who ships migrations to a browser does not
|
|
780
780
|
ship the model pen with them; a consumer who OPENS a store does, and pays
|
|
781
|
-
`./model`'s <!--fact:bundle.model-->
|
|
781
|
+
`./model`'s <!--fact:bundle.model-->41,582<!--/fact--> bytes for it.
|
package/docs/MODEL-PEN.md
CHANGED
|
@@ -197,11 +197,20 @@ no relations (MODEL-FORMAT §9.1); one physical engine sits under both.
|
|
|
197
197
|
| `collection(schema, { key?, identity?, indexes?, renamedFrom? })` | `{ schema, key?, identity?, indexes?, 'x-rename'? }` — `key` is an RFC 6901 pointer, a captured member path (`(d) => d.id` → `/id`) or `null` (the store allocates, `identity` says how); the other options ride verbatim | `CollectionSpec<Infer<B>>`, carrying the document shape its paths are checked against | native; an option outside the four, a key that is neither pointer nor lambda nor `null`, an `indexes` that is not an array of `index()` entries, all `JL0101` |
|
|
198
198
|
| `index(path, options?)` | `{ name, path, unique?, derive?, precision?, dims?, physical? }` — `path` is a captured lambda (`(p) => p.cell` → `$.cell`), a non-empty array of them (a composite), or a JSONPath string; `name` defaults to `by_<segments>`; the rest ride verbatim for the store's model walk to judge (§2.1) | `IndexPath<D>` over the collection's shape: a member the shape lacks does not compile | native; an option outside the six is `JL0101`; a lambda that answers an operator result or a surface method is `JL0102` |
|
|
199
199
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
`
|
|
200
|
+
| `expressionIndex(expression, options?)` | `{ name, expression, unique? }` — an index over a COMPUTED value: a `{ call, args }` node whose arguments are member lambdas, JSONPath strings, JSON scalars or further calls; `name` defaults to `by_<call>_<members>` | the expression's member lambdas are checked against the collection's shape | native; a node outside the vocabulary, an option outside the two, and anything that looks like SQL text are all `JL0101` |
|
|
201
|
+
|
|
202
|
+
The option set for `index()` is exactly `name`, `unique`, `derive`,
|
|
203
|
+
`precision`, `dims`, `physical` (`src/model/collection.js:18`); the
|
|
204
|
+
default name is `by_` plus the path's member segments, identifier-safe
|
|
203
205
|
(`collection.js:76-80`) — `by_series_t`, `by_x_y`.
|
|
204
206
|
|
|
207
|
+
`expressionIndex()` takes only `name` and `unique`, because an
|
|
208
|
+
expression names the members it reads itself — nothing that describes a
|
|
209
|
+
member's storage belongs beside one. The pen resolves NO function name:
|
|
210
|
+
arity and determinism are the store's to check against the declarations
|
|
211
|
+
`openStore({ expressions })` was given, and a name this pen has never
|
|
212
|
+
heard of is not an error here. What it decides is the shape.
|
|
213
|
+
|
|
205
214
|
### 2.4 The model document
|
|
206
215
|
|
|
207
216
|
The three calls that assemble the whole thing — the document, the
|
|
@@ -1053,10 +1062,10 @@ catch them:
|
|
|
1053
1062
|
|
|
1054
1063
|
## 7. Cost
|
|
1055
1064
|
|
|
1056
|
-
`@jarenjs/linq/model` builds to **<!--fact:bundle.model-->
|
|
1065
|
+
`@jarenjs/linq/model` builds to **<!--fact:bundle.model-->41,582<!--/fact--> bytes** as a minified,
|
|
1057
1066
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
1058
1067
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
1059
|
-
(<!--fact:bundle.model.kb-->
|
|
1068
|
+
(<!--fact:bundle.model.kb-->42<!--/fact--> kB) beside the other nine subpath prices in
|
|
1060
1069
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
1061
1070
|
|
|
1062
1071
|
The probe is a gate, not a report: building a two-member model as a
|
|
@@ -1074,7 +1083,7 @@ them:
|
|
|
1074
1083
|
byte of `packages/linq/src/model/`, because the subclasses are built by
|
|
1075
1084
|
this subpath rather than patched onto the base classes.
|
|
1076
1085
|
|
|
1077
|
-
The price above the schema pen's <!--fact:bundle.schema-->
|
|
1086
|
+
The price above the schema pen's <!--fact:bundle.schema-->33,156<!--/fact--> is about 8 kB: the mixin, the
|
|
1078
1087
|
three relation factories, `collection()`/`index()` with their capture,
|
|
1079
1088
|
`defineModel()` — and the refusal MESSAGES, which are most of what §4
|
|
1080
1089
|
costs. That is a deliberate trade: naming the rule and the spelling that
|
package/docs/QUERY-PEN.md
CHANGED
|
@@ -200,8 +200,9 @@ value becomes the expression:
|
|
|
200
200
|
`groupJoin`, `aggregate` (and a `mapAsync`) a relation name is an
|
|
201
201
|
ordinary member again — and a `fromDocument` chain never hops: there
|
|
202
202
|
the document decides what the items are. A hop that cannot lower is
|
|
203
|
-
`JL0105` at build time (a many-to-many
|
|
204
|
-
|
|
203
|
+
`JL0105` at build time (a composite key; a many-to-many entry that
|
|
204
|
+
does not name its join row's columns, so there is no join root to
|
|
205
|
+
lower through — MODEL-FORMAT §10.7); a member read off
|
|
205
206
|
the to-many ARRAY before `all()` is `JL0005` with the fix named, where
|
|
206
207
|
the same read off a stored array would answer nothing.
|
|
207
208
|
|
|
@@ -563,6 +564,8 @@ Runtime errors (`LinqRuntimeError`):
|
|
|
563
564
|
| `JL2004` | an asynchronous provider cannot back the synchronous surface |
|
|
564
565
|
| `JL2005` | a push queue was fed after it ended |
|
|
565
566
|
| `JL2006` | a provider answered an element terminal with something other than one array |
|
|
567
|
+
| `JL2007` | a ledger settlement named a ref that settles no started record — `createDbLedger`'s fence ([DB-CLIENT.md §2.6](DB-CLIENT.md#26-the-ledger)) |
|
|
568
|
+
| `JL2008` | a federated fetch reached its row or byte budget (§12.1) |
|
|
566
569
|
|
|
567
570
|
Engine errors (`JQ…`) from a hand-written `fromDocument` document pass
|
|
568
571
|
through unwrapped — they already carry their own code and `docPath` —
|
|
@@ -604,12 +607,19 @@ because the engine itself materialises for `$orderby`/`$groupby`):
|
|
|
604
607
|
`take(n)`, and an exception mid-chain all call `.return()` on the
|
|
605
608
|
iterator — a generator left suspended holds a file handle or a read
|
|
606
609
|
transaction open. `explain()` reports `{ barriers: [{ operator,
|
|
607
|
-
reason }], hops, document }` — `hops` the relation
|
|
608
|
-
navigated, as on the sync surface (§4)
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
610
|
+
reason }], streaming, barrier, hops, document }` — `hops` the relation
|
|
611
|
+
hops the callbacks navigated, as on the sync surface (§4); `streaming`
|
|
612
|
+
(`'row'` or `'buffered'`) and `barrier` what THIS surface does with the
|
|
613
|
+
item stream, one item at a time or a buffer at its first local barrier
|
|
614
|
+
— or, when a `mapAsync` sits in the chain, `{ split: { pushed,
|
|
615
|
+
residual } }` instead of `document` (`toDocument()` refuses with
|
|
616
|
+
`JL0005`: a host callback has no document form). Over a provider the
|
|
617
|
+
pushed document's own class — a set residual, an external the store
|
|
618
|
+
cannot bind — is the provider's `explain(document, { externals:
|
|
619
|
+
bindings })` to report, and the cursor a `for await` pulls from
|
|
620
|
+
carries the same `streaming`/`barrier` (§12). No silent caps, no silent
|
|
621
|
+
buffering: if a chain materialises, the report says which operator
|
|
622
|
+
forced it.
|
|
613
623
|
|
|
614
624
|
Re-enumeration follows the sync contract: each enumeration calls the
|
|
615
625
|
source's iterator method again. A one-shot generator object simply
|
|
@@ -645,8 +655,11 @@ operators; a per-element async *predicate* is `mapAsync` then `where`.
|
|
|
645
655
|
completion.
|
|
646
656
|
- An `AbortSignal` is threaded to every callback and aborted on early
|
|
647
657
|
termination and on failure. A rejected callback FAILS CLOSED: the
|
|
648
|
-
first
|
|
649
|
-
|
|
658
|
+
first rejection aborts every in-flight sibling and closes the source,
|
|
659
|
+
including while its next pull is pending. Ordered mode still delivers
|
|
660
|
+
preceding values and reports callback failures in source order. The
|
|
661
|
+
producer is closed once; a failed close accompanies the callback's
|
|
662
|
+
original failure in an `AggregateError`.
|
|
650
663
|
- `mapAsync` is NOT translatable to a provider. A provider-backed
|
|
651
664
|
chain that reaches it SPLITS: everything before is pushed to the
|
|
652
665
|
provider whole, everything after runs locally, and `explain()`
|
|
@@ -674,8 +687,12 @@ operators; a per-element async *predicate* is `mapAsync` then `where`.
|
|
|
674
687
|
`iterateCsvStream` output);
|
|
675
688
|
- any sync iterable (wrapped);
|
|
676
689
|
- a **cursor**: `{ next(): Promise<{done, value}>, return?() }` — the
|
|
677
|
-
shape the
|
|
678
|
-
as-is
|
|
690
|
+
shape the store's own row cursor implements (`QueryCursor`), adopted
|
|
691
|
+
as-is. A provider that offers `cursor(document, options)` — the
|
|
692
|
+
store's collections and entity sets do — is handed the pushed
|
|
693
|
+
document there when the chain is ITERATED, so a `for await` pulls
|
|
694
|
+
one row at a time from an open statement and a `break` releases it;
|
|
695
|
+
`toArray()` and the other terminals still push one whole window;
|
|
679
696
|
- a **push queue** (`createPushQueue({ highWaterMark = 1024 })`) for
|
|
680
697
|
feed/end-style readers with no pull protocol of their own (josl's
|
|
681
698
|
push parsers deliberately have no backpressure protocol; the queue
|
|
@@ -689,6 +706,72 @@ engine async (`packages/json` is untouched and strictly synchronous),
|
|
|
689
706
|
it does not add a second operator table, and it does not add
|
|
690
707
|
`selectAwait`/`whereAwait` variants.
|
|
691
708
|
|
|
709
|
+
### 12.1 The federation boundary (`federate`)
|
|
710
|
+
|
|
711
|
+
A query document reads ONE input, and a join whose sides come from two
|
|
712
|
+
unrelated sources is `JL0005` (§8). That refusal stands: nothing in a
|
|
713
|
+
chain relaxes it. What `federate()` adds is the one place to opt OUT of
|
|
714
|
+
it explicitly, by naming the sources and the bounds together:
|
|
715
|
+
|
|
716
|
+
```js
|
|
717
|
+
const fed = federate({
|
|
718
|
+
sources: { orders: shop.entity('Order'), events: analytics },
|
|
719
|
+
maxRows: 50_000,
|
|
720
|
+
maxBytes: 32 * 1024 * 1024,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const rows = await fromAsync(fed.source('orders'))
|
|
724
|
+
.where((o) => o.placedAt.gt(cutoff))
|
|
725
|
+
.join(fromAsync(fed.source('events')), (o) => o.id, (e) => e.orderId,
|
|
726
|
+
(o, e) => ({ id: o.id, at: e.at }))
|
|
727
|
+
.toArray();
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
`fed.source(name)` is an ordinary provider source (§8) with the root
|
|
731
|
+
`$.<name>[*]`, and all of a federation's sources share one scope — which
|
|
732
|
+
is exactly what admits the join. The federation is what executes it:
|
|
733
|
+
|
|
734
|
+
1. each side's own document — the filters and the projection the chain
|
|
735
|
+
already packed per side — runs against ITS source, over that
|
|
736
|
+
source's own root;
|
|
737
|
+
2. the smaller side (by declared `estimatedRows`, else the first named)
|
|
738
|
+
is streamed into a hash table keyed by the join key, counting rows
|
|
739
|
+
and serialized bytes against the budget as it fills;
|
|
740
|
+
3. the other side is streamed and PROBED: a row whose key no build row
|
|
741
|
+
carries cannot pair, so it is dropped before it costs anything;
|
|
742
|
+
4. the caller's own document runs in the engine over the two reduced
|
|
743
|
+
sets — the **resident join**, which is what decides.
|
|
744
|
+
|
|
745
|
+
Step 4 is the contract. This boundary spells no join semantics of its
|
|
746
|
+
own: the engine's `$eq` decides which rows pair, its ordering orders
|
|
747
|
+
them, its projection shapes them. The hash table bounds the FETCH and
|
|
748
|
+
nothing else, so a reduction that cannot key a value (a compound join
|
|
749
|
+
key) keeps the row rather than guessing at it.
|
|
750
|
+
|
|
751
|
+
`fed.source(name).explain(document)` answers the plan without running
|
|
752
|
+
any of it: the `strategy`, the `budget`, the `build` and `probe` sides
|
|
753
|
+
with their estimates, their own documents and whether each streams, and
|
|
754
|
+
the `resident` document the engine answers.
|
|
755
|
+
|
|
756
|
+
**A budget is a refusal, not a spill.** A side that reaches `maxRows` or
|
|
757
|
+
`maxBytes` stops at the row that WOULD have broken it and raises
|
|
758
|
+
`JL2008`; every cursor the call opened is closed exactly once, whether
|
|
759
|
+
it answered, refused, failed or was aborted. A `signal` on the call is
|
|
760
|
+
read at the ROW boundary — where a cursor can be let go without
|
|
761
|
+
abandoning a pull the source is still inside — and a declared
|
|
762
|
+
`estimatedRows` decides only which side BUILDS, never how much is held:
|
|
763
|
+
a source that under-reports is refused by the budget all the same. A source offering a
|
|
764
|
+
cursor (§12) is pulled row by row, so the bound is enforced before the
|
|
765
|
+
memory is spent; one offering only `execute` answers whole, and
|
|
766
|
+
`explain()` says `buffered` rather than pretending otherwise.
|
|
767
|
+
|
|
768
|
+
Refused, by name: more than two sides in one federated document, a
|
|
769
|
+
binding over a root the federation does not carry, and a join with no
|
|
770
|
+
equality between one member of each side — without one the fetch is the
|
|
771
|
+
cross product of two sources, which is what the budget exists to
|
|
772
|
+
refuse. Non-goals: no spill, no distributed transaction, no
|
|
773
|
+
cross-source write.
|
|
774
|
+
|
|
692
775
|
## 13. Worked examples
|
|
693
776
|
|
|
694
777
|
Every `js` fence below is EXECUTED. `test/linq/pen-docs.test.js` writes
|
|
@@ -1111,6 +1194,11 @@ proves that too, so the exclusion cannot hide a chain refusal).
|
|
|
1111
1194
|
| `JL2004` | a provider's `execute()` answered a promise on the synchronous surface |
|
|
1112
1195
|
| `JL2005` | a push queue was fed after `end()` |
|
|
1113
1196
|
| `JL2006` | a provider answered an element terminal with something other than one array |
|
|
1197
|
+
| `JL2008` | a federated fetch reached one side's row or byte budget (§12.1) |
|
|
1198
|
+
|
|
1199
|
+
`JL2007` is the client door's, not the chain's: `createDbLedger`'s stale
|
|
1200
|
+
settlement ([DB-CLIENT.md §2.6](DB-CLIENT.md#26-the-ledger)); it is
|
|
1201
|
+
listed with the runtime errors above and raised by no chain module.
|
|
1114
1202
|
|
|
1115
1203
|
Every message below is the one the chain raised when the spelling beside
|
|
1116
1204
|
it was run, with the code prefix (`JL0005: `) removed. Where a refusal
|
|
@@ -1583,14 +1671,14 @@ are shorter:
|
|
|
1583
1671
|
## 17. Cost
|
|
1584
1672
|
|
|
1585
1673
|
A consumer importing `from` from `@jarenjs/linq` and calling one
|
|
1586
|
-
terminal bundles **<!--fact:bundle.chain-->
|
|
1674
|
+
terminal bundles **<!--fact:bundle.chain-->174,264<!--/fact--> bytes** (esbuild, ESM, minified, tree-shaken,
|
|
1587
1675
|
`platform: 'neutral'`). The figure is measured by
|
|
1588
1676
|
`scripts/check-tree-shaking.js`'s chain probe and compared with this
|
|
1589
1677
|
section on every `npm run test:tree-shaking`: it is derived, never typed,
|
|
1590
1678
|
and a stale one is red here rather than wrong in a document somebody
|
|
1591
1679
|
reads.
|
|
1592
1680
|
|
|
1593
|
-
Of that, **<!--fact:bundle.chain.own-->
|
|
1681
|
+
Of that, **<!--fact:bundle.chain.own-->40,280<!--/fact--> bytes** are the chain's own modules — `sequence.js`,
|
|
1594
1682
|
`async.js`, `expression.js`, `document.js`, `provider.js`,
|
|
1595
1683
|
`concurrency.js`, `errors.js` and `schema-of.js`. The remaining ~134 kB
|
|
1596
1684
|
is the query ENGINE and the core it stands on: a chain's document has to
|
|
@@ -1611,13 +1699,13 @@ making:
|
|
|
1611
1699
|
`@jarenjs/formats`** — the client's optional peers. A consumer of the
|
|
1612
1700
|
chain alone installs nothing new;
|
|
1613
1701
|
- **no pen bytes at all**, in either direction: the pens carry no chain
|
|
1614
|
-
module either, which is what keeps a <!--fact:bundle.jslt.kb-->
|
|
1615
|
-
pen <!--fact:bundle.jslt.kb-->
|
|
1702
|
+
module either, which is what keeps a <!--fact:bundle.jslt.kb-->20<!--/fact--> kB JSLT
|
|
1703
|
+
pen <!--fact:bundle.jslt.kb-->20<!--/fact--> kB.
|
|
1616
1704
|
|
|
1617
1705
|
`docs/CONSUMING.md` states the rounded price of all ten subpaths in one
|
|
1618
1706
|
table, each figure held equal to the same measurements. Two of its rows
|
|
1619
|
-
are the ones to read together: the chain at <!--fact:bundle.chain.kb-->
|
|
1620
|
-
`./db` at <!--fact:bundle.db.kb-->
|
|
1707
|
+
are the ones to read together: the chain at <!--fact:bundle.chain.kb-->174<!--/fact--> kB and
|
|
1708
|
+
`./db` at <!--fact:bundle.db.kb-->624<!--/fact--> kB.
|
|
1621
1709
|
The client costs what the store costs, by construction, and the chain
|
|
1622
1710
|
costs what running a query costs.
|
|
1623
1711
|
|
|
@@ -1626,7 +1714,7 @@ the reason is worth knowing: a bundler counts a shared module once, and
|
|
|
1626
1714
|
the chain and every pen share the expression capture (`expression.js`)
|
|
1627
1715
|
and the coded errors under it (`errors.js`, and `@jarenjs/core`'s error
|
|
1628
1716
|
and object helpers). A consumer importing the chain AND the schema pen
|
|
1629
|
-
bundles **<!--fact:bundle.chain.withSchemaPen-->
|
|
1717
|
+
bundles **<!--fact:bundle.chain.withSchemaPen-->195,342<!--/fact--> bytes** — **<!--fact:bundle.chain.shared-->12,078<!--/fact--> bytes** less than the sum of the
|
|
1630
1718
|
figure above and [SCHEMA-PEN.md](SCHEMA-PEN.md#7-cost) §7's, which is
|
|
1631
1719
|
what those shared modules weigh. The probe measures that pair too, so
|
|
1632
1720
|
the saving is derived like everything else here. What the chain does NOT
|
package/docs/SCHEMA-PEN.md
CHANGED
|
@@ -1180,10 +1180,10 @@ hand, or generate it some other way, when:
|
|
|
1180
1180
|
|
|
1181
1181
|
## 7. Cost
|
|
1182
1182
|
|
|
1183
|
-
`@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->
|
|
1183
|
+
`@jarenjs/linq/schema` builds to **<!--fact:bundle.schema-->33,156<!--/fact--> bytes** as a minified,
|
|
1184
1184
|
tree-shaken ESM bundle — the figure `scripts/check-tree-shaking.js`
|
|
1185
1185
|
measures and `npm run test:tree-shaking` reports, published rounded
|
|
1186
|
-
(<!--fact:bundle.schema.kb-->
|
|
1186
|
+
(<!--fact:bundle.schema.kb-->33<!--/fact--> kB) beside the other nine subpath prices in
|
|
1187
1187
|
[docs/CONSUMING.md](../../../docs/CONSUMING.md).
|
|
1188
1188
|
|
|
1189
1189
|
The probe is a gate, not a report: building
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/linq",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.67.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -84,13 +84,13 @@
|
|
|
84
84
|
"prepack": "npm run build:types"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@jarenjs/core": "^0.
|
|
88
|
-
"@jarenjs/json": "^0.
|
|
87
|
+
"@jarenjs/core": "^0.67.0",
|
|
88
|
+
"@jarenjs/json": "^0.67.0"
|
|
89
89
|
},
|
|
90
90
|
"peerDependencies": {
|
|
91
|
-
"@jarenjs/db": "^0.
|
|
92
|
-
"@jarenjs/formats": "^0.
|
|
93
|
-
"@jarenjs/validate": "^0.
|
|
91
|
+
"@jarenjs/db": "^0.67.0",
|
|
92
|
+
"@jarenjs/formats": "^0.67.0",
|
|
93
|
+
"@jarenjs/validate": "^0.67.0"
|
|
94
94
|
},
|
|
95
95
|
"peerDependenciesMeta": {
|
|
96
96
|
"@jarenjs/db": {
|
package/src/app/action.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* payload-creator function.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { isJsonObject } from '@jarenjs/core/object';
|
|
22
23
|
import { LinqBuildError } from '../errors.js';
|
|
23
24
|
import { effectDescriptor, readEffects } from '../effect.js';
|
|
24
25
|
import { describeValue } from '../json-boundary.js';
|
|
@@ -52,11 +53,6 @@ const EXCLUDED_FIELDS = new Set([
|
|
|
52
53
|
'path', 'composedPath', 'clipboardData', 'submitter',
|
|
53
54
|
]);
|
|
54
55
|
|
|
55
|
-
/** @param {any} value */
|
|
56
|
-
function isPlainObject(value) {
|
|
57
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
56
|
/**
|
|
61
57
|
* A member set the pen knows, or `JL0101` naming the one it does not.
|
|
62
58
|
* @param {any} spec
|
|
@@ -120,7 +116,7 @@ export function effect(run, props = undefined) {
|
|
|
120
116
|
* transition({ state: () => null, effects: [effect('save')] });
|
|
121
117
|
*/
|
|
122
118
|
export function transition(spec) {
|
|
123
|
-
if (!
|
|
119
|
+
if (!isJsonObject(spec)) {
|
|
124
120
|
throw new LinqBuildError('JL0101',
|
|
125
121
|
`transition() takes { state?, patch?, effects? }, got ${describeValue(spec)}`);
|
|
126
122
|
}
|
|
@@ -157,7 +153,7 @@ export function action(fn, options = undefined) {
|
|
|
157
153
|
`action() takes a callback (s, x) => transition(…), got ${describeValue(fn)}`);
|
|
158
154
|
}
|
|
159
155
|
if (options !== undefined) {
|
|
160
|
-
if (!
|
|
156
|
+
if (!isJsonObject(options)) {
|
|
161
157
|
throw new LinqBuildError('JL0101',
|
|
162
158
|
`action() options are { payload?, event? }, got ${describeValue(options)}`);
|
|
163
159
|
}
|
|
@@ -232,7 +228,7 @@ export function bind(name, options = undefined) {
|
|
|
232
228
|
}
|
|
233
229
|
const out = { action: name };
|
|
234
230
|
if (options !== undefined) {
|
|
235
|
-
if (!
|
|
231
|
+
if (!isJsonObject(options)) {
|
|
236
232
|
throw new LinqBuildError('JL0101',
|
|
237
233
|
'bind() options are { payload?, event?, preventDefault?, stopPropagation? }, got '
|
|
238
234
|
+ describeValue(options));
|
package/src/app/define.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* click; the pen can see the whole document at once and refuses it.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { deepFreeze, setObjectMember } from '@jarenjs/core/object';
|
|
27
|
+
import { deepFreeze, setObjectMember, isJsonObject } from '@jarenjs/core/object';
|
|
28
28
|
|
|
29
29
|
import { LinqBuildError } from '../errors.js';
|
|
30
30
|
import { describeValue, requireJson, requireNameMap } from '../json-boundary.js';
|
|
@@ -37,11 +37,6 @@ const APP_VERSION = '0.1';
|
|
|
37
37
|
/** The members `defineApp()` takes. */
|
|
38
38
|
const APP_MEMBERS = Object.freeze(['state', 'initial', 'schema', 'view', 'actions', 'subs']);
|
|
39
39
|
|
|
40
|
-
/** @param {any} value */
|
|
41
|
-
function isPlainObject(value) {
|
|
42
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
40
|
/** A JSON value, copied: the document is a value of its own. @param {any} v */
|
|
46
41
|
const copy = (v) => JSON.parse(JSON.stringify(v));
|
|
47
42
|
|
|
@@ -65,7 +60,7 @@ function initialOf(schema, at, needed) {
|
|
|
65
60
|
}
|
|
66
61
|
if (schema.default !== undefined) return { has: true, value: copy(schema.default) };
|
|
67
62
|
if (schema.const !== undefined) return { has: true, value: copy(schema.const) };
|
|
68
|
-
if (!
|
|
63
|
+
if (!isJsonObject(schema.properties)) return { has: false };
|
|
69
64
|
|
|
70
65
|
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
71
66
|
const out = {};
|
|
@@ -114,7 +109,7 @@ function collectBoundActions(node, out) {
|
|
|
114
109
|
const name = literalName(value);
|
|
115
110
|
if (name !== null) out.add(name);
|
|
116
111
|
}
|
|
117
|
-
if (key === 'on' &&
|
|
112
|
+
if (key === 'on' && isJsonObject(value)) {
|
|
118
113
|
for (const binding of Object.values(value)) {
|
|
119
114
|
const name = literalName(binding);
|
|
120
115
|
if (name !== null) out.add(name);
|
|
@@ -137,7 +132,7 @@ function readView(view) {
|
|
|
137
132
|
+ 'bare rule array; the format requires the member and the runtime refuses an app '
|
|
138
133
|
+ 'without one (JA0002)', '/view');
|
|
139
134
|
}
|
|
140
|
-
if (!Array.isArray(view) && !
|
|
135
|
+
if (!Array.isArray(view) && !isJsonObject(view)) {
|
|
141
136
|
throw new LinqBuildError('JL0101',
|
|
142
137
|
`defineApp() view is a JSLT stylesheet document or a bare rule array, got ${describeValue(view)}`,
|
|
143
138
|
'/view');
|
|
@@ -165,7 +160,7 @@ function readView(view) {
|
|
|
165
160
|
* createApp(document, { node, validateState: new JarenValidator().compile(stateSchema) });
|
|
166
161
|
*/
|
|
167
162
|
export function defineApp(spec) {
|
|
168
|
-
if (!
|
|
163
|
+
if (!isJsonObject(spec)) {
|
|
169
164
|
throw new LinqBuildError('JL0101',
|
|
170
165
|
`defineApp() takes { state, initial?, schema?, view, actions?, subs? }, got ${describeValue(spec)}`);
|
|
171
166
|
}
|
|
@@ -199,7 +194,7 @@ export function defineApp(spec) {
|
|
|
199
194
|
/** @type {Record<string, any> | undefined} */
|
|
200
195
|
let actions;
|
|
201
196
|
if (spec.actions !== undefined) {
|
|
202
|
-
if (!
|
|
197
|
+
if (!isJsonObject(spec.actions)) {
|
|
203
198
|
throw new LinqBuildError('JL0101',
|
|
204
199
|
`defineApp() actions is an object of named action() declarations, got ${describeValue(spec.actions)}`,
|
|
205
200
|
'/actions');
|
|
@@ -207,7 +202,7 @@ export function defineApp(spec) {
|
|
|
207
202
|
requireNameMap(spec.actions, 'defineApp() actions', '/actions');
|
|
208
203
|
actions = {};
|
|
209
204
|
for (const [name, declared] of Object.entries(spec.actions)) {
|
|
210
|
-
if (!
|
|
205
|
+
if (!isJsonObject(declared) || declared[ACTION] !== true) {
|
|
211
206
|
throw new LinqBuildError('JL0101',
|
|
212
207
|
`defineApp() action '${name}' is action((s, x) => transition(…)), got ${describeValue(declared)}`,
|
|
213
208
|
`/actions/${name}`);
|
|
@@ -225,7 +220,7 @@ export function defineApp(spec) {
|
|
|
225
220
|
'/subs');
|
|
226
221
|
}
|
|
227
222
|
subs = spec.subs.map((declared, i) => {
|
|
228
|
-
if (!
|
|
223
|
+
if (!isJsonObject(declared) || declared[SUB] !== true) {
|
|
229
224
|
throw new LinqBuildError('JL0101',
|
|
230
225
|
`defineApp() subs[${i}] is sub(run, options?), got ${describeValue(declared)}`,
|
|
231
226
|
`/subs/${i}`);
|