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 +7 -0
- package/dist/{chunk-FMJIWFIS.js → chunk-B3GWIWT6.js} +250 -1
- package/dist/{chunk-7NPG5R7O.js → chunk-EMJHTUA4.js} +1 -1
- package/dist/{chunk-J5A665UW.js → chunk-FI63YKK5.js} +61 -151
- package/dist/cli.js +54 -7
- package/dist/index.d.ts +88 -3
- package/dist/index.js +28 -5
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-B-F7jw9f.d.ts → types-B9rJ1z3H.d.ts} +236 -114
- package/docs/cdc-projection.md +167 -0
- package/docs/design-patterns.md +18 -13
- package/docs/spec.md +9 -4
- package/package.json +1 -1
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
|
};
|
|
@@ -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-
|
|
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
|
-
|
|
6
|
-
} from "./chunk-J5A665UW.js";
|
|
4
|
+
buildManifest
|
|
5
|
+
} from "./chunk-FI63YKK5.js";
|
|
7
6
|
import {
|
|
8
|
-
MetadataRegistry
|
|
9
|
-
|
|
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:
|
|
1582
|
+
files: emittedFiles
|
|
1536
1583
|
}) + "\n"
|
|
1537
1584
|
);
|
|
1538
1585
|
} catch (err) {
|