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/README.md +10 -1
- package/dist/chunk-PNIZS37E.js +916 -0
- package/dist/{chunk-SGYBE2OV.js → chunk-VST3WOK3.js} +597 -113
- package/dist/{chunk-4TYK3AV6.js → chunk-YIXXTGZ6.js} +1369 -15
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +587 -75
- package/dist/index.js +535 -16
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types--tdHK3vi.d.ts → types-DuJ08bgc.d.ts} +1542 -63
- package/package.json +1 -1
- package/dist/chunk-ITHQ2EDH.js +0 -461
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-
|
|
82
|
+
} from "./chunk-VST3WOK3.js";
|
|
90
83
|
import {
|
|
91
84
|
CdcEmulator,
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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: (
|
|
604
|
-
`declarative transaction: expanded to ${
|
|
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
|
};
|