@jarenjs/linq 0.49.2 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/ARCHITECTURE.md +217 -0
  2. package/README.md +559 -17
  3. package/docs/APP-PEN.md +1143 -0
  4. package/docs/CONTRACT-PEN.md +1217 -0
  5. package/docs/DB-CLIENT.md +814 -0
  6. package/docs/FLOW-PEN.md +1026 -0
  7. package/docs/FORMS-PEN.md +940 -0
  8. package/docs/JSLT-PEN.md +955 -0
  9. package/docs/LINQ-FORMAT.md +771 -383
  10. package/docs/MIGRATION-PEN.md +781 -0
  11. package/docs/MODEL-PEN.md +1083 -0
  12. package/docs/QUERY-PEN.md +1636 -0
  13. package/docs/SCHEMA-PEN.md +1218 -0
  14. package/package.json +57 -4
  15. package/src/app/action.js +255 -0
  16. package/src/app/capture.js +63 -0
  17. package/src/app/define.js +260 -0
  18. package/src/app/index.js +20 -0
  19. package/src/app/patch.js +277 -0
  20. package/src/app/sub.js +106 -0
  21. package/src/async.js +329 -75
  22. package/src/capture-root.js +82 -0
  23. package/src/concurrency.js +9 -4
  24. package/src/contract/define.js +269 -0
  25. package/src/contract/http.js +247 -0
  26. package/src/contract/index.js +23 -0
  27. package/src/contract/operation.js +342 -0
  28. package/src/db/handle.js +86 -0
  29. package/src/db/include.js +316 -0
  30. package/src/db/index.js +19 -0
  31. package/src/db/live.js +43 -0
  32. package/src/db/membership.js +37 -0
  33. package/src/db/open.js +82 -0
  34. package/src/document.js +143 -13
  35. package/src/effect.js +65 -0
  36. package/src/errors.js +69 -6
  37. package/src/expression.js +437 -36
  38. package/src/flow/capture.js +33 -0
  39. package/src/flow/dag.js +302 -0
  40. package/src/flow/fsm.js +328 -0
  41. package/src/flow/index.js +22 -0
  42. package/src/forms/index.js +43 -0
  43. package/src/forms/rules.js +170 -0
  44. package/src/forms/submit.js +177 -0
  45. package/src/index.js +4 -2
  46. package/src/jslt/body.js +226 -0
  47. package/src/jslt/index.js +18 -0
  48. package/src/jslt/rules.js +207 -0
  49. package/src/json-boundary.js +90 -0
  50. package/src/migration/define.js +323 -0
  51. package/src/migration/index.js +15 -0
  52. package/src/migration/steps.js +248 -0
  53. package/src/model/collection.js +171 -0
  54. package/src/model/define.js +125 -0
  55. package/src/model/entity.js +307 -0
  56. package/src/model/index.js +47 -0
  57. package/src/model/relation.js +85 -0
  58. package/src/provider.js +137 -20
  59. package/src/schema/brand.js +31 -0
  60. package/src/schema/builders.js +526 -0
  61. package/src/schema/check.js +29 -0
  62. package/src/schema/emit.js +394 -0
  63. package/src/schema/factories.js +239 -0
  64. package/src/schema/index.js +37 -0
  65. package/src/schema-of.js +24 -0
  66. package/src/sequence.js +233 -103
  67. package/src/sources.js +10 -3
  68. package/types/app.d.ts +293 -0
  69. package/types/contract.d.ts +371 -0
  70. package/types/db.d.ts +188 -0
  71. package/types/flow.d.ts +285 -0
  72. package/types/forms.d.ts +253 -0
  73. package/types/index.d.ts +231 -26
  74. package/types/jslt.d.ts +193 -0
  75. package/types/migration.d.ts +201 -0
  76. package/types/model.d.ts +493 -0
  77. package/types/schema.d.ts +494 -0
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,433 @@ 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
+ How to read it: an operation is a kind (`read`, `command`, `subscribe`),
413
+ its input and output schemas by the schema pen, its named errors and its
414
+ binding. A `named()` schema is hoisted into the contract's own `$defs`
415
+ and referenced from every operation that uses it, and every member is
416
+ written in the order CONTRACT-FORMAT §12.1 fixes, so the pen's document
417
+ and the compiler's own public projection differ by nothing but the
418
+ defaults the compiler materializes. The types come with it and never
419
+ run the TypeScript projection: the same `shop` that typed the client
420
+ types the server's `typedHandlers` table and an AI toolbox's
421
+ `typedTools`. The mapping table and the worked examples are
422
+ [docs/CONTRACT-PEN.md](docs/CONTRACT-PEN.md).
423
+
424
+ ## By code: the flow pen
425
+
426
+ `@jarenjs/linq/flow` writes the two `@jarenjs/flow` documents — a
427
+ `jaren-fsm` 0.1 machine and a `jaren-dag` 0.1 dataflow. State ids, event
428
+ names and node ids are literal types, so a transition into an undeclared
429
+ state or an edge from an undeclared node is a compile error; guards,
430
+ effect props, node queries and edge selectors are captured callbacks,
431
+ never a path typed as a string.
432
+
433
+ ```js
434
+ import { defineFsm, on, state } from '@jarenjs/linq/flow';
435
+ import * as s from '@jarenjs/linq/schema';
436
+ import { compileFsm, fsmToApp } from '@jarenjs/flow';
437
+
438
+ const Approval = s.object({ fresh: s.boolean() });
439
+
440
+ const review = defineFsm({
441
+ initial: 'draft',
442
+ states: ['draft', 'review', state('published', { final: true })],
443
+ transitions: [
444
+ on('draft', 'submit').to('review'),
445
+ on('review', 'approve', { payload: Approval }).when((x) => x.payload.fresh).to('published'),
446
+ on('review').to('draft'), // a wildcard, listed last: document order is the priority
447
+ ],
448
+ });
449
+
450
+ compileFsm(review).step('review', 'approve', { payload: { fresh: true } }); // { state: 'published', final: true, … } — @jarenjs/flow takes it unchanged
451
+ fsmToApp(review); // and so does the app projection
452
+ ```
453
+
454
+ How to read it: `on(from, event)` opens a transition, `.when()` guards
455
+ it with a callback over the payload and the context, `.to()` closes it;
456
+ a transition without an event is a wildcard, and document order is the
457
+ engine's priority order. A guard is recorded, never typed as a string,
458
+ which is also how the pen refuses the one trap FLOW-FORMAT §3 names
459
+ itself — a plain-string guard that is vacuously true (`JL0102`). A
460
+ dataflow is written the same way with `defineDag`, `node` and `edge`.
461
+ The mapping table and the worked examples are
462
+ [docs/FLOW-PEN.md](docs/FLOW-PEN.md).
463
+
464
+ ## By code: the app pen
465
+
466
+ `@jarenjs/linq/app` writes the `jaren-app` 0.1 document `createApp` runs
467
+ — a whole interactive application as one JSON value — and answers the
468
+ state's JSON Schema beside it for `validateState`. Reach for it when the
469
+ state shape, the view, the actions and their patch pointers should be
470
+ one typed declaration: the initial state comes from the state schema's
471
+ own `default()`s, and a patch path is a lambda over the state that
472
+ lowers to a JSON Pointer.
473
+
474
+ ```js
475
+ import { action, append, bind, defineApp, transition } from '@jarenjs/linq/app';
476
+ import { rule } from '@jarenjs/linq/jslt';
477
+ import * as s from '@jarenjs/linq/schema';
478
+ import { createApp } from '@jarenjs/app';
479
+ import { JarenValidator } from '@jarenjs/validate';
480
+
481
+ const { document, stateSchema } = defineApp({
482
+ state: s.object({ todos: s.array(s.string()).default([]), draft: s.string().default('') }),
483
+ view: [rule('$', (v) => ['main', {},
484
+ ['button', { on: { click: bind('todo/add', { payload: v.draft }) } }, 'add']])],
485
+ actions: {
486
+ 'todo/add': action((st, x) => transition({ patch: [append((c) => c.todos, x.payload)] })),
487
+ },
488
+ });
489
+
490
+ document; // { $app: '0.1', state: { todos: [], draft: '' }, view: [...], actions: {…} } — the initial state from the schema's defaults
491
+ createApp(document, { node, validateState: new JarenValidator().compile(stateSchema) }); // @jarenjs/app takes it unchanged
492
+ ```
493
+
494
+ How to read it: the view is JSLT rules over the state (the same pen),
495
+ `bind()` names the action a DOM event dispatches with its payload, and
496
+ an action's callback is captured over APP-FORMAT §3.1's three names
497
+ (`$` the state, `$payload`, `$event`), so a patch value and an effect's props
498
+ are the SAME expression when they should be. A patch path is a lambda
499
+ over the state that lowers to a JSON Pointer — `(c) => c.todos.at(2).done`
500
+ is `/todos/2/done`, and a computed index becomes the pointer expression
501
+ `{ "$concat": ["/todos/", "$payload.i", "/done"] }`. Two refusals the
502
+ loop can only report per dispatch land at build time instead: a view
503
+ binding an action `actions` does not declare (`JA2001`), and an `$event`
504
+ field §3.1 excludes because `$event` must survive `JSON.stringify`
505
+ (`JL0102`). The mapping table and the worked examples are
506
+ [docs/APP-PEN.md](docs/APP-PEN.md).
507
+
508
+ ## By code: the forms pen
509
+
510
+ `@jarenjs/linq/forms` is the schema pen plus the `x-form` vocabulary —
511
+ `form({ visible, enabled, assert, computed, message })` on every builder
512
+ — and `assertOnSubmit()`, which answers the same rules' layer-3 `$query`
513
+ twin in one call. A rule is an annotation: nothing about what the schema
514
+ validates moves, and the rules are callbacks over `c.root`, `c.value`
515
+ and `c.pointer`, never a path typed as a string.
516
+
517
+ ```js
518
+ import * as f from '@jarenjs/linq/forms';
519
+ import { assertOnSubmit } from '@jarenjs/linq/forms';
520
+ import { buildFormModel, compileFormRules } from '@jarenjs/forms';
521
+ import { JarenValidator } from '@jarenjs/validate';
522
+
523
+ const invoice = f.object({
524
+ company: f.string().optional(),
525
+ vatId: f.string().optional().form({
526
+ visible: (c) => c.root.company.ne(''),
527
+ assert: (c) => c.root.company.eq('').or(c.value.ne('')),
528
+ message: 'VAT id is required for companies',
529
+ }),
530
+ });
531
+
532
+ invoice.schema; // a JSON Schema whose vatId carries x-form: { visible, assert, message }
533
+ compileFormRules(buildFormModel(invoice.schema)); // @jarenjs/forms: per keystroke — visibility, enablement, the message
534
+ new JarenValidator().compile(assertOnSubmit(invoice)); // @jarenjs/validate: on submit — the same rule, as a $query assertion
535
+ ```
536
+
537
+ How to read it: `form()` writes the `x-form` annotation beside the
538
+ keywords the schema already has, so `Infer<>` reads exactly as it does
539
+ on `./schema` and a validator that ignores `x-form` validates the same
540
+ data. Each rule's callback is recorded over the three names the form
541
+ evaluator binds — `c.root` the whole document, `c.value` this field,
542
+ `c.pointer` its location — and `assertOnSubmit()` rewrites the `assert`
543
+ rules into the `$query` keyword so the server checks on submit what the
544
+ form showed per keystroke. The mapping table and the worked examples
545
+ are [docs/FORMS-PEN.md](docs/FORMS-PEN.md).
546
+
547
+ ## The front door: `@jarenjs/linq/db`
548
+
549
+ `open(model, { driver })` opens `@jarenjs/db`'s store and fronts it with
550
+ handles typed from the model pen — no cast, no generate step. Every read
551
+ is the chain and pushes down; `include` emits the store's one-statement
552
+ `load` spec; `link`/`unlink` reach the store's membership API; `live` is
553
+ the store's registration with the chain's bound params; and `explain()`
554
+ names what ran at every level — the chain's document and hops, the
555
+ graph's SQL and pagination strategy, the store's residual reasons.
556
+
557
+ ```js
558
+ import { open } from '@jarenjs/linq/db';
559
+ import { nodeDriver } from '@jarenjs/db/node';
560
+ import { model } from './model.js'; // defineModel(…) from the model pen
561
+
562
+ const db = await open(model, { driver: nodeDriver() }); // validated by default: formats assert
563
+ const hot = await db.entities.Post
564
+ .where((p) => p.stars.ge(3)).orderBy((p) => p.pid).toArray(); // Post[], pushed down to SQL
565
+ const users = await db.entities.User
566
+ .include((u) => u.posts, { where: (p) => p.stars.ge(3), take: 2 }) // one statement, whatever the depth
567
+ .include((u) => u.labels, { count: true })
568
+ .toArray(); // posts: Post[], labels: number
569
+ db.entities.User.link(users[0], 'labels', 'admin'); // 'labels' only: the many-to-many members
570
+ await db.saveChanges(); // the unit of work: get, mutate, save
571
+ const live = await db.live(db.entities.Post.where((p) => p.stars.ge(3))); // { result, subscribe, close, mode }
572
+ ```
573
+
574
+ How to read it: `db.entities.Post` is a chain root typed from the model,
575
+ so `p.stars` is a number in the editor and `p.author.email` is a
576
+ declared hop; `include` is a typed `load` spec and answers a two-level
577
+ graph in ONE statement; a write is the unit of work — get an entity,
578
+ mutate it, `saveChanges()` — and `live` re-answers a chain when the
579
+ store changes. This subpath is the package's one runtime edge: it
580
+ imports `@jarenjs/db`, `@jarenjs/validate` and `@jarenjs/formats` as
581
+ OPTIONAL peer dependencies, so `npm install @jarenjs/linq` alone
582
+ installs nothing new and the `.` entry carries not one byte of them
583
+ (a tree-shaking gate holds it). What is the store's and what is the
584
+ client's — the surface, the refusals, and what the door costs, measured
585
+ against Prisma, Drizzle and Kysely — is
586
+ [docs/DB-CLIENT.md](docs/DB-CLIENT.md).
587
+
588
+ ## What a pen costs
589
+
590
+ A pen builds a **definition** — once, at module load — and the engine
591
+ compiles the document it emitted. That is the only place its price is
592
+ paid, and `benchmark/db.js` measures it as ns per build beside the
593
+ hand-written literal each pen must emit byte for byte — <!--fact:linq.penBuildCost-->schema 61.1×, model 87.1×, JSLT 72.1× a hand-written literal, and the migration pen 1.4× a hand-written document carrying the same two shape hashes<!--/fact-->.
594
+
595
+ Multiples that size are what typed builders, `$defs` hoisting, a
596
+ deep-freeze and a coded refusal per mistake cost against typing the JSON
597
+ yourself; against a request they cost nothing, because no request builds
598
+ a definition. The one pen that is nearly free is the migration pen, and
599
+ for a plain reason: a `$migration` document IS its two shape hashes, so
600
+ a hand-written one has to canonicalize and hash both models too.
601
+
602
+ The chain's price is a different shape and is published beside it: a
603
+ chain re-captures and re-emits its document on every call, so the
604
+ in-memory row in the same benchmark reports the loop, the chain and the
605
+ pre-compiled document as three separate figures.
74
606
 
