graphddb 0.5.0 → 0.5.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 CHANGED
@@ -19,7 +19,9 @@ TypeScript types.
19
19
  - ✅ **Testing adapter** —— unit-test planner / traversal / transaction / CDC with an in-memory executor.
20
20
  - ✅ **Middleware** —— host-side hooks for reads / writes (logging, tenant, authorization).
21
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).
22
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).
23
25
 
24
26
  ## 🤔 Philosophy
25
27
 
@@ -204,6 +206,11 @@ Each capability is summarized here only. For details, operator lists, and proced
204
206
  [`docs/mutation-command-derivation.md`](./docs/mutation-command-derivation.md).
205
207
  - **CDC emulator** —— change events equivalent to DynamoDB Streams, to drive and test differential
206
208
  aggregation. [`docs/cdc-emulator.md`](./docs/cdc-emulator.md).
209
+ - **CDC projection** —— a typed-consumer-IF that parses the CDC change events of a `@cdcProjected()`
210
+ source model into typed records. `Model.fromChange(event)` returns `[old, new]`, and
211
+ `DDBModel.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
212
+ consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
213
+ delivery, and idempotency are the consumer's. [`docs/cdc-projection.md`](./docs/cdc-projection.md).
207
214
  - **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
208
215
  the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / PROVISIONED / Auto
209
216
  Scaling). [`docs/cloudformation.md`](./docs/cloudformation.md).
@@ -225,6 +232,7 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
225
232
  | [Multi-language / Python bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and a Python runtime; TS↔Python conformance. |
226
233
  | [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
227
234
  | [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
235
+ | [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). |
228
236
  | [CloudFormation generation](./docs/cloudformation.md) | The `generate cloudformation` command: emit a CloudFormation template for the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / TableClass / SSE / PROVISIONED / Auto Scaling). All options, derivation rules, and scope boundary. |
229
237
  | [Benchmark](./benchmark/RESULTS.md) | Library comparison against hand-written AWS SDK, ElectroDB, DynamoDB Toolbox, and OneTable. |
230
238
 
@@ -255,6 +263,7 @@ flowchart LR
255
263
  M["TS Model (SSoT)"] --> RT["Runtime (TS)"]
256
264
  M --> PY["Python client + runtime"]
257
265
  M --> CQ["CQRS Contract"]
266
+ M --> CFN["CloudFormation template"]
258
267
  ```
259
268
 
260
269
  ## 📄 License
@@ -11,8 +11,11 @@ import {
11
11
  buildConditionExpression,
12
12
  buildDeleteInput,
13
13
  buildMaintenanceGraph,
14
+ buildProject,
14
15
  buildPutInput,
15
16
  buildReadRuntime,
17
+ buildRelation,
18
+ buildSubscribeHandler,
16
19
  buildUpdateInput,
17
20
  buildWriteRuntime,
18
21
  captureWrite,
@@ -27,13 +30,18 @@ import {
27
30
  executeTransaction,
28
31
  executeUpdate,
29
32
  getEntityWrites,
33
+ hydrate,
30
34
  isColumn,
31
35
  isEntityWritesDefinition,
32
36
  isLifecycleContract,
33
37
  isParam,
34
38
  isRawCondition,
39
+ isSelectBuilder,
35
40
  lifecyclePhaseForIntent,
41
+ normalizeSelectSpec,
42
+ normalizeTopLevelSelect,
36
43
  param,
44
+ parseChange,
37
45
  pkTemplate,
38
46
  resolveKey,
39
47
  resolveModelClass,
@@ -42,7 +50,7 @@ import {
42
50
  serializeFieldValue,
43
51
  serializeRawCondition,
44
52
  skTemplate
45
- } from "./chunk-FMJIWFIS.js";
53
+ } from "./chunk-W3GEJPPV.js";
46
54
 
47
55
  // src/spec/types.ts
48
56
  var SPEC_VERSION = "1.0";
@@ -91,7 +99,10 @@ function buildGsis(metadata) {
91
99
  unique: gsi.unique,
92
100
  inputFields: [...gsi.inputFieldNames],
93
101
  pkTemplate: pk,
94
- skTemplate: sk ?? null
102
+ skTemplate: sk ?? null,
103
+ // Purely-documentary GSI description (issue #166); emitted only when declared
104
+ // so an undescribed index is byte-identical to the pre-#166 manifest.
105
+ ...gsi.description !== void 0 ? { description: gsi.description } : {}
95
106
  };
96
107
  });
