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 +9 -0
- package/dist/{chunk-J5A665UW.js → chunk-H5TUW2WR.js} +69 -153
- package/dist/chunk-MCKGQKYU.js +15 -0
- package/dist/{chunk-7NPG5R7O.js → chunk-QBXLQNXY.js} +1 -1
- package/dist/{chunk-FMJIWFIS.js → chunk-W3GEJPPV.js} +258 -3
- package/dist/cli.js +314 -12
- package/dist/index.d.ts +99 -4
- package/dist/index.js +40 -8
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +3 -2
- package/dist/{types-B-F7jw9f.d.ts → types-m1Ect6hG.d.ts} +306 -114
- package/dist/typescript-ZUQEBJRV.js +210764 -0
- package/docs/cdc-projection.md +167 -0
- package/docs/design-patterns.md +18 -13
- package/docs/spec.md +9 -4
- package/package.json +4 -1
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-
|
|
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
|
+
};
|
|
@@ -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
|
};
|