graphddb 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,6 +19,7 @@ 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.
23
24
 
24
25
  ## 🤔 Philosophy
@@ -204,6 +205,11 @@ Each capability is summarized here only. For details, operator lists, and proced
204
205
  [`docs/mutation-command-derivation.md`](./docs/mutation-command-derivation.md).
205
206
  - **CDC emulator** —— change events equivalent to DynamoDB Streams, to drive and test differential
206
207
  aggregation. [`docs/cdc-emulator.md`](./docs/cdc-emulator.md).
208
+ - **CDC projection** —— a typed-consumer-IF that parses the CDC change events of a `@cdcProjected()`
209
+ source model into typed records. `Model.fromChange(event)` returns `[old, new]`, and
210
+ `DDBModel.subscribe(handlers)` (the sibling of `query` / `mutate`) produces a `ChangeHandler` the
211
+ consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink
212
+ delivery, and idempotency are the consumer's. [`docs/cdc-projection.md`](./docs/cdc-projection.md).
207
213
  - **CloudFormation generation** —— `graphddb generate cloudformation` emits a CloudFormation template for
208
214
  the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / PROVISIONED / Auto
209
215
  Scaling). [`docs/cloudformation.md`](./docs/cloudformation.md).
@@ -225,6 +231,7 @@ documentation available offline at hand. On GitHub, follow the links in the tabl
225
231
  | [Multi-language / Python bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and a Python runtime; TS↔Python conformance. |
226
232
  | [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
227
233
  | [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
234
+ | [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
235
  | [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
236
  | [Benchmark](./benchmark/RESULTS.md) | Library comparison against hand-written AWS SDK, ElectroDB, DynamoDB Toolbox, and OneTable. |
230
237
 
@@ -1109,6 +1109,79 @@ ${messages}`);
1109
1109
  }
1110
1110
  };
1111
1111
 
1112
+ // src/types/select-builder.ts
1113
+ var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
1114
+ var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
1115
+
1116
+ // src/select/builder.ts
1117
+ var BuilderImpl = class _BuilderImpl {
1118
+ [SELECT_BUILDER] = true;
1119
+ /** The resolved spec, exposed (non-method) so normalizers can read it. */
1120
+ [SELECT_SPEC];
1121
+ constructor(spec) {
1122
+ this[SELECT_SPEC] = spec;
1123
+ }
1124
+ clone(patch) {
1125
+ return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
1126
+ }
1127
+ filter(filter) {
1128
+ return this.clone({ filter });
1129
+ }
1130
+ limit(limit) {
1131
+ return this.clone({ limit });
1132
+ }
1133
+ after(cursor) {
1134
+ return this.clone({ after: cursor });
1135
+ }
1136
+ order(order) {
1137
+ return this.clone({ order });
1138
+ }
1139
+ };
1140
+ function isSelectBuilder(value) {
1141
+ return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
1142
+ }
1143
+ function fromBuilder(value) {
1144
+ const spec = value[SELECT_SPEC];
1145
+ return {
1146
+ select: spec.select,
1147
+ filter: spec.filter,
1148
+ limit: spec.limit,
1149
+ after: spec.after,
1150
+ order: spec.order
1151
+ };
1152
+ }
1153
+ function normalizeSelectSpec(value) {
1154
+ if (isSelectBuilder(value)) {
1155
+ return fromBuilder(value);
1156
+ }
1157
+ const obj = value;
1158
+ return {
1159
+ select: obj.select,
1160
+ filter: obj.filter,
1161
+ limit: obj.limit,
1162
+ after: obj.after,
1163
+ order: obj.order
1164
+ };
1165
+ }
1166
+ function normalizeTopLevelSelect(value) {
1167
+ if (isSelectBuilder(value)) {
1168
+ return fromBuilder(value);
1169
+ }
1170
+ return { select: value ?? {} };
1171
+ }
1172
+ function buildProject(select) {
1173
+ return new BuilderImpl({
1174
+ [SELECT_BUILDER]: true,
1175
+ select
1176
+ });
1177
+ }
1178
+ function buildRelation(select) {
1179
+ return new BuilderImpl({
1180
+ [SELECT_BUILDER]: true,
1181
+ select
1182
+ });
1183
+ }
1184
+
1112
1185
  // src/define/param.ts
1113
1186
  function isParamOptions(value) {
1114
1187
  return typeof value === "object" && value !== null;
@@ -3563,6 +3636,80 @@ function buildLogicalItem(modelClass, ctx) {
3563
3636
  return { Delete: buildDeleteInput(modelClass, ctx.input.key ?? {}, opts) };
3564
3637
  }
3565
3638
 
3639
+ // src/hydrator/hydrator.ts
3640
+ function hydrate(rawItems, select, metadata, updatable = false) {
3641
+ return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
3642
+ }
3643
+ function hydrateItem(raw, select, metadata, updatable = false) {
3644
+ const fieldMap = new Map(
3645
+ metadata.fields.map((f) => [f.propertyName, f])
3646
+ );
3647
+ const embeddedMap = new Map(
3648
+ metadata.embeddedFields.map((e) => [e.propertyName, e])
3649
+ );
3650
+ const result = {};
3651
+ for (const [fieldName, selectValue] of Object.entries(select)) {
3652
+ if (selectValue === true) {
3653
+ const value = raw[fieldName];
3654
+ if (value !== void 0) {
3655
+ const fieldMeta = fieldMap.get(fieldName);
3656
+ result[fieldName] = deserializeValue(value, fieldMeta);
3657
+ }
3658
+ } else if (typeof selectValue === "object" && selectValue !== null) {
3659
+ if ("select" in selectValue || isSelectBuilder(selectValue)) {
3660
+ continue;
3661
+ }
3662
+ const rawEmb = raw[fieldName];
3663
+ if (rawEmb && typeof rawEmb === "object") {
3664
+ result[fieldName] = hydrateEmbedded(
3665
+ rawEmb,
3666
+ selectValue,
3667
+ metadata,
3668
+ fieldName
3669
+ );
3670
+ }
3671
+ }
3672
+ }
3673
+ if (updatable) {
3674
+ attachHiddenKey(result, raw);
3675
+ }
3676
+ return result;
3677
+ }
3678
+ function hydrateEmbedded(raw, select, _metadata, _fieldName) {
3679
+ const result = {};
3680
+ for (const [key2, selectValue] of Object.entries(select)) {
3681
+ if (selectValue === true) {
3682
+ if (raw[key2] !== void 0) {
3683
+ result[key2] = raw[key2];
3684
+ }
3685
+ } else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
3686
+ const nested = raw[key2];
3687
+ if (nested && typeof nested === "object") {
3688
+ result[key2] = hydrateEmbedded(
3689
+ nested,
3690
+ selectValue,
3691
+ _metadata,
3692
+ key2
3693
+ );
3694
+ }
3695
+ }
3696
+ }
3697
+ return result;
3698
+ }
3699
+ function deserializeValue(value, fieldMeta) {
3700
+ if (!fieldMeta) return value;
3701
+ if (fieldMeta.options?.deserialize) {
3702
+ return fieldMeta.options.deserialize(value);
3703
+ }
3704
+ if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
3705
+ return new Date(value);
3706
+ }
3707
+ if (fieldMeta.options?.format === "date" && typeof value === "string") {
3708
+ return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
3709
+ }
3710
+ return value;
3711
+ }
3712
+
3566
3713
  // src/define/entity-writes.ts
3567
3714
  var MAINTAIN_TRIGGER_RE = /^[^.\s$[\]*]+\.(?:created|updated|removed)$/;
3568
3715
  function maintainTrigger(value) {
@@ -4398,6 +4545,100 @@ function buildMaintenanceGraphImpl(registry, views) {
4398
4545
  };
4399
4546
  }
4400
4547
 
4548
+ // src/cdc/from-change.ts
4549
+ function buildFullSelect(metadata) {
4550
+ const select = {};
4551
+ for (const field of metadata.fields) {
4552
+ select[field.propertyName] = true;
4553
+ }
4554
+ return select;
4555
+ }
4556
+ function eventOwnedBy(event, metadata, modelName) {
4557
+ if (event.model !== void 0) {
4558
+ return event.model === modelName;
4559
+ }
4560
+ return metadata.prefix.length > 0 && event.keys.pk.startsWith(metadata.prefix);
4561
+ }
4562
+ function hydrateImage(image, select, metadata, modelClass) {
4563
+ if (image === void 0) return null;
4564
+ const [hydrated] = hydrate([image], select, metadata);
4565
+ const instance = new modelClass();
4566
+ Object.assign(instance, hydrated);
4567
+ for (const emb of metadata.embeddedFields) {
4568
+ const rawEmb = image[emb.propertyName];
4569
+ if (rawEmb !== void 0 && rawEmb !== null) {
4570
+ instance[emb.propertyName] = rawEmb;
4571
+ }
4572
+ }
4573
+ return instance;
4574
+ }
4575
+ function parseChange(event, metadata, modelName, modelClass) {
4576
+ if (!eventOwnedBy(event, metadata, modelName)) {
4577
+ return [null, null];
4578
+ }
4579
+ const select = buildFullSelect(metadata);
4580
+ const oldRecord = hydrateImage(event.oldImage, select, metadata, modelClass);
4581
+ const newRecord = hydrateImage(event.newImage, select, metadata, modelClass);
4582
+ return [oldRecord, newRecord];
4583
+ }
4584
+
4585
+ // src/cdc/subscribe.ts
4586
+ function resolveRoutableModels(handlers) {
4587
+ const byName = /* @__PURE__ */ new Map();
4588
+ for (const [cls, meta] of MetadataRegistry.getAll()) {
4589
+ byName.set(cls.name, { cls, meta });
4590
+ }
4591
+ const routable = /* @__PURE__ */ new Map();
4592
+ for (const modelName of Object.keys(handlers)) {
4593
+ const entry = byName.get(modelName);
4594
+ if (!entry) {
4595
+ throw new Error(
4596
+ `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?`
4597
+ );
4598
+ }
4599
+ if (!entry.meta.cdcProjected) {
4600
+ throw new Error(
4601
+ `DDBModel.subscribe: model '${modelName}' is not @cdcProjected, so its change events cannot be parsed. Add @cdcProjected() to the model, or remove its handler.`
4602
+ );
4603
+ }
4604
+ routable.set(modelName, {
4605
+ modelClass: entry.cls,
4606
+ metadata: entry.meta,
4607
+ modelName
4608
+ });
4609
+ }
4610
+ return routable;
4611
+ }
4612
+ async function dispatchEvent(event, routable, handlers) {
4613
+ for (const { modelClass, metadata, modelName } of routable.values()) {
4614
+ const [oldRecord, newRecord] = parseChange(
4615
+ event,
4616
+ metadata,
4617
+ modelName,
4618
+ modelClass
4619
+ );
4620
+ if (oldRecord === null && newRecord === null) continue;
4621
+ const handler = handlers[modelName];
4622
+ await handler(oldRecord, newRecord);
4623
+ return true;
4624
+ }
4625
+ return false;
4626
+ }
4627
+ function buildSubscribeHandler(handlers) {
4628
+ const routable = resolveRoutableModels(handlers);
4629
+ return async (batch) => {
4630
+ const batchItemFailures = [];
4631
+ for (const event of batch.records) {
4632
+ try {
4633
+ await dispatchEvent(event, routable, handlers);
4634
+ } catch {
4635
+ batchItemFailures.push(event.sequenceNumber);
4636
+ }
4637
+ }
4638
+ return { batchItemFailures };
4639
+ };
4640
+ }
4641
+
4401
4642
  export {
4402
4643
  isColumn,
4403
4644
  createColumnMap,
@@ -4419,6 +4660,11 @@ export {
4419
4660
  pkTemplate,
4420
4661
  skTemplate,
4421
4662
  resolveSegmentedKey,
4663
+ isSelectBuilder,
4664
+ normalizeSelectSpec,
4665
+ normalizeTopLevelSelect,
4666
+ buildProject,
4667
+ buildRelation,
4422
4668
  param,
4423
4669
  isParam,
4424
4670
  BATCH_GET_MAX_KEYS,
@@ -4461,6 +4707,7 @@ export {
4461
4707
  resolveModelClass,
4462
4708
  TransactionContext,
4463
4709
  executeTransaction,
4710
+ hydrate,
4464
4711
  maintainTrigger,
4465
4712
  isMaintainTrigger,
4466
4713
  preview,
@@ -4473,5 +4720,7 @@ export {
4473
4720
  getEntityWrites,
4474
4721
  lifecyclePhaseForIntent,
4475
4722
  collectViewDefinitions,
4476
- buildMaintenanceGraph
4723
+ buildMaintenanceGraph,
4724
+ parseChange,
4725
+ buildSubscribeHandler
4477
4726
  };
@@ -7,7 +7,7 @@ import {
7
7
  buildMaintenanceGraph,
8
8
  buildPutInput,
9
9
  resolveModelClass
10
- } from "./chunk-FMJIWFIS.js";
10
+ } from "./chunk-B3GWIWT6.js";
11
11
 
12
12
  // src/cdc/prng.ts
13
13
  var SeededRandom = class {
@@ -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-B3GWIWT6.js";
46
54
 
47
55
  // src/spec/types.ts
48
56
  var SPEC_VERSION = "1.0";
@@ -174,79 +182,6 @@ function buildManifest(registry = MetadataRegistry) {
174
182
  };
175
183
  }
176
184
 
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
185
  // src/relation/detect.ts
251
186
  function isRelationSpec(value) {
252
187
  if (typeof value !== "object" || value === null) return false;
@@ -726,80 +661,6 @@ async function execute(operation, options) {
726
661
  return ClientManager.getExecutor().execute(operation, options);
727
662
  }
728
663
 
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
664
  // src/pagination/cursor.ts
804
665
  function encodeCursor(lastEvaluatedKey) {
805
666
  const json = JSON.stringify(lastEvaluatedKey);
@@ -3116,6 +2977,58 @@ var DDBModel = class {
3116
2977
  static mutate(envelope, options) {
3117
2978
  return executeWriteEnvelope(envelope, options);
3118
2979
  }
2980
+ /**
2981
+ * Parse a single CDC {@link ChangeEvent} into the `[oldRecord, newRecord]` tuple
2982
+ * for THIS model (issue #153 — the pure `(event) => [old, new]` typed mapper).
2983
+ * It reuses the read path's `hydrate` on `oldImage` / `newImage` (ISO 8601 →
2984
+ * `Date` for `@datetime`, embedded reconstruction) and returns typed instances of
2985
+ * this model — **all fields**, no projection (narrowing does not reduce cost; the
2986
+ * image is already on the stream).
2987
+ *
2988
+ * Routing + parse in one call: an event for a DIFFERENT model returns
2989
+ * `[null, null]` (a valid event for this model always has ≥1 non-null side, so
2990
+ * `[null, null]` is an unambiguous "not for this class" signal — no separate
2991
+ * `owns()` guard). The present side per event kind mirrors DynamoDB Streams:
2992
+ * INSERT → `[null, new]`, MODIFY → `[old, new]`, REMOVE → `[old, null]`.
2993
+ *
2994
+ * Only callable on a `@cdcProjected` model — the generated {@link CdcModelRegistry}
2995
+ * types this at the `subscribe` surface, and at runtime a non-`@cdcProjected` model
2996
+ * throws (loudly, never silently returning `[null, null]`, which would masquerade
2997
+ * as a routing miss). Delivery / sink writes / dedup are the consumer's, outside
2998
+ * this boundary (see `docs/cdc-projection.md`).
2999
+ */
3000
+ static fromChange(event) {
3001
+ const modelClass = this;
3002
+ const metadata = MetadataRegistry.get(modelClass);
3003
+ if (!metadata.cdcProjected) {
3004
+ throw new Error(
3005
+ `${modelClass.name}.fromChange: model '${modelClass.name}' is not @cdcProjected, so its change events cannot be parsed. Add @cdcProjected() to the model.`
3006
+ );
3007
+ }
3008
+ return parseChange(event, metadata, modelClass.name, modelClass);
3009
+ }
3010
+ /**
3011
+ * Build a batch CDC {@link ChangeHandler} from an envelope-style handler map keyed
3012
+ * by model name (issue #153 — the GraphQL query/mutation/**subscription** trio's
3013
+ * third sibling). Unlike `query` / `mutate`, `subscribe` DOES NOT subscribe and
3014
+ * does not hit DynamoDB: it *returns* a `(batch) => Promise<BatchResult>` the
3015
+ * consumer mounts on their own stream source (the CDC emulator locally, DynamoDB
3016
+ * Streams → Lambda in production) — delivery lives outside the boundary.
3017
+ *
3018
+ * The returned handler loops the batch, routes each event to its owning model by
3019
+ * `event.model` / `keys.pk`, typed-parses via that model's `fromChange`, calls the
3020
+ * matching handler, and — if a handler throws — records the event's
3021
+ * `sequenceNumber` into `batchItemFailures` (the `ReportBatchItemFailures`
3022
+ * redelivery protocol) and continues. It NEVER writes a sink and NEVER dedups
3023
+ * (both are consumer concerns). Handler keys are constrained to the generated
3024
+ * {@link CdcModelRegistry} (`@cdcProjected` models only); a key that is not a
3025
+ * registered `@cdcProjected` model throws at build time.
3026
+ */
3027
+ static subscribe(handlers) {
3028
+ return buildSubscribeHandler(
3029
+ handlers
3030
+ );
3031
+ }
3119
3032
  /**
3120
3033
  * Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
3121
3034
  * keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
@@ -7022,8 +6935,6 @@ export {
7022
6935
  SPEC_VERSION,
7023
6936
  MARKER_ROW_ENTITY,
7024
6937
  buildManifest,
7025
- isSelectBuilder,
7026
- normalizeSelectSpec,
7027
6938
  detectRelationFields,
7028
6939
  getImplicitKeyFields,
7029
6940
  buildProjection,
@@ -7031,7 +6942,6 @@ export {
7031
6942
  evaluateFilter,
7032
6943
  plan,
7033
6944
  execute,
7034
- hydrate,
7035
6945
  encodeCursor,
7036
6946
  decodeCursor,
7037
6947
  executeListInternal,
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  buildBridgeBundle,
4
- buildManifest,
5
- normalizeSelectSpec
6
- } from "./chunk-J5A665UW.js";
4
+ buildManifest
5
+ } from "./chunk-FI63YKK5.js";
7
6
  import {
8
- MetadataRegistry
9
- } from "./chunk-FMJIWFIS.js";
7
+ MetadataRegistry,
8
+ normalizeSelectSpec
9
+ } from "./chunk-B3GWIWT6.js";
10
10
 
11
11
  // src/cli.ts
12
12
  import { createRequire } from "module";
@@ -568,7 +568,7 @@ function createProgram(handlers2, version) {
568
568
 
569
569
  // src/cli/handlers.ts
570
570
  import { mkdirSync, writeFileSync } from "fs";
571
- import { dirname } from "path";
571
+ import { dirname, relative } from "path";
572
572
  import { pathToFileURL } from "url";
573
573
  import { isAbsolute, resolve } from "path";
574
574
 
@@ -1416,6 +1416,40 @@ function emitCloudformation(manifest, options = {}) {
1416
1416
  return (options.format ?? "yaml") === "json" ? templateToJson(template) : templateToYaml(template);
1417
1417
  }
1418
1418
 
1419
+ // src/codegen/cdc-registry.ts
1420
+ var TS_HEADER = PY_HEADER.replace("# ", "// ");
1421
+ function cdcProjectedModelNames(registry = MetadataRegistry) {
1422
+ const names = [];
1423
+ for (const [cls, metadata] of registry.getAll()) {
1424
+ if (metadata.cdcProjected) names.push(cls.name);
1425
+ }
1426
+ return names.sort();
1427
+ }
1428
+ function generateCdcRegistry(options = {}) {
1429
+ const registry = options.registry ?? MetadataRegistry;
1430
+ const modelsModule = options.modelsModule ?? "./models.js";
1431
+ const names = cdcProjectedModelNames(registry);
1432
+ const lines = [TS_HEADER, ""];
1433
+ if (names.length === 0) {
1434
+ lines.push(
1435
+ "// (no @cdcProjected models \u2014 nothing to augment CdcModelRegistry with)",
1436
+ "",
1437
+ "export {};",
1438
+ ""
1439
+ );
1440
+ return lines.join("\n");
1441
+ }
1442
+ for (const name of names) {
1443
+ lines.push(`import type { ${name} } from '${modelsModule}';`);
1444
+ }
1445
+ lines.push("", "declare module 'graphddb' {", " interface CdcModelRegistry {");
1446
+ for (const name of names) {
1447
+ lines.push(` ${name}: ${name};`);
1448
+ }
1449
+ lines.push(" }", "}", "");
1450
+ return lines.join("\n");
1451
+ }
1452
+
1419
1453
  // src/codegen/index.ts
1420
1454
  var OUTPUT_FILES = [
1421
1455
  "manifest.json",
@@ -1528,11 +1562,24 @@ var handlers = {
1528
1562
  for (const name of OUTPUT_FILES) {
1529
1563
  writeFileSync(resolve(outDir, name), files[name], "utf8");
1530
1564
  }
1565
+ const emittedFiles = [...OUTPUT_FILES];
1566
+ if (cdcProjectedModelNames().length > 0) {
1567
+ const entryAbs = isAbsolute(entry) ? entry : resolve(process.cwd(), entry);
1568
+ let modelsModule = relative(outDir, entryAbs).replace(/\\/g, "/").replace(/\.[mc]?ts$/, "");
1569
+ if (!modelsModule.startsWith(".")) modelsModule = `./${modelsModule}`;
1570
+ const cdcRegistryFile = "cdc-registry.ts";
1571
+ writeFileSync(
1572
+ resolve(outDir, cdcRegistryFile),
1573
+ generateCdcRegistry({ modelsModule }),
1574
+ "utf8"
1575
+ );
1576
+ emittedFiles.push(cdcRegistryFile);
1577
+ }
1531
1578
  process.stdout.write(
1532
1579
  JSON.stringify({
1533
1580
  status: "ok",
1534
1581
  outDir: out,
1535
- files: [...OUTPUT_FILES]
1582
+ files: emittedFiles
1536
1583
  }) + "\n"
1537
1584
  );
1538
1585
  } catch (err) {