97
108
  }
@@ -102,7 +113,10 @@ function buildRelations(metadata) {
102
113
  relations[rel.propertyName] = {
103
114
  type: rel.type,
104
115
  target: targetClass.name,
105
- keyBinding: { ...rel.keyBinding }
116
+ keyBinding: { ...rel.keyBinding },
117
+ // Purely-documentary relation description (issue #166); emitted only when
118
+ // declared so an undescribed relation is byte-identical to the pre-#166 manifest.
119
+ ...rel.options?.description !== void 0 ? { description: rel.options.description } : {}
106
120
  };
107
121
  }
108
122
  return sortedRecord(relations);
@@ -174,79 +188,6 @@ function buildManifest(registry = MetadataRegistry) {
174
188
  };
175
189
  }
176
190
 
177
- // src/types/select-builder.ts
178
- var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
179
- var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
180
-
181
- // src/select/builder.ts
182
- var BuilderImpl = class _BuilderImpl {
183
- [SELECT_BUILDER] = true;
184
- /** The resolved spec, exposed (non-method) so normalizers can read it. */
185
- [SELECT_SPEC];
186
- constructor(spec) {
187
- this[SELECT_SPEC] = spec;
188
- }
189
- clone(patch) {
190
- return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
191
- }
192
- filter(filter) {
193
- return this.clone({ filter });
194
- }
195
- limit(limit) {
196
- return this.clone({ limit });
197
- }
198
- after(cursor) {
199
- return this.clone({ after: cursor });
200
- }
201
- order(order) {
202
- return this.clone({ order });
203
- }
204
- };
205
- function isSelectBuilder(value) {
206
- return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
207
- }
208
- function fromBuilder(value) {
209
- const spec = value[SELECT_SPEC];
210
- return {
211
- select: spec.select,
212
- filter: spec.filter,
213
- limit: spec.limit,
214
- after: spec.after,
215
- order: spec.order
216
- };
217
- }
218
- function normalizeSelectSpec(value) {
219
- if (isSelectBuilder(value)) {
220
- return fromBuilder(value);
221
- }
222
- const obj = value;
223
- return {
224
- select: obj.select,
225
- filter: obj.filter,
226
- limit: obj.limit,
227
- after: obj.after,
228
- order: obj.order
229
- };
230
- }
231
- function normalizeTopLevelSelect(value) {
232
- if (isSelectBuilder(value)) {
233
- return fromBuilder(value);
234
- }
235
- return { select: value ?? {} };
236
- }
237
- function buildProject(select) {
238
- return new BuilderImpl({
239
- [SELECT_BUILDER]: true,
240
- select
241
- });
242
- }
243
- function buildRelation(select) {
244
- return new BuilderImpl({
245
- [SELECT_BUILDER]: true,
246
- select
247
- });
248
- }
249
-
250
191
  // src/relation/detect.ts
