graphddb 0.9.0 → 0.9.2
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 +89 -40
- package/dist/cdc/index.d.ts +4 -4
- package/dist/cdc/index.js +3 -3
- package/dist/{chunk-KOIJ4SNO.js → chunk-7OCXY4R6.js} +1 -1
- package/dist/{chunk-ZNU7OI5I.js → chunk-GWWRXIHF.js} +19 -4
- package/dist/{chunk-XZTA4VJH.js → chunk-HLFNCKFV.js} +84 -6
- package/dist/{chunk-SSVG6NQK.js → chunk-MBJ4JVRM.js} +28 -12
- package/dist/{chunk-NWTEUWJD.js → chunk-PHXUFAY2.js} +2 -2
- package/dist/{chunk-YGQ2NO6J.js → chunk-VECUS35D.js} +2 -2
- package/dist/cli.js +5 -5
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/internal/index.d.ts +5 -5
- package/dist/internal/index.js +4 -4
- package/dist/{key-Dne-a20Q.d.ts → key-CWytoEaE.d.ts} +54 -1
- package/dist/linter/index.d.ts +5 -5
- package/dist/{linter-Cr1Hnw7n.d.ts → linter-jEwmZotm.d.ts} +1 -1
- package/dist/{prepared-artifact-BXkARxwO.d.ts → prepared-artifact-CwH5ezFq.d.ts} +3 -3
- package/dist/spec/index.d.ts +8 -8
- package/dist/spec/index.js +5 -5
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +32 -5
- package/dist/transform/index.d.ts +1 -1
- package/dist/transform/index.js +1 -1
- package/dist/{types-CfxzTEFL.d.ts → types-CgXS-4Ox.d.ts} +37 -8
- package/dist/{types-BnFsTZwl.d.ts → types-nk5okD7d.d.ts} +1 -1
- package/docs/diagrams/execution-paths.en.svg +14 -0
- package/docs/diagrams/execution-paths.ja.svg +14 -0
- package/docs/diagrams/query-pipeline.en.svg +10 -0
- package/docs/diagrams/query-pipeline.ja.svg +10 -0
- package/docs/diagrams/ssot-derivation.en.svg +13 -0
- package/docs/diagrams/ssot-derivation.ja.svg +13 -0
- package/docs/python-bridge.md +66 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,41 +1,63 @@
|
|
|
1
1
|
# GraphDDB
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**Portable, contract-first DynamoDB access patterns.**
|
|
4
4
|
|
|
5
|
-
GraphDDB
|
|
6
|
-
|
|
7
|
-
It is not an ORM
|
|
8
|
-
|
|
5
|
+
GraphDDB lets you define your DynamoDB access patterns once in TypeScript, validate them at build time,
|
|
6
|
+
inspect the exact execution plan before anything runs, and execute the *same* contract from five
|
|
7
|
+
languages — TypeScript, Python, Rust, Go, and PHP. It is **not an ORM**: it is a contract compiler +
|
|
8
|
+
runtime for explicit DynamoDB access patterns.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
The access patterns become a portable, executable artifact. TypeScript is the single source of truth;
|
|
11
|
+
a static planner lowers each contract to a language-neutral IR (`manifest.json` + `operations.json`),
|
|
12
|
+
and every language runtime executes that IR with results pinned **byte-identical** by a shared
|
|
13
|
+
conformance suite. GraphQL-like traversal is one of the authoring surfaces — ergonomic, but not the
|
|
14
|
+
point. The point is that PK / GSI / projection / relation / cursor / operation limits stay explicit,
|
|
15
|
+
verifiable, and portable rather than hidden behind an abstraction.
|
|
16
|
+
|
|
17
|
+
## ✨ What GraphDDB does — in three layers
|
|
18
|
+
|
|
19
|
+
**1. Define** explicit DynamoDB access patterns
|
|
11
20
|
|
|
12
21
|
- ✅ **Type-safe models** —— the TS class is the single source of truth. Keys / GSIs / field types are checked at compile time.
|
|
13
|
-
- ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select
|
|
14
|
-
- ✅ **
|
|
15
|
-
- ✅ **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- ✅ **
|
|
20
|
-
- ✅ **
|
|
21
|
-
- ✅ **
|
|
22
|
-
- ✅ **
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
- ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select` (an ergonomic authoring surface — arguments → key lookup, selection → projection).
|
|
23
|
+
- ✅ **CQRS contracts** —— public Query/Command contracts as the read/write authoring surface (N+1-safe, context boundaries, composition across contracts).
|
|
24
|
+
- ✅ **Maintained access paths** —— declare embedded snapshots / aggregate counters kept synchronized atomically with the source write.
|
|
25
|
+
|
|
26
|
+
**2. Validate & explain** before anything touches DynamoDB
|
|
27
|
+
|
|
28
|
+
- ✅ **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and over-depth traversals are rejected at compile time / by the linter — equally for human- and AI-written code.
|
|
29
|
+
- ✅ **Execution plan (Explain)** —— `explain()` returns the exact DynamoDB operations, so RCU shape is inspectable up front.
|
|
30
|
+
- ✅ **Automatic access-pattern resolution** —— the runtime picks the PK / GSI from the fields you give; `select`ed fields are the only ones read and the only ones in the result type.
|
|
31
|
+
- ✅ **In-memory testing** —— unit-test planner / traversal / transaction / CDC with no Docker, no DynamoDB Local.
|
|
32
|
+
|
|
33
|
+
**3. Execute the same contract, portably, from five languages**
|
|
34
|
+
|
|
35
|
+
- ✅ **Portable executable IR** —— the static planner lowers each contract to a language-neutral bridge bundle (`manifest.json` + `operations.json`, a `components[]` graph on the shared behavior-contracts vocabulary).
|
|
36
|
+
- ✅ **5 language runtimes** —— generate clients + runtimes for **Python / Rust / Go / PHP** (TypeScript runs the IR natively), all executing the same access patterns.
|
|
37
|
+
- ✅ **5-language conformance** —— TS / Python / Rust / Go / PHP run the same cases against DynamoDB Local; CI pins the results **byte-identical**.
|
|
38
|
+
|
|
39
|
+
**Operational surface** (all built on the layers above): **Middleware** (host-side read/write hooks) ·
|
|
40
|
+
**CDC emulator** (local DynamoDB-Streams-equivalent change events) · **CDC projection**
|
|
41
|
+
(`@cdcProjected()` + `fromChange` / `subscribe` into typed records) · **Prepared statements**
|
|
42
|
+
(compile-once / execute-many, AOT static plans) · **CloudFormation generation** ·
|
|
43
|
+
**Docs generation** (Markdown + Mermaid, Handlebars-overridable).
|
|
26
44
|
|
|
27
45
|
## 🤔 Philosophy
|
|
28
46
|
|
|
47
|
+
- **Access patterns as portable contracts** —— an access pattern is defined once and becomes a
|
|
48
|
+
language-neutral, executable artifact. The same contract is validated, explained, and then executed
|
|
49
|
+
identically across languages, instead of being re-implemented per stack.
|
|
29
50
|
- **Native DynamoDB access patterns** —— express access patterns directly in code instead of hiding them
|
|
30
51
|
behind an ORM abstraction. Because PK / GSI, projection, and limits stay visible, the correct design
|
|
31
52
|
becomes the easiest path.
|
|
32
|
-
- **Graph-style traversal** —— GraphQL's query model (arguments → key lookup, selection → projection,
|
|
33
|
-
relation → Query/Get/BatchGet) corresponds naturally to DynamoDB's access patterns.
|
|
34
53
|
- **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and traversals exceeding
|
|
35
54
|
the allowed depth are rejected before execution at compile time / by the linter. The same constraints
|
|
36
55
|
apply equally to human-written and AI-generated code.
|
|
37
56
|
- **Runtime transparency** —— the runtime is not a black box. `explain()` shows the execution plan, and the
|
|
38
57
|
plan reveals the shape of RCU usage.
|
|
58
|
+
- **Graph-style traversal is a surface, not the essence** —— GraphQL's query model (arguments → key
|
|
59
|
+
lookup, selection → projection, relation → Query/Get/BatchGet) is a convenient way to *author* access
|
|
60
|
+
patterns; the essence is the explicit, portable contract underneath.
|
|
39
61
|
|
|
40
62
|
## 📦 Install
|
|
41
63
|
|
|
@@ -57,7 +79,7 @@ Node.js ≥ 22 is required.
|
|
|
57
79
|
## 🚀 Quick Start
|
|
58
80
|
|
|
59
81
|
```ts
|
|
60
|
-
import { DDBModel, model, string, key, k } from 'graphddb';
|
|
82
|
+
import { DDBModel, graphddb, model, string, key, k } from 'graphddb';
|
|
61
83
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
62
84
|
|
|
63
85
|
@model({ table: 'AppTable', prefix: 'USER' })
|
|
@@ -200,8 +222,16 @@ Each capability is summarized here only. For details, operator lists, and proced
|
|
|
200
222
|
authorization scoping). Host-only and non-serialized. For hook points, see [`docs/middleware.md`](./docs/middleware.md).
|
|
201
223
|
- **Class hydration** —— `options.hydrate` loads read results into host-language domain objects (opt-in,
|
|
202
224
|
Phase 1: `query` top level). [`docs/class-hydration.md`](./docs/class-hydration.md).
|
|
203
|
-
- **
|
|
204
|
-
conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
|
|
225
|
+
- **Multi-language bridge** —— generate Python / Rust / Go / PHP clients + runtimes with TS as the SSoT,
|
|
226
|
+
kept in step by a 5-language conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
|
|
227
|
+
- **Prepared statements** —— `graphddb.prepare($ => ({...}))` → `.execute(params)` (unified read/write).
|
|
228
|
+
"Contract-free precompilation": compile a declarative route once and execute it repeatedly with
|
|
229
|
+
different params. With the **true build-time compilation (AOT)** of `graphddb transform prepared
|
|
230
|
+
--aot`, plans are emitted as static artifacts and zero runtime compilation happens, including the
|
|
231
|
+
first call (a stale plan is loud-rejected at load; drift is caught in CI). Without the transform,
|
|
232
|
+
behavior stays identical through the lazy-slot + structural-memoization fallback. no-runtime-capture
|
|
233
|
+
is loudly enforced by a build-time lint + a runtime guard.
|
|
234
|
+
[`docs/prepared-statements.md`](./docs/prepared-statements.md).
|
|
205
235
|
- **CQRS contracts** —— public Query/Command contracts (cardinality matrix, N+1 safety, context boundaries,
|
|
206
236
|
composition across contracts). [`docs/cqrs-contract.md`](./docs/cqrs-contract.md) /
|
|
207
237
|
[`docs/mutation-command-derivation.md`](./docs/mutation-command-derivation.md).
|
|
@@ -212,6 +242,17 @@ Each capability is summarized here only. For details, operator lists, and proced
|
|
|
212
242
|
`graphddb.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
|
|
213
243
|
consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
|
|
214
244
|
delivery, and idempotency are the consumer's. [`docs/cdc-projection.md`](./docs/cdc-projection.md).
|
|
245
|
+
- **Multi-language codegen (the 3 paths, #257)** —— execution keeps 2 paths + 1 consumer surface,
|
|
246
|
+
coexisting permanently: (1) **dynamic JSON path (default)** — the `components[]` of
|
|
247
|
+
`operations.json` are loaded at runtime and interpreted by the shared `run_behavior` (for swapping
|
|
248
|
+
behavior without a deploy). (2) **codegen-static path (opt-in)** — `graphddb generate behaviors
|
|
249
|
+
--lang <ts|python|go|rust|php>` hands the same IR to the **shared upstream behavior-contracts
|
|
250
|
+
generator** (bc#13), emitting modules with the IR baked in as native literals (graphddb owns only
|
|
251
|
+
envelope + invoke — no generation logic). A generated module checks its fingerprint against the
|
|
252
|
+
live registry and loud-rejects when stale (#208 discipline). Rust generated modules use
|
|
253
|
+
`std::sync::LazyLock`, so **Rust >= 1.80** is required. (3) **typed bindings
|
|
254
|
+
(`generate python/php/rust/go`)** — the consumer-UX surface (typed repositories / DTOs), still
|
|
255
|
+
graphddb-owned; not an execution engine — it delegates to the paths above.
|
|
215
256
|
- **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
|
|
216
257
|
the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / PROVISIONED / Auto
|
|
217
258
|
Scaling). [`docs/cloudformation.md`](./docs/cloudformation.md).
|
|
@@ -232,10 +273,11 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
|
|
|
232
273
|
| [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. |
|
|
233
274
|
| [Design patterns](./docs/design-patterns.md) | Mapping representative DynamoDB design patterns onto graphddb features (how to declare `pattern` / `@model({ kind })` + `@maintainedFrom` / `@aggregate`, read & write maintenance, per-phase coverage). |
|
|
234
275
|
| [Middleware / Hooks](./docs/middleware.md) | The `graphddb.config.use` host-side middleware: hook points read R1–R5 / write W1–W5, `{ context }`, ordering. |
|
|
276
|
+
| [Prepared statements](./docs/prepared-statements.md) | `graphddb.prepare` / `.execute` (unified read/write prepared statements), the no-runtime-capture constraint, `graphddb transform prepared` (AOT static plans / compile-time hoisting + build-time lint + drift detection): choosing between the 3 tiers AOT > lazy slot + structural memoization > ad-hoc. |
|
|
235
277
|
| [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. |
|
|
236
278
|
| [Mutation → command derivation](./docs/mutation-command-derivation.md) | The internal write-plan composition DSL behind the Command IF (`entityWrites`, fragment composition, derivation of atomic `TransactWriteItems`). |
|
|
237
279
|
| [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` that loads read results into host objects (Phase 1: `query` top level). |
|
|
238
|
-
| [Multi-language
|
|
280
|
+
| [Multi-language bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and Python / Rust / Go / PHP runtimes; 5-language conformance. |
|
|
239
281
|
| [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
|
|
240
282
|
| [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
|
|
241
283
|
| [CDC projection](./docs/cdc-projection.md) | The `@cdcProjected()` + `fromChange` / `subscribe` typed-consumer-IF: a contract that parses CDC change events into typed records. Boundary (parse→typed is graphddb; subscription, sink delivery, idempotency are the consumer's). |
|
|
@@ -256,23 +298,30 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
|
|
|
256
298
|
|
|
257
299
|
A query travels from the TS Model through the Planner / Runtime to DynamoDB:
|
|
258
300
|
|
|
259
|
-
|
|
260
|
-
flowchart LR
|
|
261
|
-
M["TS Model<br/>Entity / Key / GSI / Relation"] --> P["Planner<br/>access pattern resolution / projection"]
|
|
262
|
-
P --> R["Runtime<br/>execute / retry / hydrate"]
|
|
263
|
-
R --> D[("DynamoDB")]
|
|
264
|
-
```
|
|
301
|
+

|
|
265
302
|
|
|
266
|
-
From the same TS Model (the SSoT), multiple targets are derived
|
|
303
|
+
From the same TS Model (the SSoT), multiple targets are derived. The language runtimes are fanned out to
|
|
304
|
+
4 languages through the portable IR (the `components[]` graph in `manifest.json` + `operations.json`),
|
|
305
|
+
and the execution results of all 5 languages — TS included — are pinned byte-identical by the
|
|
306
|
+
conformance suite:
|
|
267
307
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
308
|
+

|
|
309
|
+
|
|
310
|
+
| Runtime | Package | Distribution |
|
|
311
|
+
|---------|---------|--------------|
|
|
312
|
+
| TypeScript | `graphddb` | npm |
|
|
313
|
+
| Python | `graphddb-runtime` | PyPI |
|
|
314
|
+
| Rust | `graphddb_runtime` | crates.io |
|
|
315
|
+
| Go | `github.com/foo-log-inc/graphddb/go` | go module (`go/vX.Y.Z` tags) |
|
|
316
|
+
| PHP | `graphddb/runtime` | bundled in-repo (not on composer) |
|
|
317
|
+
|
|
318
|
+
Multi-language execution keeps 3 permanent paths — 2 execution paths + 1 consumer surface (see
|
|
319
|
+
"Multi-language codegen" under [More Capabilities](#-more-capabilities)):
|
|
320
|
+
|
|
321
|
+

|
|
322
|
+
|
|
323
|
+
> The figures are self-contained SVGs drawn with diagram-contracts (Draw TSX).
|
|
324
|
+
> Sources live in [`diagrams/`](./diagrams/); regenerate with `diagrams/build.sh`.
|
|
276
325
|
|
|
277
326
|
## 📄 License
|
|
278
327
|
|
package/dist/cdc/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-
|
|
2
|
-
export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-
|
|
3
|
-
import { E as EntityMetadata } from '../types-
|
|
4
|
-
import '../types-
|
|
1
|
+
import { w as CdcEmulatorOptions, x as ChangeHandler, y as Unsubscribe, F as FaultSpec, z as ConcurrentRecomputeRef, v as ChangeEvent, A as EventLog, G as ReplayOptions, H as ShardId, I as MaintainTrigger, E as EffectPath, g as ProjectionMap, j as MembershipPredicate } from '../key-CWytoEaE.js';
|
|
2
|
+
export { J as BatchResult, K as CdcMode, L as ChangeBatch, N as ChangeEventName, O as ClockMode, V as StartingPosition, X as StreamViewType, Y as SubscribeHandler, Z as SubscribeHandlers, _ as buildSubscribeHandler } from '../key-CWytoEaE.js';
|
|
3
|
+
import { E as EntityMetadata } from '../types-nk5okD7d.js';
|
|
4
|
+
import '../types-CgXS-4Ox.js';
|
|
5
5
|
import 'behavior-contracts';
|
|
6
6
|
|
|
7
7
|
/**
|
package/dist/cdc/index.js
CHANGED
|
@@ -8,12 +8,12 @@ import {
|
|
|
8
8
|
createMaintenanceDrainHandler,
|
|
9
9
|
hashString,
|
|
10
10
|
shardIdFor
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-VECUS35D.js";
|
|
12
12
|
import {
|
|
13
13
|
buildSubscribeHandler,
|
|
14
14
|
parseChange
|
|
15
|
-
} from "../chunk-
|
|
16
|
-
import "../chunk-
|
|
15
|
+
} from "../chunk-7OCXY4R6.js";
|
|
16
|
+
import "../chunk-GWWRXIHF.js";
|
|
17
17
|
import "../chunk-XTWXMOHD.js";
|
|
18
18
|
export {
|
|
19
19
|
CdcEmulator,
|
|
@@ -2070,9 +2070,21 @@ function buildUpdateInput(modelClass, entity, changes, options) {
|
|
|
2070
2070
|
const embeddedFieldNames = new Set(
|
|
2071
2071
|
meta.embeddedFields.map((e) => e.propertyName)
|
|
2072
2072
|
);
|
|
2073
|
-
const
|
|
2074
|
-
const
|
|
2075
|
-
const
|
|
2073
|
+
const addEntries = Object.entries(options?.add ?? {});
|
|
2074
|
+
const addNames = {};
|
|
2075
|
+
const addValues = {};
|
|
2076
|
+
const addClauses = [];
|
|
2077
|
+
addEntries.forEach(([field, delta], i) => {
|
|
2078
|
+
const nameKey = `#add${i}`;
|
|
2079
|
+
const valueKey = `:add${i}`;
|
|
2080
|
+
addNames[nameKey] = field;
|
|
2081
|
+
addValues[valueKey] = delta;
|
|
2082
|
+
addClauses.push(`${nameKey} ${valueKey}`);
|
|
2083
|
+
});
|
|
2084
|
+
const hasChanges = Object.keys(serializedChanges).length > 0;
|
|
2085
|
+
const updateResult = hasChanges || addClauses.length === 0 ? buildUpdateExpression(serializedChanges, embeddedFieldNames) : { expression: "", names: {}, values: {} };
|
|
2086
|
+
const names = { ...updateResult.names, ...addNames };
|
|
2087
|
+
const values = { ...updateResult.values, ...addValues };
|
|
2076
2088
|
const gsiSetClauses = buildGsiRederiveSets(
|
|
2077
2089
|
modelClass,
|
|
2078
2090
|
meta,
|
|
@@ -2090,13 +2102,16 @@ function buildUpdateInput(modelClass, entity, changes, options) {
|
|
|
2090
2102
|
Object.assign(names, condResult.names);
|
|
2091
2103
|
Object.assign(values, condResult.values);
|
|
2092
2104
|
}
|
|
2105
|
+
const setPortion = appendSetClauses(updateResult.expression, gsiSetClauses);
|
|
2106
|
+
const addPortion = addClauses.length > 0 ? `ADD ${addClauses.join(", ")}` : "";
|
|
2107
|
+
const updateExpression = [setPortion, addPortion].filter((s) => s !== "").join(" ");
|
|
2093
2108
|
const updateInput = {
|
|
2094
2109
|
TableName: tableName,
|
|
2095
2110
|
Key: {
|
|
2096
2111
|
PK: pk,
|
|
2097
2112
|
SK: sk
|
|
2098
2113
|
},
|
|
2099
|
-
UpdateExpression:
|
|
2114
|
+
UpdateExpression: updateExpression,
|
|
2100
2115
|
ExpressionAttributeNames: names
|
|
2101
2116
|
};
|
|
2102
2117
|
if (Object.keys(values).length > 0) {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
normalizeTopLevelSelect,
|
|
18
18
|
parseChange,
|
|
19
19
|
validateInlineSnapshotSelect
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-7OCXY4R6.js";
|
|
21
21
|
import {
|
|
22
22
|
EXPR_VERSION,
|
|
23
23
|
MARKER_ROW_ENTITY,
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
serializeFieldValue,
|
|
62
62
|
serializeRawCondition,
|
|
63
63
|
skTemplate
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-GWWRXIHF.js";
|
|
65
65
|
import {
|
|
66
66
|
TableMapping,
|
|
67
67
|
createColumnMap,
|
|
@@ -2520,7 +2520,12 @@ function renderWrite(op, key, params, contextLabel) {
|
|
|
2520
2520
|
return { ...base, item: renderRecord(op.item, key, params, `${contextLabel} item`) };
|
|
2521
2521
|
}
|
|
2522
2522
|
if (op.operation === "update") {
|
|
2523
|
-
|
|
2523
|
+
const add = op.add !== void 0 && Object.keys(op.add).length > 0 ? renderRecord(op.add, key, params, `${contextLabel} add`) : void 0;
|
|
2524
|
+
return {
|
|
2525
|
+
...base,
|
|
2526
|
+
changes: renderRecord(op.changes, key, params, `${contextLabel} changes`),
|
|
2527
|
+
...add !== void 0 ? { add } : {}
|
|
2528
|
+
};
|
|
2524
2529
|
}
|
|
2525
2530
|
return base;
|
|
2526
2531
|
}
|
|
@@ -2534,11 +2539,34 @@ async function executeSingleWrite(w, retry) {
|
|
|
2534
2539
|
return;
|
|
2535
2540
|
}
|
|
2536
2541
|
if (w.operation === "update") {
|
|
2542
|
+
if (w.add !== void 0) {
|
|
2543
|
+
const primaryOptions = { ...options ?? {}, add: w.add };
|
|
2544
|
+
if (w.condition === void 0) {
|
|
2545
|
+
await executeUpdate(w.modelClass, w.key, w.changes ?? {}, primaryOptions);
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
try {
|
|
2549
|
+
await executeUpdate(w.modelClass, w.key, w.changes ?? {}, primaryOptions);
|
|
2550
|
+
} catch (e) {
|
|
2551
|
+
if (!isConditionalCheckFailure(e)) throw e;
|
|
2552
|
+
const fallbackOptions = {
|
|
2553
|
+
add: w.add,
|
|
2554
|
+
...retry !== void 0 ? { retry } : {}
|
|
2555
|
+
};
|
|
2556
|
+
await executeUpdate(w.modelClass, w.key, {}, fallbackOptions);
|
|
2557
|
+
}
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2537
2560
|
await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
|
|
2538
2561
|
return;
|
|
2539
2562
|
}
|
|
2540
2563
|
await executeDelete(w.modelClass, w.key, options);
|
|
2541
2564
|
}
|
|
2565
|
+
function isConditionalCheckFailure(e) {
|
|
2566
|
+
if (e === null || typeof e !== "object") return false;
|
|
2567
|
+
const name = e.name;
|
|
2568
|
+
return name === "ConditionalCheckFailedException" || name === "ConditionalCheckFailed";
|
|
2569
|
+
}
|
|
2542
2570
|
function renderConditionCheck(check, key, params, contextLabel) {
|
|
2543
2571
|
const modelClass = check.entity.modelClass;
|
|
2544
2572
|
const referencedKey = {};
|
|
@@ -4428,8 +4456,17 @@ function isConcreteLiteral(value) {
|
|
|
4428
4456
|
const t = typeof value;
|
|
4429
4457
|
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
4430
4458
|
}
|
|
4431
|
-
function makeFragment(intent, entity, input, use, condition) {
|
|
4459
|
+
function makeFragment(intent, entity, input, use, condition, add) {
|
|
4432
4460
|
const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
|
|
4461
|
+
let checkedAdd;
|
|
4462
|
+
if (add !== void 0 && Object.keys(add).length > 0) {
|
|
4463
|
+
if (intent !== "update") {
|
|
4464
|
+
throw new Error(
|
|
4465
|
+
`mutation: the '${intent}' fragment on '${entity.name}' declares \`add\`, but an atomic \`ADD\` (issue #301) is only valid on an \`update\` fragment.`
|
|
4466
|
+
);
|
|
4467
|
+
}
|
|
4468
|
+
checkedAdd = assertFaithfulInput(add, intent, entity.name);
|
|
4469
|
+
}
|
|
4433
4470
|
if (use !== void 0 && !isEntityWritesDefinition(use)) {
|
|
4434
4471
|
throw new Error(
|
|
4435
4472
|
`mutation: the '${intent}' fragment on '${entity.name}' was given a \`use:\` that is not an \`entityWrites(...)\` save contract. \`use:\` takes the WHOLE \`writes\` set (an \`entityWrites(...)\` value), never a single lifecycle (e.g. \`Model.writes.create\`) \u2014 the fragment's intent already selects the lifecycle.`
|
|
@@ -4441,6 +4478,7 @@ function makeFragment(intent, entity, input, use, condition) {
|
|
|
4441
4478
|
intent,
|
|
4442
4479
|
entity,
|
|
4443
4480
|
input: checked,
|
|
4481
|
+
...checkedAdd !== void 0 ? { add: checkedAdd } : {},
|
|
4444
4482
|
...use !== void 0 ? { use } : {},
|
|
4445
4483
|
...checkedCondition !== void 0 ? { condition: checkedCondition } : {}
|
|
4446
4484
|
};
|
|
@@ -4536,7 +4574,8 @@ function mutation(nameOrBody, maybeBody) {
|
|
|
4536
4574
|
const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
|
|
4537
4575
|
const merged = mergeBindings(descriptor, intent, alias);
|
|
4538
4576
|
const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
|
|
4539
|
-
|
|
4577
|
+
const add = descriptor.add !== void 0 ? resolveAliasRefs(descriptor.add, aliasToIndex, alias) : void 0;
|
|
4578
|
+
return makeFragment(intent, entity, resolved, descriptor.use, descriptor.condition, add);
|
|
4540
4579
|
});
|
|
4541
4580
|
return { [COMMAND_PLAN_BRAND]: true, name, fragments };
|
|
4542
4581
|
}
|
|
@@ -5007,6 +5046,35 @@ function writeDescriptorToPlan(name, d) {
|
|
|
5007
5046
|
const model = resolveDescriptorModel(d[intent], name);
|
|
5008
5047
|
const keyFieldNames = Object.keys(d.key);
|
|
5009
5048
|
const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
|
|
5049
|
+
const addFieldNames = d.add !== void 0 ? Object.keys(d.add) : [];
|
|
5050
|
+
if (addFieldNames.length > 0) {
|
|
5051
|
+
if (intent !== "update") {
|
|
5052
|
+
throw new Error(
|
|
5053
|
+
`publishCommand: method '${name}' declares \`add\` on a '${intent}' descriptor, but an atomic \`ADD\` (issue #301) is only valid on an \`update\` (a standalone UpdateItem). A create / upsert / remove cannot carry \`add\`.`
|
|
5054
|
+
);
|
|
5055
|
+
}
|
|
5056
|
+
if (d.add === null || typeof d.add !== "object") {
|
|
5057
|
+
throw new Error(`publishCommand: method '${name}' has a non-object \`add\`.`);
|
|
5058
|
+
}
|
|
5059
|
+
for (const field of addFieldNames) {
|
|
5060
|
+
const v = d.add[field];
|
|
5061
|
+
if (!isParam(v) || v.kind !== "number") {
|
|
5062
|
+
throw new Error(
|
|
5063
|
+
`publishCommand: method '${name}' binds \`add.${field}\` to a non-\`param.number()\` value. An atomic \`ADD\` delta (issue #301) is a caller \`param.number()\` \u2014 a compile-time constant or a non-numeric param is not a valid atomic increment.`
|
|
5064
|
+
);
|
|
5065
|
+
}
|
|
5066
|
+
if (keyFieldNames.includes(field)) {
|
|
5067
|
+
throw new Error(
|
|
5068
|
+
`publishCommand: method '${name}' binds field '${field}' in both \`key\` and \`add\`. A primary-key field cannot be atomically incremented \u2014 it belongs in \`key\` only.`
|
|
5069
|
+
);
|
|
5070
|
+
}
|
|
5071
|
+
if (inputFieldNames.includes(field)) {
|
|
5072
|
+
throw new Error(
|
|
5073
|
+
`publishCommand: method '${name}' binds field '${field}' in both \`input\` (SET) and \`add\` (ADD). A field is either overwritten (\`input\`) or atomically incremented (\`add\`), never both.`
|
|
5074
|
+
);
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5010
5078
|
const plan2 = mutation(name, ($) => {
|
|
5011
5079
|
const keyRefs = (fields) => {
|
|
5012
5080
|
const out = {};
|
|
@@ -5024,7 +5092,10 @@ function writeDescriptorToPlan(name, d) {
|
|
|
5024
5092
|
const descriptor = {
|
|
5025
5093
|
[intent]: (() => model),
|
|
5026
5094
|
key: keyRefs(keyFieldNames),
|
|
5027
|
-
...inputFieldNames.length > 0 ? { input: inputRefs(d.input) } : {}
|
|
5095
|
+
...inputFieldNames.length > 0 ? { input: inputRefs(d.input) } : {},
|
|
5096
|
+
// #301: `add` deltas are always `param.number()` (validated above), so each binds
|
|
5097
|
+
// to a same-named `$.field` placeholder — the runtime resolves the caller delta.
|
|
5098
|
+
...addFieldNames.length > 0 ? { add: keyRefs(addFieldNames) } : {}
|
|
5028
5099
|
};
|
|
5029
5100
|
return { [name]: descriptor };
|
|
5030
5101
|
});
|
|
@@ -5798,6 +5869,12 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
|
|
|
5798
5869
|
if (keyFieldSet.has(field)) continue;
|
|
5799
5870
|
changes[field] = renderInputLeaf(leaf, false, index, field, resolveEntityRef);
|
|
5800
5871
|
}
|
|
5872
|
+
const add = {};
|
|
5873
|
+
if (fragment.add !== void 0) {
|
|
5874
|
+
for (const [field, leaf] of Object.entries(fragment.add)) {
|
|
5875
|
+
add[field] = renderInputLeaf(leaf, false, index, field, resolveEntityRef);
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5801
5878
|
op = {
|
|
5802
5879
|
__isContractMethodOp: true,
|
|
5803
5880
|
entity: fragment.entity,
|
|
@@ -5805,6 +5882,7 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
|
|
|
5805
5882
|
keys: wholeKeysSentinel(),
|
|
5806
5883
|
keyFields,
|
|
5807
5884
|
...fragment.intent === "update" ? { changes } : {},
|
|
5885
|
+
...fragment.intent === "update" && Object.keys(add).length > 0 ? { add } : {},
|
|
5808
5886
|
// #242: a conditioned update / remove — the write is a CAS-style gate (the
|
|
5809
5887
|
// row must satisfy the condition or the write is rejected, not silently
|
|
5810
5888
|
// committed). Absent → an unconditional update / remove (unchanged).
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
PREPARED_FORMAT_VERSION,
|
|
3
3
|
buildManifest,
|
|
4
4
|
entityFingerprint
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-PHXUFAY2.js";
|
|
6
6
|
import {
|
|
7
7
|
analyzeWriteRoute,
|
|
8
8
|
assertBundleSerializable,
|
|
@@ -21,12 +21,12 @@ import {
|
|
|
21
21
|
isContractKeyRef,
|
|
22
22
|
isContractParamRef,
|
|
23
23
|
isQueryModelContract
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-HLFNCKFV.js";
|
|
25
25
|
import {
|
|
26
26
|
detectRelationFields,
|
|
27
27
|
isInlineSnapshotSpec,
|
|
28
28
|
normalizeSelectSpec
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-7OCXY4R6.js";
|
|
30
30
|
import {
|
|
31
31
|
EXPR_VERSION,
|
|
32
32
|
MARKER_ROW_ENTITY,
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
isRawCondition,
|
|
47
47
|
param,
|
|
48
48
|
resolveModelClass
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-GWWRXIHF.js";
|
|
50
50
|
import {
|
|
51
51
|
TableMapping,
|
|
52
52
|
isColumn,
|
|
@@ -232,6 +232,7 @@ function opToDefinition(contractName, methodName, op) {
|
|
|
232
232
|
}
|
|
233
233
|
const select = op.select !== void 0 ? assertBooleanProjection(op.select, `${contractName}.${methodName} select`) : void 0;
|
|
234
234
|
const changes = op.changes !== void 0 ? convertStructure(op.changes, place, `${contractName}.${methodName} changes`) : void 0;
|
|
235
|
+
const add = op.add !== void 0 ? convertStructure(op.add, place, `${contractName}.${methodName} add`) : void 0;
|
|
235
236
|
const condition = op.condition !== void 0 ? convertCondition(op.condition, place, `${contractName}.${methodName} condition`) : void 0;
|
|
236
237
|
return {
|
|
237
238
|
__isOperationDefinition: true,
|
|
@@ -240,6 +241,7 @@ function opToDefinition(contractName, methodName, op) {
|
|
|
240
241
|
key,
|
|
241
242
|
select,
|
|
242
243
|
changes,
|
|
244
|
+
...add !== void 0 ? { add } : {},
|
|
243
245
|
...condition !== void 0 ? { condition } : {},
|
|
244
246
|
// #154: the use-case description captured on the recorded op (from the
|
|
245
247
|
// descriptor's `description`) flows into the flattened `queries[op]` /
|
|
@@ -584,6 +586,14 @@ function toElementTemplate(template, keyParams) {
|
|
|
584
586
|
);
|
|
585
587
|
}
|
|
586
588
|
function toElementRecord(record, keyParams) {
|
|
589
|
+
const out = {};
|
|
590
|
+
for (const field of Object.keys(record)) {
|
|
591
|
+
const value = record[field];
|
|
592
|
+
out[field] = typeof value === "string" ? toElementTemplate(value, keyParams) : value;
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
}
|
|
596
|
+
function toElementKeyRecord(record, keyParams) {
|
|
587
597
|
const out = {};
|
|
588
598
|
for (const field of Object.keys(record)) {
|
|
589
599
|
out[field] = toElementTemplate(record[field], keyParams);
|
|
@@ -636,7 +646,7 @@ function synthesizeBatchTransaction(commandSpec, op) {
|
|
|
636
646
|
tableName: commandSpec.tableName,
|
|
637
647
|
entity: commandSpec.entity,
|
|
638
648
|
...commandSpec.item !== void 0 ? { item: toElementRecord(commandSpec.item, keyParams) } : {},
|
|
639
|
-
...commandSpec.keyCondition !== void 0 ? { keyCondition:
|
|
649
|
+
...commandSpec.keyCondition !== void 0 ? { keyCondition: toElementKeyRecord(commandSpec.keyCondition, keyParams) } : {},
|
|
640
650
|
...commandSpec.changes !== void 0 ? { changes: toElementRecord(commandSpec.changes, keyParams) } : {},
|
|
641
651
|
...condition !== void 0 ? { condition } : {},
|
|
642
652
|
forEach: { source: "keys" }
|
|
@@ -829,8 +839,9 @@ function buildDerivedUpdateItems(derivedUpdates) {
|
|
|
829
839
|
tableName,
|
|
830
840
|
entity: update.entity.name,
|
|
831
841
|
keyCondition,
|
|
832
|
-
// The delta is a compile-time constant → a
|
|
833
|
-
|
|
842
|
+
// The delta is a compile-time constant → a TYPED numeric literal (no param),
|
|
843
|
+
// carried verbatim so it marshals as `N`, not a stringified `S "1"` (#299).
|
|
844
|
+
add: { [update.attribute]: update.amount }
|
|
834
845
|
});
|
|
835
846
|
collectTemplateParams(keyCondition, metadata, params);
|
|
836
847
|
}
|
|
@@ -890,7 +901,8 @@ function buildMaintainWriteItems(maintainWrites) {
|
|
|
890
901
|
...maintain.kind === "counter" && maintain.counter !== void 0 && maintain.counter.op === "count" ? {
|
|
891
902
|
counter: {
|
|
892
903
|
attribute: maintain.counter.attribute,
|
|
893
|
-
delta
|
|
904
|
+
// TYPED numeric delta (#299): carried verbatim so it marshals as `N`.
|
|
905
|
+
delta: maintain.counter.delta
|
|
894
906
|
}
|
|
895
907
|
} : {}
|
|
896
908
|
};
|
|
@@ -1713,7 +1725,7 @@ function conditionPorts(condition, ports) {
|
|
|
1713
1725
|
return;
|
|
1714
1726
|
case "equals": {
|
|
1715
1727
|
for (const field of Object.keys(condition.fields).sort()) {
|
|
1716
|
-
ports[`condition.equals.${field}`] =
|
|
1728
|
+
ports[`condition.equals.${field}`] = lowerTransactionValue(condition.fields[field]);
|
|
1717
1729
|
}
|
|
1718
1730
|
return;
|
|
1719
1731
|
}
|
|
@@ -1882,11 +1894,12 @@ function paramSpecs(params) {
|
|
|
1882
1894
|
function metaFor(def) {
|
|
1883
1895
|
return MetadataRegistry.get(def.entity.modelClass);
|
|
1884
1896
|
}
|
|
1885
|
-
function
|
|
1897
|
+
function templateValueLeaf(key, value) {
|
|
1886
1898
|
if (isContractParamRef(value)) return value.token;
|
|
1887
1899
|
if (isContractKeyFieldRef(value)) return `{${value.field}}`;
|
|
1888
1900
|
if (isParam(value)) return `{${key}}`;
|
|
1889
1901
|
if (value instanceof Date) return value.toISOString();
|
|
1902
|
+
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
1890
1903
|
return String(value);
|
|
1891
1904
|
}
|
|
1892
1905
|
function keyFieldNames(key) {
|
|
@@ -2220,6 +2233,8 @@ function buildCommandSpec(def) {
|
|
|
2220
2233
|
...description
|
|
2221
2234
|
};
|
|
2222
2235
|
}
|
|
2236
|
+
const addStructure = def.add;
|
|
2237
|
+
const add = addStructure !== void 0 && Object.keys(addStructure).length > 0 ? { add: templateRecord(addStructure) } : {};
|
|
2223
2238
|
return {
|
|
2224
2239
|
type: "UpdateItem",
|
|
2225
2240
|
tableName,
|
|
@@ -2227,6 +2242,7 @@ function buildCommandSpec(def) {
|
|
|
2227
2242
|
params,
|
|
2228
2243
|
keyCondition: writeKeyCondition(metadata, def.key, entity),
|
|
2229
2244
|
changes: templateRecord(def.changes),
|
|
2245
|
+
...add,
|
|
2230
2246
|
...condition ? { condition } : {},
|
|
2231
2247
|
...description
|
|
2232
2248
|
};
|
|
@@ -2253,7 +2269,7 @@ function writeKeyCondition(metadata, key, entity) {
|
|
|
2253
2269
|
function templateRecord(structure) {
|
|
2254
2270
|
const out = {};
|
|
2255
2271
|
for (const key of Object.keys(structure).sort()) {
|
|
2256
|
-
out[key] =
|
|
2272
|
+
out[key] = templateValueLeaf(key, structure[key]);
|
|
2257
2273
|
}
|
|
2258
2274
|
return out;
|
|
2259
2275
|
}
|
|
@@ -2261,7 +2277,7 @@ function extractCondition(def) {
|
|
|
2261
2277
|
return conditionInputToSpec(
|
|
2262
2278
|
def.condition,
|
|
2263
2279
|
`Command on '${def.entity.name}'`,
|
|
2264
|
-
(field, value) =>
|
|
2280
|
+
(field, value) => templateValueLeaf(field, value),
|
|
2265
2281
|
(value, name) => conditionTreeLeaf(value, name),
|
|
2266
2282
|
// A raw `cond` value slot (issue #114-B): a contract param / key ref carries
|
|
2267
2283
|
// its own name; a plain `param.*` falls through to the positional default.
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
evaluateKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-HLFNCKFV.js";
|
|
4
4
|
import {
|
|
5
5
|
SPEC_VERSION_KEY_EXPR
|
|
6
6
|
} from "./chunk-WOFRHRXY.js";
|
|
7
7
|
import {
|
|
8
8
|
MetadataRegistry
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-GWWRXIHF.js";
|
|
10
10
|
import {
|
|
11
11
|
TableMapping
|
|
12
12
|
} from "./chunk-XTWXMOHD.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildMaintenanceGraph
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-7OCXY4R6.js";
|
|
4
4
|
import {
|
|
5
5
|
ChangeCaptureRegistry,
|
|
6
6
|
ClientManager,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
buildDeleteInput,
|
|
10
10
|
buildPutInput,
|
|
11
11
|
resolveModelClass
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-GWWRXIHF.js";
|
|
13
13
|
|
|
14
14
|
// src/cdc/prng.ts
|
|
15
15
|
var SeededRandom = class {
|