@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.
- package/ARCHITECTURE.md +227 -0
- package/README.md +650 -17
- package/docs/APP-PEN.md +1143 -0
- package/docs/CONTRACT-PEN.md +1221 -0
- package/docs/DB-CLIENT.md +882 -0
- package/docs/FLOW-PEN.md +1033 -0
- package/docs/FORMS-PEN.md +940 -0
- package/docs/JSLT-PEN.md +955 -0
- package/docs/LINQ-FORMAT.md +778 -383
- package/docs/MIGRATION-PEN.md +781 -0
- package/docs/MODEL-PEN.md +1092 -0
- package/docs/QUERY-PEN.md +1724 -0
- package/docs/SCHEMA-PEN.md +1218 -0
- package/package.json +57 -4
- package/src/app/action.js +251 -0
- package/src/app/capture.js +63 -0
- package/src/app/define.js +255 -0
- package/src/app/index.js +20 -0
- package/src/app/patch.js +277 -0
- package/src/app/sub.js +106 -0
- package/src/async.js +377 -75
- package/src/capture-root.js +82 -0
- package/src/concurrency.js +48 -11
- package/src/contract/define.js +282 -0
- package/src/contract/http.js +247 -0
- package/src/contract/index.js +23 -0
- package/src/contract/operation.js +338 -0
- package/src/db/handle.js +89 -0
- package/src/db/include.js +351 -0
- package/src/db/index.js +24 -0
- package/src/db/ledger.js +195 -0
- package/src/db/live.js +43 -0
- package/src/db/membership.js +37 -0
- package/src/db/open.js +130 -0
- package/src/document.js +143 -13
- package/src/effect.js +65 -0
- package/src/errors.js +78 -6
- package/src/expression.js +463 -36
- package/src/federate.js +531 -0
- package/src/flow/capture.js +33 -0
- package/src/flow/dag.js +316 -0
- package/src/flow/fsm.js +323 -0
- package/src/flow/index.js +22 -0
- package/src/forms/index.js +43 -0
- package/src/forms/rules.js +170 -0
- package/src/forms/submit.js +177 -0
- package/src/index.js +5 -2
- package/src/jslt/body.js +226 -0
- package/src/jslt/index.js +18 -0
- package/src/jslt/rules.js +202 -0
- package/src/json-boundary.js +90 -0
- package/src/migration/define.js +318 -0
- package/src/migration/index.js +15 -0
- package/src/migration/steps.js +244 -0
- package/src/model/collection.js +273 -0
- package/src/model/define.js +125 -0
- package/src/model/entity.js +307 -0
- package/src/model/index.js +47 -0
- package/src/model/relation.js +85 -0
- package/src/provider.js +137 -20
- package/src/schema/brand.js +31 -0
- package/src/schema/builders.js +526 -0
- package/src/schema/check.js +29 -0
- package/src/schema/emit.js +394 -0
- package/src/schema/factories.js +239 -0
- package/src/schema/index.js +37 -0
- package/src/schema-of.js +24 -0
- package/src/sequence.js +233 -103
- package/src/sources.js +10 -3
- package/types/app.d.ts +293 -0
- package/types/contract.d.ts +468 -0
- package/types/db.d.ts +359 -0
- package/types/flow.d.ts +285 -0
- package/types/forms.d.ts +253 -0
- package/types/index.d.ts +296 -26
- package/types/jslt.d.ts +193 -0
- package/types/migration.d.ts +201 -0
- package/types/model.d.ts +526 -0
- package/types/schema.d.ts +494 -0
package/README.md
CHANGED
|
@@ -1,11 +1,139 @@
|
|
|
1
1
|
# @jarenjs/linq
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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/
|
|
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
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
80
|
-
|
|
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/
|
|
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-->
|