251
192
  function isRelationSpec(value) {
252
193
  if (typeof value !== "object" || value === null) return false;
@@ -726,80 +667,6 @@ async function execute(operation, options) {
726
667
  return ClientManager.getExecutor().execute(operation, options);
727
668
  }
728
669
 
729
- // src/hydrator/hydrator.ts
730
- function hydrate(rawItems, select, metadata, updatable = false) {
731
- return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
732
- }
733
- function hydrateItem(raw, select, metadata, updatable = false) {
734
- const fieldMap = new Map(
735
- metadata.fields.map((f) => [f.propertyName, f])
736
- );
737
- const embeddedMap = new Map(
738
- metadata.embeddedFields.map((e) => [e.propertyName, e])
739
- );
740
- const result = {};
741
- for (const [fieldName, selectValue] of Object.entries(select)) {
742
- if (selectValue === true) {
743
- const value = raw[fieldName];
744
- if (value !== void 0) {
745
- const fieldMeta = fieldMap.get(fieldName);
746
- result[fieldName] = deserializeValue(value, fieldMeta);
747
- }
748
- } else if (typeof selectValue === "object" && selectValue !== null) {
749
- if ("select" in selectValue || isSelectBuilder(selectValue)) {
750
- continue;
751
- }
752
- const rawEmb = raw[fieldName];
753
- if (rawEmb && typeof rawEmb === "object") {
754
- result[fieldName] = hydrateEmbedded(
755
- rawEmb,
756
- selectValue,
757
- metadata,
758
- fieldName
759
- );
760
- }
761
- }
762
- }
763
- if (updatable) {
764
- attachHiddenKey(result, raw);
765
- }
766
- return result;
767
- }
768
- function hydrateEmbedded(raw, select, _metadata, _fieldName) {
769
- const result = {};
770
- for (const [key, selectValue] of Object.entries(select)) {
771
- if (selectValue === true) {
772
- if (raw[key] !== void 0) {
773
- result[key] = raw[key];
774
- }
775
- } else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
776
- const nested = raw[key];
777
- if (nested && typeof nested === "object") {
778
- result[key] = hydrateEmbedded(
779
- nested,
780
- selectValue,
781
- _metadata,
782
- key
783
- );
784
- }
785
- }
786
- }
787
- return result;
788
- }
789
- function deserializeValue(value, fieldMeta) {
790
- if (!fieldMeta) return value;
791
- if (fieldMeta.options?.deserialize) {
792
- return fieldMeta.options.deserialize(value);
793
- }
794
- if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
795
- return new Date(value);
796
- }
797
- if (fieldMeta.options?.format === "date" && typeof value === "string") {
798
- return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
799
- }
800
- return value;
801
- }
802
-
803
670
  // src/pagination/cursor.ts
