graphddb 0.2.5 → 0.3.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/dist/index.js CHANGED
@@ -2,8 +2,6 @@ import {
2
2
  BatchGetResult,
3
3
  DDBModel,
4
4
  EDGE_WRITES_MARKER,
5
- ENTITY_WRITES_MARKER,
6
- LIFECYCLE_CONTRACT_MARKER,
7
5
  MARKER_ROW_ENTITY,
8
6
  OLD_VALUE_NAMESPACE,
9
7
  RELATION_TRAVERSAL_CONCURRENCY,
@@ -41,7 +39,6 @@ import {
41
39
  detectRelationFields,
42
40
  edgeWrites,
43
41
  encodeCursor,
44
- entityWrites,
45
42
  evaluateFilter,
46
43
  execute,
47
44
  executeBatchGet,
@@ -53,7 +50,6 @@ import {
53
50
  executeQuery,
54
51
  from,
55
52
  getEdgeWrites,
56
- getEntityWrites,
57
53
  getImplicitKeyFields,
58
54
  hydrate,
59
55
  isCommandModelContract,
@@ -64,15 +60,12 @@ import {
64
60
  isContractKeyRef,
65
61
  isContractParamRef,
66
62
  isEdgeWritesDefinition,
67
- isEntityWritesDefinition,
68
- isLifecycleContract,
69
63
  isMutationFragment,
70
64
  isMutationInputRef,
71
65
  isPlannedCommandMethod,
72
66
  isQueryModelContract,
73
67
  isSelectBuilder,
74
68
  isTransactionRef,
75
- lifecyclePhaseForIntent,
76
69
  mapWithConcurrency,
77
70
  mintContractKeyFieldRef,
78
71
  mintContractParamRef,
@@ -86,11 +79,23 @@ import {
86
79
  validateDepth,
87
80
  when,
88
81
  wholeKeysSentinel
89
- } from "./chunk-SGYBE2OV.js";
82
+ } from "./chunk-VST3WOK3.js";
90
83
  import {
91
84
  CdcEmulator,
92
- createCdcEmulator
93
- } from "./chunk-ITHQ2EDH.js";
85
+ InMemoryProjectionSink,
86
+ MAINT_OUTBOX_PK_PREFIX,
87
+ MaintenanceDrain,
88
+ ProjectionSinkDrain,
89
+ compareDesc,
90
+ createCdcEmulator,
91
+ createMaintenanceDrain,
92
+ createMaintenanceDrainHandler,
93
+ createProjectionSinkDrain,
94
+ createProjectionSinkHandler,
95
+ orderAndTrimCollection,
96
+ pathField,
97
+ projectFrom
98
+ } from "./chunk-PNIZS37E.js";
94
99
  import {
95
100
  BATCH_GET_MAX_KEYS,
96
101
  BATCH_WRITE_MAX_ITEMS,
@@ -98,15 +103,19 @@ import {
98
103
  DEFAULT_MAX_ATTEMPTS,
99
104
  DEFAULT_RETRY_POLICY,
100
105
  DynamoExecutor,
106
+ ENTITY_WRITES_MARKER,
107
+ LIFECYCLE_CONTRACT_MARKER,
101
108
  Linter,
102
109
  MAX_TRANSACT_ITEMS,
103
110
  MetadataRegistry,
104
111
  RetryingExecutor,
105
112
  TableMapping,
106
113
  TransactionContext,
114
+ ViewRegistry,
107
115
  attachModelClass,
108
116
  buildConditionExpression,
109
117
  buildDeleteInput,
118
+ buildMaintenanceGraph,
110
119
  buildPutInput,
111
120
  buildUpdateExpression,
112
121
  buildUpdateInput,
@@ -115,31 +124,49 @@ import {
115
124
  commitTransaction,
116
125
  cond,
117
126
  createDefaultLinter,
127
+ defineProjection,
128
+ defineProjections,
129
+ defineVersioned,
130
+ defineView,
131
+ defineViews,
132
+ entityWrites,
118
133
  execItemKeySignature,
119
134
  executeDelete,
120
135
  executePut,
121
136
  executeTransaction,
122
137
  executeUpdate,
138
+ getEntityWrites,
123
139
  gsi,
124
140
  gsiAmbiguityRule,
141
+ identity,
125
142
  isColumn,
143
+ isEntityWritesDefinition,
126
144
  isKeySegment,
145
+ isLifecycleContract,
146
+ isMaintainTrigger,
127
147
  isParam,
148
+ isProjectionDefinition,
128
149
  isRawCondition,
129
150
  isRetryableError,
130
151
  isRetryableTransactionCancellation,
152
+ isViewDefinition,
131
153
  k,
132
154
  key,
155
+ lifecyclePhaseForIntent,
156
+ maintainTrigger,
133
157
  missingGsiRule,
134
158
  noScanRule,
135
159
  param,
160
+ preview,
136
161
  relationDepthRule,
137
162
  requireLimitRule,
138
163
  resolveConditionTree,
139
164
  resolveKey,
140
165
  resolveModelClass,
141
- serializeFieldValue
142
- } from "./chunk-4TYK3AV6.js";
166
+ segmentFieldNames,
167
+ serializeFieldValue,
168
+ whenMember
169
+ } from "./chunk-YIXXTGZ6.js";
143
170
 
144
171
  // src/metadata/prefix.ts
145
172
  function derivePrefix(customPrefix, className) {
@@ -189,6 +216,7 @@ function validateGsiAmbiguity(primaryKey, gsiDefinitions) {
189
216
  var pendingFields = [];
190
217
  var pendingEmbedded = [];
191
218
  var pendingRelations = [];
219
+ var pendingAggregates = [];
192
220
  function collectField(field2) {
193
221
  pendingFields.push(field2);
194
222
  }
@@ -198,6 +226,9 @@ function collectEmbedded(embedded2) {
198
226
  function collectRelation(relation) {
199
227
  pendingRelations.push(relation);
200
228
  }
229
+ function collectAggregate(aggregate2) {
230
+ pendingAggregates.push(aggregate2);
231
+ }
201
232
  function drainFields() {
202
233
  const result = pendingFields;
203
234
  pendingFields = [];
@@ -213,6 +244,11 @@ function drainRelations() {
213
244
  pendingRelations = [];
214
245
  return result;
215
246
  }
247
+ function drainAggregates() {
248
+ const result = pendingAggregates;
249
+ pendingAggregates = [];
250
+ return result;
251
+ }
216
252
 
217
253
  // src/decorators/model.ts
218
254
  function model(options) {
@@ -220,6 +256,7 @@ function model(options) {
220
256
  const fields = drainFields();
221
257
  const embeddedFields = drainEmbedded();
222
258
  const relations = drainRelations();
259
+ const aggregates = drainAggregates();
223
260
  const prefix = derivePrefix(options.prefix, context.name);
224
261
  MetadataRegistry.register(target, {
225
262
  tableName: options.table,
@@ -228,6 +265,7 @@ function model(options) {
228
265
  primaryKey: null,
229
266
  gsiDefinitions: [],
230
267
  relations,
268
+ aggregates,
231
269
  embeddedFields
232
270
  });
233
271
  };
@@ -322,7 +360,7 @@ function hasMany(targetFactory, keyBinding, options) {
322
360
  });
323
361
  };
324
362
  }
325
- function belongsTo(targetFactory, keyBinding) {
363
+ function belongsTo(targetFactory, keyBinding, options) {
326
364
  return function(_value, context) {
327
365
  const propertyName = String(context.name);
328
366
  assertRelationPropertyNameAllowed(propertyName);
@@ -330,7 +368,8 @@ function belongsTo(targetFactory, keyBinding) {
330
368
  type: "belongsTo",
331
369
  propertyName,
332
370
  targetFactory,
333
- keyBinding
371
+ keyBinding,
372
+ options
334
373
  });
335
374
  };
336
375
  }
@@ -347,6 +386,29 @@ function hasOne(targetFactory, keyBinding) {
347
386
  };
348
387
  }
349
388
 
389
+ // src/decorators/aggregate.ts
390
+ function aggregate(targetFactory, keyBinding, options) {
391
+ return function(_value, context) {
392
+ collectAggregate({
393
+ propertyName: String(context.name),
394
+ targetFactory,
395
+ keyBinding,
396
+ options
397
+ });
398
+ };
399
+ }
400
+ function count() {
401
+ return { op: "count" };
402
+ }
403
+ function max(field2) {
404
+ if (typeof field2 !== "string" || field2.length === 0) {
405
+ throw new Error(
406
+ `max(...) requires a non-empty source field name to aggregate (e.g. max('createdAt')), but received ${JSON.stringify(field2)}.`
407
+ );
408
+ }
409
+ return { op: "max", field: field2 };
410
+ }
411
+
350
412
  // src/operations/declarative-transaction.ts
351
413
  var PLACEHOLDER_RE = /\{[^{}]+\}/g;
352
414
  function resolveTemplate(template, params, element) {
@@ -484,6 +546,79 @@ function entityByName(manifest, name) {
484
546
  function physicalTableName(manifest, logical) {
485
547
  return manifest.tables[logical]?.physicalName ?? logical;
486
548
  }
549
+ function applyMaintainTransform(op, args, value) {
550
+ if (op === "identity") return value;
551
+ if (op === "preview") {
552
+ const n = args[0];
553
+ if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) {
554
+ throw new Error(
555
+ `declarative transaction: a maintenance \`preview\` projection has a non-positive-integer length bound (${JSON.stringify(n)}).`
556
+ );
557
+ }
558
+ if (value === null || value === void 0) return value;
559
+ return String(value).slice(0, n);
560
+ }
561
+ throw new Error(
562
+ `declarative transaction: unknown maintenance projection op '${String(op)}' (expected 'identity' / 'preview').`
563
+ );
564
+ }
565
+ function buildMaintainProjection(maintain, params, element) {
566
+ const out = {};
567
+ for (const [attr, transform] of Object.entries(maintain.projection)) {
568
+ const value = resolveValue(`{${transform.inputField}}`, params, element);
569
+ out[attr] = applyMaintainTransform(transform.op, transform.args, value);
570
+ }
571
+ return out;
572
+ }
573
+ function buildMaintainUpdateItem(spec, maintain, table, key2, params, element) {
574
+ const update = { TableName: table, Key: key2 };
575
+ if (maintain.kind === "counter") {
576
+ const counter = maintain.counter;
577
+ if (counter === void 0) {
578
+ throw new Error(
579
+ `declarative transaction: a 'counter' maintenance write on '${spec.entity}' has no \`counter\` payload.`
580
+ );
581
+ }
582
+ update.UpdateExpression = "ADD #a0 :a0";
583
+ update.ExpressionAttributeNames = { "#a0": counter.attribute };
584
+ update.ExpressionAttributeValues = {
585
+ ":a0": typeof counter.delta === "string" ? Number(counter.delta) : counter.delta
586
+ };
587
+ return { Update: update };
588
+ }
589
+ const projection = buildMaintainProjection(maintain, params, element);
590
+ if (maintain.kind === "collection") {
591
+ const field2 = maintain.collection?.field;
592
+ if (field2 === void 0) {
593
+ throw new Error(
594
+ `declarative transaction: a 'collection' maintenance write on '${spec.entity}' has no \`collection.field\`.`
595
+ );
596
+ }
597
+ update.UpdateExpression = "SET #c = list_append(if_not_exists(#c, :empty), :item)";
598
+ update.ExpressionAttributeNames = { "#c": field2 };
599
+ update.ExpressionAttributeValues = { ":empty": [], ":item": [projection] };
600
+ } else {
601
+ const names = {};
602
+ const values = {};
603
+ const sets = [];
604
+ let i = 0;
605
+ for (const [attr, value] of Object.entries(projection)) {
606
+ names[`#m${i}`] = attr;
607
+ values[`:m${i}`] = value;
608
+ sets.push(`#m${i} = :m${i}`);
609
+ i++;
610
+ }
611
+ if (sets.length === 0) {
612
+ throw new Error(
613
+ `declarative transaction: a 'snapshot' maintenance write on '${spec.entity}' has an empty projection.`
614
+ );
615
+ }
616
+ update.UpdateExpression = `SET ${sets.join(", ")}`;
617
+ update.ExpressionAttributeNames = names;
618
+ update.ExpressionAttributeValues = values;
619
+ }
620
+ return { Update: update };
621
+ }
487
622
  function buildTransactItem(spec, manifest, params, element) {
488
623
  const literal2 = spec.literalKey === true;
489
624
  const entity = literal2 ? void 0 : entityByName(manifest, spec.entity);
@@ -511,6 +646,9 @@ function buildTransactItem(spec, manifest, params, element) {
511
646
  applyCondition(spec.condition, check, params, element);
512
647
  return { ConditionCheck: check };
513
648
  }
649
+ if (spec.maintain !== void 0) {
650
+ return buildMaintainUpdateItem(spec, spec.maintain, table, key2, params, element);
651
+ }
514
652
  const changes = resolveRecord(spec.changes, params, element);
515
653
  const adds = resolveRecord(spec.add, params, element);
516
654
  const names = {};
@@ -600,8 +738,8 @@ async function executeDeclarativeTransaction(spec, manifest, params) {
600
738
  expanded.map((e) => e.item),
601
739
  {
602
740
  modelBySignature,
603
- limitError: (count) => new Error(
604
- `declarative transaction: expanded to ${count} items, exceeds the DynamoDB TransactWriteItems limit of ${MAX_TRANSACT_ITEMS}.`
741
+ limitError: (count2) => new Error(
742
+ `declarative transaction: expanded to ${count2} items, exceeds the DynamoDB TransactWriteItems limit of ${MAX_TRANSACT_ITEMS}.`
605
743
  )
606
744
  }
607
745
  );
@@ -839,6 +977,360 @@ var queryBoundaryRule = {
839
977
  }
840
978
  };
841
979
 
980
+ // src/relation/maintenance-rebuild.ts
981
+ function toClass(m) {
982
+ try {
983
+ return resolveModelClass(m);
984
+ } catch {
985
+ return m;
986
+ }
987
+ }
988
+ var MaintenanceRebuilder = class {
989
+ graph;
990
+ constructor(opts = {}) {
991
+ const classes = (opts.models ?? []).map(toClass);
992
+ const views = opts.views ?? [];
993
+ const allClasses = [...classes, ...views.map((v) => v.viewClass)];
994
+ const scoped = allClasses.length > 0 ? new Map(allClasses.map((c) => [c, MetadataRegistry.get(c)])) : void 0;
995
+ this.graph = buildMaintenanceGraph(scoped, views);
996
+ }
997
+ /**
998
+ * Re-derive the materialized value for one owner row × maintainer from the source
999
+ * rows and report whether it differs from what the owner row currently stores. Does
1000
+ * NOT write anything (a read-only drift probe).
1001
+ *
1002
+ * @param owner The owner model class declaring the maintainer.
1003
+ * @param relationProperty The relation / `@aggregate` property the value lives under.
1004
+ * @param ownerKeyInput The owner row's key input (e.g. `{ threadId: 't1' }`).
1005
+ */
1006
+ async detectDrift(owner, relationProperty, ownerKeyInput) {
1007
+ const { item, ownerMeta, ownerKey } = this.resolve(owner, relationProperty);
1008
+ const expected = await this.recompute(item, ownerMeta, ownerKeyInput);
1009
+ const ownerRow = await this.readOwnerRow(ownerMeta, ownerKey(ownerKeyInput));
1010
+ const actual = this.actualValue(item.effect, ownerRow);
1011
+ const ownerAbsentAndEmpty = ownerRow === void 0 && isEmptyMaintained(item.effect, expected);
1012
+ return {
1013
+ relationProperty,
1014
+ ownerEntity: item.ownerEntity,
1015
+ ownerKey: ownerKey(ownerKeyInput),
1016
+ kind: item.effect.kind,
1017
+ drifted: !ownerAbsentAndEmpty && !deepEqual(normalize(expected), normalize(actual)),
1018
+ expected,
1019
+ actual
1020
+ };
1021
+ }
1022
+ /**
1023
+ * Re-derive the materialized value for one owner row × maintainer from the source
1024
+ * rows and WRITE the absolute re-derived value onto the owner row when it drifts (or
1025
+ * always, when `force`). Returns the drift verdict plus whether a repair write ran.
1026
+ *
1027
+ * @param options.force Write even when no drift is detected (default: write only on drift).
1028
+ */
1029
+ async rebuild(owner, relationProperty, ownerKeyInput, options = {}) {
1030
+ const report = await this.detectDrift(owner, relationProperty, ownerKeyInput);
1031
+ if (!report.drifted && options.force !== true) {
1032
+ return { ...report, repaired: false };
1033
+ }
1034
+ const { item, ownerMeta } = this.resolve(owner, relationProperty);
1035
+ await this.writeValue(item.effect, ownerMeta, report.ownerKey, report.expected);
1036
+ return { ...report, repaired: true };
1037
+ }
1038
+ /** Resolve an owner model + property to its {@link MaintainItem} + owner-key builder. */
1039
+ resolve(owner, relationProperty) {
1040
+ const ownerClass = toClass(owner);
1041
+ const ownerMeta = MetadataRegistry.get(ownerClass);
1042
+ const item = this.graph.items.find(
1043
+ (i) => i.relationProperty === relationProperty && (i.ownerClass === ownerClass || i.ownerClass.name === ownerClass.name)
1044
+ );
1045
+ if (item === void 0) {
1046
+ throw new Error(
1047
+ `Maintenance rebuild: no maintainer found for property '${relationProperty}' on '${ownerClass.name}'. The rebuilder repairs declared \`maintainedOn\` maintainers (snapshot / collection / counter); '${relationProperty}' declares none, or its model is not in this rebuilder's scope.`
1048
+ );
1049
+ }
1050
+ const ownerKey = (input) => {
1051
+ const { Key } = buildDeleteInput(
1052
+ ownerClass,
1053
+ input
1054
+ );
1055
+ return Key;
1056
+ };
1057
+ return { item, ownerMeta, ownerKey };
1058
+ }
1059
+ /**
1060
+ * Scan every source row bound to this owner row and re-derive the maintained value:
1061
+ *
1062
+ * - **collection** — project each source row, order + trim by the SAME read-side
1063
+ * rules (`orderBy` / `orderDir` / `maxItems`) the drain uses, return the list;
1064
+ * - **snapshot** — project the single bound source row (or `undefined` when none);
1065
+ * - **counter `count`** — the COUNT of source rows (absolute, not an ADD);
1066
+ * - **counter `max`** — the maximum source value of the tracked field.
1067
+ */
1068
+ async recompute(item, _ownerMeta, ownerKeyInput) {
1069
+ const effect = item.effect;
1070
+ if (effect.kind === "membership") {
1071
+ throw new Error(
1072
+ `Maintenance rebuild: sparse-view membership maintainer '${item.relationProperty}' on '${item.ownerEntity}' is not yet supported by the rebuild planner (#133 follow-up). A membership row appears/disappears per source row by predicate; rebuild it by re-running the source events through the drain.`
1073
+ );
1074
+ }
1075
+ const sources = await this.scanSourceRows(item, ownerKeyInput);
1076
+ if (effect.kind === "collection") {
1077
+ return this.recomputeCollection(effect, sources);
1078
+ }
1079
+ if (effect.kind === "snapshot") {
1080
+ return this.recomputeSnapshot(effect, sources);
1081
+ }
1082
+ return this.recomputeCounter(effect, sources);
1083
+ }
1084
+ /**
1085
+ * The source rows bound to one owner row. The maintenance key binding is
1086
+ * `{ ownerField: '$.entity.<sourceField>' }` (the destination owner-key field ← the
1087
+ * source row's field). INVERTED for a rebuild: the owner row supplies the value, and
1088
+ * the source partition is queried for rows whose `<sourceField>` matches — exactly the
1089
+ * set the live `hasMany` / `@aggregate` read aggregates. Queries the source via the
1090
+ * same {@link plan} the read planner uses (a partition Query), paginating fully.
1091
+ */
1092
+ async scanSourceRows(item, ownerKeyInput) {
1093
+ const sourceMeta = MetadataRegistry.get(item.sourceClass);
1094
+ const queryKey = {};
1095
+ for (const [ownerField, srcPath] of Object.entries(item.effect.keys)) {
1096
+ const sourceField = pathField(srcPath);
1097
+ queryKey[sourceField] = ownerKeyInput[ownerField];
1098
+ }
1099
+ const executor = ClientManager.getExecutor();
1100
+ const rows = [];
1101
+ let after;
1102
+ do {
1103
+ const execPlan = plan(sourceMeta, {
1104
+ key: queryKey,
1105
+ // Select all declared source fields so every projection / value source path
1106
+ // and the sort-key leaf is present on the scanned row.
1107
+ select: selectAll(sourceMeta),
1108
+ consistentRead: false,
1109
+ ...after ? { after } : {}
1110
+ });
1111
+ const op = execPlan.operations[0];
1112
+ const res = await executor.execute(op);
1113
+ for (const it of res.items) rows.push(it);
1114
+ after = res.lastEvaluatedKey;
1115
+ } while (after !== void 0);
1116
+ return rows;
1117
+ }
1118
+ /** Re-derive a bounded collection: project, then order + trim by the read-side rules. */
1119
+ recomputeCollection(effect, sources) {
1120
+ if (effect.collection.orderBy === void 0 && effect.collection.maxItems !== void 0) {
1121
+ throw new Error(
1122
+ `Maintenance rebuild: relation '${effect.collection.field}' maintains an UNORDERED bounded collection (\`maxItems: ${effect.collection.maxItems}\` with no \`read.order\`). Its trimmed contents depend on stream ARRIVAL order, which the source rows do not record, so the value cannot be faithfully rebuilt from source \u2014 a rebuild would keep a different subset than the drain converged to. Declare a \`read.order\` on the relation so the maintained collection orders by the source's sort-key field (then the rebuild and the drain agree), or rebuild it unbounded.`
1123
+ );
1124
+ }
1125
+ const projected = sources.map((row) => projectFrom(effect.project, row));
1126
+ return orderAndTrimCollection(projected, effect.collection);
1127
+ }
1128
+ /**
1129
+ * Re-derive a single-row snapshot. A snapshot mirrors ONE source row (a `belongsTo` /
1130
+ * `hasOne`, normally 1:1). In the unusual case where MORE THAN ONE source row binds, the
1131
+ * live single-value read takes the FIRST row in the source's natural query order
1132
+ * (`ScanIndexForward`, default ASC = the smallest sort-key leaf), so re-derive the same
1133
+ * row — {@link pickFirst} — for a verdict consistent with the live read. Returns
1134
+ * `undefined` when no source row is bound (the snapshot should be absent).
1135
+ */
1136
+ recomputeSnapshot(effect, sources) {
1137
+ if (sources.length === 0) return void 0;
1138
+ const chosen = sources.length === 1 ? sources[0] : pickFirst(sources, this.sourceMetaOf(effect));
1139
+ return projectFrom(effect.project, chosen);
1140
+ }
1141
+ /** The source entity metadata a maintain effect projects from (its relation target). */
1142
+ sourceMetaOf(effect) {
1143
+ const item = this.graph.items.find((i) => i.effect === effect);
1144
+ return item ? MetadataRegistry.get(item.sourceClass) : void 0;
1145
+ }
1146
+ /** Re-derive a counter: the absolute COUNT of source rows, or the MAX of the tracked field. */
1147
+ recomputeCounter(effect, sources) {
1148
+ if (effect.value.op === "count") {
1149
+ return sources.length;
1150
+ }
1151
+ const field2 = effect.value.field;
1152
+ let best;
1153
+ for (const row of sources) {
1154
+ const v = row[field2];
1155
+ if (v === void 0 || v === null) continue;
1156
+ if (best === void 0 || compareDesc(v, best) < 0) best = v;
1157
+ }
1158
+ return best;
1159
+ }
1160
+ /** Read the owner row (consistent) for the drift comparison / actual value. */
1161
+ async readOwnerRow(ownerMeta, ownerKey) {
1162
+ const res = await ClientManager.getExecutor().execute({
1163
+ type: "GetItem",
1164
+ tableName: TableMapping.resolve(ownerMeta.tableName),
1165
+ keyCondition: ownerKey,
1166
+ consistentRead: true
1167
+ });
1168
+ return res.items[0];
1169
+ }
1170
+ /** The value currently stored on the owner row for this maintainer. */
1171
+ actualValue(effect, ownerRow) {
1172
+ if (ownerRow === void 0) return void 0;
1173
+ if (effect.kind === "collection") {
1174
+ return ownerRow[effect.collection.field];
1175
+ }
1176
+ if (effect.kind === "counter") {
1177
+ return ownerRow[effect.attribute];
1178
+ }
1179
+ const out = {};
1180
+ let anyPresent = false;
1181
+ for (const attr of Object.keys(effect.project)) {
1182
+ if (Object.prototype.hasOwnProperty.call(ownerRow, attr)) {
1183
+ out[attr] = ownerRow[attr];
1184
+ anyPresent = true;
1185
+ }
1186
+ }
1187
+ return anyPresent ? out : void 0;
1188
+ }
1189
+ /** Write the re-derived ABSOLUTE value onto the owner row (the repair). */
1190
+ async writeValue(effect, ownerMeta, ownerKey, expected) {
1191
+ const TableName = TableMapping.resolve(ownerMeta.tableName);
1192
+ const executor = ClientManager.getExecutor();
1193
+ if (effect.kind === "collection") {
1194
+ await executor.update({
1195
+ TableName,
1196
+ Key: ownerKey,
1197
+ UpdateExpression: "SET #c = :list",
1198
+ ExpressionAttributeNames: { "#c": effect.collection.field },
1199
+ ExpressionAttributeValues: { ":list": expected ?? [] }
1200
+ });
1201
+ return;
1202
+ }
1203
+ if (effect.kind === "counter") {
1204
+ if (expected === void 0 || expected === null) {
1205
+ if (effect.value.op === "count") {
1206
+ await executor.update({
1207
+ TableName,
1208
+ Key: ownerKey,
1209
+ UpdateExpression: "SET #a = :v",
1210
+ ExpressionAttributeNames: { "#a": effect.attribute },
1211
+ ExpressionAttributeValues: { ":v": 0 }
1212
+ });
1213
+ } else {
1214
+ await executor.update({
1215
+ TableName,
1216
+ Key: ownerKey,
1217
+ UpdateExpression: "REMOVE #a",
1218
+ ExpressionAttributeNames: { "#a": effect.attribute }
1219
+ });
1220
+ }
1221
+ return;
1222
+ }
1223
+ await executor.update({
1224
+ TableName,
1225
+ Key: ownerKey,
1226
+ UpdateExpression: "SET #a = :v",
1227
+ ExpressionAttributeNames: { "#a": effect.attribute },
1228
+ ExpressionAttributeValues: { ":v": expected }
1229
+ });
1230
+ return;
1231
+ }
1232
+ if (expected === void 0) {
1233
+ const names2 = {};
1234
+ const removes = [];
1235
+ let i2 = 0;
1236
+ for (const attr of Object.keys(effect.project)) {
1237
+ names2[`#m${i2}`] = attr;
1238
+ removes.push(`#m${i2}`);
1239
+ i2 += 1;
1240
+ }
1241
+ if (removes.length === 0) return;
1242
+ await executor.update({
1243
+ TableName,
1244
+ Key: ownerKey,
1245
+ UpdateExpression: `REMOVE ${removes.join(", ")}`,
1246
+ ExpressionAttributeNames: names2
1247
+ });
1248
+ return;
1249
+ }
1250
+ const projection = expected;
1251
+ const names = {};
1252
+ const values = {};
1253
+ const sets = [];
1254
+ let i = 0;
1255
+ for (const [attr, value] of Object.entries(projection)) {
1256
+ names[`#m${i}`] = attr;
1257
+ values[`:m${i}`] = value;
1258
+ sets.push(`#m${i} = :m${i}`);
1259
+ i += 1;
1260
+ }
1261
+ if (sets.length === 0) return;
1262
+ await executor.update({
1263
+ TableName,
1264
+ Key: ownerKey,
1265
+ UpdateExpression: `SET ${sets.join(", ")}`,
1266
+ ExpressionAttributeNames: names,
1267
+ ExpressionAttributeValues: values
1268
+ });
1269
+ }
1270
+ };
1271
+ function selectAll(meta) {
1272
+ const select = {};
1273
+ for (const f of meta.fields) select[f.propertyName] = true;
1274
+ return select;
1275
+ }
1276
+ function pickFirst(sources, sourceMeta) {
1277
+ const skFields = sourceMeta?.primaryKey ? segmentFieldNames(sourceMeta.primaryKey.segmented.skSegments) : [];
1278
+ const orderField = skFields[skFields.length - 1];
1279
+ if (orderField === void 0) return sources[0];
1280
+ return [...sources].sort((a, b) => compareDesc(b[orderField], a[orderField]))[0];
1281
+ }
1282
+ function normalize(value) {
1283
+ if (value === void 0 || value === null) return void 0;
1284
+ if (typeof value === "number") return value;
1285
+ if (typeof value === "string" && /^-?\d+$/.test(value)) {
1286
+ const n = Number(value);
1287
+ if (Number.isSafeInteger(n)) return n;
1288
+ }
1289
+ if (Array.isArray(value)) return value.map(normalize);
1290
+ if (typeof value === "object") {
1291
+ const out = {};
1292
+ for (const [k2, v] of Object.entries(value)) {
1293
+ const nv = normalize(v);
1294
+ if (nv === void 0) continue;
1295
+ out[k2] = nv;
1296
+ }
1297
+ return out;
1298
+ }
1299
+ return value;
1300
+ }
1301
+ function isEmptyMaintained(effect, expected) {
1302
+ if (effect.kind === "collection") {
1303
+ return Array.isArray(expected) && expected.length === 0;
1304
+ }
1305
+ if (effect.kind === "counter") {
1306
+ return effect.value.op === "count" ? expected === 0 : expected === void 0 || expected === null;
1307
+ }
1308
+ return expected === void 0;
1309
+ }
1310
+ function deepEqual(a, b) {
1311
+ if (a === b) return true;
1312
+ if (a === void 0 || b === void 0 || a === null || b === null) return a === b;
1313
+ if (Array.isArray(a) && Array.isArray(b)) {
1314
+ if (a.length !== b.length) return false;
1315
+ return a.every((v, i) => deepEqual(v, b[i]));
1316
+ }
1317
+ if (typeof a === "object" && typeof b === "object") {
1318
+ const ka = Object.keys(a);
1319
+ const kb = Object.keys(b);
1320
+ if (ka.length !== kb.length) return false;
1321
+ return ka.every(
1322
+ (k2) => deepEqual(
1323
+ a[k2],
1324
+ b[k2]
1325
+ )
1326
+ );
1327
+ }
1328
+ return false;
1329
+ }
1330
+ function createMaintenanceRebuilder(opts = {}) {
1331
+ return new MaintenanceRebuilder(opts);
1332
+ }
1333
+
842
1334
  // src/define/define.ts
843
1335
  function collectParams(structure, out = {}) {
844
1336
  if (structure === null || typeof structure !== "object") return out;
@@ -1054,15 +1546,22 @@ export {
1054
1546
  DynamoExecutor,
1055
1547
  EDGE_WRITES_MARKER,
1056
1548
  ENTITY_WRITES_MARKER,
1549
+ InMemoryProjectionSink,
1057
1550
  LIFECYCLE_CONTRACT_MARKER,
1058
1551
  Linter,
1552
+ MAINT_OUTBOX_PK_PREFIX,
1059
1553
  MAX_TRANSACT_ITEMS,
1554
+ MaintenanceDrain,
1555
+ MaintenanceRebuilder,
1060
1556
  MetadataRegistry,
1061
1557
  OLD_VALUE_NAMESPACE,
1558
+ ProjectionSinkDrain,
1062
1559
  RetryingExecutor,
1063
1560
  SPEC_VERSION,
1064
1561
  TableMapping,
1065
1562
  TransactionContext,
1563
+ ViewRegistry,
1564
+ aggregate,
1066
1565
  assertBundleSerializable,
1067
1566
  assertContractBoundaries,
1068
1567
  assertContractN1Safe,
@@ -1077,6 +1576,7 @@ export {
1077
1576
  buildContexts,
1078
1577
  buildContracts,
1079
1578
  buildDeleteInput,
1579
+ buildMaintenanceGraph,
1080
1580
  buildManifest,
1081
1581
  buildOperations,
1082
1582
  buildProjection,
@@ -1094,8 +1594,14 @@ export {
1094
1594
  compileSingleFragmentPlan,
1095
1595
  cond,
1096
1596
  contractOfMethodSpec,
1597
+ count,
1097
1598
  createCdcEmulator,
1098
1599
  createDefaultLinter,
1600
+ createMaintenanceDrain,
1601
+ createMaintenanceDrainHandler,
1602
+ createMaintenanceRebuilder,
1603
+ createProjectionSinkDrain,
1604
+ createProjectionSinkHandler,
1099
1605
  datetime,
1100
1606
  decodeCursor,
1101
1607
  decodePerKeyCursor,
@@ -1103,12 +1609,17 @@ export {
1103
1609
  defineDelete,
1104
1610
  defineList,
1105
1611
  definePlan,
1612
+ defineProjection,
1613
+ defineProjections,
1106
1614
  definePut,
1107
1615
  defineQueries,
1108
1616
  defineQuery,
1109
1617
  defineTransaction,
1110
1618
  defineTransactions,
1111
1619
  defineUpdate,
1620
+ defineVersioned,
1621
+ defineView,
1622
+ defineViews,
1112
1623
  deriveEdgeWriteItems,
1113
1624
  deriveEdgeWriteItemsFor,
1114
1625
  deriveModelEdgeWriteItems,
@@ -1146,6 +1657,7 @@ export {
1146
1657
  hasMany,
1147
1658
  hasOne,
1148
1659
  hydrate,
1660
+ identity,
1149
1661
  isColumn,
1150
1662
  isCommandModelContract,
1151
1663
  isCommandPlan,
@@ -1158,21 +1670,26 @@ export {
1158
1670
  isEntityWritesDefinition,
1159
1671
  isKeySegment,
1160
1672
  isLifecycleContract,
1673
+ isMaintainTrigger,
1161
1674
  isMutationFragment,
1162
1675
  isMutationInputRef,
1163
1676
  isParam,
1164
1677
  isPlannedCommandMethod,
1678
+ isProjectionDefinition,
1165
1679
  isQueryModelContract,
1166
1680
  isRetryableError,
1167
1681
  isRetryableTransactionCancellation,
1168
1682
  isSelectBuilder,
1169
1683
  isTransactionRef,
1684
+ isViewDefinition,
1170
1685
  k,
1171
1686
  key,
1172
1687
  lifecyclePhaseForIntent,
1173
1688
  list,
1174
1689
  literal,
1690
+ maintainTrigger,
1175
1691
  map,
1692
+ max,
1176
1693
  mintContractKeyFieldRef,
1177
1694
  mintContractParamRef,
1178
1695
  missingGsiRule,
@@ -1183,6 +1700,7 @@ export {
1183
1700
  numberSet,
1184
1701
  param,
1185
1702
  plan,
1703
+ preview,
1186
1704
  publicCommandModel,
1187
1705
  publicQueryModel,
1188
1706
  query,
@@ -1199,5 +1717,6 @@ export {
1199
1717
  validateDepth,
1200
1718
  validateGsiAmbiguity,
1201
1719
  when,
1720
+ whenMember,
1202
1721
  wholeKeysSentinel
1203
1722
  };