graphddb 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cdc/index.d.ts +2 -2
- package/dist/{chunk-N5NQM3SO.js → chunk-GFGVDF4W.js} +500 -12
- package/dist/cli.js +155 -6
- package/dist/{from-change-CFzBy7aU.d.ts → from-change-pnURY-cV.d.ts} +1 -1
- package/dist/{index-CtJbTrfB.d.ts → index-Eg94ChE1.d.ts} +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +11 -1
- package/dist/linter/index.d.ts +4 -4
- package/dist/{maintenance-view-adapter-CFeasCKo.d.ts → maintenance-view-adapter-BP2CJDdz.d.ts} +2489 -2245
- package/dist/{registry-DbqmFyab.d.ts → registry-Cv9nl_3i.d.ts} +1 -1
- package/dist/{relation-depth-0TiWr5OW.d.ts → relation-depth-BR0y7Q1i.d.ts} +1 -1
- package/dist/spec/index.d.ts +3 -3
- package/dist/spec/index.js +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/transform/index.d.ts +167 -0
- package/dist/transform/index.js +719 -0
- package/docs/prepared-statements.md +165 -0
- package/package.json +5 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# GraphDDB Prepared Statements
|
|
2
|
+
|
|
3
|
+
`DDBModel.prepare($ => ({...}))` → `.execute(params)` is the read/write-unified
|
|
4
|
+
**prepared statement**: a declarative route body is compiled **once**, and each
|
|
5
|
+
`execute(params)` binds the per-call values into the precompiled plan and runs
|
|
6
|
+
it through the **same execution cores** `DDBModel.mutate` / `Model.query` /
|
|
7
|
+
`Model.list` and the public CQRS contracts use — effects are identical by
|
|
8
|
+
construction (design #203, runtime #205, compile-time transform #206).
|
|
9
|
+
|
|
10
|
+
## The three execution layers (orthogonal, pick per use)
|
|
11
|
+
|
|
12
|
+
| Use | API |
|
|
13
|
+
|---|---|
|
|
14
|
+
| ad-hoc one-shot | `DDBModel.mutate({...})` / `Model.query(...)` / `Model.list(...)` — recompiles per call |
|
|
15
|
+
| repeated hot path (prepared statement) | `DDBModel.prepare($ => ({...}))` → `.execute(params)` |
|
|
16
|
+
| public CQRS contract | `publicCommandModel({...})` / `publicQueryModel({...})` |
|
|
17
|
+
|
|
18
|
+
`prepare` is the missing middle: **precompiled without the contract ceremony**.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// write
|
|
22
|
+
const createPost = DDBModel.prepare(($) => ({
|
|
23
|
+
post: { create: () => Post, key: { threadId: $.threadId, postId: $.postId }, input: { body: $.body } },
|
|
24
|
+
}));
|
|
25
|
+
await createPost.execute({ threadId, postId, body });
|
|
26
|
+
|
|
27
|
+
// read — symmetric; only the key values (+ limit/cursor/consistentRead) are dynamic
|
|
28
|
+
const userById = DDBModel.prepare(($) => ({
|
|
29
|
+
user: { query: () => User, key: { userId: $.userId }, select: { userId: true, name: true } },
|
|
30
|
+
}));
|
|
31
|
+
await userById.execute({ userId });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## The key constraint: no runtime capture
|
|
35
|
+
|
|
36
|
+
A prepared body must be **pure-declarative**: it may reference only
|
|
37
|
+
|
|
38
|
+
- `$.<name>` — per-call values, bound at `execute`;
|
|
39
|
+
- **module-static** references — imports, module-scope `const` / `class` /
|
|
40
|
+
`enum`, the `() => Model` model refs, shared `const` select templates;
|
|
41
|
+
- static literals (string / number / boolean / bigint / `null`).
|
|
42
|
+
|
|
43
|
+
Capturing a per-call runtime value into the body — an enclosing function's
|
|
44
|
+
parameter or `let`, `p.foo`, `this`, mutable module state, any helper **call**
|
|
45
|
+
(the indirect-capture channel; even `Date.now()` is a fresh per-call value) —
|
|
46
|
+
breaks preparability and is rejected **loudly**:
|
|
47
|
+
|
|
48
|
+
- **at build time** by the static lint (`graphddb transform prepared`, below) —
|
|
49
|
+
the primary, phase-2 enforcement, including indirect capture via same-file
|
|
50
|
+
helper constants (transitively verified);
|
|
51
|
+
- **at runtime** by the phase-1 guard (`assertNoRuntimeCapture` and the
|
|
52
|
+
recording `$` proxy) as a backstop for what a per-file syntactic pass cannot
|
|
53
|
+
see (cross-module mutable state, mutated template objects).
|
|
54
|
+
|
|
55
|
+
## Zero overhead vs. fallback: the compile-time transform
|
|
56
|
+
|
|
57
|
+
**With the transform applied, prepared statements are zero-overhead per
|
|
58
|
+
operation regardless of where you write them. Without it, they still behave
|
|
59
|
+
identically — they just pay a small per-call memoization cost.** That is the
|
|
60
|
+
whole contract:
|
|
61
|
+
|
|
62
|
+
| Build | Per-op cost of an inline `prepare(fn).execute()` | Result |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| **transform applied** (`graphddb transform prepared --write`) | **zero** planning / compiling / structure hashing — the call site is normalized to a module-scope prepared slot; after the first call it is a nullish check + `.execute` | identical |
|
|
65
|
+
| **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 |
|
|
66
|
+
| hand-hoisted module-level `const stmt = DDBModel.prepare(...)` | zero (already optimal; the transform leaves it untouched) | identical |
|
|
67
|
+
|
|
68
|
+
The transform exists to remove the placement footgun: without it, an inline
|
|
69
|
+
`prepare` is *correct but warmer* than a module-level one (silent-slow). With
|
|
70
|
+
it, both spellings compile to the same optimal form.
|
|
71
|
+
|
|
72
|
+
### CLI
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# build lint (CI gate): verify no-runtime-capture, report hoistable inline
|
|
76
|
+
# call sites as notes; exit 1 on violations
|
|
77
|
+
graphddb transform prepared --src src/
|
|
78
|
+
|
|
79
|
+
# apply: rewrite verified inline call sites in place (idempotent)
|
|
80
|
+
graphddb transform prepared --src src/ --write
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The rewrite normalizes each inline call site to a module-scope **lazy slot**:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
// before (inline, per-call structural memoization)
|
|
87
|
+
export async function getUser(userId: string) {
|
|
88
|
+
return DDBModel.prepare(($) => ({
|
|
89
|
+
u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
|
|
90
|
+
})).execute({ userId });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// after `graphddb transform prepared --write`
|
|
94
|
+
let __gddbPrepared1;
|
|
95
|
+
export async function getUser(userId: string) {
|
|
96
|
+
return (__gddbPrepared1 ??= DDBModel.prepare(($) => ({
|
|
97
|
+
u: { query: () => User, key: { userId: $.userId }, select: USER_SELECT },
|
|
98
|
+
}))).execute({ userId });
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The body stays lexically in place (every reference resolves exactly as
|
|
103
|
+
before — no TDZ / module-init-order hazards with models declared later in the
|
|
104
|
+
file), the plan compiles once per module at first use, and every later call is
|
|
105
|
+
a single nullish check straight into `.execute`. Files containing any
|
|
106
|
+
violation are **never rewritten** — the build fails loudly instead.
|
|
107
|
+
|
|
108
|
+
A file is scanned when it contains a `DDBModel.prepare` call site; detection
|
|
109
|
+
follows import aliases (`import { DDBModel as M }`), namespace imports
|
|
110
|
+
(`g.DDBModel`), module-const aliases, and same-file `extends DDBModel`
|
|
111
|
+
subclasses. Call sites already evaluated once per module load (module-scope
|
|
112
|
+
`const`, static class fields) are left as-is.
|
|
113
|
+
|
|
114
|
+
### Programmatic API
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { transformPreparedSource, lintPreparedSource } from 'graphddb/transform';
|
|
118
|
+
|
|
119
|
+
const result = transformPreparedSource(fileName, sourceText); // { text, changed, diagnostics, ... }
|
|
120
|
+
const diagnostics = lintPreparedSource(fileName, sourceText); // check-only (the build gate)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Build-time only (requires the optional `typescript` dependency, like the JSDoc
|
|
124
|
+
codegen pass); wire it into a bundler plugin or a pre-build step if you prefer
|
|
125
|
+
that over the CLI.
|
|
126
|
+
|
|
127
|
+
### What the static lint proves — and its limits
|
|
128
|
+
|
|
129
|
+
The lint accepts only the declarative grammar the runtime itself accepts
|
|
130
|
+
(object/array literals, static literals, `$.<name>`, module-static reference
|
|
131
|
+
chains, `() => Model` thunks), applied **transitively** through same-file
|
|
132
|
+
module-const templates. It therefore rejects, per-file and soundly: direct
|
|
133
|
+
captures (enclosing-scope bindings, `this`), indirect captures via **any** call
|
|
134
|
+
or `new`, mutable module state (`let` / `var`), unresolved globals, per-call
|
|
135
|
+
control flow (conditionals, computed keys, substitution templates), and bodies
|
|
136
|
+
it cannot statically see.
|
|
137
|
+
|
|
138
|
+
Per-file syntactic analysis cannot prove two things: an **imported** binding is
|
|
139
|
+
accepted as module-static (that is the design's allowance for `() => Model`
|
|
140
|
+
refs — a pathological mutable re-export is invisible), and mutation of a
|
|
141
|
+
same-file `const` **object** between calls. Both are backstopped by the
|
|
142
|
+
phase-1 runtime guard (only `$`-refs and scalar static literals are bindable
|
|
143
|
+
value leaves) and by the fallback's correctness: an undetected unhoistable
|
|
144
|
+
site is never wrong, only warmer.
|
|
145
|
+
|
|
146
|
+
Detection also does **not check the import's module specifier**: a binding
|
|
147
|
+
imported as `DDBModel` from **any** package (`import { DDBModel } from
|
|
148
|
+
'<other-package>'`) matches, so another library's same-named `DDBModel.prepare`
|
|
149
|
+
call site is transformed / linted too (a false transform or a spurious lint
|
|
150
|
+
error is possible). Take care in files that mix graphddb with a same-named
|
|
151
|
+
foreign API.
|
|
152
|
+
|
|
153
|
+
## Semantics (both layers, guaranteed identical)
|
|
154
|
+
|
|
155
|
+
- **write** — the params-independent op template compiles once
|
|
156
|
+
(`compileWriteFragment`); `execute` runs the identical core the envelope and
|
|
157
|
+
`publicCommandModel` use: derived maintainers and atomic
|
|
158
|
+
`TransactWriteItems` semantics match exactly. Multi-alias bodies compose one
|
|
159
|
+
atomic transaction (`mode: 'parallel'` opts out per call).
|
|
160
|
+
- **read** — the params-independent products (resolved model, normalized
|
|
161
|
+
select, validated relation structure) compute once; `execute` binds key
|
|
162
|
+
values + per-call pagination/consistency and runs `executeQuery` /
|
|
163
|
+
`executeList` exactly like `Model.query` / `Model.list`.
|
|
164
|
+
- A body is all-read or all-write; an option the target core would ignore is a
|
|
165
|
+
loud compile-time reject (never a silent drop).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "graphddb",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Graph data modeling on DynamoDB with adjacency list pattern",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"./cdc": {
|
|
30
30
|
"types": "./dist/cdc/index.d.ts",
|
|
31
31
|
"import": "./dist/cdc/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./transform": {
|
|
34
|
+
"types": "./dist/transform/index.d.ts",
|
|
35
|
+
"import": "./dist/transform/index.js"
|
|
32
36
|
}
|
|
33
37
|
},
|
|
34
38
|
"bin": {
|