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.
Files changed (34) hide show
  1. package/README.md +89 -40
  2. package/dist/cdc/index.d.ts +4 -4
  3. package/dist/cdc/index.js +3 -3
  4. package/dist/{chunk-KOIJ4SNO.js → chunk-7OCXY4R6.js} +1 -1
  5. package/dist/{chunk-ZNU7OI5I.js → chunk-GWWRXIHF.js} +19 -4
  6. package/dist/{chunk-XZTA4VJH.js → chunk-HLFNCKFV.js} +84 -6
  7. package/dist/{chunk-SSVG6NQK.js → chunk-MBJ4JVRM.js} +28 -12
  8. package/dist/{chunk-NWTEUWJD.js → chunk-PHXUFAY2.js} +2 -2
  9. package/dist/{chunk-YGQ2NO6J.js → chunk-VECUS35D.js} +2 -2
  10. package/dist/cli.js +5 -5
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +3 -3
  13. package/dist/internal/index.d.ts +5 -5
  14. package/dist/internal/index.js +4 -4
  15. package/dist/{key-Dne-a20Q.d.ts → key-CWytoEaE.d.ts} +54 -1
  16. package/dist/linter/index.d.ts +5 -5
  17. package/dist/{linter-Cr1Hnw7n.d.ts → linter-jEwmZotm.d.ts} +1 -1
  18. package/dist/{prepared-artifact-BXkARxwO.d.ts → prepared-artifact-CwH5ezFq.d.ts} +3 -3
  19. package/dist/spec/index.d.ts +8 -8
  20. package/dist/spec/index.js +5 -5
  21. package/dist/testing/index.d.ts +2 -2
  22. package/dist/testing/index.js +32 -5
  23. package/dist/transform/index.d.ts +1 -1
  24. package/dist/transform/index.js +1 -1
  25. package/dist/{types-CfxzTEFL.d.ts → types-CgXS-4Ox.d.ts} +37 -8
  26. package/dist/{types-BnFsTZwl.d.ts → types-nk5okD7d.d.ts} +1 -1
  27. package/docs/diagrams/execution-paths.en.svg +14 -0
  28. package/docs/diagrams/execution-paths.ja.svg +14 -0
  29. package/docs/diagrams/query-pipeline.en.svg +10 -0
  30. package/docs/diagrams/query-pipeline.ja.svg +10 -0
  31. package/docs/diagrams/ssot-derivation.en.svg +13 -0
  32. package/docs/diagrams/ssot-derivation.ja.svg +13 -0
  33. package/docs/python-bridge.md +66 -28
  34. package/package.json +1 -1
package/README.md CHANGED
@@ -1,41 +1,63 @@
1
1
  # GraphDDB
2
2
 
3
- **Build GraphQL-like queries directly on DynamoDB —— while keeping every access pattern explicit.**
3
+ **Portable, contract-first DynamoDB access patterns.**
4
4
 
