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.
@@ -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-CPTV3H2U.js";
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: TableMapping.resolve(metadata.tableName)
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,