@jarenjs/linq 0.49.2 → 0.66.1

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 (79) hide show
  1. package/ARCHITECTURE.md +227 -0
  2. package/README.md +650 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1221 -0
  5. package/docs/DB-CLIENT.md +882 -0
  6. package/docs/FLOW-PEN.md +1033 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +778 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1092 -0
  12. package/docs/QUERY-PEN.md +1724 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +251 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +255 -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 +377 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +48 -11
  24. package/src/contract/define.js +282 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +338 -0
  28. package/src/db/handle.js +89 -0
  29. package/src/db/include.js +351 -0
  30. package/src/db/index.js +24 -0
  31. package/src/db/ledger.js +195 -0
  32. package/src/db/live.js +43 -0
  33. package/src/db/membership.js +37 -0
  34. package/src/db/open.js +130 -0
  35. package/src/document.js +143 -13
  36. package/src/effect.js +65 -0
  37. package/src/errors.js +78 -6
  38. package/src/expression.js +463 -36
  39. package/src/federate.js +531 -0
  40. package/src/flow/capture.js +33 -0
  41. package/src/flow/dag.js +316 -0
  42. package/src/flow/fsm.js +323 -0
  43. package/src/flow/index.js +22 -0
  44. package/src/forms/index.js +43 -0
  45. package/src/forms/rules.js +170 -0
  46. package/src/forms/submit.js +177 -0
  47. package/src/index.js +5 -2
  48. package/src/jslt/body.js +226 -0
  49. package/src/jslt/index.js +18 -0
  50. package/src/jslt/rules.js +202 -0
  51. package/src/json-boundary.js +90 -0
  52. package/src/migration/define.js +318 -0
  53. package/src/migration/index.js +15 -0
  54. package/src/migration/steps.js +244 -0
  55. package/src/model/collection.js +273 -0
  56. package/src/model/define.js +125 -0
  57. package/src/model/entity.js +307 -0
  58. package/src/model/index.js +47 -0
  59. package/src/model/relation.js +85 -0
  60. package/src/provider.js +137 -20
  61. package/src/schema/brand.js +31 -0
  62. package/src/schema/builders.js +526 -0
  63. package/src/schema/check.js +29 -0
  64. package/src/schema/emit.js +394 -0
  65. package/src/schema/factories.js +239 -0
  66. package/src/schema/index.js +37 -0
  67. package/src/schema-of.js +24 -0
  68. package/src/sequence.js +233 -103
  69. package/src/sources.js +10 -3
  70. package/types/app.d.ts +293 -0
  71. package/types/contract.d.ts +468 -0
  72. package/types/db.d.ts +359 -0
  73. package/types/flow.d.ts +285 -0
  74. package/types/forms.d.ts +253 -0
  75. package/types/index.d.ts +296 -26
  76. package/types/jslt.d.ts +193 -0
  77. package/types/migration.d.ts +201 -0
  78. package/types/model.d.ts +526 -0
  79. package/types/schema.d.ts +494 -0
package/README.md CHANGED
@@ -1,11 +1,139 @@
1
1
  # @jarenjs/linq
2
2
 
