graphddb 0.3.0 → 0.4.0
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 +1 -0
- package/dist/{chunk-6AIAHP3A.js → chunk-72VZYOSG.js} +850 -1
- package/dist/{chunk-QQNP43JL.js → chunk-GWFZVNAV.js} +347 -2
- package/dist/{chunk-QEOFIXTN.js → chunk-PC54CW5P.js} +231 -554
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +673 -5
- package/dist/index.js +658 -21
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-xJRn5qkv.d.ts → types-CpCo8yHP.d.ts} +398 -66
- package/package.json +1 -1
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,
|
|
@@ -16,7 +14,6 @@ import {
|
|
|
16
14
|
buildBridgeBundle,
|
|
17
15
|
buildContexts,
|
|
18
16
|
buildContracts,
|
|
19
|
-
buildMaintenanceGraph,
|
|
20
17
|
buildManifest,
|
|
21
18
|
buildOperations,
|
|
22
19
|
buildProjection,
|
|
@@ -42,7 +39,6 @@ import {
|
|
|
42
39
|
detectRelationFields,
|
|
43
40
|
edgeWrites,
|
|
44
41
|
encodeCursor,
|
|
45
|
-
entityWrites,
|
|
46
42
|
evaluateFilter,
|
|
47
43
|
execute,
|
|
48
44
|
executeBatchGet,
|
|
@@ -54,10 +50,8 @@ import {
|
|
|
54
50
|
executeQuery,
|
|
55
51
|
from,
|
|
56
52
|
getEdgeWrites,
|
|
57
|
-
getEntityWrites,
|
|
58
53
|
getImplicitKeyFields,
|
|
59
54
|
hydrate,
|
|
60
|
-
identity,
|
|
61
55
|
isCommandModelContract,
|
|
62
56
|
isCommandPlan,
|
|
63
57
|
isContractComposeNode,
|
|
@@ -66,23 +60,17 @@ import {
|
|
|
66
60
|
isContractKeyRef,
|
|
67
61
|
isContractParamRef,
|
|
68
62
|
isEdgeWritesDefinition,
|
|
69
|
-
isEntityWritesDefinition,
|
|
70
|
-
isLifecycleContract,
|
|
71
|
-
isMaintainTrigger,
|
|
72
63
|
isMutationFragment,
|
|
73
64
|
isMutationInputRef,
|
|
74
65
|
isPlannedCommandMethod,
|
|
75
66
|
isQueryModelContract,
|
|
76
67
|
isSelectBuilder,
|
|
77
68
|
isTransactionRef,
|
|
78
|
-
lifecyclePhaseForIntent,
|
|
79
|
-
maintainTrigger,
|
|
80
69
|
mapWithConcurrency,
|
|
81
70
|
mintContractKeyFieldRef,
|
|
82
71
|
mintContractParamRef,
|
|
83
72
|
mutation,
|
|
84
73
|
plan,
|
|
85
|
-
preview,
|
|
86
74
|
publicCommandModel,
|
|
87
75
|
publicQueryModel,
|
|
88
76
|
query,
|
|
@@ -91,11 +79,19 @@ import {
|
|
|
91
79
|
validateDepth,
|
|
92
80
|
when,
|
|
93
81
|
wholeKeysSentinel
|
|
94
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-PC54CW5P.js";
|
|
95
83
|
import {
|
|
96
84
|
CdcEmulator,
|
|
97
|
-
|
|
98
|
-
|
|
85
|
+
MAINT_OUTBOX_PK_PREFIX,
|
|
86
|
+
MaintenanceDrain,
|
|
87
|
+
compareDesc,
|
|
88
|
+
createCdcEmulator,
|
|
89
|
+
createMaintenanceDrain,
|
|
90
|
+
createMaintenanceDrainHandler,
|
|
91
|
+
orderAndTrimCollection,
|
|
92
|
+
pathField,
|
|
93
|
+
projectFrom
|
|
94
|
+
} from "./chunk-GWFZVNAV.js";
|
|
99
95
|
import {
|
|
100
96
|
BATCH_GET_MAX_KEYS,
|
|
101
97
|
BATCH_WRITE_MAX_ITEMS,
|
|
@@ -103,6 +99,8 @@ import {
|
|
|
103
99
|
DEFAULT_MAX_ATTEMPTS,
|
|
104
100
|
DEFAULT_RETRY_POLICY,
|
|
105
101
|
DynamoExecutor,
|
|
102
|
+
ENTITY_WRITES_MARKER,
|
|
103
|
+
LIFECYCLE_CONTRACT_MARKER,
|
|
106
104
|
Linter,
|
|
107
105
|
MAX_TRANSACT_ITEMS,
|
|
108
106
|
MetadataRegistry,
|
|
@@ -112,39 +110,51 @@ import {
|
|
|
112
110
|
attachModelClass,
|
|
113
111
|
buildConditionExpression,
|
|
114
112
|
buildDeleteInput,
|
|
113
|
+
buildMaintenanceGraph,
|
|
115
114
|
buildPutInput,
|
|
116
115
|
buildUpdateExpression,
|
|
117
116
|
buildUpdateInput,
|
|
118
117
|
collapseSameKeyItems,
|
|
119
118
|
collectRawConditionParams,
|
|
119
|
+
collectViewDefinitions,
|
|
120
120
|
commitTransaction,
|
|
121
121
|
cond,
|
|
122
122
|
createDefaultLinter,
|
|
123
|
+
entityWrites,
|
|
123
124
|
execItemKeySignature,
|
|
124
125
|
executeDelete,
|
|
125
126
|
executePut,
|
|
126
127
|
executeTransaction,
|
|
127
128
|
executeUpdate,
|
|
129
|
+
getEntityWrites,
|
|
128
130
|
gsi,
|
|
129
131
|
gsiAmbiguityRule,
|
|
132
|
+
identity,
|
|
130
133
|
isColumn,
|
|
134
|
+
isEntityWritesDefinition,
|
|
131
135
|
isKeySegment,
|
|
136
|
+
isLifecycleContract,
|
|
137
|
+
isMaintainTrigger,
|
|
132
138
|
isParam,
|
|
133
139
|
isRawCondition,
|
|
134
140
|
isRetryableError,
|
|
135
141
|
isRetryableTransactionCancellation,
|
|
136
142
|
k,
|
|
137
143
|
key,
|
|
144
|
+
lifecyclePhaseForIntent,
|
|
145
|
+
maintainTrigger,
|
|
138
146
|
missingGsiRule,
|
|
139
147
|
noScanRule,
|
|
140
148
|
param,
|
|
149
|
+
preview,
|
|
141
150
|
relationDepthRule,
|
|
142
151
|
requireLimitRule,
|
|
143
152
|
resolveConditionTree,
|
|
144
153
|
resolveKey,
|
|
145
154
|
resolveModelClass,
|
|
155
|
+
segmentFieldNames,
|
|
146
156
|
serializeFieldValue
|
|
147
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-72VZYOSG.js";
|
|
148
158
|
|
|
149
159
|
// src/metadata/prefix.ts
|
|
150
160
|
function derivePrefix(customPrefix, className) {
|
|
@@ -195,6 +205,7 @@ var pendingFields = [];
|
|
|
195
205
|
var pendingEmbedded = [];
|
|
196
206
|
var pendingRelations = [];
|
|
197
207
|
var pendingAggregates = [];
|
|
208
|
+
var pendingMaintainedFrom = [];
|
|
198
209
|
function collectField(field2) {
|
|
199
210
|
pendingFields.push(field2);
|
|
200
211
|
}
|
|
@@ -207,6 +218,9 @@ function collectRelation(relation) {
|
|
|
207
218
|
function collectAggregate(aggregate2) {
|
|
208
219
|
pendingAggregates.push(aggregate2);
|
|
209
220
|
}
|
|
221
|
+
function collectMaintainedFrom(decl) {
|
|
222
|
+
pendingMaintainedFrom.push(decl);
|
|
223
|
+
}
|
|
210
224
|
function drainFields() {
|
|
211
225
|
const result = pendingFields;
|
|
212
226
|
pendingFields = [];
|
|
@@ -227,6 +241,11 @@ function drainAggregates() {
|
|
|
227
241
|
pendingAggregates = [];
|
|
228
242
|
return result;
|
|
229
243
|
}
|
|
244
|
+
function drainMaintainedFrom() {
|
|
245
|
+
const result = pendingMaintainedFrom;
|
|
246
|
+
pendingMaintainedFrom = [];
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
230
249
|
|
|
231
250
|
// src/decorators/model.ts
|
|
232
251
|
function model(options) {
|
|
@@ -235,7 +254,19 @@ function model(options) {
|
|
|
235
254
|
const embeddedFields = drainEmbedded();
|
|
236
255
|
const relations = drainRelations();
|
|
237
256
|
const aggregates = drainAggregates();
|
|
257
|
+
const maintainedFrom2 = drainMaintainedFrom();
|
|
238
258
|
const prefix = derivePrefix(options.prefix, context.name);
|
|
259
|
+
const kind = options.kind ?? "entity";
|
|
260
|
+
if (kind === "entity" && maintainedFrom2.length > 0) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
`@model on '${String(context.name)}': ${maintainedFrom2.length} \`@maintainedFrom\` declaration(s) are present but \`kind\` is 'entity' (the default). A maintained view must declare \`@model({ kind: 'materializedView' | 'sparseView' })\` \u2014 a plain entity does not carry view maintainers.`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (kind !== "entity" && maintainedFrom2.length === 0) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`@model on '${String(context.name)}': \`kind: '${kind}'\` declares a maintained view but no \`@maintainedFrom(...)\` source is stacked. A view with no source is maintained by nothing \u2014 add at least one \`@maintainedFrom\` decorator.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
239
270
|
MetadataRegistry.register(target, {
|
|
240
271
|
tableName: options.table,
|
|
241
272
|
prefix,
|
|
@@ -244,7 +275,9 @@ function model(options) {
|
|
|
244
275
|
gsiDefinitions: [],
|
|
245
276
|
relations,
|
|
246
277
|
aggregates,
|
|
247
|
-
embeddedFields
|
|
278
|
+
embeddedFields,
|
|
279
|
+
kind,
|
|
280
|
+
maintainedFrom: maintainedFrom2
|
|
248
281
|
});
|
|
249
282
|
};
|
|
250
283
|
}
|
|
@@ -314,7 +347,95 @@ function embedded(modelFactory) {
|
|
|
314
347
|
};
|
|
315
348
|
}
|
|
316
349
|
|
|
350
|
+
// src/decorators/maintained-symbolic.ts
|
|
351
|
+
var SOURCE_REF = /* @__PURE__ */ Symbol("graphddb:maintainedSourceRef");
|
|
352
|
+
var SELF_REF = /* @__PURE__ */ Symbol("graphddb:maintainedSelfRef");
|
|
353
|
+
function isSourceRef(value) {
|
|
354
|
+
return typeof value === "object" && value !== null && value[SOURCE_REF] === true;
|
|
355
|
+
}
|
|
356
|
+
function isSelfRef(value) {
|
|
357
|
+
return typeof value === "object" && value !== null && value[SELF_REF] === true;
|
|
358
|
+
}
|
|
359
|
+
function createSourceProxy() {
|
|
360
|
+
const cache = /* @__PURE__ */ new Map();
|
|
361
|
+
return new Proxy(
|
|
362
|
+
{},
|
|
363
|
+
{
|
|
364
|
+
get(_t, prop) {
|
|
365
|
+
if (typeof prop !== "string") return void 0;
|
|
366
|
+
let ref = cache.get(prop);
|
|
367
|
+
if (!ref) {
|
|
368
|
+
ref = { [SOURCE_REF]: true, field: prop, path: `$.entity.${prop}` };
|
|
369
|
+
cache.set(prop, ref);
|
|
370
|
+
}
|
|
371
|
+
return ref;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
function createSelfProxy() {
|
|
377
|
+
const cache = /* @__PURE__ */ new Map();
|
|
378
|
+
return new Proxy(
|
|
379
|
+
{},
|
|
380
|
+
{
|
|
381
|
+
get(_t, prop) {
|
|
382
|
+
if (typeof prop !== "string") return void 0;
|
|
383
|
+
let ref = cache.get(prop);
|
|
384
|
+
if (!ref) {
|
|
385
|
+
ref = { [SELF_REF]: true, field: prop };
|
|
386
|
+
cache.set(prop, ref);
|
|
387
|
+
}
|
|
388
|
+
return ref;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
317
394
|
// src/decorators/relation.ts
|
|
395
|
+
function lowerVersionedProjectionValue(propertyName, attr, value) {
|
|
396
|
+
if (isSourceRef(value)) return identity(value.path);
|
|
397
|
+
if (value && typeof value === "object" && value.kind === "transform") {
|
|
398
|
+
return value;
|
|
399
|
+
}
|
|
400
|
+
throw new Error(
|
|
401
|
+
`@hasOne/@hasMany versioned relation '${propertyName}': projection target '${attr}' must be a \`source.<field>\` reference or a transform (e.g. \`preview(source.body, 120)\`), got ${JSON.stringify(value)}.`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
function lowerVersionedOptions(propertyName, opts) {
|
|
405
|
+
if (!opts || typeof opts !== "object" || opts.pattern !== "versionedLatest" && opts.pattern !== "versionedHistory") {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`@hasOne/@hasMany versioned relation '${propertyName}': the (self, source) => options callback must return \`{ pattern: 'versionedLatest' | 'versionedHistory', on, project }\`.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (!Array.isArray(opts.on) || opts.on.length === 0) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
`@hasOne/@hasMany versioned relation '${propertyName}': \`on\` must be a non-empty array of lifecycle events (e.g. \`['created', 'updated']\`).`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
const projectEntries = Object.entries(opts.project ?? {});
|
|
416
|
+
if (projectEntries.length === 0) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
`@hasOne/@hasMany versioned relation '${propertyName}': \`project\` is empty \u2014 a versioned row with no captured attributes projects nothing.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const project = {};
|
|
422
|
+
for (const [attr, value] of projectEntries) {
|
|
423
|
+
project[attr] = lowerVersionedProjectionValue(propertyName, attr, value);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
pattern: opts.pattern,
|
|
427
|
+
versioned: {
|
|
428
|
+
mode: opts.pattern === "versionedLatest" ? "latest" : "history",
|
|
429
|
+
on: [...opts.on],
|
|
430
|
+
project,
|
|
431
|
+
...opts.consistency !== void 0 ? { consistency: opts.consistency } : {},
|
|
432
|
+
...opts.updateMode !== void 0 ? { updateMode: opts.updateMode } : {}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function isVersionedCallback(arg) {
|
|
437
|
+
return typeof arg === "function";
|
|
438
|
+
}
|
|
318
439
|
var RESERVED_RELATION_PROPERTY_NAMES = /* @__PURE__ */ new Set([
|
|
319
440
|
"items"
|
|
320
441
|
]);
|
|
@@ -325,15 +446,34 @@ function assertRelationPropertyNameAllowed(propertyName) {
|
|
|
325
446
|
);
|
|
326
447
|
}
|
|
327
448
|
}
|
|
328
|
-
function hasMany(targetFactory,
|
|
449
|
+
function hasMany(targetFactory, keyBindingOrCallback, options) {
|
|
329
450
|
return function(_value, context) {
|
|
330
451
|
const propertyName = String(context.name);
|
|
331
452
|
assertRelationPropertyNameAllowed(propertyName);
|
|
453
|
+
if (isVersionedCallback(keyBindingOrCallback)) {
|
|
454
|
+
const opts = keyBindingOrCallback(
|
|
455
|
+
createSelfProxy(),
|
|
456
|
+
createSourceProxy()
|
|
457
|
+
);
|
|
458
|
+
if (opts.pattern !== "versionedHistory") {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`@hasMany versioned relation '${propertyName}': a \`@hasMany\` maintains an append-only HISTORY, so its pattern must be 'versionedHistory' (got '${opts.pattern}'). Use \`@hasOne\` for 'versionedLatest' (the single overwritten pointer).`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
collectRelation({
|
|
464
|
+
type: "hasMany",
|
|
465
|
+
propertyName,
|
|
466
|
+
targetFactory,
|
|
467
|
+
keyBinding: {},
|
|
468
|
+
options: lowerVersionedOptions(propertyName, opts)
|
|
469
|
+
});
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
332
472
|
collectRelation({
|
|
333
473
|
type: "hasMany",
|
|
334
474
|
propertyName,
|
|
335
475
|
targetFactory,
|
|
336
|
-
keyBinding,
|
|
476
|
+
keyBinding: keyBindingOrCallback,
|
|
337
477
|
options
|
|
338
478
|
});
|
|
339
479
|
};
|
|
@@ -351,15 +491,34 @@ function belongsTo(targetFactory, keyBinding, options) {
|
|
|
351
491
|
});
|
|
352
492
|
};
|
|
353
493
|
}
|
|
354
|
-
function hasOne(targetFactory,
|
|
494
|
+
function hasOne(targetFactory, keyBindingOrCallback) {
|
|
355
495
|
return function(_value, context) {
|
|
356
496
|
const propertyName = String(context.name);
|
|
357
497
|
assertRelationPropertyNameAllowed(propertyName);
|
|
498
|
+
if (isVersionedCallback(keyBindingOrCallback)) {
|
|
499
|
+
const opts = keyBindingOrCallback(
|
|
500
|
+
createSelfProxy(),
|
|
501
|
+
createSourceProxy()
|
|
502
|
+
);
|
|
503
|
+
if (opts.pattern !== "versionedLatest") {
|
|
504
|
+
throw new Error(
|
|
505
|
+
`@hasOne versioned relation '${propertyName}': a \`@hasOne\` maintains a single LATEST pointer (overwritten per revision), so its pattern must be 'versionedLatest' (got '${opts.pattern}'). Use \`@hasMany\` for 'versionedHistory' (append-only per-version rows).`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
collectRelation({
|
|
509
|
+
type: "hasOne",
|
|
510
|
+
propertyName,
|
|
511
|
+
targetFactory,
|
|
512
|
+
keyBinding: {},
|
|
513
|
+
options: lowerVersionedOptions(propertyName, opts)
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
358
517
|
collectRelation({
|
|
359
518
|
type: "hasOne",
|
|
360
519
|
propertyName,
|
|
361
520
|
targetFactory,
|
|
362
|
-
keyBinding
|
|
521
|
+
keyBinding: keyBindingOrCallback
|
|
363
522
|
});
|
|
364
523
|
};
|
|
365
524
|
}
|
|
@@ -387,6 +546,118 @@ function max(field2) {
|
|
|
387
546
|
return { op: "max", field: field2 };
|
|
388
547
|
}
|
|
389
548
|
|
|
549
|
+
// src/decorators/maintained.ts
|
|
550
|
+
function whenMember(operand, op, value) {
|
|
551
|
+
const raw = isSourceRef(operand) ? operand.path : operand;
|
|
552
|
+
const path = raw.startsWith("$.input.") || raw.startsWith("$.entity.") ? raw : `$.entity.${raw}`;
|
|
553
|
+
const needsValue = op === "eq" || op === "ne";
|
|
554
|
+
if (needsValue && value === void 0) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`whenMember(source.\u2026, '${op}', \u2026): the '${op}' op requires a comparand value (e.g. \`whenMember(source.status, '${op}', 'active')\`).`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
return needsValue ? { path, op, value } : { path, op };
|
|
560
|
+
}
|
|
561
|
+
function lowerProjectionValue(modelName, attr, value) {
|
|
562
|
+
if (isSourceRef(value)) return identity(value.path);
|
|
563
|
+
if (value && typeof value === "object" && value.kind === "transform") {
|
|
564
|
+
return value;
|
|
565
|
+
}
|
|
566
|
+
throw new Error(
|
|
567
|
+
`@maintainedFrom on '${modelName}': projection target '${attr}' must be a \`source.<field>\` reference or a transform (e.g. \`preview(source.body, 120)\`), got ${JSON.stringify(value)}. A maintained projection captures only symbolic source references \u2014 a literal / computed value is not allowed.`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
function lowerKeyBind(modelName, keyBind) {
|
|
571
|
+
const out = {};
|
|
572
|
+
const entries = Object.entries(keyBind);
|
|
573
|
+
if (entries.length === 0) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`@maintainedFrom on '${modelName}': \`keyBind\` is empty. The view-row key binding (view-key field \u2192 \`source.<field>\`) is required to resolve which view row a source event maintains.`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
for (const [viewKeyField, ref] of entries) {
|
|
579
|
+
if (!isSourceRef(ref)) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
`@maintainedFrom on '${modelName}': \`keyBind.${viewKeyField}\` must be a \`source.<field>\` reference (e.g. \`{ ${viewKeyField}: source.${viewKeyField} }\`), got ${JSON.stringify(ref)}.`
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
out[viewKeyField] = ref.path;
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
function maintainedFrom(source, callback) {
|
|
589
|
+
return function(target, context) {
|
|
590
|
+
const modelName = String(context.name ?? target.name ?? "(anonymous)");
|
|
591
|
+
const selfProxy = createSelfProxy();
|
|
592
|
+
const sourceProxy = createSourceProxy();
|
|
593
|
+
const opts = callback(selfProxy, sourceProxy);
|
|
594
|
+
if (!opts || typeof opts !== "object") {
|
|
595
|
+
throw new Error(
|
|
596
|
+
`@maintainedFrom on '${modelName}': the (self, source) => options callback must return an options object.`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
if (!Array.isArray(opts.on) || opts.on.length === 0) {
|
|
600
|
+
throw new Error(
|
|
601
|
+
`@maintainedFrom on '${modelName}': \`on\` must be a non-empty array of lifecycle events (e.g. \`['created', 'removed']\`).`
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
for (const ev of opts.on) {
|
|
605
|
+
if (ev !== "created" && ev !== "updated" && ev !== "removed") {
|
|
606
|
+
throw new Error(
|
|
607
|
+
`@maintainedFrom on '${modelName}': \`on\` event ${JSON.stringify(ev)} is not one of 'created' / 'updated' / 'removed'.`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const keyBind = lowerKeyBind(modelName, opts.keyBind);
|
|
612
|
+
const projectEntries = Object.entries(opts.project ?? {});
|
|
613
|
+
if (projectEntries.length === 0) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`@maintainedFrom on '${modelName}': \`project\` is empty. A maintained view with no captured attributes projects nothing \u2014 declare a \`project\` map (e.g. \`{ title: source.title, textPreview: preview(source.body, 120) }\`).`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const project = {};
|
|
619
|
+
for (const [attr, value] of projectEntries) {
|
|
620
|
+
project[attr] = lowerProjectionValue(modelName, attr, value);
|
|
621
|
+
}
|
|
622
|
+
if (opts.collection && opts.when) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`@maintainedFrom on '${modelName}': a declaration carries both \`collection\` and \`when\`. They are mutually exclusive \u2014 a collection slice holds a maintained list; a membership (\`when\`) row appears/disappears as a whole.`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
let collection;
|
|
628
|
+
if (opts.collection) {
|
|
629
|
+
const c = opts.collection;
|
|
630
|
+
if (!isSelfRef(c.field)) {
|
|
631
|
+
throw new Error(
|
|
632
|
+
`@maintainedFrom on '${modelName}': \`collection.field\` must be a \`self.<field>\` reference (the view attribute holding the list, e.g. \`self.latestPosts\`), got ${JSON.stringify(c.field)}.`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
if (c.orderBy !== void 0 && !isSourceRef(c.orderBy)) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
`@maintainedFrom on '${modelName}': \`collection.orderBy\` must be a \`source.<field>\` reference, got ${JSON.stringify(c.orderBy)}.`
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
collection = {
|
|
641
|
+
field: c.field.field,
|
|
642
|
+
...c.maxItems !== void 0 ? { maxItems: c.maxItems } : {},
|
|
643
|
+
...c.order !== void 0 ? { order: c.order } : {},
|
|
644
|
+
...c.orderBy !== void 0 ? { orderBy: c.orderBy.path } : {}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
const metadata = {
|
|
648
|
+
sourceFactory: source,
|
|
649
|
+
on: [...opts.on],
|
|
650
|
+
keyBind,
|
|
651
|
+
project,
|
|
652
|
+
...collection ? { collection } : {},
|
|
653
|
+
...opts.when ? { when: opts.when } : {},
|
|
654
|
+
...opts.consistency !== void 0 ? { consistency: opts.consistency } : {},
|
|
655
|
+
...opts.updateMode !== void 0 ? { updateMode: opts.updateMode } : {}
|
|
656
|
+
};
|
|
657
|
+
collectMaintainedFrom(metadata);
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
390
661
|
// src/operations/declarative-transaction.ts
|
|
391
662
|
var PLACEHOLDER_RE = /\{[^{}]+\}/g;
|
|
392
663
|
function resolveTemplate(template, params, element) {
|
|
@@ -955,6 +1226,363 @@ var queryBoundaryRule = {
|
|
|
955
1226
|
}
|
|
956
1227
|
};
|
|
957
1228
|
|
|
1229
|
+
// src/relation/maintenance-rebuild.ts
|
|
1230
|
+
function toClass(m) {
|
|
1231
|
+
try {
|
|
1232
|
+
return resolveModelClass(m);
|
|
1233
|
+
} catch {
|
|
1234
|
+
return m;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
var MaintenanceRebuilder = class {
|
|
1238
|
+
graph;
|
|
1239
|
+
constructor(opts = {}) {
|
|
1240
|
+
const classes = (opts.models ?? []).map(toClass);
|
|
1241
|
+
const explicitViews = opts.views;
|
|
1242
|
+
const allClasses = [
|
|
1243
|
+
...classes,
|
|
1244
|
+
...(explicitViews ?? []).map((v) => v.viewClass)
|
|
1245
|
+
];
|
|
1246
|
+
const scoped = allClasses.length > 0 ? new Map(allClasses.map((c) => [c, MetadataRegistry.get(c)])) : void 0;
|
|
1247
|
+
this.graph = buildMaintenanceGraph(scoped, explicitViews);
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Re-derive the materialized value for one owner row × maintainer from the source
|
|
1251
|
+
* rows and report whether it differs from what the owner row currently stores. Does
|
|
1252
|
+
* NOT write anything (a read-only drift probe).
|
|
1253
|
+
*
|
|
1254
|
+
* @param owner The owner model class declaring the maintainer.
|
|
1255
|
+
* @param relationProperty The relation / `@aggregate` property the value lives under.
|
|
1256
|
+
* @param ownerKeyInput The owner row's key input (e.g. `{ threadId: 't1' }`).
|
|
1257
|
+
*/
|
|
1258
|
+
async detectDrift(owner, relationProperty, ownerKeyInput) {
|
|
1259
|
+
const { item, ownerMeta, ownerKey } = this.resolve(owner, relationProperty);
|
|
1260
|
+
const expected = await this.recompute(item, ownerMeta, ownerKeyInput);
|
|
1261
|
+
const ownerRow = await this.readOwnerRow(ownerMeta, ownerKey(ownerKeyInput));
|
|
1262
|
+
const actual = this.actualValue(item.effect, ownerRow);
|
|
1263
|
+
const ownerAbsentAndEmpty = ownerRow === void 0 && isEmptyMaintained(item.effect, expected);
|
|
1264
|
+
return {
|
|
1265
|
+
relationProperty,
|
|
1266
|
+
ownerEntity: item.ownerEntity,
|
|
1267
|
+
ownerKey: ownerKey(ownerKeyInput),
|
|
1268
|
+
kind: item.effect.kind,
|
|
1269
|
+
drifted: !ownerAbsentAndEmpty && !deepEqual(normalize(expected), normalize(actual)),
|
|
1270
|
+
expected,
|
|
1271
|
+
actual
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Re-derive the materialized value for one owner row × maintainer from the source
|
|
1276
|
+
* rows and WRITE the absolute re-derived value onto the owner row when it drifts (or
|
|
1277
|
+
* always, when `force`). Returns the drift verdict plus whether a repair write ran.
|
|
1278
|
+
*
|
|
1279
|
+
* @param options.force Write even when no drift is detected (default: write only on drift).
|
|
1280
|
+
*/
|
|
1281
|
+
async rebuild(owner, relationProperty, ownerKeyInput, options = {}) {
|
|
1282
|
+
const report = await this.detectDrift(owner, relationProperty, ownerKeyInput);
|
|
1283
|
+
if (!report.drifted && options.force !== true) {
|
|
1284
|
+
return { ...report, repaired: false };
|
|
1285
|
+
}
|
|
1286
|
+
const { item, ownerMeta } = this.resolve(owner, relationProperty);
|
|
1287
|
+
await this.writeValue(item.effect, ownerMeta, report.ownerKey, report.expected);
|
|
1288
|
+
return { ...report, repaired: true };
|
|
1289
|
+
}
|
|
1290
|
+
/** Resolve an owner model + property to its {@link MaintainItem} + owner-key builder. */
|
|
1291
|
+
resolve(owner, relationProperty) {
|
|
1292
|
+
const ownerClass = toClass(owner);
|
|
1293
|
+
const ownerMeta = MetadataRegistry.get(ownerClass);
|
|
1294
|
+
const item = this.graph.items.find(
|
|
1295
|
+
(i) => i.relationProperty === relationProperty && (i.ownerClass === ownerClass || i.ownerClass.name === ownerClass.name)
|
|
1296
|
+
);
|
|
1297
|
+
if (item === void 0) {
|
|
1298
|
+
throw new Error(
|
|
1299
|
+
`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.`
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
const ownerKey = (input) => {
|
|
1303
|
+
const { Key } = buildDeleteInput(
|
|
1304
|
+
ownerClass,
|
|
1305
|
+
input
|
|
1306
|
+
);
|
|
1307
|
+
return Key;
|
|
1308
|
+
};
|
|
1309
|
+
return { item, ownerMeta, ownerKey };
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* Scan every source row bound to this owner row and re-derive the maintained value:
|
|
1313
|
+
*
|
|
1314
|
+
* - **collection** — project each source row, order + trim by the SAME read-side
|
|
1315
|
+
* rules (`orderBy` / `orderDir` / `maxItems`) the drain uses, return the list;
|
|
1316
|
+
* - **snapshot** — project the single bound source row (or `undefined` when none);
|
|
1317
|
+
* - **counter `count`** — the COUNT of source rows (absolute, not an ADD);
|
|
1318
|
+
* - **counter `max`** — the maximum source value of the tracked field.
|
|
1319
|
+
*/
|
|
1320
|
+
async recompute(item, _ownerMeta, ownerKeyInput) {
|
|
1321
|
+
const effect = item.effect;
|
|
1322
|
+
if (effect.kind === "membership") {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`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.`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
const sources = await this.scanSourceRows(item, ownerKeyInput);
|
|
1328
|
+
if (effect.kind === "collection") {
|
|
1329
|
+
return this.recomputeCollection(effect, sources);
|
|
1330
|
+
}
|
|
1331
|
+
if (effect.kind === "snapshot") {
|
|
1332
|
+
return this.recomputeSnapshot(effect, sources);
|
|
1333
|
+
}
|
|
1334
|
+
return this.recomputeCounter(effect, sources);
|
|
1335
|
+
}
|
|
1336
|
+
/**
|
|
1337
|
+
* The source rows bound to one owner row. The maintenance key binding is
|
|
1338
|
+
* `{ ownerField: '$.entity.<sourceField>' }` (the destination owner-key field ← the
|
|
1339
|
+
* source row's field). INVERTED for a rebuild: the owner row supplies the value, and
|
|
1340
|
+
* the source partition is queried for rows whose `<sourceField>` matches — exactly the
|
|
1341
|
+
* set the live `hasMany` / `@aggregate` read aggregates. Queries the source via the
|
|
1342
|
+
* same {@link plan} the read planner uses (a partition Query), paginating fully.
|
|
1343
|
+
*/
|
|
1344
|
+
async scanSourceRows(item, ownerKeyInput) {
|
|
1345
|
+
const sourceMeta = MetadataRegistry.get(item.sourceClass);
|
|
1346
|
+
const queryKey = {};
|
|
1347
|
+
for (const [ownerField, srcPath] of Object.entries(item.effect.keys)) {
|
|
1348
|
+
const sourceField = pathField(srcPath);
|
|
1349
|
+
queryKey[sourceField] = ownerKeyInput[ownerField];
|
|
1350
|
+
}
|
|
1351
|
+
const executor = ClientManager.getExecutor();
|
|
1352
|
+
const rows = [];
|
|
1353
|
+
let after;
|
|
1354
|
+
do {
|
|
1355
|
+
const execPlan = plan(sourceMeta, {
|
|
1356
|
+
key: queryKey,
|
|
1357
|
+
// Select all declared source fields so every projection / value source path
|
|
1358
|
+
// and the sort-key leaf is present on the scanned row.
|
|
1359
|
+
select: selectAll(sourceMeta),
|
|
1360
|
+
consistentRead: false,
|
|
1361
|
+
...after ? { after } : {}
|
|
1362
|
+
});
|
|
1363
|
+
const op = execPlan.operations[0];
|
|
1364
|
+
const res = await executor.execute(op);
|
|
1365
|
+
for (const it of res.items) rows.push(it);
|
|
1366
|
+
after = res.lastEvaluatedKey;
|
|
1367
|
+
} while (after !== void 0);
|
|
1368
|
+
return rows;
|
|
1369
|
+
}
|
|
1370
|
+
/** Re-derive a bounded collection: project, then order + trim by the read-side rules. */
|
|
1371
|
+
recomputeCollection(effect, sources) {
|
|
1372
|
+
if (effect.collection.orderBy === void 0 && effect.collection.maxItems !== void 0) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
`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.`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
const projected = sources.map((row) => projectFrom(effect.project, row));
|
|
1378
|
+
return orderAndTrimCollection(projected, effect.collection);
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Re-derive a single-row snapshot. A snapshot mirrors ONE source row (a `belongsTo` /
|
|
1382
|
+
* `hasOne`, normally 1:1). In the unusual case where MORE THAN ONE source row binds, the
|
|
1383
|
+
* live single-value read takes the FIRST row in the source's natural query order
|
|
1384
|
+
* (`ScanIndexForward`, default ASC = the smallest sort-key leaf), so re-derive the same
|
|
1385
|
+
* row — {@link pickFirst} — for a verdict consistent with the live read. Returns
|
|
1386
|
+
* `undefined` when no source row is bound (the snapshot should be absent).
|
|
1387
|
+
*/
|
|
1388
|
+
recomputeSnapshot(effect, sources) {
|
|
1389
|
+
if (sources.length === 0) return void 0;
|
|
1390
|
+
const chosen = sources.length === 1 ? sources[0] : pickFirst(sources, this.sourceMetaOf(effect));
|
|
1391
|
+
return projectFrom(effect.project, chosen);
|
|
1392
|
+
}
|
|
1393
|
+
/** The source entity metadata a maintain effect projects from (its relation target). */
|
|
1394
|
+
sourceMetaOf(effect) {
|
|
1395
|
+
const item = this.graph.items.find((i) => i.effect === effect);
|
|
1396
|
+
return item ? MetadataRegistry.get(item.sourceClass) : void 0;
|
|
1397
|
+
}
|
|
1398
|
+
/** Re-derive a counter: the absolute COUNT of source rows, or the MAX of the tracked field. */
|
|
1399
|
+
recomputeCounter(effect, sources) {
|
|
1400
|
+
if (effect.value.op === "count") {
|
|
1401
|
+
return sources.length;
|
|
1402
|
+
}
|
|
1403
|
+
const field2 = effect.value.field;
|
|
1404
|
+
let best;
|
|
1405
|
+
for (const row of sources) {
|
|
1406
|
+
const v = row[field2];
|
|
1407
|
+
if (v === void 0 || v === null) continue;
|
|
1408
|
+
if (best === void 0 || compareDesc(v, best) < 0) best = v;
|
|
1409
|
+
}
|
|
1410
|
+
return best;
|
|
1411
|
+
}
|
|
1412
|
+
/** Read the owner row (consistent) for the drift comparison / actual value. */
|
|
1413
|
+
async readOwnerRow(ownerMeta, ownerKey) {
|
|
1414
|
+
const res = await ClientManager.getExecutor().execute({
|
|
1415
|
+
type: "GetItem",
|
|
1416
|
+
tableName: TableMapping.resolve(ownerMeta.tableName),
|
|
1417
|
+
keyCondition: ownerKey,
|
|
1418
|
+
consistentRead: true
|
|
1419
|
+
});
|
|
1420
|
+
return res.items[0];
|
|
1421
|
+
}
|
|
1422
|
+
/** The value currently stored on the owner row for this maintainer. */
|
|
1423
|
+
actualValue(effect, ownerRow) {
|
|
1424
|
+
if (ownerRow === void 0) return void 0;
|
|
1425
|
+
if (effect.kind === "collection") {
|
|
1426
|
+
return ownerRow[effect.collection.field];
|
|
1427
|
+
}
|
|
1428
|
+
if (effect.kind === "counter") {
|
|
1429
|
+
return ownerRow[effect.attribute];
|
|
1430
|
+
}
|
|
1431
|
+
const out = {};
|
|
1432
|
+
let anyPresent = false;
|
|
1433
|
+
for (const attr of Object.keys(effect.project)) {
|
|
1434
|
+
if (Object.prototype.hasOwnProperty.call(ownerRow, attr)) {
|
|
1435
|
+
out[attr] = ownerRow[attr];
|
|
1436
|
+
anyPresent = true;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return anyPresent ? out : void 0;
|
|
1440
|
+
}
|
|
1441
|
+
/** Write the re-derived ABSOLUTE value onto the owner row (the repair). */
|
|
1442
|
+
async writeValue(effect, ownerMeta, ownerKey, expected) {
|
|
1443
|
+
const TableName = TableMapping.resolve(ownerMeta.tableName);
|
|
1444
|
+
const executor = ClientManager.getExecutor();
|
|
1445
|
+
if (effect.kind === "collection") {
|
|
1446
|
+
await executor.update({
|
|
1447
|
+
TableName,
|
|
1448
|
+
Key: ownerKey,
|
|
1449
|
+
UpdateExpression: "SET #c = :list",
|
|
1450
|
+
ExpressionAttributeNames: { "#c": effect.collection.field },
|
|
1451
|
+
ExpressionAttributeValues: { ":list": expected ?? [] }
|
|
1452
|
+
});
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
if (effect.kind === "counter") {
|
|
1456
|
+
if (expected === void 0 || expected === null) {
|
|
1457
|
+
if (effect.value.op === "count") {
|
|
1458
|
+
await executor.update({
|
|
1459
|
+
TableName,
|
|
1460
|
+
Key: ownerKey,
|
|
1461
|
+
UpdateExpression: "SET #a = :v",
|
|
1462
|
+
ExpressionAttributeNames: { "#a": effect.attribute },
|
|
1463
|
+
ExpressionAttributeValues: { ":v": 0 }
|
|
1464
|
+
});
|
|
1465
|
+
} else {
|
|
1466
|
+
await executor.update({
|
|
1467
|
+
TableName,
|
|
1468
|
+
Key: ownerKey,
|
|
1469
|
+
UpdateExpression: "REMOVE #a",
|
|
1470
|
+
ExpressionAttributeNames: { "#a": effect.attribute }
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
await executor.update({
|
|
1476
|
+
TableName,
|
|
1477
|
+
Key: ownerKey,
|
|
1478
|
+
UpdateExpression: "SET #a = :v",
|
|
1479
|
+
ExpressionAttributeNames: { "#a": effect.attribute },
|
|
1480
|
+
ExpressionAttributeValues: { ":v": expected }
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (expected === void 0) {
|
|
1485
|
+
const names2 = {};
|
|
1486
|
+
const removes = [];
|
|
1487
|
+
let i2 = 0;
|
|
1488
|
+
for (const attr of Object.keys(effect.project)) {
|
|
1489
|
+
names2[`#m${i2}`] = attr;
|
|
1490
|
+
removes.push(`#m${i2}`);
|
|
1491
|
+
i2 += 1;
|
|
1492
|
+
}
|
|
1493
|
+
if (removes.length === 0) return;
|
|
1494
|
+
await executor.update({
|
|
1495
|
+
TableName,
|
|
1496
|
+
Key: ownerKey,
|
|
1497
|
+
UpdateExpression: `REMOVE ${removes.join(", ")}`,
|
|
1498
|
+
ExpressionAttributeNames: names2
|
|
1499
|
+
});
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
const projection = expected;
|
|
1503
|
+
const names = {};
|
|
1504
|
+
const values = {};
|
|
1505
|
+
const sets = [];
|
|
1506
|
+
let i = 0;
|
|
1507
|
+
for (const [attr, value] of Object.entries(projection)) {
|
|
1508
|
+
names[`#m${i}`] = attr;
|
|
1509
|
+
values[`:m${i}`] = value;
|
|
1510
|
+
sets.push(`#m${i} = :m${i}`);
|
|
1511
|
+
i += 1;
|
|
1512
|
+
}
|
|
1513
|
+
if (sets.length === 0) return;
|
|
1514
|
+
await executor.update({
|
|
1515
|
+
TableName,
|
|
1516
|
+
Key: ownerKey,
|
|
1517
|
+
UpdateExpression: `SET ${sets.join(", ")}`,
|
|
1518
|
+
ExpressionAttributeNames: names,
|
|
1519
|
+
ExpressionAttributeValues: values
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
};
|
|
1523
|
+
function selectAll(meta) {
|
|
1524
|
+
const select = {};
|
|
1525
|
+
for (const f of meta.fields) select[f.propertyName] = true;
|
|
1526
|
+
return select;
|
|
1527
|
+
}
|
|
1528
|
+
function pickFirst(sources, sourceMeta) {
|
|
1529
|
+
const skFields = sourceMeta?.primaryKey ? segmentFieldNames(sourceMeta.primaryKey.segmented.skSegments) : [];
|
|
1530
|
+
const orderField = skFields[skFields.length - 1];
|
|
1531
|
+
if (orderField === void 0) return sources[0];
|
|
1532
|
+
return [...sources].sort((a, b) => compareDesc(b[orderField], a[orderField]))[0];
|
|
1533
|
+
}
|
|
1534
|
+
function normalize(value) {
|
|
1535
|
+
if (value === void 0 || value === null) return void 0;
|
|
1536
|
+
if (typeof value === "number") return value;
|
|
1537
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
1538
|
+
const n = Number(value);
|
|
1539
|
+
if (Number.isSafeInteger(n)) return n;
|
|
1540
|
+
}
|
|
1541
|
+
if (Array.isArray(value)) return value.map(normalize);
|
|
1542
|
+
if (typeof value === "object") {
|
|
1543
|
+
const out = {};
|
|
1544
|
+
for (const [k2, v] of Object.entries(value)) {
|
|
1545
|
+
const nv = normalize(v);
|
|
1546
|
+
if (nv === void 0) continue;
|
|
1547
|
+
out[k2] = nv;
|
|
1548
|
+
}
|
|
1549
|
+
return out;
|
|
1550
|
+
}
|
|
1551
|
+
return value;
|
|
1552
|
+
}
|
|
1553
|
+
function isEmptyMaintained(effect, expected) {
|
|
1554
|
+
if (effect.kind === "collection") {
|
|
1555
|
+
return Array.isArray(expected) && expected.length === 0;
|
|
1556
|
+
}
|
|
1557
|
+
if (effect.kind === "counter") {
|
|
1558
|
+
return effect.value.op === "count" ? expected === 0 : expected === void 0 || expected === null;
|
|
1559
|
+
}
|
|
1560
|
+
return expected === void 0;
|
|
1561
|
+
}
|
|
1562
|
+
function deepEqual(a, b) {
|
|
1563
|
+
if (a === b) return true;
|
|
1564
|
+
if (a === void 0 || b === void 0 || a === null || b === null) return a === b;
|
|
1565
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1566
|
+
if (a.length !== b.length) return false;
|
|
1567
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
1568
|
+
}
|
|
1569
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
1570
|
+
const ka = Object.keys(a);
|
|
1571
|
+
const kb = Object.keys(b);
|
|
1572
|
+
if (ka.length !== kb.length) return false;
|
|
1573
|
+
return ka.every(
|
|
1574
|
+
(k2) => deepEqual(
|
|
1575
|
+
a[k2],
|
|
1576
|
+
b[k2]
|
|
1577
|
+
)
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1580
|
+
return false;
|
|
1581
|
+
}
|
|
1582
|
+
function createMaintenanceRebuilder(opts = {}) {
|
|
1583
|
+
return new MaintenanceRebuilder(opts);
|
|
1584
|
+
}
|
|
1585
|
+
|
|
958
1586
|
// src/define/define.ts
|
|
959
1587
|
function collectParams(structure, out = {}) {
|
|
960
1588
|
if (structure === null || typeof structure !== "object") return out;
|
|
@@ -1172,7 +1800,10 @@ export {
|
|
|
1172
1800
|
ENTITY_WRITES_MARKER,
|
|
1173
1801
|
LIFECYCLE_CONTRACT_MARKER,
|
|
1174
1802
|
Linter,
|
|
1803
|
+
MAINT_OUTBOX_PK_PREFIX,
|
|
1175
1804
|
MAX_TRANSACT_ITEMS,
|
|
1805
|
+
MaintenanceDrain,
|
|
1806
|
+
MaintenanceRebuilder,
|
|
1176
1807
|
MetadataRegistry,
|
|
1177
1808
|
OLD_VALUE_NAMESPACE,
|
|
1178
1809
|
RetryingExecutor,
|
|
@@ -1206,6 +1837,7 @@ export {
|
|
|
1206
1837
|
buildUpdateInput,
|
|
1207
1838
|
collectContractBoundaryViolations,
|
|
1208
1839
|
collectContractN1Violations,
|
|
1840
|
+
collectViewDefinitions,
|
|
1209
1841
|
compileFilterExpression,
|
|
1210
1842
|
compileFragment,
|
|
1211
1843
|
compileMutationPlan,
|
|
@@ -1215,6 +1847,9 @@ export {
|
|
|
1215
1847
|
count,
|
|
1216
1848
|
createCdcEmulator,
|
|
1217
1849
|
createDefaultLinter,
|
|
1850
|
+
createMaintenanceDrain,
|
|
1851
|
+
createMaintenanceDrainHandler,
|
|
1852
|
+
createMaintenanceRebuilder,
|
|
1218
1853
|
datetime,
|
|
1219
1854
|
decodeCursor,
|
|
1220
1855
|
decodePerKeyCursor,
|
|
@@ -1294,6 +1929,7 @@ export {
|
|
|
1294
1929
|
list,
|
|
1295
1930
|
literal,
|
|
1296
1931
|
maintainTrigger,
|
|
1932
|
+
maintainedFrom,
|
|
1297
1933
|
map,
|
|
1298
1934
|
max,
|
|
1299
1935
|
mintContractKeyFieldRef,
|
|
@@ -1323,5 +1959,6 @@ export {
|
|
|
1323
1959
|
validateDepth,
|
|
1324
1960
|
validateGsiAmbiguity,
|
|
1325
1961
|
when,
|
|
1962
|
+
whenMember,
|
|
1326
1963
|
wholeKeysSentinel
|
|
1327
1964
|
};
|