804
671
  function encodeCursor(lastEvaluatedKey) {
805
672
  const json = JSON.stringify(lastEvaluatedKey);
@@ -3116,6 +2983,58 @@ var DDBModel = class {
3116
2983
  static mutate(envelope, options) {
3117
2984
  return executeWriteEnvelope(envelope, options);
3118
2985
  }
2986
+ /**
2987
+ * Parse a single CDC {@link ChangeEvent} into the `[oldRecord, newRecord]` tuple
2988
+ * for THIS model (issue #153 — the pure `(event) => [old, new]` typed mapper).
2989
+ * It reuses the read path's `hydrate` on `oldImage` / `newImage` (ISO 8601 →
2990
+ * `Date` for `@datetime`, embedded reconstruction) and returns typed instances of
2991
+ * this model — **all fields**, no projection (narrowing does not reduce cost; the
2992
+ * image is already on the stream).
2993
+ *
2994
+ * Routing + parse in one call: an event for a DIFFERENT model returns
2995
+ * `[null, null]` (a valid event for this model always has ≥1 non-null side, so
2996
+ * `[null, null]` is an unambiguous "not for this class" signal — no separate
2997
+ * `owns()` guard). The present side per event kind mirrors DynamoDB Streams:
2998
+ * INSERT → `[null, new]`, MODIFY → `[old, new]`, REMOVE → `[old, null]`.
2999
+ *
3000
+ * Only callable on a `@cdcProjected` model — the generated {@link CdcModelRegistry}
3001
+ * types this at the `subscribe` surface, and at runtime a non-`@cdcProjected` model
3002
+ * throws (loudly, never silently returning `[null, null]`, which would masquerade
3003
+ * as a routing miss). Delivery / sink writes / dedup are the consumer's, outside
3004
+ * this boundary (see `docs/cdc-projection.md`).
3005
+ */
3006
+ static fromChange(event) {
3007
+ const modelClass = this;
3008
+ const metadata = MetadataRegistry.get(modelClass);
3009
+ if (!metadata.cdcProjected) {
3010
+ throw new Error(
3011
+ `${modelClass.name}.fromChange: model '${modelClass.name}' is not @cdcProjected, so its change events cannot be parsed. Add @cdcProjected() to the model.`
3012
+ );
3013
+ }
3014
+ return parseChange(event, metadata, modelClass.name, modelClass);
3015
+ }
3016
+ /**
3017
+ * Build a batch CDC {@link ChangeHandler} from an envelope-style handler map keyed
3018
+ * by model name (issue #153 — the GraphQL query/mutation/**subscription** trio's
3019
+ * third sibling). Unlike `query` / `mutate`, `subscribe` DOES NOT subscribe and
3020
+ * does not hit DynamoDB: it *returns* a `(batch) => Promise<BatchResult>` the
3021
+ * consumer mounts on their own stream source (the CDC emulator locally, DynamoDB
3022
+ * Streams → Lambda in production) — delivery lives outside the boundary.
3023
+ *
3024
+ * The returned handler loops the batch, routes each event to its owning model by
3025
+ * `event.model` / `keys.pk`, typed-parses via that model's `fromChange`, calls the
3026
+ * matching handler, and — if a handler throws — records the event's
3027
+ * `sequenceNumber` into `batchItemFailures` (the `ReportBatchItemFailures`
3028
+ * redelivery protocol) and continues. It NEVER writes a sink and NEVER dedups
3029
+ * (both are consumer concerns). Handler keys are constrained to the generated
3030
+ * {@link CdcModelRegistry} (`@cdcProjected` models only); a key that is not a
3031
+ * registered `@cdcProjected` model throws at build time.
3032
+ */
3033
+ static subscribe(handlers) {
3034
+ return buildSubscribeHandler(
3035
+ handlers
3036
+ );
3037
+ }
3119
3038
  /**
3120
3039
  * Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
3121
3040
  * keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
@@ -7022,8 +6941,6 @@ export {
7022
6941
  SPEC_VERSION,
7023
6942
  MARKER_ROW_ENTITY,
7024
6943
  buildManifest,
7025
- isSelectBuilder,
7026
- normalizeSelectSpec,
7027
6944
  detectRelationFields,
7028
6945
  getImplicitKeyFields,
7029
6946
  buildProjection,
@@ -7031,7 +6948,6 @@ export {
7031
6948
  evaluateFilter,
7032
6949
  plan,
7033
6950
  execute,
7034
- hydrate,
7035
6951
  encodeCursor,
7036
6952
  decodeCursor,
7037
6953
  executeListInternal,
@@ -0,0 +1,15 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+ var __commonJS = (cb, mod) => function __require2() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+
12
+ export {
13
+ __require,
14
+ __commonJS
15
+ };
@@ -7,7 +7,7 @@ import {
7
7
  buildMaintenanceGraph,
8
8
  buildPutInput,
9
9
  resolveModelClass
10
- } from "./chunk-FMJIWFIS.js";
10
+ } from "./chunk-W3GEJPPV.js";
11
11
 
12
12
  // src/cdc/prng.ts
13
13
  var SeededRandom = class {
@@ -113,7 +113,10 @@ function gsi(indexName, builder, options) {
113
113
  indexName,
114
114
  segmented,
115
115
  inputFieldNames,
116
- unique: options?.unique ?? false
116
+ unique: options?.unique ?? false,
117
+ // Purely-documentary description (issue #166); emitted only when declared so an
118
+ // undescribed index carries no `description` key (byte-identical to pre-#166).
119
+ ...options?.description !== void 0 ? { description: options.description } : {}
117
120
  };
118
121
  }
119
122
 
@@ -1084,7 +1087,10 @@ var MetadataRegistry = class {
1084
1087
  indexName: value.indexName,
1085
1088
  segmented: value.segmented,
1086
1089
  inputFieldNames: value.inputFieldNames,
1087
- unique: value.unique
1090
+ unique: value.unique,
1091
+ // Purely-documentary GSI description (issue #166); copied only when declared
1092
+ // so an undescribed index is byte-identical to the pre-#166 metadata.
1093
+ ...value.description !== void 0 ? { description: value.description } : {}
1088
1094
  });
1089
1095
  }
1090
1096
  }
@@ -1109,6 +1115,79 @@ ${messages}`);
1109
1115
  }
1110
1116
  };
1111
1117
 
1118
+ // src/types/select-builder.ts
1119
+ var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
1120
+ var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
1121
+
1122
+ // src/select/builder.ts
1123
+ var BuilderImpl = class _BuilderImpl {
1124
+ [SELECT_BUILDER] = true;
1125
+ /** The resolved spec, exposed (non-method) so normalizers can read it. */
1126
+ [SELECT_SPEC];
1127
+ constructor(spec) {
1128
+ this[SELECT_SPEC] = spec;
1129
+ }
1130
+ clone(patch) {
1131
+ return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
1132
+ }
1133
+ filter(filter) {
1134
+ return this.clone({ filter });
1135
+ }
1136
+ limit(limit) {
1137
+ return this.clone({ limit });
1138
+ }
1139
+ after(cursor) {
1140
+ return this.clone({ after: cursor });
1141
+ }
1142
+ order(order) {
1143
+ return this.clone({ order });
1144
+ }
1145
+ };
1146
+ function isSelectBuilder(value) {
1147
+ return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
1148
+ }
1149
+ function fromBuilder(value) {
1150
+ const spec = value[SELECT_SPEC];
1151
+ return {
1152
+ select: spec.select,
1153
+ filter: spec.filter,
1154
+ limit: spec.limit,
1155
+ after: spec.after,
1156
+ order: spec.order
1157
+ };
1158
+ }
1159
+ function normalizeSelectSpec(value) {
1160
+ if (isSelectBuilder(value)) {
1161
+ return fromBuilder(value);
1162
+ }
1163
+ const obj = value;
1164
+ return {
1165
+ select: obj.select,
1166
+ filter: obj.filter,
1167
+ limit: obj.limit,
1168
+ after: obj.after,
1169
+ order: obj.order
1170
+ };
1171
+ }
1172
+ function normalizeTopLevelSelect(value) {
1173
+ if (isSelectBuilder(value)) {
1174
+ return fromBuilder(value);
1175
+ }
1176
+ return { select: value ?? {} };
1177
+ }
1178
+ function buildProject(select) {
1179
+ return new BuilderImpl({
1180
+ [SELECT_BUILDER]: true,
1181
+ select
1182
+ });
1183
+ }
1184
+ function buildRelation(select) {
1185
+ return new BuilderImpl({
1186
+ [SELECT_BUILDER]: true,
1187
+ select
1188
+ });
1189
+ }
1190
+
1112
1191
  // src/define/param.ts
1113
1192
  function isParamOptions(value) {
1114
1193
  return typeof value === "object" && value !== null;
@@ -3563,6 +3642,80 @@ function buildLogicalItem(modelClass, ctx) {
3563
3642
  return { Delete: buildDeleteInput(modelClass, ctx.input.key ?? {}, opts) };
3564
3643
  }
3565
3644
 
3645
+ // src/hydrator/hydrator.ts
3646
+ function hydrate(rawItems, select, metadata, updatable = false) {
3647
+ return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
3648
+ }
3649
+ function hydrateItem(raw, select, metadata, updatable = false) {
3650
+ const fieldMap = new Map(
3651
+ metadata.fields.map((f) => [f.propertyName, f])
3652
+ );
3653
+ const embeddedMap = new Map(
3654
+ metadata.embeddedFields.map((e) => [e.propertyName, e])
3655
+ );
3656
+ const result = {};
3657
+ for (const [fieldName, selectValue] of Object.entries(select)) {
3658
+ if (selectValue === true) {
3659
+ const value = raw[fieldName];
3660
+ if (value !== void 0) {
3661
+ const fieldMeta = fieldMap.get(fieldName);
3662
+ result[fieldName] = deserializeValue(value, fieldMeta);
3663
+ }
3664
+ } else if (typeof selectValue === "object" && selectValue !== null) {
3665
+ if ("select" in selectValue || isSelectBuilder(selectValue)) {
3666
+ continue;
3667
+ }
3668
+ const rawEmb = raw[fieldName];
3669
+ if (rawEmb && typeof rawEmb === "object") {
3670
+ result[fieldName] = hydrateEmbedded(
3671
+ rawEmb,
3672
+ selectValue,
3673
+ metadata,
3674
+ fieldName
3675
+ );
3676
+ }
3677
+ }
3678
+ }
3679
+ if (updatable) {
3680
+ attachHiddenKey(result, raw);
3681
+ }
3682
+ return result;
3683
+ }
3684
+ function hydrateEmbedded(raw, select, _metadata, _fieldName) {
3685
+ const result = {};
3686
+ for (const [key2, selectValue] of Object.entries(select)) {
3687
+ if (selectValue === true) {
3688
+ if (raw[key2] !== void 0) {
3689
+ result[key2] = raw[key2];
3690
+ }
3691
+ } else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
3692
+ const nested = raw[key2];
3693
+ if (nested && typeof nested === "object") {
3694
+ result[key2] = hydrateEmbedded(
3695
+ nested,
3696
+ selectValue,
3697
+ _metadata,
3698
+ key2
3699
+ );
3700
+ }
3701
+ }
3702
+ }
3703
+ return result;
3704
+ }
3705
+ function deserializeValue(value, fieldMeta) {
3706
+ if (!fieldMeta) return value;
3707
+ if (fieldMeta.options?.deserialize) {
3708
+ return fieldMeta.options.deserialize(value);
3709
+ }
3710
+ if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
3711
+ return new Date(value);
3712
+ }
3713
+ if (fieldMeta.options?.format === "date" && typeof value === "string") {
3714
+ return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
3715
+ }
3716
+ return value;
3717
+ }
3718
+
3566
3719
  // src/define/entity-writes.ts