3
- A C#-familiar fluent query surface whose output is a **plain JSON
4
- query document**. You write `from(users).where(u =>
5
- u.age.gt(21)).orderBy(u => u.name)`; what exists afterwards is data
6
- inspectable, serializable, executable by the `@jarenjs/json` engine in
3
+ **Write it once, in typed JavaScript. Keep it as data.**
4
+
5
+ Every engine in this repository runs a JSON document. The validator runs
6
+ a schema, the store runs a model and a query, the transformer runs a
7
+ stylesheet, the flow engine runs a state machine, the app runtime runs a
8
+ whole application, the form evaluator runs a form. That is a rare
9
+ strength, and until now it came with a price: a document can be stored,
10
+ versioned, diffed, sent to a browser, handed to a model and pushed down
11
+ to a database — everything a closure cannot do — but you had to write
12
+ it by hand, as JSON, with nothing checking it until it ran.
13
+
14
+ This package removes the price. You write the query, the schema, the
15
+ model, the migration, the contract, the stylesheet, the state machine,
16
+ the dataflow, the application or the form as ordinary typed JavaScript,
17
+ and what comes back is the document — exactly the one its engine
18
+ already takes, byte for byte, with its types derived beside it. The
19
+ chain is the pen; the document is the deliverable.
20
+
21
+ ```js
22
+ import { fromAsync } from '@jarenjs/linq';
23
+ import * as m from '@jarenjs/linq/model';
24
+ import { open } from '@jarenjs/linq/db';
25
+ import { nodeDriver } from '@jarenjs/db/node';
26
+
27
+ // One function. It never says where the users live.
28
+ const adults = (users) => fromAsync(users).where((u) => u.age.gt(21)).orderBy((u) => u.name);
29
+
30
+ await adults(rows).toArray(); // over an array: runs in memory, in the @jarenjs/json engine
31
+ adults(rows).toDocument(); // { $for: { it: ['$[*]'] }, $where: { $gt: ['$it.age', 21] }, $orderby: { $key: '$it.name' }, $return: '$it' }
32
+
33
+ const model = m.defineModel({ entities: {
34
+ User: m.object({ id: m.string().identity('uuid'), name: m.string(), age: m.integer() }),
35
+ } });
36
+ const db = await open(model, { driver: nodeDriver() });
37
+ await adults(db.entities.User).toArray(); // User[] — the same function, the same document, pushed down to SQL
38
+ ```
39
+
40
+ Look at what the second line answers: not a closure, a value. That
41
+ value can be saved next to the rows it queries, replayed a year later,
42
+ shipped to a browser and run there, put in a pull request where a
43
+ reviewer can read it, or handed to a language model as the thing to
44
+ produce. The last line runs it against a database without a line of
45
+ SQL — and nothing is hidden: the chain's `explain()` shows the document
46
+ it sent, and the store's `explain(document)` shows what that became,
47
+ `SELECT … FROM "User" WHERE "age" > ? ORDER BY "name"`.
48
+
49
+ ## What you gain
50
+
51
+ - **One idiom for ten kinds of document.** `(u) => u.age.gt(21)` is a
52
+ query predicate. The same shape is a schema's cross-field `check()`,
53
+ a form's `assert`, a state machine's guard, a stylesheet rule's body,
54
+ an app action's patch. Every callback is recorded by one proxy over
55
+ one expression vocabulary — one [mapping table](docs/QUERY-PEN.md) —
56
+ so what you learn writing your first query is what writes everything
57
+ else in this repository.
58
+ - **Types you did not generate.** `Infer<>` reads a schema, `InferMeta<>`
59
+ a model, `ContractOf<>` a contract; they come out of the builder as you
60
+ write it. A gate holds them equal to the declarations `@jarenjs/emit`
61
+ generates from the same documents, so the generate step becomes
62
+ optional — keep it or delete it, they agree.
63
+ - **Mistakes move to the earliest place they can be caught.** A
64
+ transition into a state you never declared, a field your migration
65
+ transform forgot, a view binding an action `actions` does not list —
66
+ these stop compiling, or refuse at build time with a code that names
67
+ the fix (`JL0101`–`JL0107`). Not at dispatch, not in production, not
68
+ in a log.
69
+ - **Data you can do anything with.** A closure can be called. A
70
+ document can also be inspected, serialized, diffed, cached, signed,
71
+ sent, stored and executed somewhere else. Every pen answers the
72
+ document, and the chain answers it on demand with `toDocument()`.
73
+ - **One chain, everywhere data lives.** In memory, over an async cursor
74
+ or stream, or pushed down to SQL through `@jarenjs/db`: the same chain
75
+ emits a byte-identical document through every driver (test-pinned),
76
+ so a streaming answer equals the in-memory answer by construction, and
77
+ a query written against an array is already a query against the store.
78
+ - **Small, and honest about the rest.** A pen imports no engine — a
79
+ schema-only bundle carries no chain and no validator — and
80
+ `npm install @jarenjs/linq` installs nothing else. What a pen costs is
81
+ measured and printed [below](#what-a-pen-costs); where the store's
82
+ front door loses to Prisma, Drizzle or Kysely is a published table,
83
+ not a footnote.
84
+
85
+ ## The map
86
+
87
+ | You want to write | Import | The engine that runs it, unchanged | Start here |
88
+ | --- | --- | --- | --- |
89
+ | a query | `@jarenjs/linq` | `@jarenjs/json` in memory; `@jarenjs/db` pushed down | [the chain](#the-chain) · [QUERY-PEN](docs/QUERY-PEN.md) |
90
+ | a JSON Schema | `@jarenjs/linq/schema` | `@jarenjs/validate` | [schema pen](#by-code-the-schema-pen) · [SCHEMA-PEN](docs/SCHEMA-PEN.md) |
91
+ | a database model | `@jarenjs/linq/model` | `@jarenjs/db`'s `openStore` | [model pen](#by-code-the-model-pen) · [MODEL-PEN](docs/MODEL-PEN.md) |
92
+ | a transform | `@jarenjs/linq/jslt` | `@jarenjs/json/jslt` | [JSLT pen](#by-code-the-jslt-pen) · [JSLT-PEN](docs/JSLT-PEN.md) |
93
+ | a migration | `@jarenjs/linq/migration` | `jaren-db` | [migration pen](#by-code-the-migration-pen) · [MIGRATION-PEN](docs/MIGRATION-PEN.md) |
94
+ | an API contract | `@jarenjs/linq/contract` | `@jarenjs/contract` — client, server and AI tools | [contract pen](#by-code-the-contract-pen) · [CONTRACT-PEN](docs/CONTRACT-PEN.md) |
95
+ | a state machine, a dataflow | `@jarenjs/linq/flow` | `@jarenjs/flow` | [flow pen](#by-code-the-flow-pen) · [FLOW-PEN](docs/FLOW-PEN.md) |
96
+ | an application | `@jarenjs/linq/app` | `@jarenjs/app` | [app pen](#by-code-the-app-pen) · [APP-PEN](docs/APP-PEN.md) |
97
+ | a form | `@jarenjs/linq/forms` | `@jarenjs/forms` | [forms pen](#by-code-the-forms-pen) · [FORMS-PEN](docs/FORMS-PEN.md) |
98
+ | typed handles on the store | `@jarenjs/linq/db` | the front door, not a pen | [the front door](#the-front-door-jarenjslinqdb) · [DB-CLIENT](docs/DB-CLIENT.md) |
99
+
100
+ The rules every pen keeps — what a pen may spell, what it must refuse,
101
+ how its document and its types are held equal to the engine's — are one
102
+ binder, [docs/LINQ-FORMAT.md](docs/LINQ-FORMAT.md). Every worked example
103
+ in every pen document is executed by a test, so what a document shows
104
+ is what runs.
105
+
106
+ ## If you are a model reading this
107
+
108
+ You can produce every document in this repository two ways: emit the
109
+ JSON directly, or write the pen's JavaScript and let it emit the JSON.
110
+ Prefer the pen whenever you can run code, because it fails early and
111
+ specifically. The vocabulary is closed and each method is a row in a
112
+ mapping table, so there is nothing to guess; a method that is not in the
113
+ table is `JL0001`–`JL0007` at build time, a shape a format cannot carry
114
+ is `JL0101`–`JL0107`, and every message names the fix. Three rules keep
115
+ you out of the traps:
116
+
117
+ 1. **Spell comparisons and logic as methods.** `u.age.gt(21)`,
118
+ `a.and(b)`, `x.not()`, `s.eq('')`. A proxy cannot overload
119
+ JavaScript's own operators: `u.age > 21` throws a plain `TypeError`,
120
+ and `a && b` or `cond ? x : y` evaluate against the proxy object and
121
+ produce a wrong document silently.
122
+ 2. **Reach for the builder where you would write a schema.** Every pen
123
+ that carries a schema takes a schema-pen builder in that position —
124
+ a contract's `input`, a machine's `payload`, an app's `state`.
125
+ 3. **Read the pen document before its format.** The pen document is the
126
+ mapping table for what you are writing; the format document is what
127
+ the engine reads. Start with the table's row, and copy its example —
128
+ a test executed it.
129
+
130
+ ## The chain
131
+
132
+ `.` is the chain: you write `from(users).where(u =>
133
+ u.age.gt(21)).orderBy(u => u.name)`, and what exists afterwards is data
134
+ — inspectable, serializable, executable by the `@jarenjs/json` engine in
7
135
  memory, streamed over a cursor, or pushed into a database by any
8
- provider. The chain is the pen; the document is the deliverable.
136
+ provider.
9
137
 
10
138
  ```js
11
139
  import { from } from '@jarenjs/linq';
@@ -16,7 +144,7 @@ const adults = from(users)
16
144
  .select((u) => ({ id: u.id, name: u.name }));
17
145
 
18
146
  adults.toArray(); // runs in memory, deferred until now
19
- adults.toDocument(); // { $for: { it: '$[*]' }, $where: { $gt: ['$it.age', 21] }, … }
147
+ adults.toDocument(); // { $for: { it: ['$[*]'] }, $where: { $gt: ['$it.age', 21] }, … }
20
148
  ```
21
149
 
22
150
  **The C# comparison, stated honestly.** The operator names, deferred
@@ -25,7 +153,7 @@ is a recording proxy, never source-text inspection, so callbacks must
25
153
  use the expression surface (`u.age.gt(21)`, not `u.age > 21` — a
26
154
  JavaScript proxy cannot overload `>`); the operator vocabulary is the
27
155
  query engine's, closed and documented in the
28
- [mapping table](docs/LINQ-FORMAT.md); and `IQueryable`'s role is
156
+ [mapping table](docs/QUERY-PEN.md); and `IQueryable`'s role is
29
157
  played by the provider seam below.
30
158
 
31
159
  - **Deferred and immutable.** A `Sequence` holds a stage list; nothing
@@ -35,7 +163,9 @@ played by the provider seam below.
35
163
  common path precisely and degrade to honest `unknown` — never a
36
164
  wrong type — with runtime twins pinning every claim.
37
165
  - **The async story (the obvious objection, answered).** `fromAsync`
38
- runs the SAME operator set over cursors and streams — async is a
166
+ runs the same operator set over cursors and streams — joins only over
167
+ a provider, pushed whole, because a single-pass stream cannot be read
168
+ twice — and async is a
39
169
  boundary, not a colour (the same chain emits a byte-identical
40
170
  document through both drivers, test-pinned). Element-wise async
41
171
  work happens in exactly one place, `mapAsync`, with a REQUIRED
@@ -66,21 +196,524 @@ played by the provider seam below.
66
196
  method. `.params({ query })` binds the query vector at call time, so
67
197
  one compiled document serves every question.
68
198
  - **The provider contract.** Any object with
69
- `execute(queryDocument, { externals })` is a provider.
70
- `@jarenjs/db` implements it a chain over a SQLite-backed
71
- collection pushes to SQL with no import edge in either direction.
72
- `mapAsync` splits a provider chain into a pushed prefix and a local
73
- residual, and `explain()` shows the split.
199
+ `execute(queryDocument, { externals })` is a provider; one carrying
200
+ `root` binds its items through that root, and two sharing a `scope`
201
+ may be joined in one document. `@jarenjs/db` implements it a chain
202
+ over a SQLite-backed collection or an entity set
203
+ (`from(store.sync.entity('Post'))`, `fromAsync(store.entity('Post'))`)
204
+ pushes to SQL — the chain imports no store; the package's one runtime
205
+ edge is the client subpath's (`@jarenjs/linq/db`, below) and it runs
206
+ one way, toward the store; two entity sets
207
+ of one store join in ONE statement; the store itself, serving several
208
+ roots, is refused by name (`JL0007`). A provider carrying a relation
209
+ table (`relations` — an entity set does) lets a declared relation
210
+ NAVIGATE: `p.author.email` and `u.posts.all().count()` are hops,
211
+ lowered at capture to the correlated phrases the engine and the store
212
+ both run, so the document never carries a relation name and
213
+ `explain().hops` lists what was navigated; a many-to-many hop is
214
+ `JL0105` until the join table is a queryable root. An asynchronous provider is
215
+ `fromAsync`'s: the document arrives whole and `execute` may answer a
216
+ promise. `mapAsync` splits a provider chain into a pushed prefix and a
217
+ local residual, and `explain()` shows the split.
218
+
219
+ ## By code: the schema pen
220
+
221
+ `@jarenjs/linq/schema` writes standard JSON Schema 2020-12 documents —
222
+ the structural keywords, the constraints and the annotations, each with
223
+ a method of its own, plus `$query` cross-field rules, `$defs`/`$ref`
224
+ recursion and the normalizer's per-field predicates — with
225
+ `Infer<>`/`Input<>` types a gate proves equal to `@jarenjs/emit`'s
226
+ generated declarations. Reach for it when you are describing the shape
227
+ of data: for validation, for a form, or as the base of an entity.
228
+
229
+ ```js
230
+ import * as s from '@jarenjs/linq/schema';
231
+ import type { Infer } from '@jarenjs/linq/schema';
232
+ import { JarenValidator } from '@jarenjs/validate';
233
+
234
+ const User = s.object({
235
+ id: s.string().uuid(),
236
+ name: s.string().min(1),
237
+ created: s.datetime(),
238
+ age: s.integer().optional(),
239
+ }).check((u) => u.created.year().ge(1970)); // a cross-field rule, captured into $query
240
+
241
+ User.schema; // { type: 'object', properties: {…}, required: [...], additionalProperties: false, $query: {…} }
242
+ type User = Infer<typeof User>; // { id: string; name: string; created: DateTime; age?: number }
243
+
244
+ const isUser = new JarenValidator().compile(User.schema); // @jarenjs/validate takes the document unchanged
245
+ isUser(input); // true | false, the validator's verdict
246
+ from(rows).ofType(User); // Sequence<User> — the chain takes a builder where it took a document
247
+ ```
248
+
249
+ How to read it: every method writes one keyword, so the builder and the
250
+ document are the same thing read from two sides — `JSON.stringify(User)`
251
+ is the document, frozen, and `User.schema` is the same value. Objects are
252
+ closed by default (`.open()` admits more), `.optional()` is what leaves a
253
+ property out of `required`, and a `check()` is a rule over several
254
+ fields, recorded by the same proxy the chain uses. A pen imports no
255
+ engine, so a schema-only bundle carries no chain and no validator. What
256
+ a pen cannot spell it refuses at build time with a coded error
257
+ (`JL0101`–`JL0107`) naming the fix — there is no `.transform()` and no
258
+ function `refine`: cross-field rules are `check()`, transforms are
259
+ application code. The mapping table, the worked examples a test
260
+ executes and every refusal are [docs/SCHEMA-PEN.md](docs/SCHEMA-PEN.md);
261
+ the rules every pen keeps are [docs/LINQ-FORMAT.md](docs/LINQ-FORMAT.md).
262
+
263
+ ## By code: the model pen
264
+
265
+ `@jarenjs/linq/model` is the schema pen with the store's vocabulary
266
+ subclassed onto it — the `x-entity` members `@jarenjs/db` reads
267
+ (`key()`, `unique()`, `index()`, `version()`, `identity('uuid' |
268
+ 'auto')`, `default()`, `column('integer' | 'json')`), the three relation
269
+ spellings, and `defineModel({ entities, collections })`, which emits
270
+ exactly the `$model` 0.1 document `openStore` takes. Reach for it when a
271
+ store's entities, keys and relations should be declared once and typed
272
+ everywhere: `InferMeta<>` is the `EntityMetaMap` `typedStore<E>` wants,
273
+ with no generate step.
274
+
275
+ ```js
276
+ import * as m from '@jarenjs/linq/model';
277
+ import type { InferMeta } from '@jarenjs/linq/model';
278
+ import { openStore } from '@jarenjs/db';
279
+ import { nodeDriver } from '@jarenjs/db/node';
280
+
281
+ export const model = m.defineModel({ entities: {
282
+ User: m.object({
283
+ id: m.string().identity('uuid'),
284
+ email: m.string().email().unique(),
285
+ posts: m.rel.hasMany('Post', { via: 'authorId', onDelete: 'cascade' }),
286
+ }),
287
+ Post: m.object({
288
+ pid: m.integer().identity('auto'),
289
+ title: m.string(),
290
+ authorId: m.string(),
291
+ author: m.rel.hasOne('User', { via: 'authorId', onDelete: 'cascade' }),
292
+ }),
293
+ } });
294
+
295
+ model; // { $model: '0.1', entities: {…} } — the document itself, shape-hashed as the store hashes it
296
+ const store = await openStore(model, { driver: nodeDriver() }); // @jarenjs/db takes it unchanged
297
+ type Meta = InferMeta<typeof model>; // the EntityMetaMap typedStore<E> wants — no generate step
298
+ ```
299
+
300
+ How to read it: an entity is a schema-pen object whose members carry
301
+ store annotations; a relation is a member like any other, spelled
302
+ `rel.hasOne` / `rel.hasMany` / `rel.belongsToMany`, and it is what lets a
303
+ chain over this store navigate `p.author.email` as a hop. The value
304
+ `defineModel` returns IS the `$model` document, and its shape hash equals
305
+ the store's own, so a model written this way is a model the migration
306
+ engine already understands. `InferMeta<>` is the point: `@jarenjs/db`'s
307
+ typed surface wanted an `EntityMetaMap` that `@jarenjs/emit` had to
308
+ generate from a model file, and a repo gate holds the pen's type equal
309
+ to that generated map — so the codegen step is optional rather than
310
+ load-bearing. The mapping table, member by member, is
311
+ [docs/MODEL-PEN.md](docs/MODEL-PEN.md); what the model document itself
312
+ means is [MODEL-FORMAT.md](../db/docs/MODEL-FORMAT.md).
313
+
314
+ ## By code: the JSLT pen
315
+
316
+ `@jarenjs/linq/jslt` writes `$jslt` 0.1 stylesheets: rule bodies are
317
+ callbacks captured over the matched value, with `root`/`path` and the
318
+ declared parameters as typed externals, and `apply()` spells the
319
+ apply-templates operator. Reach for it when one document is being
320
+ transformed into another and the rules should be typed rather than
321
+ hand-written JSON.
322
+
323
+ ```js
324
+ import { stylesheet, rule, apply } from '@jarenjs/linq/jslt';
325
+ import { compileJsltStylesheet } from '@jarenjs/json/jslt';
326
+ import { createTypeTestCompiler } from '@jarenjs/validate/query';
327
+
328
+ const book = stylesheet([
329
+ rule({ schema: { type: 'object', required: ['isbn'] } },
330
+ (v) => ({ title: v.title, children: [apply(v.chapters.all())] })), // apply-templates over every chapter
331
+ rule({ schema: { type: 'object', required: ['heading'] } },
332
+ (v) => ({ name: v.heading })),
333
+ ]);
334
+
335
+ const transform = compileJsltStylesheet(book, { compileTypeTest: createTypeTestCompiler() }); // @jarenjs/json/jslt takes it unchanged
336
+ transform({ isbn: '1', title: 'T', chapters: [{ heading: 'A' }] }); // { title: 'T', children: [{ name: 'A' }] }
337
+ ```
338
+
339
+ How to read it: a `rule` is a match (a path, or a `schema` the type-test
340
+ compiler judges) and a body; the body's callback is recorded over the
341
+ matched value, so `v.title` becomes the `$` path the engine reads and
342
+ `v.chapters.all()` a query over the array. `apply()` hands the matched
343
+ values back to the stylesheet, which is how the second rule runs once
344
+ per chapter. `rule()`/`stylesheet()` write the rule object and the
345
+ envelope byte-equal to the format's own Appendix A, and the pen refuses
346
+ the one shape the engine only catches at run time — an `apply` as a bare
347
+ object member (`JL0102`). The mapping table and the worked examples are
348
+ [docs/JSLT-PEN.md](docs/JSLT-PEN.md).
349
+
350
+ ## By code: the migration pen
351
+
352
+ `@jarenjs/linq/migration` writes `$migration` 0.1 documents between two
353
+ models, hashing their shapes exactly as the store does, with the data
354
+ transform typed old row → new row. Reach for it when `jaren-db plan` has
355
+ drafted a migration and a human has to fill the part the planner could
356
+ not: `fromPlanned(planned, { from, to })` lets a typed `transform`
357
+ replace the draft, and a draft left alone still refuses to run.
358
+
359
+ ```js
360
+ import { fromPlanned } from '@jarenjs/linq/migration';
361
+ import { model as v1 } from './models/v1.js'; // the previous model, kept beside the current one
362
+ import { model as v2 } from './model.js';
363
+
364
+ export default fromPlanned(planned, { from: v1, to: v2 })
365
+ .transform('User', (u) => ({ id: u.id, name: u.name, handle: u.name.lower() }));
366
+ // ^ the old row, typed ^ the new row, checked: a dropped `handle` does not compile
367
+ ```
368
+
369
+ How to read it: a migration is the two models' shape hashes plus a list
370
+ of steps. `defineMigration({ id, from, to })` writes one from scratch and
371
+ `.ddl()`, `.sql()`, `.transform()`, `.assert()`, `.derive()` and
372
+ `.step()` write the step kinds MIGRATION-FORMAT names; `fromPlanned`
373
+ starts from the document `jaren-db plan` wrote instead — the planner
374
+ still plans, the pen types the human part. `jaren-db` loads model and
375
+ migration modules beside JSON, plans from the committed
376
+ `model.snapshot.json`, refuses a module that is not pure and, in CI, a
377
+ model that moved without a plan (`jaren-db check`). The mapping table
378
+ and the worked examples are
379
+ [docs/MIGRATION-PEN.md](docs/MIGRATION-PEN.md).
380
+
381
+ ## By code: the contract pen
382
+
383
+ `@jarenjs/linq/contract` writes `$contract` 0.1 documents — the
384
+ operations two ends exchange, their schemas, their policy and their HTTP
385
+ binding. Reach for it when one declared API should type all three
386
+ consumers: `ContractOf<typeof shop>` is the operation map, and
387
+ `typedClient`, `typedHandlers` and `typedTools` carry it onto a client,
388
+ a handler table and an AI toolbox.
389
+
390
+ ```js
391
+ import * as s from '@jarenjs/linq/schema';
392
+ import { defineContract, command, error, http, typedClient } from '@jarenjs/linq/contract';
393
+ import { compileContract } from '@jarenjs/contract';
394
+ import { openHttpClient } from '@jarenjs/contract/client';
395
+
396
+ const Product = s.named('Product', s.object({ id: s.integer(), name: s.string() }).open());
397
+
398
+ export const shop = defineContract({ id: 'shop' }, {
399
+ 'product.save': command({
400
+ input: s.object({ id: s.integer(), product: Product }).open(),
401
+ output: Product,
402
+ errors: { conflict: error({ status: 409 }) },
403
+ http: http({ method: 'PUT', path: '/api/products/{id}' }),
404
+ }),
405
+ });
406
+
407
+ shop.document; // { $contract: '0.1', id: 'shop', $defs: { Product: {…} }, operations: {…} } — @jarenjs/contract takes it unchanged
408
+ const api = typedClient(openHttpClient(compileContract(shop.document), { baseUrl }), shop);
409
+ const outcome = await api.invoke('product.save', { id: 1, product }); // Outcome<Product>: ok | conflict, both typed
410
+ ```
411
+
412
+ An HTTP client takes `typedHttpClient` instead: the same typed client
413
+ plus `bytes` over the opaque operations (`OpaqueOf<typeof shop>`), whose
414
+ success is a live response stream rather than a JSON value.
415
+
416
+ How to read it: an operation is a kind (`read`, `command`, `subscribe`),
417
+ its input and output schemas by the schema pen, its named errors and its
418
+ binding. A `named()` schema is hoisted into the contract's own `$defs`
419
+ and referenced from every operation that uses it, and every member is
420
+ written in the order CONTRACT-FORMAT §12.1 fixes, so the pen's document
421
+ and the compiler's own public projection differ by nothing but the
422
+ defaults the compiler materializes. The types come with it and never
423
+ run the TypeScript projection: the same `shop` that typed the client
424
+ types the server's `typedHandlers` table and an AI toolbox's
425
+ `typedTools`. The mapping table and the worked examples are
426
+ [docs/CONTRACT-PEN.md](docs/CONTRACT-PEN.md).
427
+
428
+ ## By code: the flow pen
429
+
430
+ `@jarenjs/linq/flow` writes the two `@jarenjs/flow` documents — a
431
+ `jaren-fsm` 0.1 machine and a `jaren-dag` 0.1 dataflow. State ids, event
432
+ names and node ids are literal types, so a transition into an undeclared
433
+ state or an edge from an undeclared node is a compile error; guards,
434
+ effect props, node queries and edge selectors are captured callbacks,
435
+ never a path typed as a string.
436
+
437
+ ```js
438
+ import { defineFsm, on, state } from '@jarenjs/linq/flow';
439
+ import * as s from '@jarenjs/linq/schema';
440
+ import { compileFsm, fsmToApp } from '@jarenjs/flow';
441
+
442
+ const Approval = s.object({ fresh: s.boolean() });
443
+
444
+ const review = defineFsm({
445
+ initial: 'draft',
446
+ states: ['draft', 'review', state('published', { final: true })],
447
+ transitions: [
448
+ on('draft', 'submit').to('review'),
449
+ on('review', 'approve', { payload: Approval }).when((x) => x.payload.fresh).to('published'),
450
+ on('review').to('draft'), // a wildcard, listed last: document order is the priority
451
+ ],
452
+ });
453
+
454
+ compileFsm(review).step('review', 'approve', { payload: { fresh: true } }); // { state: 'published', final: true, … } — @jarenjs/flow takes it unchanged
455
+ fsmToApp(review); // and so does the app projection
456
+ ```
457
+
458
+ How to read it: `on(from, event)` opens a transition, `.when()` guards
459
+ it with a callback over the payload and the context, `.to()` closes it;
460
+ a transition without an event is a wildcard, and document order is the
461
+ engine's priority order. A guard is recorded, never typed as a string,
462
+ which is also how the pen refuses the one trap FLOW-FORMAT §3 names
463
+ itself — a plain-string guard that is vacuously true (`JL0102`). A
464
+ dataflow is written the same way with `defineDag`, `node` and `edge`.
465
+ The mapping table and the worked examples are
466
+ [docs/FLOW-PEN.md](docs/FLOW-PEN.md).
467
+
468
+ ## By code: the app pen
469
+
470
+ `@jarenjs/linq/app` writes the `jaren-app` 0.1 document `createApp` runs
471
+ — a whole interactive application as one JSON value — and answers the
472
+ state's JSON Schema beside it for `validateState`. Reach for it when the
473
+ state shape, the view, the actions and their patch pointers should be
474
+ one typed declaration: the initial state comes from the state schema's
475
+ own `default()`s, and a patch path is a lambda over the state that
476
+ lowers to a JSON Pointer.
477
+
478
+ ```js
479
+ import { action, append, bind, defineApp, transition } from '@jarenjs/linq/app';
480
+ import { rule } from '@jarenjs/linq/jslt';
481
+ import * as s from '@jarenjs/linq/schema';
482
+ import { createApp } from '@jarenjs/app';
483
+ import { JarenValidator } from '@jarenjs/validate';
484
+
485
+ const { document, stateSchema } = defineApp({
486
+ state: s.object({ todos: s.array(s.string()).default([]), draft: s.string().default('') }),
487
+ view: [rule('$', (v) => ['main', {},
488
+ ['button', { on: { click: bind('todo/add', { payload: v.draft }) } }, 'add']])],
489
+ actions: {
490
+ 'todo/add': action((st, x) => transition({ patch: [append((c) => c.todos, x.payload)] })),
491
+ },
492
+ });
493
+
494
+ document; // { $app: '0.1', state: { todos: [], draft: '' }, view: [...], actions: {…} } — the initial state from the schema's defaults
495
+ createApp(document, { node, validateState: new JarenValidator().compile(stateSchema) }); // @jarenjs/app takes it unchanged
496
+ ```
497
+
498
+ How to read it: the view is JSLT rules over the state (the same pen),
499
+ `bind()` names the action a DOM event dispatches with its payload, and
500
+ an action's callback is captured over APP-FORMAT §3.1's three names
501
+ (`$` the state, `$payload`, `$event`), so a patch value and an effect's props
502
+ are the SAME expression when they should be. A patch path is a lambda
503
+ over the state that lowers to a JSON Pointer — `(c) => c.todos.at(2).done`
504
+ is `/todos/2/done`, and a computed index becomes the pointer expression
505
+ `{ "$concat": ["/todos/", "$payload.i", "/done"] }`. Two refusals the
506
+ loop can only report per dispatch land at build time instead: a view
507
+ binding an action `actions` does not declare (`JA2001`), and an `$event`
508
+ field §3.1 excludes because `$event` must survive `JSON.stringify`
509
+ (`JL0102`). The mapping table and the worked examples are
510
+ [docs/APP-PEN.md](docs/APP-PEN.md).
511
+
512
+ ## By code: the forms pen
513
+
514
+ `@jarenjs/linq/forms` is the schema pen plus the `x-form` vocabulary —
515
+ `form({ visible, enabled, assert, computed, message })` on every builder
516
+ — and `assertOnSubmit()`, which answers the same rules' layer-3 `$query`
517
+ twin in one call. A rule is an annotation: nothing about what the schema
518
+ validates moves, and the rules are callbacks over `c.root`, `c.value`
519
+ and `c.pointer`, never a path typed as a string.
520
+
521
+ ```js
522
+ import * as f from '@jarenjs/linq/forms';
523
+ import { assertOnSubmit } from '@jarenjs/linq/forms';
524
+ import { buildFormModel, compileFormRules } from '@jarenjs/forms';
525
+ import { JarenValidator } from '@jarenjs/validate';
526
+
527
+ const invoice = f.object({
528
+ company: f.string().optional(),
529
+ vatId: f.string().optional().form({
530
+ visible: (c) => c.root.company.ne(''),
531
+ assert: (c) => c.root.company.eq('').or(c.value.ne('')),
532
+ message: 'VAT id is required for companies',
533
+ }),
534
+ });
535
+
536
+ invoice.schema; // a JSON Schema whose vatId carries x-form: { visible, assert, message }
537
+ compileFormRules(buildFormModel(invoice.schema)); // @jarenjs/forms: per keystroke — visibility, enablement, the message
538
+ new JarenValidator().compile(assertOnSubmit(invoice)); // @jarenjs/validate: on submit — the same rule, as a $query assertion
539
+ ```
540
+
541
+ How to read it: `form()` writes the `x-form` annotation beside the
542
+ keywords the schema already has, so `Infer<>` reads exactly as it does
543
+ on `./schema` and a validator that ignores `x-form` validates the same
544
+ data. Each rule's callback is recorded over the three names the form
545
+ evaluator binds — `c.root` the whole document, `c.value` this field,
546
+ `c.pointer` its location — and `assertOnSubmit()` rewrites the `assert`
547
+ rules into the `$query` keyword so the server checks on submit what the
548
+ form showed per keystroke. The mapping table and the worked examples
549
+ are [docs/FORMS-PEN.md](docs/FORMS-PEN.md).
550
+
551
+ ## The front door: `@jarenjs/linq/db`
552
+
553
+ `open(model, { driver })` opens `@jarenjs/db`'s store and fronts it with
554
+ handles typed from the model pen — no cast, no generate step. Every read
555
+ is the chain and pushes down; `include` emits the store's one-statement
556
+ `load` spec; `link`/`unlink` reach the store's membership API; `live` is
557
+ the store's registration with the chain's bound params; and `explain()`
558
+ names what ran at every level — the chain's document and hops, the
559
+ graph's SQL and pagination strategy, the store's residual reasons.
560
+
561
+ ```js
562
+ import { open } from '@jarenjs/linq/db';
563
+ import { nodeDriver } from '@jarenjs/db/node';
564
+ import { model } from './model.js'; // defineModel(…) from the model pen
565
+
566
+ const db = await open(model, { driver: nodeDriver() }); // validated by default: formats assert
567
+ const hot = await db.entities.Post
568
+ .where((p) => p.stars.ge(3)).orderBy((p) => p.pid).toArray(); // Post[], pushed down to SQL
569
+ const users = await db.entities.User
570
+ .include((u) => u.posts, { where: (p) => p.stars.ge(3), take: 2 }) // one statement, whatever the depth
571
+ .include((u) => u.labels, { count: true })
572
+ .toArray(); // posts: Post[], labels: number
573
+ db.entities.User.link(users[0], 'labels', 'admin'); // 'labels' only: the many-to-many members
574
+ await db.saveChanges(); // the unit of work: get, mutate, save
575
+ const live = await db.live(db.entities.Post.where((p) => p.stars.ge(3))); // { result, subscribe, close, mode }
576
+
577
+ await db.transaction(async (tx) => { // a client of its own, inside the transaction
578
+ tx.entities.Post.add({ title: 'draft', stars: 0, authorId: users[0].id });
579
+ await tx.saveChanges(); // its own unit of work: nobody else sees it
580
+ });
581
+ ```
582
+
583
+ `createDbLedger(db)` is the third export: the `@jarenjs/contract`
584
+ idempotency ledger over a declared collection of the store the client
585
+ opened — root claims under an immediate transaction so two processes
586
+ see one `new`, a transaction client's settlements inside the host's own
587
+ transaction, a persisted generation fence (`JL2007` for a stale ref) —
588
+ with no edge from the contract package to a store
589
+ ([DB-CLIENT §2.6](docs/DB-CLIENT.md#26-the-ledger)).
590
+
591
+ How to read it: `db.entities.Post` is a chain root typed from the model,
592
+ so `p.stars` is a number in the editor and `p.author.email` is a
593
+ declared hop; `include` is a typed `load` spec and answers a two-level
594
+ graph in ONE statement; a write is the unit of work — get an entity,
595
+ mutate it, `saveChanges()` — and `live` re-answers a chain when the
596
+ store changes.
597
+
598
+ Every large read is a real cursor or a bounded page. A `for await`
599
+ over a chain pulls one row per item from an open statement and a
600
+ `break` releases it (three rows of twenty thousand cost three rows);
601
+ `graph.cursor()` yields one root graph per pull with its includes
602
+ attached, every include bounded per root (`maxRows`, `maxBytes`, a
603
+ coded refusal — never a truncated graph); and `graph.page({ limit,
604
+ after, maxBytes })` — `db.entities.Post.graph()` opens a graph with
605
+ nothing included — pages over a composite keyset with the primary key
606
+ appended, answering `{ items, continuation, hasMore, snapshot }`
607
+ whose continuation is typed by the declared `orderBy`/`thenBy` and is
608
+ unsigned and structural: the host signs it. The store's `explain()`
609
+ says what each read will do (`streaming`, `barrier`, `budget`) before
610
+ it runs:
611
+
612
+ ```js
613
+ for await (const post of db.entities.Post.where((p) => p.stars.ge(3))) { // one row per pull
614
+ if (post.stars > 100) break; // the statement is released here
615
+ }
616
+ const page = await db.entities.Post.graph()
617
+ .orderByDescending((p) => p.stars).thenBy((p) => p.pid)
618
+ .page({ limit: 20, maxBytes: 65536 }); // { items, continuation, hasMore, snapshot: false }
619
+ const next = await db.entities.Post.graph()
620
+ .orderByDescending((p) => p.stars).thenBy((p) => p.pid)
621
+ .page({ limit: 20, after: page.continuation }); // the same ordering, or JD0035
622
+ ```
623
+
624
+ `transaction` hands its callback a client of the same shape, over the
625
+ store INSIDE the transaction, with a unit of work of its own — so two
626
+ request handlers on one client hold two records for the same entity key
627
+ and neither sees the other's pending state. The outer `db.entities.X` is
628
+ by construction an unrelated caller: from inside, it waits for the commit
629
+ and then names itself `JD0012` rather than joining a transaction it is
630
+ not part of. One client is safe for a handler per request. The
631
+ transaction client is pinned to its EXACT scope — a handle kept past its
632
+ callback refuses `JD2070` instead of following a later transaction — and
633
+ forwards the store's `tx.savepoints` (MODEL-FORMAT §5.2), so a callback
634
+ can create, roll back to and release a named checkpoint mid-transaction
635
+ without throwing for control flow:
636
+
637
+ ```js
638
+ await db.transaction(async (tx) => {
639
+ await tx.savepoints.create('before-optional-import');
640
+ tx.entities.Post.add({ title: 'optional', stars: 0, authorId: users[0].id });
641
+ const report = await tx.saveChanges(); // landed inside the transaction
642
+ if (report.fallbacks > 0) { // …until the caller changes its mind
643
+ await tx.savepoints.rollbackTo('before-optional-import'); // the add is pending again
644
+ }
645
+ await tx.savepoints.release('before-optional-import'); // the checkpoint is spent; the rest commits
646
+ });
647
+ ```
648
+
649
+ This subpath is the package's one runtime edge: it
650
+ imports `@jarenjs/db`, `@jarenjs/validate` and `@jarenjs/formats` as
651
+ OPTIONAL peer dependencies, so `npm install @jarenjs/linq` alone
652
+ installs nothing new and the `.` entry carries not one byte of them
653
+ (a tree-shaking gate holds it). What is the store's and what is the
654
+ client's — the surface, the refusals, and what the door costs, measured
655
+ against Prisma, Drizzle and Kysely — is
656
+ [docs/DB-CLIENT.md](docs/DB-CLIENT.md).
657
+
658
+ ## What a pen costs
659
+
660
+ A pen builds a **definition** — once, at module load — and the engine
661
+ compiles the document it emitted. That is the only place its price is
662
+ paid, and `benchmark/db.js` measures it as ns per build beside the
663
+ hand-written literal each pen must emit byte for byte — <!--fact:linq.penBuildCost-->schema 55.7×, model 89.4×, JSLT 61.2× a hand-written literal, and the migration pen 1.4× a hand-written document carrying the same two shape hashes<!--/fact-->.
664
+
665
+ Multiples that size are what typed builders, `$defs` hoisting, a
666
+ deep-freeze and a coded refusal per mistake cost against typing the JSON
667
+ yourself; against a request they cost nothing, because no request builds
668
+ a definition. The one pen that is nearly free is the migration pen, and
669
+ for a plain reason: a `$migration` document IS its two shape hashes, so
670
+ a hand-written one has to canonicalize and hash both models too.
671
+
672
+ The chain's price is a different shape and is published beside it: a
673
+ chain re-captures and re-emits its document on every call, so the
674
+ in-memory row in the same benchmark reports the loop, the chain and the
675
+ pre-compiled document as three separate figures.
74
676
 
75
677
  ## What this is not
76
678
 
77
- Not an ORMentities, storage and migrations live in `@jarenjs/db`.
679
+ Not a storage engine the store is `@jarenjs/db`'s: its model, its
680
+ tables, its translator, its unit of work and its migrations live there,
681
+ and `@jarenjs/linq/db` is that store's front door, not a second engine
682
+ (it adds no storage semantics and duplicates no algorithm; every read it
683
+ makes is a query document or a `load` spec the store already runs, and
684
+ the store never imports this package).
78
685
  Not expression trees over arbitrary methods — the vocabulary is the
79
- query engine's, and a construct outside it fails loudly at build time
80
- with a coded error (`JL0001`–`JL0006`) rather than guessing. Not a
686
+ query engine's, and an unknown METHOD fails loudly at build time with
687
+ a coded error (`JL0001`–`JL0007`; the pens' and the client's refusals are `JL0101`–`JL0107`)
688
+ rather than guessing. JavaScript's own
689
+ operators are the one thing a proxy cannot trap: `&&`, `||`, `!`, `?:`
690
+ and `===` evaluate against the proxy object and yield a wrong document
691
+ silently (use `.and()`/`.or()`/`.not()`), and `u.age > 21` or `u.age + 1`
692
+ throw a plain `TypeError` — the format doc's §3 lists them. Not a
81
693
  general lazy-iterable library — if you don't want a query document,
82
694
  you don't want this package.
83
695
 
84
696
  The normative mapping — every operator, its emitted phrase, and the
85
- deliberate deviations — is [docs/LINQ-FORMAT.md](docs/LINQ-FORMAT.md);
697
+ deliberate deviations — is [docs/QUERY-PEN.md](docs/QUERY-PEN.md);
86
698
  internals are in [ARCHITECTURE.md](ARCHITECTURE.md).
699
+
700
+ ## Exports
701
+
702
+ Every subpath a consumer can import, derived from the manifest by
703
+ `npm run docs:derive` (`npm run docs:check` fails when the two drift):
704
+
705
+ <!--fact:exports.linq-->
706
+ | Import | Kind | Declarations |
707
+ |---|---|---|
708
+ | `@jarenjs/linq` | JavaScript | declared |
709
+ | `@jarenjs/linq/schema` | JavaScript | declared |
710
+ | `@jarenjs/linq/model` | JavaScript | declared |
711
+ | `@jarenjs/linq/jslt` | JavaScript | declared |
712
+ | `@jarenjs/linq/migration` | JavaScript | declared |
713
+ | `@jarenjs/linq/contract` | JavaScript | declared |
714
+ | `@jarenjs/linq/flow` | JavaScript | declared |
715
+ | `@jarenjs/linq/app` | JavaScript | declared |
716
+ | `@jarenjs/linq/forms` | JavaScript | declared |
717
+ | `@jarenjs/linq/db` | JavaScript | declared |
718
+ | `@jarenjs/linq/package.json` | metadata | — |
719
+ <!--/fact-->