5
- GraphDDB is a type-safe Runtime that maps DynamoDB's native access patterns onto a GraphQL-like query
6
- model. PK / GSI / projection / relation / cursor / operation limits are all kept visible rather than hidden.
7
- It is not an ORM —— it is for **defining, validating, and executing** DynamoDB access patterns through
8
- TypeScript types.
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
- ## Features
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
- - ✅ **Automatic access-pattern resolution** —— the runtime picks the PK / GSI from the fields you give.
15
- - ✅ **Projection** —— only `select`ed fields are read, and only those appear in the result type.
16
- - ✅ **Execution plan (Explain)** —— inspect the execution plan before touching DynamoDB.
17
- - **CQRS contracts** —— public Query/Command contracts (N+1-safe, context boundaries).
18
- - ✅ **Python generation** —— generate a Python client + runtime from TS as the SSoT (conformance-verified).
19
- - ✅ **Testing adapter** —— unit-test planner / traversal / transaction / CDC with an in-memory executor.
20
- - ✅ **Middleware** —— host-side hooks for reads / writes (logging, tenant, authorization).
21
- - ✅ **CDC emulator** —— drive and test change events equivalent to DynamoDB Streams locally.
22
- - ✅ **CDC projection** —— `@cdcProjected()` + `fromChange` / `subscribe` parse CDC change events into typed records (sink delivery / idempotency stay with the consumer).
23
- - ✅ **Maintained access paths** —— keep embedded snapshots / aggregate counters synchronized atomically.
24
- - ✅ **CloudFormation generation** —— emit a CloudFormation template for the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / TableClass / SSE / PROVISIONED / Auto Scaling).
25
- - ✅ **Docs generation** —— generate a model specification (Markdown + Mermaid) from the TS models; templates are Handlebars and user-overridable.
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
- - **Python bridge** —— generate a Python client + runtime with TS as the SSoT, kept in step by a TS↔Python
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 / Python bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and a Python runtime; TS↔Python conformance. |
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
- ```mermaid
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
+ ![Query execution pipeline: TS Model → Planner → Runtime → DynamoDB](./docs/diagrams/query-pipeline.en.svg)
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
- ```mermaid
269
- flowchart LR
270
- M["TS Model (SSoT)"] --> RT["Runtime (TS)"]
271
- M --> PY["Python client + runtime"]
272
- M --> CQ["CQRS Contract"]
273
- M --> CFN["CloudFormation template"]
274
- M --> DOC["Model docs (Markdown + Mermaid)"]
275
- ```
308
+ ![SSoT derivation: TS Model → TS runtime / portable IR → 4 language runtimes / CQRS / CloudFormation / docs](./docs/diagrams/ssot-derivation.en.svg)
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
+ ![The three multi-language execution paths: authoring → static planner → portable IR → dynamic JSON / codegen-static / typed bindings](./docs/diagrams/execution-paths.en.svg)
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
 
@@ -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-Dne-a20Q.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-Dne-a20Q.js';
3
- import { E as EntityMetadata } from '../types-BnFsTZwl.js';
4
- import '../types-CfxzTEFL.js';
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-YGQ2NO6J.js";
11
+ } from "../chunk-VECUS35D.js";
12
12
  import {
13
13
  buildSubscribeHandler,
14
14
  parseChange
15
- } from "../chunk-KOIJ4SNO.js";
16
- import "../chunk-ZNU7OI5I.js";
15
+ } from "../chunk-7OCXY4R6.js";
16
+ import "../chunk-GWWRXIHF.js";
17
17
  import "../chunk-XTWXMOHD.js";
18
18
  export {
19
19
  CdcEmulator,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  MetadataRegistry,
3
3
  attachHiddenKey
4
- } from "./chunk-ZNU7OI5I.js";
4
+ } from "./chunk-GWWRXIHF.js";
5
5
  import {
6
6
  resolveKey,
7
7
  segmentFieldNames
@@ -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 updateResult = buildUpdateExpression(serializedChanges, embeddedFieldNames);
2074
- const names = { ...updateResult.names };
2075
- const values = { ...updateResult.values };
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: appendSetClauses(updateResult.expression, gsiSetClauses),
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-KOIJ4SNO.js";
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-ZNU7OI5I.js";
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
- return { ...base, changes: renderRecord(op.changes, key, params, `${contextLabel} changes`) };
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
- return makeFragment(intent, entity, resolved, descriptor.use, descriptor.condition);
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-NWTEUWJD.js";
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-XZTA4VJH.js";
24
+ } from "./chunk-HLFNCKFV.js";
25
25
  import {
26
26
  detectRelationFields,
27
27
  isInlineSnapshotSpec,
28
28
  normalizeSelectSpec
29
- } from "./chunk-KOIJ4SNO.js";
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-ZNU7OI5I.js";
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: toElementRecord(commandSpec.keyCondition, keyParams) } : {},
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 literal numeric template (no param).
833
- add: { [update.attribute]: String(update.amount) }
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: String(maintain.counter.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}`] = lowerTransactionTemplate(condition.fields[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 templateLeaf(key, value) {
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] = templateLeaf(key, structure[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) => templateLeaf(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-XZTA4VJH.js";
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-ZNU7OI5I.js";
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-KOIJ4SNO.js";
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-ZNU7OI5I.js";
12
+ } from "./chunk-GWWRXIHF.js";
13
13
 
14
14
  // src/cdc/prng.ts
15
15
  var SeededRandom = class {