graphddb 0.7.10 → 0.8.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/README.md +6 -6
- package/dist/cdc/index.d.ts +389 -5
- package/dist/cdc/index.js +4 -4
- package/dist/{chunk-AD6ZQTTE.js → chunk-GS4C5VGO.js} +2 -6
- package/dist/{chunk-DFUKGU2Q.js → chunk-HNY2EJPV.js} +216 -229
- package/dist/{chunk-3ZU2VW3L.js → chunk-L2NEDS7U.js} +582 -781
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LAT64YCZ.js +1987 -0
- package/dist/chunk-S2NI4PBW.js +187 -0
- package/dist/{chunk-EOJDN3SA.js → chunk-T44OB5GU.js} +3757 -6138
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -254
- package/dist/index.d.ts +23 -1550
- package/dist/index.js +94 -1850
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BAZ9uBGe.d.ts → key-DZtjAQDh.d.ts} +573 -1817
- package/dist/linter/index.d.ts +39 -7
- package/dist/linter/index.js +22 -4
- package/dist/{registry-LWE54Sdc.d.ts → linter-DQY7gUEk.d.ts} +22 -22
- package/dist/prepared-artifact-HFealr1q.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -5
- package/dist/spec/index.js +22 -18
- package/dist/testing/index.d.ts +2 -3
- package/dist/testing/index.js +4 -4
- package/dist/transform/index.d.ts +1 -1
- package/dist/transform/index.js +4 -4
- package/dist/{types-BQLzTEqh.d.ts → types-2PMXEn5x.d.ts} +8 -10
- package/dist/types-DW__-Icc.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +79 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +96 -65
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +14 -4
- package/dist/chunk-3UD3XIF2.js +0 -860
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-Ty95KA8C.d.ts +0 -327
- package/dist/index-Dc7d8mWI.d.ts +0 -1089
- package/dist/relation-depth-BRS513Tq.d.ts +0 -36
package/docs/cqrs-contract.md
CHANGED
|
@@ -83,7 +83,9 @@ field list argument. The access pattern — primary key or GSI — is resolved f
|
|
|
83
83
|
the `Model` plus the `key` fields named in the descriptor:
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
|
-
|
|
86
|
+
import { graphddb, param } from 'graphddb';
|
|
87
|
+
|
|
88
|
+
const UserByEmail = graphddb.publishQuery({
|
|
87
89
|
get: { query: User, key: { email: param.string() }, select: { userId: true, email: true } },
|
|
88
90
|
});
|
|
89
91
|
```
|
|
@@ -95,7 +97,7 @@ key, select, options? }`, with exactly one of `query` / `list`. `select` and
|
|
|
95
97
|
`options` are separate keys (no `select` nested inside `options`).
|
|
96
98
|
|
|
97
99
|
```ts
|
|
98
|
-
export const ArticleById =
|
|
100
|
+
export const ArticleById = graphddb.publishQuery({
|
|
99
101
|
get: { query: Article, key: { articleId: param.string() },
|
|
100
102
|
select: { articleId: true, title: true, body: true },
|
|
101
103
|
options: { consistentRead: false } },
|
|
@@ -193,7 +195,7 @@ write-use-case Methods. A command method is a single declarative write
|
|
|
193
195
|
result?, mode? }`, with exactly one of `create` / `update` / `remove`.
|
|
194
196
|
|
|
195
197
|
```ts
|
|
196
|
-
export const UserCommands =
|
|
198
|
+
export const UserCommands = graphddb.publishCommand({
|
|
197
199
|
disable: { update: User, key: { userId: param.string() },
|
|
198
200
|
input: { status: 'disabled', disableReason: param.string() },
|
|
199
201
|
result: { select: { userId: true, status: true } } },
|
|
@@ -214,7 +216,7 @@ export const UserCommands = publicCommandModel({
|
|
|
214
216
|
**or** a raw `cond\`…\`` fragment whose value slots may be `param.*` bindings:
|
|
215
217
|
|
|
216
218
|
```ts
|
|
217
|
-
export const UserCommands =
|
|
219
|
+
export const UserCommands = graphddb.publishCommand({
|
|
218
220
|
// declarative operators in a write condition
|
|
219
221
|
promote: { update: User, key: { userId: param.string() },
|
|
220
222
|
input: { role: 'admin' },
|
|
@@ -261,6 +263,52 @@ addMany: { create: GroupMembership, key: { groupId: param.string(), userId: para
|
|
|
261
263
|
`GraphQL contrast`: `mode: 'transaction'` is atomic all-or-nothing; `mode:
|
|
262
264
|
'parallel'` is non-atomic per-field partial success.
|
|
263
265
|
|
|
266
|
+
### Composite (multi-entity atomic) methods — the `writes` body
|
|
267
|
+
|
|
268
|
+
A single write descriptor covers one entity. A **composite** method writes
|
|
269
|
+
**multiple entities atomically** — the canonical case being a cross-entity write
|
|
270
|
+
that has **no relation to fall back on** (e.g. marking a hash-keyed aggregate node
|
|
271
|
+
`dirty` alongside a chunk's terminal update). Author it with the **`writes` body**
|
|
272
|
+
form:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
export const ChunkCompleteCommands = graphddb.publishCommand({
|
|
276
|
+
complete: {
|
|
277
|
+
// the public input schema — names + types the params the body references
|
|
278
|
+
input: { chunkId: param.string(), nodeId: param.string() },
|
|
279
|
+
// the mutation body: `$ => ({ alias: descriptor, … })`; `$` refers to the inputs
|
|
280
|
+
writes: ($) => ({
|
|
281
|
+
chunk: { update: Chunk, key: { chunkId: $.chunkId }, input: { done: true } },
|
|
282
|
+
agg: { update: AggNode, key: { nodeId: $.nodeId }, input: { dirty: true } },
|
|
283
|
+
}),
|
|
284
|
+
// optional read-back projection (keyed on the primary — first — fragment)
|
|
285
|
+
result: { select: { chunkId: true, done: true } },
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- `writes` is a body `$ => ({ alias: writeDescriptor, … })`. Each alias value is a
|
|
291
|
+
write descriptor (`{ create | update | remove | upsert: Model, key, input?,
|
|
292
|
+
condition? }` — the same shape as a single-descriptor method, minus `result` /
|
|
293
|
+
`mode`, which are declared on the composite as a whole). The `$` proxy fields are
|
|
294
|
+
the input fields declared in the separate **`input`** schema; a later descriptor
|
|
295
|
+
may reference an earlier alias's written field via `$.alias.field`.
|
|
296
|
+
- **Single-alias** `writes` ⇒ one write. **Multi-alias** ⇒ an **atomic composite**:
|
|
297
|
+
it is promoted to a single `TransactWriteItems` by DynamoDB necessity (there is no
|
|
298
|
+
non-atomic multi-item form for a cross-entity write), so no `mode` ceremony is
|
|
299
|
+
needed — a multi-alias composite is always atomic.
|
|
300
|
+
- `input` (the public param schema), `result` (read-back), and `mode` are specified
|
|
301
|
+
**alongside** the body, never baked into it.
|
|
302
|
+
- **No `mutation(...)` / `defineScpTransaction(...)` import is needed** —
|
|
303
|
+
`publishCommand` compiles the `writes` body internally. The emitted
|
|
304
|
+
`operations.json` wire is byte-identical to the equivalent internal composition.
|
|
305
|
+
- The single-descriptor form above stays the sugar for a single-entity write; the
|
|
306
|
+
`writes` body is the additive form for the multi-entity atomic case.
|
|
307
|
+
|
|
308
|
+
> The full **SCP native-syntax** composite body (imperative `$`/`?:`/`&&`/`.map`
|
|
309
|
+
> transaction sources) is a separate surface, deferred to a later release; the
|
|
310
|
+
> `writes` descriptor-map body above is the supported public composite form today.
|
|
311
|
+
|
|
264
312
|
#### The 25-item limit is a hard error, never a silent split
|
|
265
313
|
|
|
266
314
|
`TransactWriteItems` is capped at **25 items** and is atomic. A
|
|
@@ -276,7 +324,7 @@ read dependency, declared with the `query($ => ({...}))` builder; a cross-
|
|
|
276
324
|
fragment reference uses `$.alias.field`:
|
|
277
325
|
|
|
278
326
|
```ts
|
|
279
|
-
export const AccountAccess =
|
|
327
|
+
export const AccountAccess = graphddb.publishQuery({
|
|
280
328
|
get: query($ => ({
|
|
281
329
|
account: { query: Account, key: { accountId: param.string() },
|
|
282
330
|
select: { accountId: true } },
|
|
@@ -361,7 +409,8 @@ single-model lint framework.
|
|
|
361
409
|
|
|
362
410
|
A method body is evaluated at definition time with throwing sentinels in place
|
|
363
411
|
of `keys` / `params`, recorded through a model recorder, and subjected to the
|
|
364
|
-
same hardening
|
|
412
|
+
same no-runtime-capture build-time hardening the prepared-statement compiler
|
|
413
|
+
applies:
|
|
365
414
|
|
|
366
415
|
1. **Throwing sentinel Proxy** — any property / method access other than the
|
|
367
416
|
internal brand reads or primitive coercion throws at the access site.
|
|
@@ -489,31 +538,41 @@ input produces a byte-identical pre-contract document).
|
|
|
489
538
|
|
|
490
539
|
## Public API surface
|
|
491
540
|
|
|
492
|
-
Definition DSL (`src/define/contract.ts`)
|
|
541
|
+
Definition DSL (`src/define/contract.ts`) — import the `graphddb` namespace
|
|
542
|
+
(`import { graphddb } from 'graphddb'`); `graphddb.publishQuery` /
|
|
543
|
+
`graphddb.publishCommand` are the sole read/write authoring surface:
|
|
493
544
|
|
|
494
545
|
```ts
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
546
|
+
graphddb.publishQuery({ alias: { query | list: Model, key, select, options? } })
|
|
547
|
+
// single-entity write
|
|
548
|
+
graphddb.publishCommand({ alias: { create | update | remove: Model, key, input, condition?, result?, use?, mode? } })
|
|
549
|
+
// multi-entity atomic (composite) write — the `writes` body form (#293)
|
|
550
|
+
graphddb.publishCommand({ alias: { input, writes: $ => ({ alias: descriptor, … }), result?, mode? } })
|
|
499
551
|
param.string() / param.number() / ... // descriptor key/input parameters
|
|
500
552
|
```
|
|
501
553
|
|
|
502
|
-
|
|
554
|
+
A query method may compose another query contract's method (cross-fragment refs
|
|
555
|
+
via `$.alias.field`); a command method composes its derived write ops and
|
|
556
|
+
maintenance writes into one plan, and a **composite** command method's `writes`
|
|
557
|
+
body composes a multi-entity atomic write (§ Composite methods). These are all
|
|
558
|
+
declared inside the `publishQuery` / `publishCommand` map — there is no separate
|
|
559
|
+
public `query()` / `mutation()` composite verb (removed in 0.8.0; the composite
|
|
560
|
+
`writes` body is compiled internally, needing no import).
|
|
503
561
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
562
|
+
Execution is the two `graphddb.*` verbs (`graphddb.query` / `graphddb.mutate`) —
|
|
563
|
+
they route contract methods through the in-process runtime.
|
|
564
|
+
|
|
565
|
+
The contract runtime (`executeQueryMethod` / `executeCommandMethod` /
|
|
566
|
+
per-key-cursor helpers, `src/runtime/*`) is **internal** machinery behind those
|
|
567
|
+
verbs — not a public export.
|
|
509
568
|
|
|
510
|
-
Build / static analysis
|
|
569
|
+
Build / static analysis is the advanced `graphddb/spec` subpath:
|
|
511
570
|
|
|
512
571
|
```ts
|
|
572
|
+
import { buildContracts, buildContexts } from 'graphddb/spec';
|
|
573
|
+
|
|
513
574
|
buildContracts(contracts) // → { contracts, queries, commands, transactions }
|
|
514
575
|
buildContexts(contexts) // → context-ownership specs
|
|
515
|
-
assertContractN1Safe(name, spec, isGsiPoint?)
|
|
516
|
-
assertContractBoundaries(...) // context-boundary lint
|
|
517
576
|
```
|
|
518
577
|
|
|
519
578
|
## Query vs Command
|
package/docs/design-patterns.md
CHANGED
|
@@ -542,11 +542,11 @@ GSI). *Dual-edge form* — declare `w.dualEdge(forward, inverse)` on the forward
|
|
|
542
542
|
keep a **two-row** bidirectional edge in sync without an inverse GSI. The dual edge can
|
|
543
543
|
be recorded in either of two write vocabularies carrying the SAME `w.dualEdge`:
|
|
544
544
|
|
|
545
|
-
- **`entityWrites` (recommended when driving through the command
|
|
545
|
+
- **`entityWrites` (recommended when driving through the command IF, #195)** —
|
|
546
546
|
place `w.dualEdge(...)` in a lifecycle's `edges` array. The dual edge is then derived by
|
|
547
|
-
`
|
|
548
|
-
rows in the SAME atomic transaction with no low-level handwork. Use
|
|
549
|
-
is created / removed through a command.
|
|
547
|
+
the command runtime behind `graphddb.publishCommand` / `graphddb.mutate`, so a write over
|
|
548
|
+
the edge maintains BOTH rows in the SAME atomic transaction with no low-level handwork. Use
|
|
549
|
+
this form if the edge is created / removed through a command.
|
|
550
550
|
- **`edgeWrites` (the low-level edge-only primitive)** — the historical adjacency-only
|
|
551
551
|
`writes` member. Same two-row synchronization, but driven through the low-level edge-write
|
|
552
552
|
path rather than the command IF.
|
|
@@ -626,7 +626,7 @@ actual sink write happens inside the consumer's CDC infrastructure, outside grap
|
|
|
626
626
|
That runtime was **removed in 0.4.0** (issue #152). External projection is now a
|
|
627
627
|
**typed-consumer-IF contract** (issue #153): mark the source model with
|
|
628
628
|
`@cdcProjected()`, then parse a CDC change event into typed instances — `Model.fromChange(event)`
|
|
629
|
-
returns `[oldRecord, newRecord]`, and `
|
|
629
|
+
returns `[oldRecord, newRecord]`, and `graphddb.subscribe(handlers)` (the GraphQL-style
|
|
630
630
|
sibling of `query` / `mutate`) returns a `ChangeHandler` the consumer mounts on its own
|
|
631
631
|
stream. graphddb owns the *parse → typed record* contract; subscription, sink delivery,
|
|
632
632
|
and idempotency stay with the consumer. See [`cdc-projection.md`](./cdc-projection.md)
|
package/docs/docs-generation.md
CHANGED
|
@@ -40,15 +40,15 @@ graphddb generate docs --entry models.ts --out doc.md
|
|
|
40
40
|
`<dir>/index.md` (backward compatible). Either way the command prints a JSON
|
|
41
41
|
result (`{"status":"ok","outDir":…,"files":[…]}`) to stdout, where `files`
|
|
42
42
|
reports the written path(s).
|
|
43
|
-
- **`--
|
|
44
|
-
|
|
45
|
-
Patterns and Mutation Contracts sections; omit it and those sections
|
|
46
|
-
Defaults to the entry module.
|
|
43
|
+
- **`--contracts`** (optional) points at the module exporting your `contracts`
|
|
44
|
+
map (the `graphddb.publishQuery` / `graphddb.publishCommand` results). It powers
|
|
45
|
+
the Access Patterns and Mutation Contracts sections; omit it and those sections
|
|
46
|
+
are empty. Defaults to the entry module.
|
|
47
47
|
|
|
48
48
|
To also document your access patterns and mutations:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
graphddb generate docs --entry models.ts --
|
|
51
|
+
graphddb generate docs --entry models.ts --contracts contracts.ts --out docs/
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## Output sections
|
|
@@ -78,7 +78,7 @@ flag (PITR, table class, encryption) render as `as configured at deployment`.
|
|
|
78
78
|
| --- | --- | --- |
|
|
79
79
|
| `--entry <file>` | (required) | Entry module registering the entity models. |
|
|
80
80
|
| `--out <path>` | (required) | Output path. A `*.md` file is written verbatim at that path; any other value is a directory and `index.md` is written inside it. |
|
|
81
|
-
| `--
|
|
81
|
+
| `--contracts <file>` | entry module | Module exporting the `contracts` map for Access Patterns / Mutation Contracts. |
|
|
82
82
|
| `--format <format>` | `markdown` | Output format (only `markdown` today). |
|
|
83
83
|
| `--split <mode>` | `none` | `none` emits a single `index.md`. `entity` is reserved for a future per-entity split. |
|
|
84
84
|
| `--template-dir <dir>` | — | Directory of override `<partial>.hbs` templates. |
|
package/docs/middleware.md
CHANGED
|
@@ -25,10 +25,10 @@ without embedding any of it in your query or model definitions.
|
|
|
25
25
|
## Registration
|
|
26
26
|
|
|
27
27
|
```ts
|
|
28
|
-
import {
|
|
28
|
+
import { graphddb } from 'graphddb';
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
graphddb.config.use(middleware); // register (call as many times as you like)
|
|
31
|
+
graphddb.config.clearMiddleware(); // remove all (e.g. in test teardown)
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Each registered object may set any subset of read and/or write hooks. Order
|
|
@@ -45,8 +45,8 @@ every relation fan-out operation. A call with no `context` sees `{}`.
|
|
|
45
45
|
|
|
46
46
|
```ts
|
|
47
47
|
await User.query(key, select, { context: { tenantId, actor } });
|
|
48
|
-
await User.
|
|
49
|
-
await
|
|
48
|
+
await User.putItem(item, { context: { actor } });
|
|
49
|
+
await graphddb.query({ users: { query: User, key: keys, select: { name: true } } }, { context: { tenantId } });
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
`context` is an ordinary host-language value and is **never** serialized.
|
|
@@ -102,9 +102,9 @@ Context types: `ReadRequestCtx` (R1/R4/R5) carries `kind: 'query' | 'list' |
|
|
|
102
102
|
`relationPath` (`[]` for the root read, `['orders']` for a direct relation,
|
|
103
103
|
`['orders', 'product']` for a nested one).
|
|
104
104
|
|
|
105
|
-
### `
|
|
105
|
+
### Batch read (`graphddb.query({ key: K[] })`)
|
|
106
106
|
|
|
107
|
-
The multi-key read
|
|
107
|
+
The multi-key read path fires the same hooks: R1/R4/R5 with kind
|
|
108
108
|
`'batchGet'` (the mutable batch input is `ctx.params.requests`), and R2/R3/R5 per
|
|
109
109
|
underlying `BatchGetItem` op (one per ≤100-key chunk). Note DynamoDB
|
|
110
110
|
`BatchGetItem` has no server-side `FilterExpression`, so an R2 hook scopes a batch
|
|
@@ -130,7 +130,7 @@ an optional `transaction.id` shared across the ops of one atomic batch.
|
|
|
130
130
|
`PersistCtx` (W3/W4/W5) carries the mutable `items` and the `origins` of the
|
|
131
131
|
batch.
|
|
132
132
|
|
|
133
|
-
Atomicity: in `
|
|
133
|
+
Atomicity: in `graphddb.mutate({ mode: 'transaction' })`, persist
|
|
134
134
|
hooks fire **once** for the whole atomic batch and any `before` `throw` aborts the
|
|
135
135
|
entire transaction; in `parallel` mode each op is independent.
|
|
136
136
|
|
|
@@ -148,26 +148,26 @@ entire transaction; in `parallel` mode each op is independent.
|
|
|
148
148
|
|
|
149
149
|
```ts
|
|
150
150
|
// Tenant scope on every read, including relation fan-out (R2)
|
|
151
|
-
|
|
151
|
+
graphddb.config.use({ read: { op: { before(ctx) {
|
|
152
152
|
addFilter(ctx.operation, 'tenantId', ctx.context.tenantId);
|
|
153
153
|
} } } });
|
|
154
154
|
|
|
155
155
|
// Inject audit fields on every write (W1) — flows through derivation
|
|
156
|
-
|
|
156
|
+
graphddb.config.use({ write: { before(ctx) {
|
|
157
157
|
if (ctx.kind !== 'delete') ctx.input.item = { ...ctx.input.item, updatedBy: ctx.context.actor };
|
|
158
158
|
} } });
|
|
159
159
|
|
|
160
160
|
// Soft delete by rewrite (W1) — stored as an update
|
|
161
|
-
|
|
161
|
+
graphddb.config.use({ write: { before(ctx) {
|
|
162
162
|
if (ctx.kind === 'delete') { ctx.kind = 'update'; ctx.input.changes = { deletedAt: Date.now() }; }
|
|
163
163
|
} } });
|
|
164
164
|
|
|
165
165
|
// Redact non-admin reads (R4) and audit the real atomic batch (W3)
|
|
166
|
-
|
|
167
|
-
|
|
166
|
+
graphddb.config.use({ read: { afterFetch: (ctx, r) => ctx.context.actor?.isAdmin ? r : redact(r) } });
|
|
167
|
+
graphddb.config.use({ write: { persist: { before(ctx) { auditLog(ctx.items, ctx.context); } } } });
|
|
168
168
|
|
|
169
169
|
// Latency metric using per-request scratch (R1 → R4) and metrics on error (R5)
|
|
170
|
-
|
|
170
|
+
graphddb.config.use({ read: {
|
|
171
171
|
before(ctx) { ctx.state.t0 = performance.now(); },
|
|
172
172
|
afterFetch(ctx, r) { metric(`read.${ctx.kind}`, performance.now() - (ctx.state.t0 as number)); return r; },
|
|
173
173
|
onError(ctx) { count(`read.${ctx.kind}.error`); /* return nothing → rethrow */ },
|
|
@@ -178,7 +178,7 @@ DDBModel.use({ read: {
|
|
|
178
178
|
|
|
179
179
|
The Python runtime exposes the same hook points (R1–R5, W1–W5), interface shape,
|
|
180
180
|
ordering, atomicity, `context` injection, and `onError` recovery, registered
|
|
181
|
-
through the Python runtime's equivalent of `
|
|
181
|
+
through the Python runtime's equivalent of `graphddb.config.use`. Because hooks are
|
|
182
182
|
host-only and never serialized, the two runtimes implement them independently
|
|
183
183
|
with no shared serialized surface; TS↔Python conformance is unaffected.
|
|
184
184
|
|
|
@@ -65,12 +65,12 @@ On top of that, the write-semantics vocabulary is declared additively (it does
|
|
|
65
65
|
|
|
66
66
|
### Command derivation, not hand-authored ops
|
|
67
67
|
|
|
68
|
-
Earlier, the contract command layer (`
|
|
68
|
+
Earlier, the contract command layer (`graphddb.publishCommand`, issue #64) had the
|
|
69
69
|
author *write* the operation: a method body resolved to a single `put` /
|
|
70
|
-
`update` / `delete` (`CommandSpec`) or a hand-built `
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
`update` / `delete` (`CommandSpec`) or a hand-built atomic `TransactWriteItems`.
|
|
71
|
+
The command compiler now derives the plan from declared write semantics: an
|
|
72
|
+
**intent → derive ops** step exists, turning the procedural DSL into a
|
|
73
|
+
*derivable contract*.
|
|
74
74
|
|
|
75
75
|
---
|
|
76
76
|
|
|
@@ -91,8 +91,9 @@ existing `CommandSpec` / `TransactionSpec` / `executionPlan` SSoT:
|
|
|
91
91
|
| idempotency | client-token guard item (`attribute_not_exists`) | tx + idempotency declaration |
|
|
92
92
|
|
|
93
93
|
A multi-effect create (item + edge + counter + uniqueness + event) is one
|
|
94
|
-
**atomic `TransactWriteItems`** — exactly what `
|
|
95
|
-
with the new pieces being *declarations* the
|
|
94
|
+
**atomic `TransactWriteItems`** — exactly what `graphddb.mutate({ mode:
|
|
95
|
+
'transaction' })` serializes, with the new pieces being *declarations* the
|
|
96
|
+
compiler turns into items. The
|
|
96
97
|
≤25-item limit and the "no per-item condition in `BatchWriteItem`" rules (#64)
|
|
97
98
|
apply unchanged.
|
|
98
99
|
|
|
@@ -119,7 +120,7 @@ Two capabilities the derivation depends on, both now implemented:
|
|
|
119
120
|
`mutation` is an **internal DSL for declaring a set of updates that must execute
|
|
120
121
|
atomically** — a *write-plan composition language* used as the *implementation*
|
|
121
122
|
of a Command. The only externally-exposed surface is the fixed **Command IF**
|
|
122
|
-
(`
|
|
123
|
+
(`graphddb.publishCommand`, #64): params in, result out.
|
|
123
124
|
|
|
124
125
|
```
|
|
125
126
|
Public Command IF (external, fixed) = what other services depend on. ← the only public surface
|
|
@@ -234,34 +235,41 @@ override with a custom write contract — and it takes the whole `writes` set,
|
|
|
234
235
|
never `writes.create` (that would double-specify the intent). A cross-fragment
|
|
235
236
|
data dependency is a `$.alias.field` reference. The compiler merges ALL
|
|
236
237
|
fragments into ONE atomic plan. The **public surface is the Command IF**
|
|
237
|
-
(`
|
|
238
|
+
(`graphddb.publishCommand`, #64), whose method is *implemented by* the mutation.
|
|
238
239
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
input: { userId: param.string(), title: param.string(), body: param.string() } },
|
|
247
|
-
audit: { create: AuditLogModel, key: { id: param.string() }, // fragment 2 — composed atomically
|
|
248
|
-
input: { action: 'post.created', actorId: $.post.userId, postId: $.post.postId } },
|
|
249
|
-
}));
|
|
240
|
+
The public surface is `graphddb.publishCommand` — a map of method name → single
|
|
241
|
+
declarative write **descriptor** (`{ create | update | remove: Model, key, input?,
|
|
242
|
+
condition?, result?, use?, mode? }`). The INTENT (`create` / `update` / `remove`)
|
|
243
|
+
selects the lifecycle from the target's `writes`; the compiler folds the derived
|
|
244
|
+
effects (edges, counters, uniqueness guards, outbox, idempotency, and any
|
|
245
|
+
composed maintenance/audit writes) into ONE atomic `TransactWriteItems`. The
|
|
246
|
+
author writes only the descriptor:
|
|
250
247
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// `use:` takes the WHOLE writes set (not writes.create) — the intent picks the
|
|
254
|
-
// lifecycle, so writing `use: PostModel.writes.create` would double-specify it.
|
|
248
|
+
```ts
|
|
249
|
+
import { graphddb, param } from 'graphddb';
|
|
255
250
|
|
|
256
251
|
// ── public Command IF (external, fixed — the only public surface) ──
|
|
257
|
-
export const PostCommands =
|
|
258
|
-
create
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
252
|
+
export const PostCommands = graphddb.publishCommand({
|
|
253
|
+
// Intent `create` selects PostModel.writes.create; the descriptor's derived
|
|
254
|
+
// effects + any composed maintenance/audit writes are merged into one atomic
|
|
255
|
+
// TransactWriteItems by the compiler.
|
|
256
|
+
create: {
|
|
257
|
+
create: PostModel,
|
|
258
|
+
key: { postId: param.string() },
|
|
259
|
+
input: {
|
|
260
|
+
requestId: param.string().optional(),
|
|
261
|
+
userId: param.string(),
|
|
262
|
+
title: param.string(),
|
|
263
|
+
body: param.string(),
|
|
264
|
+
},
|
|
265
|
+
result: { select: { postId: true, title: true } }, // return = read projection
|
|
266
|
+
},
|
|
264
267
|
});
|
|
268
|
+
|
|
269
|
+
// Override the default save contract only when a non-default one is wanted:
|
|
270
|
+
// create: { create: PostModel, use: CustomPostWrites, key: {...}, input: { … } }
|
|
271
|
+
// `use:` takes the WHOLE writes set (not writes.create) — the intent picks the
|
|
272
|
+
// lifecycle, so writing `use: PostModel.writes.create` would double-specify it.
|
|
265
273
|
```
|
|
266
274
|
|
|
267
275
|
Compilation (`compileFragment` for a single fragment;
|
|
@@ -283,18 +291,20 @@ Mutation = { alias₁: fragment₁, alias₂: fragment₂, … } (each: intent
|
|
|
283
291
|
→ attach executionPlan (#70); resolve return selection as a read projection
|
|
284
292
|
```
|
|
285
293
|
|
|
286
|
-
At runtime, every write path — single command, multi-
|
|
287
|
-
|
|
288
|
-
`commitTransaction` orchestration**
|
|
289
|
-
op-aware collapse of same-physical-key
|
|
290
|
-
atomic commit, and the CDC write-capture loop
|
|
291
|
-
caller carries its own collapse / limit / commit
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
At runtime, every write path — a single command, a multi-effect composed
|
|
295
|
+
descriptor, and an atomic `graphddb.mutate({ mode: 'transaction' })` — routes
|
|
296
|
+
through the **single shared `commitTransaction` orchestration**
|
|
297
|
+
(`src/runtime/transaction-commit.ts`, #97): op-aware collapse of same-physical-key
|
|
298
|
+
collisions, the ≤25 enforcement, the atomic commit, and the CDC write-capture loop
|
|
299
|
+
all live in that one place, so no caller carries its own collapse / limit / commit
|
|
300
|
+
/ capture logic.
|
|
301
|
+
|
|
302
|
+
Naming: the internal composition DSL (`src/define/mutation.ts`) is **not public** —
|
|
303
|
+
the public Command IF is `graphddb.publishCommand({ alias: descriptor })`, where
|
|
304
|
+
each `descriptor` is `{ create | update | remove: Model, key, input?, … }`.
|
|
295
305
|
`CommandPlan` = internal execution plan; `entityWrites` = model save-semantics.
|
|
296
|
-
|
|
297
|
-
|
|
306
|
+
Descriptor intents are the keys `create` / `update` / `remove` (`remove`, not
|
|
307
|
+
`del`).
|
|
298
308
|
|
|
299
309
|
This is the **same SSoT** as #64 (`CommandSpec`/`TransactionSpec`) and #70
|
|
300
310
|
(`executionPlan`) — the compiler *populates* it from the adopted save contract
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# GraphDDB Prepared Statements
|
|
2
2
|
|
|
3
|
-
`
|
|
3
|
+
`graphddb.prepare($ => ({...}))` → `.execute(params)` is the read/write-unified
|
|
4
4
|
**prepared statement**: a declarative route body is compiled **once**, and each
|
|
5
5
|
`execute(params)` binds the per-call values into the precompiled plan and runs
|
|
6
|
-
it through the **same execution cores** `
|
|
6
|
+
it through the **same execution cores** `graphddb.mutate` / `Model.query` /
|
|
7
7
|
`Model.list` and the public CQRS contracts use — effects are identical by
|
|
8
8
|
construction (design #203, runtime #205, compile-time transform #206, static
|
|
9
9
|
AOT plans #208).
|
|
@@ -12,21 +12,21 @@ AOT plans #208).
|
|
|
12
12
|
|
|
13
13
|
| Use | API |
|
|
14
14
|
|---|---|
|
|
15
|
-
| ad-hoc one-shot | `
|
|
16
|
-
| repeated hot path (prepared statement) | `
|
|
17
|
-
| public CQRS contract | `
|
|
15
|
+
| ad-hoc one-shot | `graphddb.mutate({...})` / `Model.query(...)` / `Model.list(...)` — recompiles per call |
|
|
16
|
+
| repeated hot path (prepared statement) | `graphddb.prepare($ => ({...}))` → `.execute(params)` |
|
|
17
|
+
| public CQRS contract | `graphddb.publishCommand({...})` / `graphddb.publishQuery({...})` |
|
|
18
18
|
|
|
19
19
|
`prepare` is the missing middle: **precompiled without the contract ceremony**.
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
22
|
// write
|
|
23
|
-
const createPost =
|
|
23
|
+
const createPost = graphddb.prepare(($) => ({
|
|
24
24
|
post: { create: () => Post, key: { threadId: $.threadId, postId: $.postId }, input: { body: $.body } },
|
|
25
25
|
}));
|
|
26
26
|
await createPost.execute({ threadId, postId, body });
|
|
27
27
|
|
|
28
28
|
// read — symmetric; only the key values (+ limit/cursor/consistentRead) are dynamic
|
|
29
|
-
const userById =
|
|
29
|
+
const userById = graphddb.prepare(($) => ({
|
|
30
30
|
user: { query: () => User, key: { userId: $.userId }, select: { userId: true, name: true } },
|
|
31
31
|
}));
|
|
32
32
|
await userById.execute({ userId });
|
|
@@ -65,7 +65,7 @@ whole contract:
|
|
|
65
65
|
| **AOT** (`graphddb transform prepared --aot <artifact> --write`, #208) | **zero compilation at runtime, including the first call** — the plan was compiled at BUILD time into a static artifact; the call site loads it and every call binds params into the frozen plan | identical |
|
|
66
66
|
| **transform applied** (`graphddb transform prepared --write`) | zero per-op after the FIRST call (the lazy slot compiles once per module at first use) | identical |
|
|
67
67
|
| **no transform** (fallback) | phase-1 **structural memoization**: the body is re-evaluated and structure-hashed per call, then the compiled handle is reused from a bounded LRU (no recompile) | identical |
|
|
68
|
-
| hand-hoisted module-level `const stmt =
|
|
68
|
+
| hand-hoisted module-level `const stmt = graphddb.prepare(...)` | compile once at first use (runtime); the transform leaves it untouched | identical |
|
|
69
69
|
|
|
70
70
|
The transform exists to remove the placement footgun: without it, an inline
|
|
71
71
|
`prepare` is *correct but warmer* than a module-level one (silent-slow). With
|
|
@@ -108,7 +108,7 @@ evaluates it:
|
|
|
108
108
|
|
|
109
109
|
```ts
|
|
110
110
|
// after `--aot src/graphddb.prepared.ts --write`
|
|
111
|
-
import { loadPreparedPlan as __gddbLoadPreparedPlan } from 'graphddb';
|
|
111
|
+
import { loadPreparedPlan as __gddbLoadPreparedPlan } from 'graphddb/internal';
|
|
112
112
|
import __gddbPreparedPlans from './graphddb.prepared.js';
|
|
113
113
|
let __gddbPrepared1;
|
|
114
114
|
export async function getUser(userId: string) {
|
|
@@ -152,7 +152,7 @@ The rewrite normalizes each inline call site to a module-scope **lazy slot**:
|
|
|
152
152
|
```ts
|
|
153
153
|
// before (inline, per-call structural memoization)
|
|
154
154
|
export async function getUser(userId: string) {
|
|
155
|
-
return
|
|
155
|
+
return graphddb.prepare(($) => ({
|
|
156
156
|
u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
|
|
157
157
|
})).execute({ userId });
|
|
158
158
|
}
|
|
@@ -160,7 +160,7 @@ export async function getUser(userId: string) {
|
|
|
160
160
|
// after `graphddb transform prepared --write`
|
|
161
161
|
let __gddbPrepared1;
|
|
162
162
|
export async function getUser(userId: string) {
|
|
163
|
-
return (__gddbPrepared1 ??=
|
|
163
|
+
return (__gddbPrepared1 ??= graphddb.prepare(($) => ({
|
|
164
164
|
u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
|
|
165
165
|
}))).execute({ userId });
|
|
166
166
|
}
|
|
@@ -172,7 +172,7 @@ file), the plan compiles once per module at first use, and every later call is
|
|
|
172
172
|
a single nullish check straight into `.execute`. Files containing any
|
|
173
173
|
violation are **never rewritten** — the build fails loudly instead.
|
|
174
174
|
|
|
175
|
-
A file is scanned when it contains a `
|
|
175
|
+
A file is scanned when it contains a `graphddb.prepare` call site; detection
|
|
176
176
|
follows import aliases (`import { DDBModel as M }`), namespace imports
|
|
177
177
|
(`g.DDBModel`), module-const aliases, and same-file `extends DDBModel`
|
|
178
178
|
subclasses. Call sites already evaluated once per module load (module-scope
|
|
@@ -212,7 +212,7 @@ site is never wrong, only warmer.
|
|
|
212
212
|
|
|
213
213
|
Detection also does **not check the import's module specifier**: a binding
|
|
214
214
|
imported as `DDBModel` from **any** package (`import { DDBModel } from
|
|
215
|
-
'<other-package>'`) matches, so another library's same-named `
|
|
215
|
+
'<other-package>'`) matches, so another library's same-named `graphddb.prepare`
|
|
216
216
|
call site is transformed / linted too (a false transform or a spurious lint
|
|
217
217
|
error is possible). Take care in files that mix graphddb with a same-named
|
|
218
218
|
foreign API.
|
|
@@ -221,7 +221,7 @@ foreign API.
|
|
|
221
221
|
|
|
222
222
|
- **write** — the params-independent op template compiles once
|
|
223
223
|
(`compileWriteFragment`); `execute` runs the identical core the envelope and
|
|
224
|
-
`
|
|
224
|
+
`graphddb.publishCommand` use: derived maintainers and atomic
|
|
225
225
|
`TransactWriteItems` semantics match exactly. Multi-alias bodies compose one
|
|
226
226
|
atomic transaction (`mode: 'parallel'` opts out per call).
|
|
227
227
|
- **read** — the params-independent products (resolved model, normalized
|