3567
3720
  var MAINTAIN_TRIGGER_RE = /^[^.\s$[\]*]+\.(?:created|updated|removed)$/;
3568
3721
  function maintainTrigger(value) {
@@ -4398,6 +4551,100 @@ function buildMaintenanceGraphImpl(registry, views) {
4398
4551
  };
4399
4552
  }
4400
4553
 
4554
+ // src/cdc/from-change.ts
4555
+ function buildFullSelect(metadata) {
4556
+ const select = {};
4557
+ for (const field of metadata.fields) {
4558
+ select[field.propertyName] = true;
4559
+ }
4560
+ return select;
4561
+ }
4562
+ function eventOwnedBy(event, metadata, modelName) {
4563
+ if (event.model !== void 0) {
4564
+ return event.model === modelName;
4565
+ }
4566
+ return metadata.prefix.length > 0 && event.keys.pk.startsWith(metadata.prefix);
4567
+ }
4568
+ function hydrateImage(image, select, metadata, modelClass) {
4569
+ if (image === void 0) return null;
4570
+ const [hydrated] = hydrate([image], select, metadata);
4571
+ const instance = new modelClass();
4572
+ Object.assign(instance, hydrated);
4573
+ for (const emb of metadata.embeddedFields) {
4574
+ const rawEmb = image[emb.propertyName];
4575
+ if (rawEmb !== void 0 && rawEmb !== null) {
4576
+ instance[emb.propertyName] = rawEmb;
4577
+ }
4578
+ }
4579
+ return instance;
4580
+ }
4581
+ function parseChange(event, metadata, modelName, modelClass) {
4582
+ if (!eventOwnedBy(event, metadata, modelName)) {
4583
+ return [null, null];
4584
+ }
4585
+ const select = buildFullSelect(metadata);
4586
+ const oldRecord = hydrateImage(event.oldImage, select, metadata, modelClass);
4587
+ const newRecord = hydrateImage(event.newImage, select, metadata, modelClass);
4588
+ return [oldRecord, newRecord];
4589
+ }
4590
+
4591
+ // src/cdc/subscribe.ts
4592
+ function resolveRoutableModels(handlers) {
4593
+ const byName = /* @__PURE__ */ new Map();
4594
+ for (const [cls, meta] of MetadataRegistry.getAll()) {
4595
+ byName.set(cls.name, { cls, meta });
4596
+ }
4597
+ const routable = /* @__PURE__ */ new Map();
4598
+ for (const modelName of Object.keys(handlers)) {
4599
+ const entry = byName.get(modelName);
4600
+ if (!entry) {
4601
+ throw new Error(
4602
+ `DDBModel.subscribe: no model named '${modelName}' is registered. Did you import the model module (so its @model decorator ran) and spell the handler key exactly as the class name?`
4603
+ );
4604
+ }
4605
+ if (!entry.meta.cdcProjected) {
4606
+ throw new Error(
4607
+ `DDBModel.subscribe: model '${modelName}' is not @cdcProjected, so its change events cannot be parsed. Add @cdcProjected() to the model, or remove its handler.`
4608
+ );
4609
+ }
4610
+ routable.set(modelName, {
4611
+ modelClass: entry.cls,
4612
+ metadata: entry.meta,
4613
+ modelName
4614
+ });
4615
+ }
4616
+ return routable;
4617
+ }
4618
+ async function dispatchEvent(event, routable, handlers) {
4619
+ for (const { modelClass, metadata, modelName } of routable.values()) {
4620
+ const [oldRecord, newRecord] = parseChange(
4621
+ event,
4622
+ metadata,
4623
+ modelName,
4624
+ modelClass
4625
+ );
4626
+ if (oldRecord === null && newRecord === null) continue;
4627
+ const handler = handlers[modelName];
4628
+ await handler(oldRecord, newRecord);
4629
+ return true;
4630
+ }
4631
+ return false;
4632
+ }
4633
+ function buildSubscribeHandler(handlers) {
4634
+ const routable = resolveRoutableModels(handlers);
4635
+ return async (batch) => {
4636
+ const batchItemFailures = [];
4637
+ for (const event of batch.records) {
4638
+ try {
4639
+ await dispatchEvent(event, routable, handlers);
4640
+ } catch {
4641
+ batchItemFailures.push(event.sequenceNumber);
4642
+ }
4643
+ }
4644
+ return { batchItemFailures };
4645
+ };
4646
+ }
4647
+
4401
4648
  export {
4402
4649
  isColumn,
4403
4650
  createColumnMap,
@@ -4419,6 +4666,11 @@ export {
4419
4666
  pkTemplate,
4420
4667
  skTemplate,
4421
4668
  resolveSegmentedKey,
4669
+ isSelectBuilder,
4670
+ normalizeSelectSpec,
4671
+ normalizeTopLevelSelect,
4672
+ buildProject,
4673
+ buildRelation,
4422
4674
  param,
4423
4675
  isParam,
4424
4676
  BATCH_GET_MAX_KEYS,
@@ -4461,6 +4713,7 @@ export {
4461
4713
  resolveModelClass,
4462
4714
  TransactionContext,
4463
4715
  executeTransaction,
4716
+ hydrate,
4464
4717
  maintainTrigger,
4465
4718
  isMaintainTrigger,
4466
4719
  preview,
@@ -4473,5 +4726,7 @@ export {
4473
4726
  getEntityWrites,
4474
4727
  lifecyclePhaseForIntent,
4475
4728
  collectViewDefinitions,
4476
- buildMaintenanceGraph
4729
+ buildMaintenanceGraph,
4730
+ parseChange,
4731
+ buildSubscribeHandler
4477
4732
  };