graphddb 0.4.3 → 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 +12 -2
- package/dist/{chunk-CPTV3H2U.js → chunk-B3GWIWT6.js} +401 -26
- package/dist/{chunk-BROCT574.js → chunk-EMJHTUA4.js} +1 -1
- package/dist/{chunk-G5RWWBAL.js → chunk-FI63YKK5.js} +83 -153
- package/dist/cli.js +831 -9
- package/dist/index.d.ts +150 -3
- package/dist/index.js +45 -6
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-BWOrWcbd.d.ts → types-B9rJ1z3H.d.ts} +278 -114
- package/docs/cdc-projection.md +167 -0
- package/docs/cloudformation.md +294 -0
- package/docs/design-patterns.md +18 -13
- package/docs/spec.md +9 -4
- package/package.json +4 -2
|
@@ -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";
|
|
@@ -129,7 +137,11 @@ function buildEntity(metadata) {
|
|
|
129
137
|
key: buildKey(metadata.primaryKey),
|
|
130
138
|
gsis: buildGsis(metadata),
|
|
131
139
|
relations: buildRelations(metadata),
|
|
132
|
-
...metadata.description !== void 0 ? { description: metadata.description } : {}
|
|
140
|
+
...metadata.description !== void 0 ? { description: metadata.description } : {},
|
|
141
|
+
// TTL (issue #172, Epic #167 — C4): a physical-schema fact carried on the
|
|
142
|
+
// manifest so the CFn emitter can render `TimeToLiveSpecification`. Absent
|
|
143
|
+
// when no `@ttl` field is declared (byte-identical to the pre-#172 manifest).
|
|
144
|
+
...metadata.ttlAttribute !== void 0 ? { ttlAttribute: metadata.ttlAttribute } : {}
|
|
133
145
|
};
|
|
134
146
|
}
|
|
135
147
|
function buildManifest(registry = MetadataRegistry) {
|
|
@@ -140,12 +152,28 @@ function buildManifest(registry = MetadataRegistry) {
|
|
|
140
152
|
}
|
|
141
153
|
const entities = {};
|
|
142
154
|
const tables = {};
|
|
155
|
+
const ttlByPhysicalTable = /* @__PURE__ */ new Map();
|
|
143
156
|
for (const name of [...entitiesByName.keys()].sort()) {
|
|
144
157
|
const metadata = entitiesByName.get(name);
|
|
145
158
|
entities[name] = buildEntity(metadata);
|
|
159
|
+
const physicalName = TableMapping.resolve(metadata.tableName);
|
|
146
160
|
tables[metadata.tableName] = {
|
|
147
|
-
physicalName
|
|
161
|
+
physicalName
|
|
148
162
|
};
|
|
163
|
+
if (metadata.ttlAttribute !== void 0) {
|
|
164
|
+
const existing = ttlByPhysicalTable.get(physicalName);
|
|
165
|
+
if (existing && existing.attr !== metadata.ttlAttribute) {
|
|
166
|
+
throw new Error(
|
|
167
|
+
`Manifest build: physical table '${physicalName}' has more than one TTL attribute \u2014 entity '${existing.entity}' declares \`@ttl\` on '${existing.attr}' and entity '${name}' on '${metadata.ttlAttribute}'. DynamoDB allows exactly one TTL attribute per table; align the \`@ttl\` attribute across all entities sharing this table (or drop one).`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
if (!existing) {
|
|
171
|
+
ttlByPhysicalTable.set(physicalName, {
|
|
172
|
+
entity: name,
|
|
173
|
+
attr: metadata.ttlAttribute
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
149
177
|
}
|
|
150
178
|
return {
|
|
151
179
|
version: SPEC_VERSION,
|
|
@@ -154,79 +182,6 @@ function buildManifest(registry = MetadataRegistry) {
|
|
|
154
182
|
};
|
|
155
183
|
}
|
|
156
184
|
|
|
157
|
-
// src/types/select-builder.ts
|
|
158
|
-
var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
|
|
159
|
-
var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
|
|
160
|
-
|
|
161
|
-
// src/select/builder.ts
|
|
162
|
-
var BuilderImpl = class _BuilderImpl {
|
|
163
|
-
[SELECT_BUILDER] = true;
|
|
164
|
-
/** The resolved spec, exposed (non-method) so normalizers can read it. */
|
|
165
|
-
[SELECT_SPEC];
|
|
166
|
-
constructor(spec) {
|
|
167
|
-
this[SELECT_SPEC] = spec;
|
|
168
|
-
}
|
|
169
|
-
clone(patch) {
|
|
170
|
-
return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
|
|
171
|
-
}
|
|
172
|
-
filter(filter) {
|
|
173
|
-
return this.clone({ filter });
|
|
174
|
-
}
|
|
175
|
-
limit(limit) {
|
|
176
|
-
return this.clone({ limit });
|
|
177
|
-
}
|
|
178
|
-
after(cursor) {
|
|
179
|
-
return this.clone({ after: cursor });
|
|
180
|
-
}
|
|
181
|
-
order(order) {
|
|
182
|
-
return this.clone({ order });
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
function isSelectBuilder(value) {
|
|
186
|
-
return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
|
|
187
|
-
}
|
|
188
|
-
function fromBuilder(value) {
|
|
189
|
-
const spec = value[SELECT_SPEC];
|
|
190
|
-
return {
|
|
191
|
-
select: spec.select,
|
|
192
|
-
filter: spec.filter,
|
|
193
|
-
limit: spec.limit,
|
|
194
|
-
after: spec.after,
|
|
195
|
-
order: spec.order
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
function normalizeSelectSpec(value) {
|
|
199
|
-
if (isSelectBuilder(value)) {
|
|
200
|
-
return fromBuilder(value);
|
|
201
|
-
}
|
|
202
|
-
const obj = value;
|
|
203
|
-
return {
|
|
204
|
-
select: obj.select,
|
|
205
|
-
filter: obj.filter,
|
|
206
|
-
limit: obj.limit,
|
|
207
|
-
after: obj.after,
|
|
208
|
-
order: obj.order
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function normalizeTopLevelSelect(value) {
|
|
212
|
-
if (isSelectBuilder(value)) {
|
|
213
|
-
return fromBuilder(value);
|
|
214
|
-
}
|
|
215
|
-
return { select: value ?? {} };
|
|
216
|
-
}
|
|
217
|
-
function buildProject(select) {
|
|
218
|
-
return new BuilderImpl({
|
|
219
|
-
[SELECT_BUILDER]: true,
|
|
220
|
-
select
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
function buildRelation(select) {
|
|
224
|
-
return new BuilderImpl({
|
|
225
|
-
[SELECT_BUILDER]: true,
|
|
226
|
-
select
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
185
|
// src/relation/detect.ts
|
|
231
186
|
function isRelationSpec(value) {
|
|
232
187
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -706,80 +661,6 @@ async function execute(operation, options) {
|
|
|
706
661
|
return ClientManager.getExecutor().execute(operation, options);
|
|
707
662
|
}
|
|
708
663
|
|
|
709
|
-
// src/hydrator/hydrator.ts
|
|
710
|
-
function hydrate(rawItems, select, metadata, updatable = false) {
|
|
711
|
-
return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
|
|
712
|
-
}
|
|
713
|
-
function hydrateItem(raw, select, metadata, updatable = false) {
|
|
714
|
-
const fieldMap = new Map(
|
|
715
|
-
metadata.fields.map((f) => [f.propertyName, f])
|
|
716
|
-
);
|
|
717
|
-
const embeddedMap = new Map(
|
|
718
|
-
metadata.embeddedFields.map((e) => [e.propertyName, e])
|
|
719
|
-
);
|
|
720
|
-
const result = {};
|
|
721
|
-
for (const [fieldName, selectValue] of Object.entries(select)) {
|
|
722
|
-
if (selectValue === true) {
|
|
723
|
-
const value = raw[fieldName];
|
|
724
|
-
if (value !== void 0) {
|
|
725
|
-
const fieldMeta = fieldMap.get(fieldName);
|
|
726
|
-
result[fieldName] = deserializeValue(value, fieldMeta);
|
|
727
|
-
}
|
|
728
|
-
} else if (typeof selectValue === "object" && selectValue !== null) {
|
|
729
|
-
if ("select" in selectValue || isSelectBuilder(selectValue)) {
|
|
730
|
-
continue;
|
|
731
|
-
}
|
|
732
|
-
const rawEmb = raw[fieldName];
|
|
733
|
-
if (rawEmb && typeof rawEmb === "object") {
|
|
734
|
-
result[fieldName] = hydrateEmbedded(
|
|
735
|
-
rawEmb,
|
|
736
|
-
selectValue,
|
|
737
|
-
metadata,
|
|
738
|
-
fieldName
|
|
739
|
-
);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (updatable) {
|
|
744
|
-
attachHiddenKey(result, raw);
|
|
745
|
-
}
|
|
746
|
-
return result;
|
|
747
|
-
}
|
|
748
|
-
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
749
|
-
const result = {};
|
|
750
|
-
for (const [key, selectValue] of Object.entries(select)) {
|
|
751
|
-
if (selectValue === true) {
|
|
752
|
-
if (raw[key] !== void 0) {
|
|
753
|
-
result[key] = raw[key];
|
|
754
|
-
}
|
|
755
|
-
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
756
|
-
const nested = raw[key];
|
|
757
|
-
if (nested && typeof nested === "object") {
|
|
758
|
-
result[key] = hydrateEmbedded(
|
|
759
|
-
nested,
|
|
760
|
-
selectValue,
|
|
761
|
-
_metadata,
|
|
762
|
-
key
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return result;
|
|
768
|
-
}
|
|
769
|
-
function deserializeValue(value, fieldMeta) {
|
|
770
|
-
if (!fieldMeta) return value;
|
|
771
|
-
if (fieldMeta.options?.deserialize) {
|
|
772
|
-
return fieldMeta.options.deserialize(value);
|
|
773
|
-
}
|
|
774
|
-
if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
|
|
775
|
-
return new Date(value);
|
|
776
|
-
}
|
|
777
|
-
if (fieldMeta.options?.format === "date" && typeof value === "string") {
|
|
778
|
-
return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
|
|
779
|
-
}
|
|
780
|
-
return value;
|
|
781
|
-
}
|
|
782
|
-
|
|
783
664
|
// src/pagination/cursor.ts
|
|
784
665
|
function encodeCursor(lastEvaluatedKey) {
|
|
785
666
|
const json = JSON.stringify(lastEvaluatedKey);
|
|
@@ -3096,6 +2977,58 @@ var DDBModel = class {
|
|
|
3096
2977
|
static mutate(envelope, options) {
|
|
3097
2978
|
return executeWriteEnvelope(envelope, options);
|
|
3098
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
|
+
}
|
|
3099
3032
|
/**
|
|
3100
3033
|
* Multi-key, multi-model batch read. Each {@link BatchGetRequest} contributes its
|
|
3101
3034
|
* keys to a chunked `BatchGetItem` per physical table; results are re-grouped by
|
|
@@ -7002,8 +6935,6 @@ export {
|
|
|
7002
6935
|
SPEC_VERSION,
|
|
7003
6936
|
MARKER_ROW_ENTITY,
|
|
7004
6937
|
buildManifest,
|
|
7005
|
-
isSelectBuilder,
|
|
7006
|
-
normalizeSelectSpec,
|
|
7007
6938
|
detectRelationFields,
|
|
7008
6939
|
getImplicitKeyFields,
|
|
7009
6940
|
buildProjection,
|
|
@@ -7011,7 +6942,6 @@ export {
|
|
|
7011
6942
|
evaluateFilter,
|
|
7012
6943
|
plan,
|
|
7013
6944
|
execute,
|
|
7014
|
-
hydrate,
|
|
7015
6945
|
encodeCursor,
|
|
7016
6946
|
decodeCursor,
|
|
7017
6947
|
executeListInternal,
|