75
607
  ## What this is not
76
608
 
77
- Not an ORMentities, storage and migrations live in `@jarenjs/db`.
609
+ Not a storage engine the store is `@jarenjs/db`'s: its model, its
610
+ tables, its translator, its unit of work and its migrations live there,
611
+ and `@jarenjs/linq/db` is that store's front door, not a second engine
612
+ (it adds no storage semantics and duplicates no algorithm; every read it
613
+ makes is a query document or a `load` spec the store already runs, and
614
+ the store never imports this package).
78
615
  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
616
+ query engine's, and an unknown METHOD fails loudly at build time with
617
+ a coded error (`JL0001`–`JL0007`; the pens' and the client's refusals are `JL0101`–`JL0107`)
618
+ rather than guessing. JavaScript's own
619
+ operators are the one thing a proxy cannot trap: `&&`, `||`, `!`, `?:`
620
+ and `===` evaluate against the proxy object and yield a wrong document
621
+ silently (use `.and()`/`.or()`/`.not()`), and `u.age > 21` or `u.age + 1`
622
+ throw a plain `TypeError` — the format doc's §3 lists them. Not a
81
623
  general lazy-iterable library — if you don't want a query document,
82
624
  you don't want this package.
83
625
 
84
626
  The normative mapping — every operator, its emitted phrase, and the
85
- deliberate deviations — is [docs/LINQ-FORMAT.md](docs/LINQ-FORMAT.md);
627
+ deliberate deviations — is [docs/QUERY-PEN.md](docs/QUERY-PEN.md);
86
628
  internals are in [ARCHITECTURE.md](ARCHITECTURE.md).