graphddb 0.7.9 → 0.8.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 +6 -6
- package/dist/cdc/index.d.ts +389 -4
- package/dist/cdc/index.js +4 -3
- package/dist/{chunk-ZPNRLOKA.js → chunk-GS4C5VGO.js} +4 -6
- package/dist/chunk-HNY2EJPV.js +1184 -0
- package/dist/{chunk-NYM7K2ST.js → chunk-I4LEJ4TF.js} +3812 -6724
- package/dist/{chunk-PFFPLD4B.js → chunk-L2NEDS7U.js} +725 -2112
- package/dist/chunk-L4QRCHRQ.js +278 -0
- package/dist/chunk-LGHSZIEE.js +187 -0
- package/dist/chunk-N4NWYNGZ.js +1987 -0
- package/dist/{chunk-PDUVTYC5.js → chunk-XTWXMOHD.js} +0 -1
- package/dist/cli.js +63 -252
- package/dist/index.d.ts +23 -1548
- package/dist/index.js +100 -1778
- package/dist/internal/index.d.ts +84 -0
- package/dist/internal/index.js +701 -0
- package/dist/{maintenance-view-adapter-BATUh_I8.d.ts → key-DR7_lpyk.d.ts} +538 -2975
- package/dist/linter/index.d.ts +39 -6
- package/dist/linter/index.js +22 -4
- package/dist/{registry-CXhP4TaE.d.ts → linter-C-vypgut.d.ts} +22 -22
- package/dist/prepared-artifact-BpPgkXEo.d.ts +281 -0
- package/dist/spec/index.d.ts +506 -4
- package/dist/spec/index.js +36 -17
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +4 -3
- package/dist/transform/index.d.ts +460 -1
- package/dist/transform/index.js +2085 -2
- package/dist/types-2PMXEn5x.d.ts +1205 -0
- package/dist/types-BXLzIcQD.d.ts +450 -0
- package/docs/cdc-projection.md +5 -5
- package/docs/class-hydration.md +1 -1
- package/docs/cqrs-contract.md +28 -20
- package/docs/design-patterns.md +5 -5
- package/docs/docs-generation.md +6 -6
- package/docs/middleware.md +15 -15
- package/docs/mutation-command-derivation.md +52 -42
- package/docs/prepared-statements.md +14 -14
- package/docs/python-bridge.md +113 -66
- package/docs/spec.md +153 -124
- package/docs/testing.md +9 -8
- package/package.json +20 -5
- package/dist/chunk-MMVHOUM4.js +0 -24
- package/dist/from-change-DanwjE5b.d.ts +0 -327
- package/dist/index-CtPJSMrc.d.ts +0 -934
- package/dist/relation-depth-Dg3yhl7S.d.ts +0 -36
|
@@ -4,10 +4,8 @@ import {
|
|
|
4
4
|
createColumnMap,
|
|
5
5
|
createDefaultLinter,
|
|
6
6
|
isColumn,
|
|
7
|
-
isKeyDefinition
|
|
8
|
-
|
|
9
|
-
segmentFieldNames
|
|
10
|
-
} from "./chunk-PDUVTYC5.js";
|
|
7
|
+
isKeyDefinition
|
|
8
|
+
} from "./chunk-XTWXMOHD.js";
|
|
11
9
|
|
|
12
10
|
// src/decorators/gsi.ts
|
|
13
11
|
var GSI_MARKER = /* @__PURE__ */ Symbol("graphddb:gsi");
|
|
@@ -172,146 +170,6 @@ ${messages}`);
|
|
|
172
170
|
}
|
|
173
171
|
};
|
|
174
172
|
|
|
175
|
-
// src/types/select-builder.ts
|
|
176
|
-
var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
|
|
177
|
-
var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
|
|
178
|
-
|
|
179
|
-
// src/select/builder.ts
|
|
180
|
-
var BuilderImpl = class _BuilderImpl {
|
|
181
|
-
[SELECT_BUILDER] = true;
|
|
182
|
-
/** The resolved spec, exposed (non-method) so normalizers can read it. */
|
|
183
|
-
[SELECT_SPEC];
|
|
184
|
-
constructor(spec) {
|
|
185
|
-
this[SELECT_SPEC] = spec;
|
|
186
|
-
}
|
|
187
|
-
clone(patch) {
|
|
188
|
-
return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
|
|
189
|
-
}
|
|
190
|
-
filter(filter) {
|
|
191
|
-
return this.clone({ filter });
|
|
192
|
-
}
|
|
193
|
-
limit(limit) {
|
|
194
|
-
return this.clone({ limit });
|
|
195
|
-
}
|
|
196
|
-
after(cursor) {
|
|
197
|
-
return this.clone({ after: cursor });
|
|
198
|
-
}
|
|
199
|
-
order(order) {
|
|
200
|
-
return this.clone({ order });
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
function isSelectBuilder(value) {
|
|
204
|
-
return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
|
|
205
|
-
}
|
|
206
|
-
function fromBuilder(value) {
|
|
207
|
-
const spec = value[SELECT_SPEC];
|
|
208
|
-
return {
|
|
209
|
-
select: spec.select,
|
|
210
|
-
filter: spec.filter,
|
|
211
|
-
limit: spec.limit,
|
|
212
|
-
after: spec.after,
|
|
213
|
-
order: spec.order
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
function normalizeSelectSpec(value) {
|
|
217
|
-
if (isSelectBuilder(value)) {
|
|
218
|
-
return fromBuilder(value);
|
|
219
|
-
}
|
|
220
|
-
const obj = value;
|
|
221
|
-
return {
|
|
222
|
-
select: obj.select,
|
|
223
|
-
filter: obj.filter,
|
|
224
|
-
limit: obj.limit,
|
|
225
|
-
after: obj.after,
|
|
226
|
-
order: obj.order
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
function normalizeTopLevelSelect(value) {
|
|
230
|
-
if (isSelectBuilder(value)) {
|
|
231
|
-
return fromBuilder(value);
|
|
232
|
-
}
|
|
233
|
-
return { select: value ?? {} };
|
|
234
|
-
}
|
|
235
|
-
function buildProject(select) {
|
|
236
|
-
return new BuilderImpl({
|
|
237
|
-
[SELECT_BUILDER]: true,
|
|
238
|
-
select
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
function buildRelation(select) {
|
|
242
|
-
return new BuilderImpl({
|
|
243
|
-
[SELECT_BUILDER]: true,
|
|
244
|
-
select
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// src/relation/detect.ts
|
|
249
|
-
function isRelationSpec(value) {
|
|
250
|
-
if (typeof value !== "object" || value === null) return false;
|
|
251
|
-
return "select" in value || isSelectBuilder(value);
|
|
252
|
-
}
|
|
253
|
-
function isInlineSnapshotSpec(value) {
|
|
254
|
-
return typeof value === "object" && value !== null && !isSelectBuilder(value) && value.inline === true && !("select" in value);
|
|
255
|
-
}
|
|
256
|
-
function detectRelationFields(select, metadata) {
|
|
257
|
-
const result = [];
|
|
258
|
-
for (const [fieldName, value] of Object.entries(select)) {
|
|
259
|
-
if (isRelationSpec(value)) {
|
|
260
|
-
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
261
|
-
if (rel) {
|
|
262
|
-
result.push(rel);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
return result;
|
|
267
|
-
}
|
|
268
|
-
function validateInlineSnapshotSelect(select, metadata, resolveTargetMeta) {
|
|
269
|
-
for (const [fieldName, value] of Object.entries(select)) {
|
|
270
|
-
if (isInlineSnapshotSpec(value)) {
|
|
271
|
-
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
272
|
-
if (!rel) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
`Inline read '{ ${fieldName}: { inline: true } }' is only valid on an embeddedSnapshot relation, but '${fieldName}' is not a relation on '${metadata.prefix}'. Use '${fieldName}: true' for a scalar / embedded field.`
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
if (rel.options?.pattern !== "embeddedSnapshot") {
|
|
278
|
-
throw new Error(
|
|
279
|
-
`Inline read '{ ${fieldName}: { inline: true } }' is only valid on an embeddedSnapshot relation. '${fieldName}' is a plain '${rel.type}' relation (no 'pattern: embeddedSnapshot'); use '{ ${fieldName}: { select: \u2026 } }' to traverse to its child rows instead.`
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (typeof value === "object" && value !== null && isRelationSpec(value)) {
|
|
285
|
-
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
286
|
-
if (!rel) continue;
|
|
287
|
-
const targetMeta = resolveTargetMeta(rel);
|
|
288
|
-
const nested = normalizeSelectSpec(value).select;
|
|
289
|
-
if (nested) {
|
|
290
|
-
validateInlineSnapshotSelect(nested, targetMeta, resolveTargetMeta);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
function getImplicitKeyFields(select, metadata) {
|
|
296
|
-
const relations = detectRelationFields(select, metadata);
|
|
297
|
-
const needed = /* @__PURE__ */ new Set();
|
|
298
|
-
for (const rel of relations) {
|
|
299
|
-
if (rel.type === "refs" && rel.refs) {
|
|
300
|
-
const listField = rel.refs.from;
|
|
301
|
-
if (!(listField in select) || select[listField] !== true) {
|
|
302
|
-
needed.add(listField);
|
|
303
|
-
}
|
|
304
|
-
continue;
|
|
305
|
-
}
|
|
306
|
-
for (const sourceField of Object.values(rel.keyBinding)) {
|
|
307
|
-
if (!(sourceField in select) || select[sourceField] !== true) {
|
|
308
|
-
needed.add(sourceField);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
return [...needed];
|
|
313
|
-
}
|
|
314
|
-
|
|
315
173
|
// src/define/param.ts
|
|
316
174
|
function isParamOptions(value) {
|
|
317
175
|
return typeof value === "object" && value !== null;
|
|
@@ -456,6 +314,194 @@ async function batchWriteChunkWithRetry(docClient, tableName, entries, toCommand
|
|
|
456
314
|
}
|
|
457
315
|
}
|
|
458
316
|
|
|
317
|
+
// src/executor/batch-executor.ts
|
|
318
|
+
async function executeBatchGet(docClient, operation) {
|
|
319
|
+
if (operation.keys.length === 0) {
|
|
320
|
+
return { items: [] };
|
|
321
|
+
}
|
|
322
|
+
const chunks = chunkArray(operation.keys, BATCH_GET_MAX_KEYS);
|
|
323
|
+
const chunkResults = await Promise.all(
|
|
324
|
+
chunks.map(
|
|
325
|
+
(chunk) => batchGetChunkWithRetry(docClient, operation.tableName, {
|
|
326
|
+
Keys: chunk,
|
|
327
|
+
...operation.projectionExpression ? { ProjectionExpression: operation.projectionExpression } : {},
|
|
328
|
+
...operation.expressionAttributeNames ? { ExpressionAttributeNames: operation.expressionAttributeNames } : {},
|
|
329
|
+
...operation.consistentRead ? { ConsistentRead: true } : {}
|
|
330
|
+
})
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
const allItems = [];
|
|
334
|
+
for (const chunkItems of chunkResults) {
|
|
335
|
+
allItems.push(...chunkItems);
|
|
336
|
+
}
|
|
337
|
+
return { items: allItems };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/executor/dynamo-executor.ts
|
|
341
|
+
var DynamoExecutor = class {
|
|
342
|
+
async execute(operation, _options) {
|
|
343
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
344
|
+
if (operation.type === "GetItem") {
|
|
345
|
+
return executeGetItem(docClient, operation);
|
|
346
|
+
}
|
|
347
|
+
if (operation.type === "BatchGetItem") {
|
|
348
|
+
return this.batchGet(operation);
|
|
349
|
+
}
|
|
350
|
+
return executeQuery(docClient, operation);
|
|
351
|
+
}
|
|
352
|
+
async batchGet(input, _options) {
|
|
353
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
354
|
+
return executeBatchGet(docClient, { type: "BatchGetItem", ...input });
|
|
355
|
+
}
|
|
356
|
+
async put(input, options) {
|
|
357
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
358
|
+
const { PutCommand } = await loadLibDynamoDB();
|
|
359
|
+
const result = await docClient.send(
|
|
360
|
+
new PutCommand(
|
|
361
|
+
options?.returnOldImage ? { ...input, ReturnValues: "ALL_OLD" } : input
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
return toWriteResult(result);
|
|
365
|
+
}
|
|
366
|
+
async update(input, options) {
|
|
367
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
368
|
+
const { UpdateCommand } = await loadLibDynamoDB();
|
|
369
|
+
const result = await docClient.send(
|
|
370
|
+
new UpdateCommand(
|
|
371
|
+
options?.returnOldImage ? { ...input, ReturnValues: "ALL_OLD" } : input
|
|
372
|
+
)
|
|
373
|
+
);
|
|
374
|
+
return toWriteResult(result);
|
|
375
|
+
}
|
|
376
|
+
async delete(input, options) {
|
|
377
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
378
|
+
const { DeleteCommand } = await loadLibDynamoDB();
|
|
379
|
+
const result = await docClient.send(
|
|
380
|
+
new DeleteCommand(
|
|
381
|
+
options?.returnOldImage ? { ...input, ReturnValues: "ALL_OLD" } : input
|
|
382
|
+
)
|
|
383
|
+
);
|
|
384
|
+
return toWriteResult(result);
|
|
385
|
+
}
|
|
386
|
+
async batchWrite(tableName, items, _options) {
|
|
387
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
388
|
+
for (const chunk of chunkArray(items, BATCH_WRITE_MAX_ITEMS)) {
|
|
389
|
+
await batchWriteChunkWithRetry(
|
|
390
|
+
docClient,
|
|
391
|
+
tableName,
|
|
392
|
+
chunk,
|
|
393
|
+
toBatchWriteCommandItem,
|
|
394
|
+
matchUnprocessed
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
async transactWrite(items, _options) {
|
|
399
|
+
if (items.length === 0) return;
|
|
400
|
+
const docClient = await ClientManager.getDocumentClient();
|
|
401
|
+
const { TransactWriteCommand } = await loadLibDynamoDB();
|
|
402
|
+
await docClient.send(
|
|
403
|
+
new TransactWriteCommand({ TransactItems: items })
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
function toWriteResult(result) {
|
|
408
|
+
const oldItem = result.Attributes;
|
|
409
|
+
return oldItem ? { oldItem } : {};
|
|
410
|
+
}
|
|
411
|
+
function toBatchWriteCommandItem(item) {
|
|
412
|
+
if (item.type === "put") {
|
|
413
|
+
return { PutRequest: { Item: item.item } };
|
|
414
|
+
}
|
|
415
|
+
return { DeleteRequest: { Key: item.key } };
|
|
416
|
+
}
|
|
417
|
+
function matchUnprocessed(unprocessed, items) {
|
|
418
|
+
return unprocessed.map((u) => {
|
|
419
|
+
if (u.PutRequest?.Item) {
|
|
420
|
+
const { PK, SK } = u.PutRequest.Item;
|
|
421
|
+
const match = items.find(
|
|
422
|
+
(it) => it.type === "put" && it.item.PK === PK && it.item.SK === SK
|
|
423
|
+
);
|
|
424
|
+
if (!match) {
|
|
425
|
+
throw new Error("Unable to match unprocessed BatchWrite put request");
|
|
426
|
+
}
|
|
427
|
+
return match;
|
|
428
|
+
}
|
|
429
|
+
if (u.DeleteRequest?.Key) {
|
|
430
|
+
const { PK, SK } = u.DeleteRequest.Key;
|
|
431
|
+
const match = items.find(
|
|
432
|
+
(it) => it.type === "delete" && it.key.PK === PK && it.key.SK === SK
|
|
433
|
+
);
|
|
434
|
+
if (!match) {
|
|
435
|
+
throw new Error("Unable to match unprocessed BatchWrite delete request");
|
|
436
|
+
}
|
|
437
|
+
return match;
|
|
438
|
+
}
|
|
439
|
+
throw new Error("Unsupported unprocessed BatchWrite request shape");
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
async function executeGetItem(docClient, operation) {
|
|
443
|
+
const { GetCommand } = await loadLibDynamoDB();
|
|
444
|
+
const cmd = new GetCommand({
|
|
445
|
+
TableName: operation.tableName,
|
|
446
|
+
Key: operation.keyCondition,
|
|
447
|
+
...operation.projectionExpression ? { ProjectionExpression: operation.projectionExpression } : {},
|
|
448
|
+
...operation.expressionAttributeNames ? { ExpressionAttributeNames: operation.expressionAttributeNames } : {},
|
|
449
|
+
...operation.consistentRead ? { ConsistentRead: true } : {}
|
|
450
|
+
});
|
|
451
|
+
const result = await docClient.send(cmd);
|
|
452
|
+
if (result.Item) {
|
|
453
|
+
return { items: [result.Item] };
|
|
454
|
+
}
|
|
455
|
+
return { items: [] };
|
|
456
|
+
}
|
|
457
|
+
async function executeQuery(docClient, operation) {
|
|
458
|
+
const { QueryCommand } = await loadLibDynamoDB();
|
|
459
|
+
const keyEntries = Object.entries(operation.keyCondition);
|
|
460
|
+
const conditionParts = [];
|
|
461
|
+
const exprAttrNames = {
|
|
462
|
+
...operation.expressionAttributeNames ?? {},
|
|
463
|
+
...operation.filterExpressionAttributeNames ?? {}
|
|
464
|
+
};
|
|
465
|
+
const exprAttrValues = {
|
|
466
|
+
...operation.filterExpressionAttributeValues ?? {}
|
|
467
|
+
};
|
|
468
|
+
for (let i = 0; i < keyEntries.length; i++) {
|
|
469
|
+
const [attrName, value] = keyEntries[i];
|
|
470
|
+
const nameAlias2 = `#k${i}`;
|
|
471
|
+
const valueAlias2 = `:k${i}`;
|
|
472
|
+
exprAttrNames[nameAlias2] = attrName;
|
|
473
|
+
exprAttrValues[valueAlias2] = value;
|
|
474
|
+
conditionParts.push(`${nameAlias2} = ${valueAlias2}`);
|
|
475
|
+
}
|
|
476
|
+
if (operation.rangeCondition) {
|
|
477
|
+
const rc = operation.rangeCondition;
|
|
478
|
+
const idx = keyEntries.length;
|
|
479
|
+
const nameAlias2 = `#k${idx}`;
|
|
480
|
+
const valueAlias2 = `:k${idx}`;
|
|
481
|
+
exprAttrNames[nameAlias2] = rc.key;
|
|
482
|
+
exprAttrValues[valueAlias2] = rc.value;
|
|
483
|
+
conditionParts.push(`begins_with(${nameAlias2}, ${valueAlias2})`);
|
|
484
|
+
}
|
|
485
|
+
const cmd = new QueryCommand({
|
|
486
|
+
TableName: operation.tableName,
|
|
487
|
+
KeyConditionExpression: conditionParts.join(" AND "),
|
|
488
|
+
ExpressionAttributeNames: exprAttrNames,
|
|
489
|
+
ExpressionAttributeValues: exprAttrValues,
|
|
490
|
+
...operation.indexName ? { IndexName: operation.indexName } : {},
|
|
491
|
+
...operation.filterExpression ? { FilterExpression: operation.filterExpression } : {},
|
|
492
|
+
...operation.projectionExpression ? { ProjectionExpression: operation.projectionExpression } : {},
|
|
493
|
+
...operation.scanIndexForward != null ? { ScanIndexForward: operation.scanIndexForward } : {},
|
|
494
|
+
...operation.limit != null ? { Limit: operation.limit } : {},
|
|
495
|
+
...operation.exclusiveStartKey ? { ExclusiveStartKey: operation.exclusiveStartKey } : {},
|
|
496
|
+
...operation.consistentRead ? { ConsistentRead: true } : {}
|
|
497
|
+
});
|
|
498
|
+
const result = await docClient.send(cmd);
|
|
499
|
+
return {
|
|
500
|
+
items: result.Items ?? [],
|
|
501
|
+
lastEvaluatedKey: result.LastEvaluatedKey
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
459
505
|
// src/executor/retry-policy.ts
|
|
460
506
|
var DEFAULT_MAX_ATTEMPTS = 10;
|
|
461
507
|
var DEFAULT_RETRY_POLICY = {
|
|
@@ -595,7 +641,7 @@ var RetryingExecutor = class {
|
|
|
595
641
|
/**
|
|
596
642
|
* @param inner the wrapped executor (the real {@link DynamoExecutor}).
|
|
597
643
|
* @param getGlobalPolicy reads the globally-configured policy (or `null` for the
|
|
598
|
-
* built-in default) at CALL time, so a `
|
|
644
|
+
* built-in default) at CALL time, so a `graphddb.config.retry(...)` after the
|
|
599
645
|
* executor is constructed still takes effect.
|
|
600
646
|
*/
|
|
601
647
|
constructor(inner, getGlobalPolicy) {
|
|
@@ -675,7 +721,7 @@ var ClientManager = class {
|
|
|
675
721
|
/**
|
|
676
722
|
* The globally-configured single-op retry policy (issue #111). `null` ⇒ the
|
|
677
723
|
* always-on built-in {@link DEFAULT_RETRY_POLICY} applies (retry is enabled out
|
|
678
|
-
* of the box). Set via
|
|
724
|
+
* of the box). Set via `graphddb.config.retry` / {@link setRetryPolicy};
|
|
679
725
|
* read at call time by the default {@link RetryingExecutor}.
|
|
680
726
|
*/
|
|
681
727
|
static retryPolicy = null;
|
|
@@ -683,7 +729,7 @@ var ClientManager = class {
|
|
|
683
729
|
* The host-runtime read-middleware registry (issue #50 / #138) — a process-wide
|
|
684
730
|
* ordered list of {@link Middleware}, the same global-config + injectable-seam
|
|
685
731
|
* pattern as {@link retryPolicy} / {@link executor}. Registered via
|
|
686
|
-
*
|
|
732
|
+
* `graphddb.config.use` / {@link use}, snapshotted per read, and NEVER serialized
|
|
687
733
|
* (it never touches the planner / spec / `operations.json`, so the bridge #48 is
|
|
688
734
|
* unaffected). {@link reset} clears it.
|
|
689
735
|
*/
|
|
@@ -695,7 +741,7 @@ var ClientManager = class {
|
|
|
695
741
|
static getClient() {
|
|
696
742
|
if (!this.client) {
|
|
697
743
|
throw new Error(
|
|
698
|
-
"DynamoDB client is not configured. Call
|
|
744
|
+
"DynamoDB client is not configured. Call graphddb.config.client(new DynamoDBClient({...})) first."
|
|
699
745
|
);
|
|
700
746
|
}
|
|
701
747
|
return this.client;
|
|
@@ -757,12 +803,12 @@ var ClientManager = class {
|
|
|
757
803
|
/**
|
|
758
804
|
* Register a read {@link Middleware} (issue #50 / #138). Appended last, so its
|
|
759
805
|
* `before*` hooks run last (FIFO) and its `afterFetch` / `onError` hooks run
|
|
760
|
-
* first (LIFO). Mirrors {@link setRetryPolicy}; backs
|
|
806
|
+
* first (LIFO). Mirrors {@link setRetryPolicy}; backs `graphddb.config.use`.
|
|
761
807
|
*/
|
|
762
808
|
static use(mw) {
|
|
763
809
|
this.middleware.use(mw);
|
|
764
810
|
}
|
|
765
|
-
/** Remove every registered read middleware (teardown / tests). Backs
|
|
811
|
+
/** Remove every registered read middleware (teardown / tests). Backs `graphddb.config.clearMiddleware`. */
|
|
766
812
|
static clearMiddleware() {
|
|
767
813
|
this.middleware.clear();
|
|
768
814
|
}
|
|
@@ -785,194 +831,6 @@ var ClientManager = class {
|
|
|
785
831
|
}
|
|
786
832
|
};
|
|
787
833
|
|
|
788
|
-
// src/executor/batch-executor.ts
|
|
789
|
-
async function executeBatchGet(docClient, operation) {
|
|
790
|
-
if (operation.keys.length === 0) {
|
|
791
|
-
return { items: [] };
|
|
792
|
-
}
|
|
793
|
-
const chunks = chunkArray(operation.keys, BATCH_GET_MAX_KEYS);
|
|
794
|
-
const chunkResults = await Promise.all(
|
|
795
|
-
chunks.map(
|
|
796
|
-
(chunk) => batchGetChunkWithRetry(docClient, operation.tableName, {
|
|
797
|
-
Keys: chunk,
|
|
798
|
-
...operation.projectionExpression ? { ProjectionExpression: operation.projectionExpression } : {},
|
|
799
|
-
...operation.expressionAttributeNames ? { ExpressionAttributeNames: operation.expressionAttributeNames } : {},
|
|
800
|
-
...operation.consistentRead ? { ConsistentRead: true } : {}
|
|
801
|
-
})
|
|
802
|
-
)
|
|
803
|
-
);
|
|
804
|
-
const allItems = [];
|
|
805
|
-
for (const chunkItems of chunkResults) {
|
|
806
|
-
allItems.push(...chunkItems);
|
|
807
|
-
}
|
|
808
|
-
return { items: allItems };
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
// src/executor/dynamo-executor.ts
|
|
812
|
-
var DynamoExecutor = class {
|
|
813
|
-
async execute(operation, _options) {
|
|
814
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
815
|
-
if (operation.type === "GetItem") {
|
|
816
|
-
return executeGetItem(docClient, operation);
|
|
817
|
-
}
|
|
818
|
-
if (operation.type === "BatchGetItem") {
|
|
819
|
-
return this.batchGet(operation);
|
|
820
|
-
}
|
|
821
|
-
return executeQuery(docClient, operation);
|
|
822
|
-
}
|
|
823
|
-
async batchGet(input, _options) {
|
|
824
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
825
|
-
return executeBatchGet(docClient, { type: "BatchGetItem", ...input });
|
|
826
|
-
}
|
|
827
|
-
async put(input, options) {
|
|
828
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
829
|
-
const { PutCommand } = await loadLibDynamoDB();
|
|
830
|
-
const result = await docClient.send(
|
|
831
|
-
new PutCommand(
|
|
832
|
-
options?.returnOldImage ? { ...input, ReturnValues: "ALL_OLD" } : input
|
|
833
|
-
)
|
|
834
|
-
);
|
|
835
|
-
return toWriteResult(result);
|
|
836
|
-
}
|
|
837
|
-
async update(input, options) {
|
|
838
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
839
|
-
const { UpdateCommand } = await loadLibDynamoDB();
|
|
840
|
-
const result = await docClient.send(
|
|
841
|
-
new UpdateCommand(
|
|
842
|
-
options?.returnOldImage ? { ...input, ReturnValues: "ALL_OLD" } : input
|
|
843
|
-
)
|
|
844
|
-
);
|
|
845
|
-
return toWriteResult(result);
|
|
846
|
-
}
|
|
847
|
-
async delete(input, options) {
|
|
848
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
849
|
-
const { DeleteCommand } = await loadLibDynamoDB();
|
|
850
|
-
const result = await docClient.send(
|
|
851
|
-
new DeleteCommand(
|
|
852
|
-
options?.returnOldImage ? { ...input, ReturnValues: "ALL_OLD" } : input
|
|
853
|
-
)
|
|
854
|
-
);
|
|
855
|
-
return toWriteResult(result);
|
|
856
|
-
}
|
|
857
|
-
async batchWrite(tableName, items, _options) {
|
|
858
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
859
|
-
for (const chunk of chunkArray(items, BATCH_WRITE_MAX_ITEMS)) {
|
|
860
|
-
await batchWriteChunkWithRetry(
|
|
861
|
-
docClient,
|
|
862
|
-
tableName,
|
|
863
|
-
chunk,
|
|
864
|
-
toBatchWriteCommandItem,
|
|
865
|
-
matchUnprocessed
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
async transactWrite(items, _options) {
|
|
870
|
-
if (items.length === 0) return;
|
|
871
|
-
const docClient = await ClientManager.getDocumentClient();
|
|
872
|
-
const { TransactWriteCommand } = await loadLibDynamoDB();
|
|
873
|
-
await docClient.send(
|
|
874
|
-
new TransactWriteCommand({ TransactItems: items })
|
|
875
|
-
);
|
|
876
|
-
}
|
|
877
|
-
};
|
|
878
|
-
function toWriteResult(result) {
|
|
879
|
-
const oldItem = result.Attributes;
|
|
880
|
-
return oldItem ? { oldItem } : {};
|
|
881
|
-
}
|
|
882
|
-
function toBatchWriteCommandItem(item) {
|
|
883
|
-
if (item.type === "put") {
|
|
884
|
-
return { PutRequest: { Item: item.item } };
|
|
885
|
-
}
|
|
886
|
-
return { DeleteRequest: { Key: item.key } };
|
|
887
|
-
}
|
|
888
|
-
function matchUnprocessed(unprocessed, items) {
|
|
889
|
-
return unprocessed.map((u) => {
|
|
890
|
-
if (u.PutRequest?.Item) {
|
|
891
|
-
const { PK, SK } = u.PutRequest.Item;
|
|
892
|
-
const match = items.find(
|
|
893
|
-
(it) => it.type === "put" && it.item.PK === PK && it.item.SK === SK
|
|
894
|
-
);
|
|
895
|
-
if (!match) {
|
|
896
|
-
throw new Error("Unable to match unprocessed BatchWrite put request");
|
|
897
|
-
}
|
|
898
|
-
return match;
|
|
899
|
-
}
|
|
900
|
-
if (u.DeleteRequest?.Key) {
|
|
901
|
-
const { PK, SK } = u.DeleteRequest.Key;
|
|
902
|
-
const match = items.find(
|
|
903
|
-
(it) => it.type === "delete" && it.key.PK === PK && it.key.SK === SK
|
|
904
|
-
);
|
|
905
|
-
if (!match) {
|
|
906
|
-
throw new Error("Unable to match unprocessed BatchWrite delete request");
|
|
907
|
-
}
|
|
908
|
-
return match;
|
|
909
|
-
}
|
|
910
|
-
throw new Error("Unsupported unprocessed BatchWrite request shape");
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
async function executeGetItem(docClient, operation) {
|
|
914
|
-
const { GetCommand } = await loadLibDynamoDB();
|
|
915
|
-
const cmd = new GetCommand({
|
|
916
|
-
TableName: operation.tableName,
|
|
917
|
-
Key: operation.keyCondition,
|
|
918
|
-
...operation.projectionExpression ? { ProjectionExpression: operation.projectionExpression } : {},
|
|
919
|
-
...operation.expressionAttributeNames ? { ExpressionAttributeNames: operation.expressionAttributeNames } : {},
|
|
920
|
-
...operation.consistentRead ? { ConsistentRead: true } : {}
|
|
921
|
-
});
|
|
922
|
-
const result = await docClient.send(cmd);
|
|
923
|
-
if (result.Item) {
|
|
924
|
-
return { items: [result.Item] };
|
|
925
|
-
}
|
|
926
|
-
return { items: [] };
|
|
927
|
-
}
|
|
928
|
-
async function executeQuery(docClient, operation) {
|
|
929
|
-
const { QueryCommand } = await loadLibDynamoDB();
|
|
930
|
-
const keyEntries = Object.entries(operation.keyCondition);
|
|
931
|
-
const conditionParts = [];
|
|
932
|
-
const exprAttrNames = {
|
|
933
|
-
...operation.expressionAttributeNames ?? {},
|
|
934
|
-
...operation.filterExpressionAttributeNames ?? {}
|
|
935
|
-
};
|
|
936
|
-
const exprAttrValues = {
|
|
937
|
-
...operation.filterExpressionAttributeValues ?? {}
|
|
938
|
-
};
|
|
939
|
-
for (let i = 0; i < keyEntries.length; i++) {
|
|
940
|
-
const [attrName, value] = keyEntries[i];
|
|
941
|
-
const nameAlias2 = `#k${i}`;
|
|
942
|
-
const valueAlias2 = `:k${i}`;
|
|
943
|
-
exprAttrNames[nameAlias2] = attrName;
|
|
944
|
-
exprAttrValues[valueAlias2] = value;
|
|
945
|
-
conditionParts.push(`${nameAlias2} = ${valueAlias2}`);
|
|
946
|
-
}
|
|
947
|
-
if (operation.rangeCondition) {
|
|
948
|
-
const rc = operation.rangeCondition;
|
|
949
|
-
const idx = keyEntries.length;
|
|
950
|
-
const nameAlias2 = `#k${idx}`;
|
|
951
|
-
const valueAlias2 = `:k${idx}`;
|
|
952
|
-
exprAttrNames[nameAlias2] = rc.key;
|
|
953
|
-
exprAttrValues[valueAlias2] = rc.value;
|
|
954
|
-
conditionParts.push(`begins_with(${nameAlias2}, ${valueAlias2})`);
|
|
955
|
-
}
|
|
956
|
-
const cmd = new QueryCommand({
|
|
957
|
-
TableName: operation.tableName,
|
|
958
|
-
KeyConditionExpression: conditionParts.join(" AND "),
|
|
959
|
-
ExpressionAttributeNames: exprAttrNames,
|
|
960
|
-
ExpressionAttributeValues: exprAttrValues,
|
|
961
|
-
...operation.indexName ? { IndexName: operation.indexName } : {},
|
|
962
|
-
...operation.filterExpression ? { FilterExpression: operation.filterExpression } : {},
|
|
963
|
-
...operation.projectionExpression ? { ProjectionExpression: operation.projectionExpression } : {},
|
|
964
|
-
...operation.scanIndexForward != null ? { ScanIndexForward: operation.scanIndexForward } : {},
|
|
965
|
-
...operation.limit != null ? { Limit: operation.limit } : {},
|
|
966
|
-
...operation.exclusiveStartKey ? { ExclusiveStartKey: operation.exclusiveStartKey } : {},
|
|
967
|
-
...operation.consistentRead ? { ConsistentRead: true } : {}
|
|
968
|
-
});
|
|
969
|
-
const result = await docClient.send(cmd);
|
|
970
|
-
return {
|
|
971
|
-
items: result.Items ?? [],
|
|
972
|
-
lastEvaluatedKey: result.LastEvaluatedKey
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
|
-
|
|
976
834
|
// src/select/cond.ts
|
|
977
835
|
var RAW_COND = /* @__PURE__ */ Symbol.for("graphddb.rawCond");
|
|
978
836
|
function isRawCondition(value) {
|
|
@@ -1038,17 +896,6 @@ function serializeRawCondition(raw, renderSlot) {
|
|
|
1038
896
|
}
|
|
1039
897
|
return { expression: out, names, values };
|
|
1040
898
|
}
|
|
1041
|
-
function collectRawConditionParams(raw) {
|
|
1042
|
-
const out = [];
|
|
1043
|
-
let paramIndex = 0;
|
|
1044
|
-
for (const part of raw.parts) {
|
|
1045
|
-
if (isColumn(part)) continue;
|
|
1046
|
-
if (isParam(part)) {
|
|
1047
|
-
out.push({ name: condParamName(paramIndex++), param: part });
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
return out;
|
|
1051
|
-
}
|
|
1052
899
|
|
|
1053
900
|
// src/expression/condition-expression.ts
|
|
1054
901
|
var CONDITION_OPERATOR_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1298,69 +1145,6 @@ function serializeFieldValue(value, fieldMeta) {
|
|
|
1298
1145
|
return value;
|
|
1299
1146
|
}
|
|
1300
1147
|
|
|
1301
|
-
// src/expression/update-expression.ts
|
|
1302
|
-
function isPlainObject(value) {
|
|
1303
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
1304
|
-
}
|
|
1305
|
-
function flattenChanges(obj, parentPath, embeddedFieldNames) {
|
|
1306
|
-
const sets = [];
|
|
1307
|
-
const removes = [];
|
|
1308
|
-
for (const [fieldName, value] of Object.entries(obj)) {
|
|
1309
|
-
const currentPath = [...parentPath, fieldName];
|
|
1310
|
-
if (value === void 0) {
|
|
1311
|
-
removes.push({ path: currentPath });
|
|
1312
|
-
} else if (embeddedFieldNames.has(currentPath[0]) && isPlainObject(value)) {
|
|
1313
|
-
const nested = flattenChanges(value, currentPath, embeddedFieldNames);
|
|
1314
|
-
sets.push(...nested.sets);
|
|
1315
|
-
removes.push(...nested.removes);
|
|
1316
|
-
} else {
|
|
1317
|
-
sets.push({ path: currentPath, value });
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
return { sets, removes };
|
|
1321
|
-
}
|
|
1322
|
-
function buildUpdateExpression(changes, embeddedFieldNames) {
|
|
1323
|
-
const { sets, removes } = flattenChanges(changes, [], embeddedFieldNames);
|
|
1324
|
-
if (sets.length === 0 && removes.length === 0) {
|
|
1325
|
-
throw new Error("No changes provided for update.");
|
|
1326
|
-
}
|
|
1327
|
-
const names = {};
|
|
1328
|
-
const values = {};
|
|
1329
|
-
let valueCounter = 0;
|
|
1330
|
-
const setClauses = [];
|
|
1331
|
-
for (const op of sets) {
|
|
1332
|
-
const pathExpr = op.path.map((segment) => {
|
|
1333
|
-
const nameKey = `#${segment}`;
|
|
1334
|
-
names[nameKey] = segment;
|
|
1335
|
-
return nameKey;
|
|
1336
|
-
}).join(".");
|
|
1337
|
-
const valueKey = `:val${valueCounter++}`;
|
|
1338
|
-
values[valueKey] = op.value;
|
|
1339
|
-
setClauses.push(`${pathExpr} = ${valueKey}`);
|
|
1340
|
-
}
|
|
1341
|
-
const removeClauses = [];
|
|
1342
|
-
for (const op of removes) {
|
|
1343
|
-
const pathExpr = op.path.map((segment) => {
|
|
1344
|
-
const nameKey = `#${segment}`;
|
|
1345
|
-
names[nameKey] = segment;
|
|
1346
|
-
return nameKey;
|
|
1347
|
-
}).join(".");
|
|
1348
|
-
removeClauses.push(pathExpr);
|
|
1349
|
-
}
|
|
1350
|
-
const parts = [];
|
|
1351
|
-
if (setClauses.length > 0) {
|
|
1352
|
-
parts.push(`SET ${setClauses.join(", ")}`);
|
|
1353
|
-
}
|
|
1354
|
-
if (removeClauses.length > 0) {
|
|
1355
|
-
parts.push(`REMOVE ${removeClauses.join(", ")}`);
|
|
1356
|
-
}
|
|
1357
|
-
return {
|
|
1358
|
-
expression: parts.join(" "),
|
|
1359
|
-
names,
|
|
1360
|
-
values
|
|
1361
|
-
};
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
1148
|
// src/metadata/segments.ts
|
|
1365
1149
|
var SEGMENT_DELIMITER = "#";
|
|
1366
1150
|
function segmentComplete(segment, values) {
|
|
@@ -1500,34 +1284,6 @@ function missingGsiFields(gsi2, available) {
|
|
|
1500
1284
|
return gsi2.inputFieldNames.filter((f) => available[f] === void 0);
|
|
1501
1285
|
}
|
|
1502
1286
|
|
|
1503
|
-
// src/hydrator/hidden-key.ts
|
|
1504
|
-
var GRAPHDDB_KEY = /* @__PURE__ */ Symbol("graphddb:key");
|
|
1505
|
-
function attachHiddenKey(result, raw) {
|
|
1506
|
-
const pk = raw.PK;
|
|
1507
|
-
if (typeof pk !== "string") {
|
|
1508
|
-
throw new Error(
|
|
1509
|
-
"Cannot attach updatable key: raw item is missing a string `PK` attribute."
|
|
1510
|
-
);
|
|
1511
|
-
}
|
|
1512
|
-
const sk = raw.SK;
|
|
1513
|
-
const value = {
|
|
1514
|
-
PK: pk,
|
|
1515
|
-
SK: typeof sk === "string" ? sk : ""
|
|
1516
|
-
};
|
|
1517
|
-
Object.defineProperty(result, GRAPHDDB_KEY, {
|
|
1518
|
-
value,
|
|
1519
|
-
enumerable: false,
|
|
1520
|
-
writable: false,
|
|
1521
|
-
configurable: false
|
|
1522
|
-
});
|
|
1523
|
-
return result;
|
|
1524
|
-
}
|
|
1525
|
-
function readHiddenKey(entity) {
|
|
1526
|
-
const value = entity[GRAPHDDB_KEY];
|
|
1527
|
-
if (value === void 0) return void 0;
|
|
1528
|
-
return value;
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
1287
|
// src/capture/registry.ts
|
|
1532
1288
|
var ChangeCaptureRegistryImpl = class {
|
|
1533
1289
|
subscribers = /* @__PURE__ */ new Set();
|
|
@@ -1869,102 +1625,187 @@ function ctxModelFor(modelClass) {
|
|
|
1869
1625
|
return resolved;
|
|
1870
1626
|
}
|
|
1871
1627
|
|
|
1872
|
-
// src/
|
|
1873
|
-
function
|
|
1874
|
-
|
|
1875
|
-
|
|
1628
|
+
// src/expression/update-expression.ts
|
|
1629
|
+
function isPlainObject(value) {
|
|
1630
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
1631
|
+
}
|
|
1632
|
+
function flattenChanges(obj, parentPath, embeddedFieldNames) {
|
|
1633
|
+
const sets = [];
|
|
1634
|
+
const removes = [];
|
|
1635
|
+
for (const [fieldName, value] of Object.entries(obj)) {
|
|
1636
|
+
const currentPath = [...parentPath, fieldName];
|
|
1637
|
+
if (value === void 0) {
|
|
1638
|
+
removes.push({ path: currentPath });
|
|
1639
|
+
} else if (embeddedFieldNames.has(currentPath[0]) && isPlainObject(value)) {
|
|
1640
|
+
const nested = flattenChanges(value, currentPath, embeddedFieldNames);
|
|
1641
|
+
sets.push(...nested.sets);
|
|
1642
|
+
removes.push(...nested.removes);
|
|
1643
|
+
} else {
|
|
1644
|
+
sets.push({ path: currentPath, value });
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
return { sets, removes };
|
|
1648
|
+
}
|
|
1649
|
+
function buildUpdateExpression(changes, embeddedFieldNames) {
|
|
1650
|
+
const { sets, removes } = flattenChanges(changes, [], embeddedFieldNames);
|
|
1651
|
+
if (sets.length === 0 && removes.length === 0) {
|
|
1652
|
+
throw new Error("No changes provided for update.");
|
|
1653
|
+
}
|
|
1654
|
+
const names = {};
|
|
1655
|
+
const values = {};
|
|
1656
|
+
let valueCounter = 0;
|
|
1657
|
+
const setClauses = [];
|
|
1658
|
+
for (const op of sets) {
|
|
1659
|
+
const pathExpr = op.path.map((segment) => {
|
|
1660
|
+
const nameKey = `#${segment}`;
|
|
1661
|
+
names[nameKey] = segment;
|
|
1662
|
+
return nameKey;
|
|
1663
|
+
}).join(".");
|
|
1664
|
+
const valueKey = `:val${valueCounter++}`;
|
|
1665
|
+
values[valueKey] = op.value;
|
|
1666
|
+
setClauses.push(`${pathExpr} = ${valueKey}`);
|
|
1667
|
+
}
|
|
1668
|
+
const removeClauses = [];
|
|
1669
|
+
for (const op of removes) {
|
|
1670
|
+
const pathExpr = op.path.map((segment) => {
|
|
1671
|
+
const nameKey = `#${segment}`;
|
|
1672
|
+
names[nameKey] = segment;
|
|
1673
|
+
return nameKey;
|
|
1674
|
+
}).join(".");
|
|
1675
|
+
removeClauses.push(pathExpr);
|
|
1676
|
+
}
|
|
1677
|
+
const parts = [];
|
|
1678
|
+
if (setClauses.length > 0) {
|
|
1679
|
+
parts.push(`SET ${setClauses.join(", ")}`);
|
|
1680
|
+
}
|
|
1681
|
+
if (removeClauses.length > 0) {
|
|
1682
|
+
parts.push(`REMOVE ${removeClauses.join(", ")}`);
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
expression: parts.join(" "),
|
|
1686
|
+
names,
|
|
1687
|
+
values
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// src/hydrator/hidden-key.ts
|
|
1692
|
+
var GRAPHDDB_KEY = /* @__PURE__ */ Symbol("graphddb:key");
|
|
1693
|
+
function attachHiddenKey(result, raw) {
|
|
1694
|
+
const pk = raw.PK;
|
|
1695
|
+
if (typeof pk !== "string") {
|
|
1876
1696
|
throw new Error(
|
|
1877
|
-
|
|
1697
|
+
"Cannot attach updatable key: raw item is missing a string `PK` attribute."
|
|
1878
1698
|
);
|
|
1879
1699
|
}
|
|
1880
|
-
const
|
|
1881
|
-
const
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1700
|
+
const sk = raw.SK;
|
|
1701
|
+
const value = {
|
|
1702
|
+
PK: pk,
|
|
1703
|
+
SK: typeof sk === "string" ? sk : ""
|
|
1704
|
+
};
|
|
1705
|
+
Object.defineProperty(result, GRAPHDDB_KEY, {
|
|
1706
|
+
value,
|
|
1707
|
+
enumerable: false,
|
|
1708
|
+
writable: false,
|
|
1709
|
+
configurable: false
|
|
1710
|
+
});
|
|
1711
|
+
return result;
|
|
1712
|
+
}
|
|
1713
|
+
function readHiddenKey(entity) {
|
|
1714
|
+
const value = entity[GRAPHDDB_KEY];
|
|
1715
|
+
if (value === void 0) return void 0;
|
|
1716
|
+
return value;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
// src/operations/update.ts
|
|
1720
|
+
function resolveUpdateKey(modelClass, primaryKey, entity, fieldMap) {
|
|
1721
|
+
const hidden = readHiddenKey(entity);
|
|
1722
|
+
if (hidden) {
|
|
1723
|
+
return { pk: hidden.PK, sk: hidden.SK };
|
|
1724
|
+
}
|
|
1725
|
+
const missing = primaryKey.inputFieldNames.filter(
|
|
1726
|
+
(f) => entity[f] === void 0
|
|
1727
|
+
);
|
|
1728
|
+
if (missing.length > 0) {
|
|
1729
|
+
const fieldList = missing.join(", ");
|
|
1730
|
+
throw new Error(
|
|
1731
|
+
`Cannot resolve update key for '${modelClass.name}': missing key field(s) [${fieldList}]. Provide an explicit key (update({ ${fieldList} }, changes)), select all key fields, or query with { updatable: true }.`
|
|
1732
|
+
);
|
|
1887
1733
|
}
|
|
1888
1734
|
const keyInput = {};
|
|
1889
|
-
for (const fieldName of
|
|
1890
|
-
|
|
1735
|
+
for (const fieldName of primaryKey.inputFieldNames) {
|
|
1736
|
+
let value = entity[fieldName];
|
|
1737
|
+
const fieldMeta = fieldMap.get(fieldName);
|
|
1738
|
+
if (fieldMeta && value !== void 0) {
|
|
1739
|
+
value = serializeFieldValue(value, fieldMeta);
|
|
1740
|
+
}
|
|
1741
|
+
keyInput[fieldName] = value;
|
|
1891
1742
|
}
|
|
1892
1743
|
const { pk, sk } = resolveSegmentedKey(
|
|
1893
|
-
|
|
1744
|
+
primaryKey.segmented,
|
|
1894
1745
|
keyInput,
|
|
1895
1746
|
`${modelClass.name} primary key`
|
|
1896
1747
|
);
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const value = item[emb.propertyName];
|
|
1904
|
-
if (value !== void 0) {
|
|
1905
|
-
dynamoItem[emb.propertyName] = value;
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1748
|
+
return { pk, sk: sk ?? "" };
|
|
1749
|
+
}
|
|
1750
|
+
function buildGsiRederiveSets(modelClass, meta, entity, serializedChanges, fieldMap, names, values, options) {
|
|
1751
|
+
const changedFields = new Set(Object.keys(serializedChanges));
|
|
1752
|
+
const affected = affectedGsis(meta, changedFields);
|
|
1753
|
+
if (affected.length === 0) return [];
|
|
1908
1754
|
const available = {};
|
|
1909
|
-
for (const fieldName of
|
|
1910
|
-
|
|
1755
|
+
for (const [fieldName, raw] of Object.entries(entity)) {
|
|
1756
|
+
if (raw === void 0) continue;
|
|
1757
|
+
const fm = fieldMap.get(fieldName);
|
|
1758
|
+
available[fieldName] = fm ? serializeFieldValue(raw, fm) : raw;
|
|
1911
1759
|
}
|
|
1912
|
-
|
|
1913
|
-
|
|
1760
|
+
Object.assign(available, serializedChanges);
|
|
1761
|
+
const clauses = [];
|
|
1762
|
+
let i = 0;
|
|
1763
|
+
for (const gsi2 of affected) {
|
|
1764
|
+
const missing = missingGsiFields(gsi2, available);
|
|
1765
|
+
if (missing.length > 0) {
|
|
1766
|
+
if (options?.rederive === "read-modify-write") continue;
|
|
1767
|
+
const changed = gsi2.inputFieldNames.filter((f) => changedFields.has(f));
|
|
1768
|
+
throw new Error(
|
|
1769
|
+
`Cannot update '${modelClass.name}': updating ${fmtFields(changed)} affects index '${gsi2.indexName}' (also depends on ${fmtFields(missing)}); provide ${missing.length > 1 ? "them" : "it"}, or use { rederive: 'read-modify-write' }.`
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1772
|
+
const { pkAttr, pk, skAttr, sk } = deriveGsiKey(
|
|
1914
1773
|
gsi2,
|
|
1915
1774
|
available,
|
|
1916
1775
|
`${modelClass.name} GSI '${gsi2.indexName}'`
|
|
1917
1776
|
);
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
putInput.ExpressionAttributeNames = condResult.names;
|
|
1930
|
-
}
|
|
1931
|
-
if (Object.keys(condResult.values).length > 0) {
|
|
1932
|
-
putInput.ExpressionAttributeValues = condResult.values;
|
|
1933
|
-
}
|
|
1777
|
+
const pkN = `#gsi${i}pk`;
|
|
1778
|
+
const pkV = `:gsi${i}pk`;
|
|
1779
|
+
names[pkN] = pkAttr;
|
|
1780
|
+
values[pkV] = pk;
|
|
1781
|
+
clauses.push(`${pkN} = ${pkV}`);
|
|
1782
|
+
const skN = `#gsi${i}sk`;
|
|
1783
|
+
const skV = `:gsi${i}sk`;
|
|
1784
|
+
names[skN] = skAttr;
|
|
1785
|
+
values[skV] = sk;
|
|
1786
|
+
clauses.push(`${skN} = ${skV}`);
|
|
1787
|
+
i++;
|
|
1934
1788
|
}
|
|
1935
|
-
return
|
|
1789
|
+
return clauses;
|
|
1936
1790
|
}
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
if (mw) {
|
|
1940
|
-
await executeSingleWriteWithHooks({ kind: "put", modelClass, item, options }, mw);
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
await executePutCore(modelClass, item, options);
|
|
1791
|
+
function fmtFields(fields) {
|
|
1792
|
+
return fields.map((f) => `'${f}'`).join(", ");
|
|
1944
1793
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
const
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
table: putInput.TableName,
|
|
1957
|
-
model: modelClass.name,
|
|
1958
|
-
op: "put",
|
|
1959
|
-
keys: { pk: String(putInput.Item.PK), sk: String(putInput.Item.SK ?? "") },
|
|
1960
|
-
newItem: putInput.Item,
|
|
1961
|
-
...oldItem ? { oldItem } : {}
|
|
1962
|
-
});
|
|
1794
|
+
function appendSetClauses(expression, clauses) {
|
|
1795
|
+
if (clauses.length === 0) return expression;
|
|
1796
|
+
const extra = clauses.join(", ");
|
|
1797
|
+
if (/(^|\s)SET\s/.test(expression)) {
|
|
1798
|
+
const removeIdx = expression.indexOf(" REMOVE ");
|
|
1799
|
+
if (removeIdx === -1) {
|
|
1800
|
+
return `${expression}, ${extra}`;
|
|
1801
|
+
}
|
|
1802
|
+
const setPart = expression.slice(0, removeIdx);
|
|
1803
|
+
const rest = expression.slice(removeIdx);
|
|
1804
|
+
return `${setPart}, ${extra}${rest}`;
|
|
1963
1805
|
}
|
|
1806
|
+
return `SET ${extra} ${expression}`.trimEnd();
|
|
1964
1807
|
}
|
|
1965
|
-
|
|
1966
|
-
// src/operations/delete.ts
|
|
1967
|
-
function buildDeleteInput(modelClass, keyObj, options) {
|
|
1808
|
+
function buildUpdateInput(modelClass, entity, changes, options) {
|
|
1968
1809
|
const meta = MetadataRegistry.get(modelClass);
|
|
1969
1810
|
if (!meta.primaryKey) {
|
|
1970
1811
|
throw new Error(
|
|
@@ -1975,259 +1816,12 @@ function buildDeleteInput(modelClass, keyObj, options) {
|
|
|
1975
1816
|
const fieldMap = new Map(
|
|
1976
1817
|
meta.fields.map((f) => [f.propertyName, f])
|
|
1977
1818
|
);
|
|
1978
|
-
const
|
|
1979
|
-
for (const fieldName of meta.primaryKey.inputFieldNames) {
|
|
1980
|
-
let value = keyObj[fieldName];
|
|
1819
|
+
for (const fieldName of Object.keys(changes)) {
|
|
1981
1820
|
const fieldMeta = fieldMap.get(fieldName);
|
|
1982
|
-
if (fieldMeta
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
}
|
|
1987
|
-
const { pk, sk } = resolveSegmentedKey(
|
|
1988
|
-
meta.primaryKey.segmented,
|
|
1989
|
-
keyInput,
|
|
1990
|
-
`${modelClass.name} primary key`
|
|
1991
|
-
);
|
|
1992
|
-
const deleteInput = {
|
|
1993
|
-
TableName: tableName,
|
|
1994
|
-
Key: {
|
|
1995
|
-
PK: pk,
|
|
1996
|
-
SK: sk ?? ""
|
|
1997
|
-
}
|
|
1998
|
-
};
|
|
1999
|
-
if (options?.condition) {
|
|
2000
|
-
const condResult = buildConditionExpression(options.condition);
|
|
2001
|
-
deleteInput.ConditionExpression = condResult.expression;
|
|
2002
|
-
if (Object.keys(condResult.names).length > 0) {
|
|
2003
|
-
deleteInput.ExpressionAttributeNames = condResult.names;
|
|
2004
|
-
}
|
|
2005
|
-
if (Object.keys(condResult.values).length > 0) {
|
|
2006
|
-
deleteInput.ExpressionAttributeValues = condResult.values;
|
|
2007
|
-
}
|
|
2008
|
-
}
|
|
2009
|
-
return deleteInput;
|
|
2010
|
-
}
|
|
2011
|
-
async function executeDelete(modelClass, keyObj, options) {
|
|
2012
|
-
const mw = writeRuntimeFor(options);
|
|
2013
|
-
if (mw) {
|
|
2014
|
-
await executeSingleWriteWithHooks({ kind: "delete", modelClass, key: keyObj, options }, mw);
|
|
2015
|
-
return;
|
|
2016
|
-
}
|
|
2017
|
-
await executeDeleteCore(modelClass, keyObj, options);
|
|
2018
|
-
}
|
|
2019
|
-
async function executeDeleteCore(modelClass, keyObj, options, persistVia, forceOldImage) {
|
|
2020
|
-
const deleteInput = buildDeleteInput(modelClass, keyObj, options);
|
|
2021
|
-
const wantsOld = ChangeCaptureRegistry.wantsOldImage(modelClass) || forceOldImage === true;
|
|
2022
|
-
const send = (input) => ClientManager.getExecutor().delete(input, {
|
|
2023
|
-
...wantsOld ? { returnOldImage: true } : {},
|
|
2024
|
-
...options?.retry !== void 0 ? { retry: options.retry } : {}
|
|
2025
|
-
});
|
|
2026
|
-
const result = persistVia ? await persistVia(deleteInput, send) : await send(deleteInput);
|
|
2027
|
-
if (ChangeCaptureRegistry.hasSubscribers()) {
|
|
2028
|
-
const oldItem = result.oldItem;
|
|
2029
|
-
captureWrite({
|
|
2030
|
-
table: deleteInput.TableName,
|
|
2031
|
-
model: modelClass.name,
|
|
2032
|
-
op: "delete",
|
|
2033
|
-
keys: { pk: String(deleteInput.Key.PK), sk: String(deleteInput.Key.SK ?? "") },
|
|
2034
|
-
...oldItem ? { oldItem } : {}
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// src/operations/write-hooks.ts
|
|
2040
|
-
function writeRuntimeFor(options) {
|
|
2041
|
-
const mw = buildWriteRuntime(options?.context);
|
|
2042
|
-
return mw.active ? mw : void 0;
|
|
2043
|
-
}
|
|
2044
|
-
async function executeSingleWriteWithHooks(req, mw) {
|
|
2045
|
-
const input = {};
|
|
2046
|
-
if (req.item !== void 0) input.item = { ...req.item };
|
|
2047
|
-
if (req.key !== void 0) input.key = { ...req.key };
|
|
2048
|
-
if (req.changes !== void 0) input.changes = { ...req.changes };
|
|
2049
|
-
if (req.options !== void 0) input.options = { ...req.options };
|
|
2050
|
-
const ctx = mw.writeCtx(req.kind, ctxModelFor(req.modelClass), input);
|
|
2051
|
-
try {
|
|
2052
|
-
await mw.runWriteBefore(ctx);
|
|
2053
|
-
const change2 = await persistRewrittenWrite(req.modelClass, ctx, mw);
|
|
2054
|
-
await mw.runWriteAfter(ctx, change2);
|
|
2055
|
-
} catch (err) {
|
|
2056
|
-
await mw.runWriteError(ctx, err);
|
|
2057
|
-
}
|
|
2058
|
-
}
|
|
2059
|
-
async function persistRewrittenWrite(modelClass, ctx, mw) {
|
|
2060
|
-
const opts = ctx.input.options;
|
|
2061
|
-
const origin = { model: ctx.model, kind: ctx.kind };
|
|
2062
|
-
const forceOldImage = mw.hasWriteAfter;
|
|
2063
|
-
const cap = {};
|
|
2064
|
-
if (ctx.kind === "put") {
|
|
2065
|
-
await executePutCore(
|
|
2066
|
-
modelClass,
|
|
2067
|
-
ctx.input.item ?? {},
|
|
2068
|
-
opts,
|
|
2069
|
-
(input, send) => runPersist(mw, { Put: input }, origin, cap, (it) => {
|
|
2070
|
-
const put = it.Put;
|
|
2071
|
-
cap.newImage = put.Item;
|
|
2072
|
-
return send(put);
|
|
2073
|
-
}),
|
|
2074
|
-
forceOldImage
|
|
2075
|
-
);
|
|
2076
|
-
return change(cap, true);
|
|
2077
|
-
}
|
|
2078
|
-
if (ctx.kind === "update") {
|
|
2079
|
-
await executeUpdateCore(
|
|
2080
|
-
modelClass,
|
|
2081
|
-
ctx.input.key ?? {},
|
|
2082
|
-
ctx.input.changes ?? {},
|
|
2083
|
-
opts,
|
|
2084
|
-
(input, send) => runPersist(mw, { Update: input }, origin, cap, (it) => {
|
|
2085
|
-
const upd = it.Update;
|
|
2086
|
-
const old = cap.result?.oldItem;
|
|
2087
|
-
cap.newImage = { ...old ?? upd.Key, ...ctx.input.changes ?? {} };
|
|
2088
|
-
return send(upd);
|
|
2089
|
-
}),
|
|
2090
|
-
forceOldImage
|
|
2091
|
-
);
|
|
2092
|
-
return change(cap, true);
|
|
2093
|
-
}
|
|
2094
|
-
await executeDeleteCore(
|
|
2095
|
-
modelClass,
|
|
2096
|
-
ctx.input.key ?? {},
|
|
2097
|
-
opts,
|
|
2098
|
-
(input, send) => runPersist(
|
|
2099
|
-
mw,
|
|
2100
|
-
{ Delete: input },
|
|
2101
|
-
origin,
|
|
2102
|
-
cap,
|
|
2103
|
-
(it) => send(it.Delete)
|
|
2104
|
-
),
|
|
2105
|
-
forceOldImage
|
|
2106
|
-
);
|
|
2107
|
-
return change(cap, false);
|
|
2108
|
-
}
|
|
2109
|
-
function change(cap, withNew) {
|
|
2110
|
-
const out = {};
|
|
2111
|
-
const oldImage = cap.result?.oldItem;
|
|
2112
|
-
if (oldImage !== void 0) out.oldImage = oldImage;
|
|
2113
|
-
if (withNew && cap.newImage !== void 0) {
|
|
2114
|
-
out.newImage = cap.newImage;
|
|
2115
|
-
}
|
|
2116
|
-
return out;
|
|
2117
|
-
}
|
|
2118
|
-
async function runPersist(mw, item, origin, cap, send) {
|
|
2119
|
-
const ctx = mw.persistCtx([item], [origin]);
|
|
2120
|
-
const result = await mw.runPersist(ctx, async (items) => send(items[0]));
|
|
2121
|
-
cap.result = result;
|
|
2122
|
-
return result;
|
|
2123
|
-
}
|
|
2124
|
-
|
|
2125
|
-
// src/operations/update.ts
|
|
2126
|
-
function resolveUpdateKey(modelClass, primaryKey, entity, fieldMap) {
|
|
2127
|
-
const hidden = readHiddenKey(entity);
|
|
2128
|
-
if (hidden) {
|
|
2129
|
-
return { pk: hidden.PK, sk: hidden.SK };
|
|
2130
|
-
}
|
|
2131
|
-
const missing = primaryKey.inputFieldNames.filter(
|
|
2132
|
-
(f) => entity[f] === void 0
|
|
2133
|
-
);
|
|
2134
|
-
if (missing.length > 0) {
|
|
2135
|
-
const fieldList = missing.join(", ");
|
|
2136
|
-
throw new Error(
|
|
2137
|
-
`Cannot resolve update key for '${modelClass.name}': missing key field(s) [${fieldList}]. Provide an explicit key (update({ ${fieldList} }, changes)), select all key fields, or query with { updatable: true }.`
|
|
2138
|
-
);
|
|
2139
|
-
}
|
|
2140
|
-
const keyInput = {};
|
|
2141
|
-
for (const fieldName of primaryKey.inputFieldNames) {
|
|
2142
|
-
let value = entity[fieldName];
|
|
2143
|
-
const fieldMeta = fieldMap.get(fieldName);
|
|
2144
|
-
if (fieldMeta && value !== void 0) {
|
|
2145
|
-
value = serializeFieldValue(value, fieldMeta);
|
|
2146
|
-
}
|
|
2147
|
-
keyInput[fieldName] = value;
|
|
2148
|
-
}
|
|
2149
|
-
const { pk, sk } = resolveSegmentedKey(
|
|
2150
|
-
primaryKey.segmented,
|
|
2151
|
-
keyInput,
|
|
2152
|
-
`${modelClass.name} primary key`
|
|
2153
|
-
);
|
|
2154
|
-
return { pk, sk: sk ?? "" };
|
|
2155
|
-
}
|
|
2156
|
-
function buildGsiRederiveSets(modelClass, meta, entity, serializedChanges, fieldMap, names, values, options) {
|
|
2157
|
-
const changedFields = new Set(Object.keys(serializedChanges));
|
|
2158
|
-
const affected = affectedGsis(meta, changedFields);
|
|
2159
|
-
if (affected.length === 0) return [];
|
|
2160
|
-
const available = {};
|
|
2161
|
-
for (const [fieldName, raw] of Object.entries(entity)) {
|
|
2162
|
-
if (raw === void 0) continue;
|
|
2163
|
-
const fm = fieldMap.get(fieldName);
|
|
2164
|
-
available[fieldName] = fm ? serializeFieldValue(raw, fm) : raw;
|
|
2165
|
-
}
|
|
2166
|
-
Object.assign(available, serializedChanges);
|
|
2167
|
-
const clauses = [];
|
|
2168
|
-
let i = 0;
|
|
2169
|
-
for (const gsi2 of affected) {
|
|
2170
|
-
const missing = missingGsiFields(gsi2, available);
|
|
2171
|
-
if (missing.length > 0) {
|
|
2172
|
-
if (options?.rederive === "read-modify-write") continue;
|
|
2173
|
-
const changed = gsi2.inputFieldNames.filter((f) => changedFields.has(f));
|
|
2174
|
-
throw new Error(
|
|
2175
|
-
`Cannot update '${modelClass.name}': updating ${fmtFields(changed)} affects index '${gsi2.indexName}' (also depends on ${fmtFields(missing)}); provide ${missing.length > 1 ? "them" : "it"}, or use { rederive: 'read-modify-write' }.`
|
|
2176
|
-
);
|
|
2177
|
-
}
|
|
2178
|
-
const { pkAttr, pk, skAttr, sk } = deriveGsiKey(
|
|
2179
|
-
gsi2,
|
|
2180
|
-
available,
|
|
2181
|
-
`${modelClass.name} GSI '${gsi2.indexName}'`
|
|
2182
|
-
);
|
|
2183
|
-
const pkN = `#gsi${i}pk`;
|
|
2184
|
-
const pkV = `:gsi${i}pk`;
|
|
2185
|
-
names[pkN] = pkAttr;
|
|
2186
|
-
values[pkV] = pk;
|
|
2187
|
-
clauses.push(`${pkN} = ${pkV}`);
|
|
2188
|
-
const skN = `#gsi${i}sk`;
|
|
2189
|
-
const skV = `:gsi${i}sk`;
|
|
2190
|
-
names[skN] = skAttr;
|
|
2191
|
-
values[skV] = sk;
|
|
2192
|
-
clauses.push(`${skN} = ${skV}`);
|
|
2193
|
-
i++;
|
|
2194
|
-
}
|
|
2195
|
-
return clauses;
|
|
2196
|
-
}
|
|
2197
|
-
function fmtFields(fields) {
|
|
2198
|
-
return fields.map((f) => `'${f}'`).join(", ");
|
|
2199
|
-
}
|
|
2200
|
-
function appendSetClauses(expression, clauses) {
|
|
2201
|
-
if (clauses.length === 0) return expression;
|
|
2202
|
-
const extra = clauses.join(", ");
|
|
2203
|
-
if (/(^|\s)SET\s/.test(expression)) {
|
|
2204
|
-
const removeIdx = expression.indexOf(" REMOVE ");
|
|
2205
|
-
if (removeIdx === -1) {
|
|
2206
|
-
return `${expression}, ${extra}`;
|
|
2207
|
-
}
|
|
2208
|
-
const setPart = expression.slice(0, removeIdx);
|
|
2209
|
-
const rest = expression.slice(removeIdx);
|
|
2210
|
-
return `${setPart}, ${extra}${rest}`;
|
|
2211
|
-
}
|
|
2212
|
-
return `SET ${extra} ${expression}`.trimEnd();
|
|
2213
|
-
}
|
|
2214
|
-
function buildUpdateInput(modelClass, entity, changes, options) {
|
|
2215
|
-
const meta = MetadataRegistry.get(modelClass);
|
|
2216
|
-
if (!meta.primaryKey) {
|
|
2217
|
-
throw new Error(
|
|
2218
|
-
`No primary key defined for '${modelClass.name}'. Ensure the model has a static \`keys = key(...)\` definition.`
|
|
2219
|
-
);
|
|
2220
|
-
}
|
|
2221
|
-
const tableName = TableMapping.resolve(meta.tableName);
|
|
2222
|
-
const fieldMap = new Map(
|
|
2223
|
-
meta.fields.map((f) => [f.propertyName, f])
|
|
2224
|
-
);
|
|
2225
|
-
for (const fieldName of Object.keys(changes)) {
|
|
2226
|
-
const fieldMeta = fieldMap.get(fieldName);
|
|
2227
|
-
if (fieldMeta?.options?.readonly) {
|
|
2228
|
-
throw new Error(
|
|
2229
|
-
`Cannot update readonly field '${fieldName}' on '${modelClass.name}'. Readonly fields can only be set during put.`
|
|
2230
|
-
);
|
|
1821
|
+
if (fieldMeta?.options?.readonly) {
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
`Cannot update readonly field '${fieldName}' on '${modelClass.name}'. Readonly fields can only be set during put.`
|
|
1824
|
+
);
|
|
2231
1825
|
}
|
|
2232
1826
|
}
|
|
2233
1827
|
const { pk, sk } = resolveUpdateKey(
|
|
@@ -2400,1391 +1994,448 @@ async function executeUpdateCore(modelClass, entity, changes, options, persistVi
|
|
|
2400
1994
|
}
|
|
2401
1995
|
}
|
|
2402
1996
|
|
|
2403
|
-
// src/
|
|
2404
|
-
function
|
|
2405
|
-
const
|
|
2406
|
-
|
|
2407
|
-
const sig = view.keySignature(item);
|
|
2408
|
-
const bucket = groups.get(sig);
|
|
2409
|
-
if (bucket === void 0) groups.set(sig, [item]);
|
|
2410
|
-
else bucket.push(item);
|
|
2411
|
-
}
|
|
2412
|
-
const replacement = /* @__PURE__ */ new Map();
|
|
2413
|
-
const dropped = /* @__PURE__ */ new Set();
|
|
2414
|
-
for (const bucket of groups.values()) {
|
|
2415
|
-
if (bucket.length === 1) continue;
|
|
2416
|
-
const kinds = new Set(bucket.map((it) => view.kind(it)));
|
|
2417
|
-
if (kinds.size === 2 && kinds.has("Delete") && kinds.has("Put")) {
|
|
2418
|
-
for (const it of bucket) dropped.add(it);
|
|
2419
|
-
continue;
|
|
2420
|
-
}
|
|
2421
|
-
if (kinds.size === 2 && kinds.has("Put") && kinds.has("Update")) {
|
|
2422
|
-
throw new Error(
|
|
2423
|
-
`Contract runtime: a derived counter (\`increment\`) resolves to the SAME item as the entity write at runtime (the call-time key values made an aliased target coincide with the written row). A Put+Update on one transaction item is unsupported \u2014 DynamoDB rejects touching one key twice, and collapsing it would silently drop the increment. A \`w.increment(...)\` must target a DIFFERENT row (e.g. a parent aggregate's counter); to maintain a counter ON the written entity, write the field as part of the entity itself, not via a derived counter onto its own row.`
|
|
2424
|
-
);
|
|
2425
|
-
}
|
|
2426
|
-
if (kinds.size === 1 && kinds.has("Update")) {
|
|
2427
|
-
replacement.set(bucket[0], view.mergeAdds(bucket));
|
|
2428
|
-
for (const it of bucket.slice(1)) dropped.add(it);
|
|
2429
|
-
continue;
|
|
2430
|
-
}
|
|
2431
|
-
if (kinds.size === 1 && (kinds.has("Put") || kinds.has("Delete"))) {
|
|
2432
|
-
for (const it of bucket.slice(1)) dropped.add(it);
|
|
2433
|
-
continue;
|
|
2434
|
-
}
|
|
1997
|
+
// src/operations/delete.ts
|
|
1998
|
+
function buildDeleteInput(modelClass, keyObj, options) {
|
|
1999
|
+
const meta = MetadataRegistry.get(modelClass);
|
|
2000
|
+
if (!meta.primaryKey) {
|
|
2435
2001
|
throw new Error(
|
|
2436
|
-
`
|
|
2437
|
-
);
|
|
2438
|
-
}
|
|
2439
|
-
const out = [];
|
|
2440
|
-
for (const item of items) {
|
|
2441
|
-
if (dropped.has(item)) continue;
|
|
2442
|
-
out.push(replacement.get(item) ?? item);
|
|
2443
|
-
}
|
|
2444
|
-
return out;
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
// src/runtime/transaction-commit.ts
|
|
2448
|
-
var MAX_TRANSACT_ITEMS = 25;
|
|
2449
|
-
var execItemCollapseView = {
|
|
2450
|
-
kind: execItemKind,
|
|
2451
|
-
keySignature: execItemKeySignature,
|
|
2452
|
-
mergeAdds: mergeAddUpdates
|
|
2453
|
-
};
|
|
2454
|
-
function collapseSameKeyItems(items) {
|
|
2455
|
-
return collapseSameKey(items, execItemCollapseView);
|
|
2456
|
-
}
|
|
2457
|
-
async function commitTransaction(items, options = {}) {
|
|
2458
|
-
const collapsed = collapseSameKeyItems(items);
|
|
2459
|
-
if (collapsed.length === 0) return collapsed;
|
|
2460
|
-
if (collapsed.length > MAX_TRANSACT_ITEMS) {
|
|
2461
|
-
throw options.limitError ? options.limitError(collapsed.length) : new Error(
|
|
2462
|
-
`Transaction exceeds DynamoDB limit of ${MAX_TRANSACT_ITEMS} items`
|
|
2002
|
+
`No primary key defined for '${modelClass.name}'. Ensure the model has a static \`keys = key(...)\` definition.`
|
|
2463
2003
|
);
|
|
2464
2004
|
}
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2005
|
+
const tableName = TableMapping.resolve(meta.tableName);
|
|
2006
|
+
const fieldMap = new Map(
|
|
2007
|
+
meta.fields.map((f) => [f.propertyName, f])
|
|
2468
2008
|
);
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
}
|
|
2478
|
-
captureTransactItems(committed, options.modelBySignature ?? EMPTY_MODEL_MAP);
|
|
2479
|
-
return committed;
|
|
2480
|
-
}
|
|
2481
|
-
var EMPTY_MODEL_MAP = /* @__PURE__ */ new Map();
|
|
2482
|
-
function captureTransactItems(items, modelBySignature) {
|
|
2483
|
-
if (!ChangeCaptureRegistry.hasSubscribers()) return;
|
|
2484
|
-
for (const item of items) {
|
|
2485
|
-
if ("ConditionCheck" in item) continue;
|
|
2486
|
-
captureWrite(captureMetaForItem(item, modelBySignature));
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
2489
|
-
function captureMetaForItem(item, modelBySignature) {
|
|
2490
|
-
const model = modelBySignature.get(execItemKeySignature(item));
|
|
2491
|
-
const labelled = model !== void 0 ? { model } : {};
|
|
2492
|
-
if ("Put" in item) {
|
|
2493
|
-
const Item = item.Put.Item;
|
|
2494
|
-
return {
|
|
2495
|
-
table: item.Put.TableName,
|
|
2496
|
-
...labelled,
|
|
2497
|
-
op: "put",
|
|
2498
|
-
keys: { pk: String(Item.PK), sk: String(Item.SK ?? "") },
|
|
2499
|
-
newItem: Item
|
|
2500
|
-
};
|
|
2501
|
-
}
|
|
2502
|
-
if ("Update" in item) {
|
|
2503
|
-
return {
|
|
2504
|
-
table: item.Update.TableName,
|
|
2505
|
-
...labelled,
|
|
2506
|
-
op: "update",
|
|
2507
|
-
keys: { pk: String(item.Update.Key.PK), sk: String(item.Update.Key.SK ?? "") }
|
|
2508
|
-
};
|
|
2009
|
+
const keyInput = {};
|
|
2010
|
+
for (const fieldName of meta.primaryKey.inputFieldNames) {
|
|
2011
|
+
let value = keyObj[fieldName];
|
|
2012
|
+
const fieldMeta = fieldMap.get(fieldName);
|
|
2013
|
+
if (fieldMeta && value !== void 0) {
|
|
2014
|
+
value = serializeFieldValue(value, fieldMeta);
|
|
2015
|
+
}
|
|
2016
|
+
keyInput[fieldName] = value;
|
|
2509
2017
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2018
|
+
const { pk, sk } = resolveSegmentedKey(
|
|
2019
|
+
meta.primaryKey.segmented,
|
|
2020
|
+
keyInput,
|
|
2021
|
+
`${modelClass.name} primary key`
|
|
2022
|
+
);
|
|
2023
|
+
const deleteInput = {
|
|
2024
|
+
TableName: tableName,
|
|
2025
|
+
Key: {
|
|
2026
|
+
PK: pk,
|
|
2027
|
+
SK: sk ?? ""
|
|
2028
|
+
}
|
|
2515
2029
|
};
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
if ("Delete" in item) return `${item.Delete.TableName}#${part(item.Delete.Key)}`;
|
|
2522
|
-
return `${item.ConditionCheck.TableName}#${part(item.ConditionCheck.Key)}`;
|
|
2523
|
-
}
|
|
2524
|
-
function execItemKind(item) {
|
|
2525
|
-
if ("Put" in item) return "Put";
|
|
2526
|
-
if ("Update" in item) return "Update";
|
|
2527
|
-
if ("Delete" in item) return "Delete";
|
|
2528
|
-
return "ConditionCheck";
|
|
2529
|
-
}
|
|
2530
|
-
function mergeAddUpdates(updates) {
|
|
2531
|
-
const first = updates[0];
|
|
2532
|
-
if (!("Update" in first)) {
|
|
2533
|
-
throw new Error("Contract runtime: mergeAddUpdates received a non-Update item.");
|
|
2534
|
-
}
|
|
2535
|
-
const deltas = /* @__PURE__ */ new Map();
|
|
2536
|
-
for (const it of updates) {
|
|
2537
|
-
if (!("Update" in it)) {
|
|
2538
|
-
throw new Error("Contract runtime: cannot merge a non-Update item into an ADD.");
|
|
2030
|
+
if (options?.condition) {
|
|
2031
|
+
const condResult = buildConditionExpression(options.condition);
|
|
2032
|
+
deleteInput.ConditionExpression = condResult.expression;
|
|
2033
|
+
if (Object.keys(condResult.names).length > 0) {
|
|
2034
|
+
deleteInput.ExpressionAttributeNames = condResult.names;
|
|
2539
2035
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
for (const [namePlaceholder, attr] of Object.entries(names2)) {
|
|
2543
|
-
const valuePlaceholder = `:${namePlaceholder.slice(1)}`;
|
|
2544
|
-
const delta = values2[valuePlaceholder];
|
|
2545
|
-
if (typeof delta !== "number") {
|
|
2546
|
-
throw new Error(
|
|
2547
|
-
`Contract runtime: cannot merge same-key counter updates \u2014 an ADD delta on '${attr}' is not numeric.`
|
|
2548
|
-
);
|
|
2549
|
-
}
|
|
2550
|
-
deltas.set(attr, (deltas.get(attr) ?? 0) + delta);
|
|
2036
|
+
if (Object.keys(condResult.values).length > 0) {
|
|
2037
|
+
deleteInput.ExpressionAttributeValues = condResult.values;
|
|
2551
2038
|
}
|
|
2552
2039
|
}
|
|
2553
|
-
|
|
2554
|
-
const values = {};
|
|
2555
|
-
const clauses = [];
|
|
2556
|
-
let i = 0;
|
|
2557
|
-
for (const [attr, delta] of deltas) {
|
|
2558
|
-
names[`#a${i}`] = attr;
|
|
2559
|
-
values[`:a${i}`] = delta;
|
|
2560
|
-
clauses.push(`#a${i} :a${i}`);
|
|
2561
|
-
i++;
|
|
2562
|
-
}
|
|
2563
|
-
const merged = {
|
|
2564
|
-
TableName: first.Update.TableName,
|
|
2565
|
-
Key: first.Update.Key,
|
|
2566
|
-
UpdateExpression: `ADD ${clauses.join(", ")}`,
|
|
2567
|
-
ExpressionAttributeNames: names,
|
|
2568
|
-
ExpressionAttributeValues: values
|
|
2569
|
-
};
|
|
2570
|
-
return { Update: merged };
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
// src/operations/transaction.ts
|
|
2574
|
-
var MODEL_CLASS_KEY = /* @__PURE__ */ Symbol.for("graphddb.modelClass");
|
|
2575
|
-
function attachModelClass(modelStatic, modelClass) {
|
|
2576
|
-
Object.defineProperty(modelStatic, MODEL_CLASS_KEY, {
|
|
2577
|
-
value: modelClass,
|
|
2578
|
-
enumerable: false,
|
|
2579
|
-
configurable: false,
|
|
2580
|
-
writable: false
|
|
2581
|
-
});
|
|
2582
|
-
return modelStatic;
|
|
2040
|
+
return deleteInput;
|
|
2583
2041
|
}
|
|
2584
|
-
function
|
|
2585
|
-
const
|
|
2586
|
-
if (
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
);
|
|
2042
|
+
async function executeDelete(modelClass, keyObj, options) {
|
|
2043
|
+
const mw = writeRuntimeFor(options);
|
|
2044
|
+
if (mw) {
|
|
2045
|
+
await executeSingleWriteWithHooks({ kind: "delete", modelClass, key: keyObj, options }, mw);
|
|
2046
|
+
return;
|
|
2590
2047
|
}
|
|
2591
|
-
|
|
2048
|
+
await executeDeleteCore(modelClass, keyObj, options);
|
|
2592
2049
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
get itemCount() {
|
|
2605
|
-
return this.items.length;
|
|
2606
|
-
}
|
|
2607
|
-
put(model, item, options) {
|
|
2608
|
-
this.assertWithinLimit();
|
|
2609
|
-
const modelClass = resolveModelClass(model);
|
|
2610
|
-
const putInput = buildPutInput(modelClass, item, options);
|
|
2611
|
-
this.items.push({ Put: putInput });
|
|
2612
|
-
this.logicalOps.push({ kind: "put", modelClass, item, options });
|
|
2613
|
-
this.captureMeta.push({
|
|
2614
|
-
modelName: modelClass.name,
|
|
2615
|
-
op: "put",
|
|
2616
|
-
table: putInput.TableName,
|
|
2617
|
-
keys: { pk: String(putInput.Item.PK), sk: String(putInput.Item.SK ?? "") },
|
|
2618
|
-
newItem: putInput.Item
|
|
2619
|
-
});
|
|
2620
|
-
}
|
|
2621
|
-
update(model, entity, changes, options) {
|
|
2622
|
-
this.assertWithinLimit();
|
|
2623
|
-
const modelClass = resolveModelClass(model);
|
|
2624
|
-
const updateInput = buildUpdateInput(modelClass, entity, changes, options);
|
|
2625
|
-
this.items.push({ Update: updateInput });
|
|
2626
|
-
this.logicalOps.push({ kind: "update", modelClass, key: entity, changes, options });
|
|
2627
|
-
this.captureMeta.push({
|
|
2628
|
-
modelName: modelClass.name,
|
|
2629
|
-
op: "update",
|
|
2630
|
-
table: updateInput.TableName,
|
|
2631
|
-
keys: { pk: String(updateInput.Key.PK), sk: String(updateInput.Key.SK ?? "") }
|
|
2632
|
-
});
|
|
2633
|
-
}
|
|
2634
|
-
delete(model, key, options) {
|
|
2635
|
-
this.assertWithinLimit();
|
|
2636
|
-
const modelClass = resolveModelClass(model);
|
|
2637
|
-
const deleteInput = buildDeleteInput(modelClass, key, options);
|
|
2638
|
-
this.items.push({ Delete: deleteInput });
|
|
2639
|
-
this.logicalOps.push({ kind: "delete", modelClass, key, options });
|
|
2640
|
-
this.captureMeta.push({
|
|
2641
|
-
modelName: modelClass.name,
|
|
2642
|
-
op: "delete",
|
|
2050
|
+
async function executeDeleteCore(modelClass, keyObj, options, persistVia, forceOldImage) {
|
|
2051
|
+
const deleteInput = buildDeleteInput(modelClass, keyObj, options);
|
|
2052
|
+
const wantsOld = ChangeCaptureRegistry.wantsOldImage(modelClass) || forceOldImage === true;
|
|
2053
|
+
const send = (input) => ClientManager.getExecutor().delete(input, {
|
|
2054
|
+
...wantsOld ? { returnOldImage: true } : {},
|
|
2055
|
+
...options?.retry !== void 0 ? { retry: options.retry } : {}
|
|
2056
|
+
});
|
|
2057
|
+
const result = persistVia ? await persistVia(deleteInput, send) : await send(deleteInput);
|
|
2058
|
+
if (ChangeCaptureRegistry.hasSubscribers()) {
|
|
2059
|
+
const oldItem = result.oldItem;
|
|
2060
|
+
captureWrite({
|
|
2643
2061
|
table: deleteInput.TableName,
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
* Add a read-only `ConditionCheck` assertion on a keyed item (issue #81). The
|
|
2649
|
-
* item is **not** mutated; the `options.condition` (e.g.
|
|
2650
|
-
* `{ attributeExists: 'PK' }` to require the row exists) is asserted, and a
|
|
2651
|
-
* failed assertion cancels the **whole** `TransactWriteItems` atomically. This
|
|
2652
|
-
* is the foundation for referential-integrity derivation (proposal: `requires
|
|
2653
|
-
* <Entity> exists`). A ConditionCheck emits no change-capture record (it writes
|
|
2654
|
-
* nothing).
|
|
2655
|
-
*
|
|
2656
|
-
* @throws if `options.condition` is missing — a ConditionCheck without an
|
|
2657
|
-
* assertion is meaningless.
|
|
2658
|
-
*/
|
|
2659
|
-
conditionCheck(model, key, options) {
|
|
2660
|
-
if (!options || !options.condition) {
|
|
2661
|
-
throw new Error(
|
|
2662
|
-
"TransactionContext.conditionCheck requires a `condition` (the read-only assertion it makes); e.g. `{ condition: { attributeExists: 'PK' } }`."
|
|
2663
|
-
);
|
|
2664
|
-
}
|
|
2665
|
-
this.assertWithinLimit();
|
|
2666
|
-
const modelClass = resolveModelClass(model);
|
|
2667
|
-
const { TableName, Key } = buildDeleteInput(modelClass, key);
|
|
2668
|
-
const cond2 = buildConditionExpression(options.condition);
|
|
2669
|
-
const check = {
|
|
2670
|
-
TableName,
|
|
2671
|
-
Key,
|
|
2672
|
-
ConditionExpression: cond2.expression
|
|
2673
|
-
};
|
|
2674
|
-
if (Object.keys(cond2.names).length > 0) {
|
|
2675
|
-
check.ExpressionAttributeNames = cond2.names;
|
|
2676
|
-
}
|
|
2677
|
-
if (Object.keys(cond2.values).length > 0) {
|
|
2678
|
-
check.ExpressionAttributeValues = cond2.values;
|
|
2679
|
-
}
|
|
2680
|
-
this.items.push({ ConditionCheck: check });
|
|
2681
|
-
this.logicalOps.push(null);
|
|
2682
|
-
}
|
|
2683
|
-
/** @internal */
|
|
2684
|
-
getTransactItems() {
|
|
2685
|
-
return this.items;
|
|
2686
|
-
}
|
|
2687
|
-
/** @internal — the recorded logical write ops (issue #139 W1), `null` per ConditionCheck slot. */
|
|
2688
|
-
getLogicalOps() {
|
|
2689
|
-
return this.logicalOps;
|
|
2690
|
-
}
|
|
2691
|
-
/** @internal — capture descriptors for the write-capture seam (issue #72). */
|
|
2692
|
-
getCaptureMeta() {
|
|
2693
|
-
return this.captureMeta;
|
|
2694
|
-
}
|
|
2695
|
-
assertWithinLimit() {
|
|
2696
|
-
if (this.items.length >= MAX_TRANSACT_ITEMS) {
|
|
2697
|
-
throw new Error(
|
|
2698
|
-
`Transaction exceeds DynamoDB limit of ${MAX_TRANSACT_ITEMS} items`
|
|
2699
|
-
);
|
|
2700
|
-
}
|
|
2701
|
-
}
|
|
2702
|
-
};
|
|
2703
|
-
async function executeTransaction(fn, options) {
|
|
2704
|
-
const tx = new TransactionContext();
|
|
2705
|
-
await fn(tx);
|
|
2706
|
-
let transactItems = tx.getTransactItems();
|
|
2707
|
-
if (transactItems.length === 0) {
|
|
2708
|
-
return;
|
|
2709
|
-
}
|
|
2710
|
-
const writeMw = buildWriteRuntime(options?.context);
|
|
2711
|
-
if (writeMw.active) {
|
|
2712
|
-
const txId = { id: /* @__PURE__ */ Symbol("graphddb.transaction") };
|
|
2713
|
-
const logical = tx.getLogicalOps();
|
|
2714
|
-
const items = [...transactItems];
|
|
2715
|
-
const origins = [];
|
|
2716
|
-
const writeCtxs = [];
|
|
2717
|
-
const modelBySignature2 = /* @__PURE__ */ new Map();
|
|
2718
|
-
for (let i = 0; i < logical.length; i++) {
|
|
2719
|
-
const op = logical[i];
|
|
2720
|
-
if (op === null) continue;
|
|
2721
|
-
const input = {};
|
|
2722
|
-
if (op.item !== void 0) input.item = { ...op.item };
|
|
2723
|
-
if (op.key !== void 0) input.key = { ...op.key };
|
|
2724
|
-
if (op.changes !== void 0) input.changes = { ...op.changes };
|
|
2725
|
-
if (op.options !== void 0) input.options = { ...op.options };
|
|
2726
|
-
const ctx = writeMw.writeCtx(op.kind, ctxModelFor(op.modelClass), input, txId);
|
|
2727
|
-
await writeMw.runWriteBefore(ctx);
|
|
2728
|
-
const built = buildLogicalItem(op.modelClass, ctx);
|
|
2729
|
-
items[i] = built;
|
|
2730
|
-
modelBySignature2.set(execItemKeySignature(built), op.modelClass.name);
|
|
2731
|
-
origins.push({ model: ctx.model, kind: ctx.kind });
|
|
2732
|
-
writeCtxs.push(ctx);
|
|
2733
|
-
}
|
|
2734
|
-
transactItems = items;
|
|
2735
|
-
await commitTransaction(transactItems, {
|
|
2736
|
-
modelBySignature: modelBySignature2,
|
|
2737
|
-
middleware: { runtime: writeMw, origins, transaction: txId }
|
|
2062
|
+
model: modelClass.name,
|
|
2063
|
+
op: "delete",
|
|
2064
|
+
keys: { pk: String(deleteInput.Key.PK), sk: String(deleteInput.Key.SK ?? "") },
|
|
2065
|
+
...oldItem ? { oldItem } : {}
|
|
2738
2066
|
});
|
|
2739
|
-
for (let j = writeCtxs.length - 1; j >= 0; j--) {
|
|
2740
|
-
await writeMw.runWriteAfter(writeCtxs[j], {});
|
|
2741
|
-
}
|
|
2742
|
-
return;
|
|
2743
2067
|
}
|
|
2744
|
-
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2745
|
-
for (const meta of tx.getCaptureMeta()) {
|
|
2746
|
-
if (meta.modelName) {
|
|
2747
|
-
modelBySignature.set(
|
|
2748
|
-
`${meta.table}#${meta.keys.pk}#${meta.keys.sk}`,
|
|
2749
|
-
meta.modelName
|
|
2750
|
-
);
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
await commitTransaction(transactItems, {
|
|
2754
|
-
modelBySignature
|
|
2755
|
-
});
|
|
2756
|
-
}
|
|
2757
|
-
function buildLogicalItem(modelClass, ctx) {
|
|
2758
|
-
const opts = ctx.input.options;
|
|
2759
|
-
if (ctx.kind === "put") {
|
|
2760
|
-
return { Put: buildPutInput(modelClass, ctx.input.item ?? {}, opts) };
|
|
2761
|
-
}
|
|
2762
|
-
if (ctx.kind === "update") {
|
|
2763
|
-
return {
|
|
2764
|
-
Update: buildUpdateInput(modelClass, ctx.input.key ?? {}, ctx.input.changes ?? {}, opts)
|
|
2765
|
-
};
|
|
2766
|
-
}
|
|
2767
|
-
return { Delete: buildDeleteInput(modelClass, ctx.input.key ?? {}, opts) };
|
|
2768
2068
|
}
|
|
2769
2069
|
|
|
2770
|
-
// src/
|
|
2771
|
-
function
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
function hydrateItem(raw, select, metadata, updatable = false) {
|
|
2775
|
-
const fieldMap = new Map(
|
|
2776
|
-
metadata.fields.map((f) => [f.propertyName, f])
|
|
2777
|
-
);
|
|
2778
|
-
const embeddedMap = new Map(
|
|
2779
|
-
metadata.embeddedFields.map((e) => [e.propertyName, e])
|
|
2780
|
-
);
|
|
2781
|
-
const result = {};
|
|
2782
|
-
for (const [fieldName, selectValue] of Object.entries(select)) {
|
|
2783
|
-
if (selectValue === true) {
|
|
2784
|
-
const value = raw[fieldName];
|
|
2785
|
-
if (value !== void 0) {
|
|
2786
|
-
const fieldMeta = fieldMap.get(fieldName);
|
|
2787
|
-
result[fieldName] = deserializeValue(value, fieldMeta);
|
|
2788
|
-
}
|
|
2789
|
-
} else if (isInlineSnapshotSpec(selectValue)) {
|
|
2790
|
-
const value = raw[fieldName];
|
|
2791
|
-
if (value !== void 0) {
|
|
2792
|
-
result[fieldName] = value;
|
|
2793
|
-
}
|
|
2794
|
-
} else if (typeof selectValue === "object" && selectValue !== null) {
|
|
2795
|
-
if ("select" in selectValue || isSelectBuilder(selectValue)) {
|
|
2796
|
-
continue;
|
|
2797
|
-
}
|
|
2798
|
-
const rawEmb = raw[fieldName];
|
|
2799
|
-
if (rawEmb && typeof rawEmb === "object") {
|
|
2800
|
-
result[fieldName] = hydrateEmbedded(
|
|
2801
|
-
rawEmb,
|
|
2802
|
-
selectValue,
|
|
2803
|
-
metadata,
|
|
2804
|
-
fieldName
|
|
2805
|
-
);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
if (updatable) {
|
|
2810
|
-
attachHiddenKey(result, raw);
|
|
2811
|
-
}
|
|
2812
|
-
return result;
|
|
2813
|
-
}
|
|
2814
|
-
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
2815
|
-
const result = {};
|
|
2816
|
-
for (const [key, selectValue] of Object.entries(select)) {
|
|
2817
|
-
if (selectValue === true) {
|
|
2818
|
-
if (raw[key] !== void 0) {
|
|
2819
|
-
result[key] = raw[key];
|
|
2820
|
-
}
|
|
2821
|
-
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
2822
|
-
const nested = raw[key];
|
|
2823
|
-
if (nested && typeof nested === "object") {
|
|
2824
|
-
result[key] = hydrateEmbedded(
|
|
2825
|
-
nested,
|
|
2826
|
-
selectValue,
|
|
2827
|
-
_metadata,
|
|
2828
|
-
key
|
|
2829
|
-
);
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
return result;
|
|
2834
|
-
}
|
|
2835
|
-
function deserializeValue(value, fieldMeta) {
|
|
2836
|
-
if (!fieldMeta) return value;
|
|
2837
|
-
if (fieldMeta.options?.deserialize) {
|
|
2838
|
-
return fieldMeta.options.deserialize(value);
|
|
2839
|
-
}
|
|
2840
|
-
if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
|
|
2841
|
-
return new Date(value);
|
|
2842
|
-
}
|
|
2843
|
-
if (fieldMeta.options?.format === "date" && typeof value === "string") {
|
|
2844
|
-
return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
|
|
2845
|
-
}
|
|
2846
|
-
return value;
|
|
2070
|
+
// src/operations/write-hooks.ts
|
|
2071
|
+
function writeRuntimeFor(options) {
|
|
2072
|
+
const mw = buildWriteRuntime(options?.context);
|
|
2073
|
+
return mw.active ? mw : void 0;
|
|
2847
2074
|
}
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
if (
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2075
|
+
async function executeSingleWriteWithHooks(req, mw) {
|
|
2076
|
+
const input = {};
|
|
2077
|
+
if (req.item !== void 0) input.item = { ...req.item };
|
|
2078
|
+
if (req.key !== void 0) input.key = { ...req.key };
|
|
2079
|
+
if (req.changes !== void 0) input.changes = { ...req.changes };
|
|
2080
|
+
if (req.options !== void 0) input.options = { ...req.options };
|
|
2081
|
+
const ctx = mw.writeCtx(req.kind, ctxModelFor(req.modelClass), input);
|
|
2082
|
+
try {
|
|
2083
|
+
await mw.runWriteBefore(ctx);
|
|
2084
|
+
const change2 = await persistRewrittenWrite(req.modelClass, ctx, mw);
|
|
2085
|
+
await mw.runWriteAfter(ctx, change2);
|
|
2086
|
+
} catch (err) {
|
|
2087
|
+
await mw.runWriteError(ctx, err);
|
|
2856
2088
|
}
|
|
2857
|
-
return value;
|
|
2858
2089
|
}
|
|
2859
|
-
function
|
|
2860
|
-
|
|
2861
|
-
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2090
|
+
async function persistRewrittenWrite(modelClass, ctx, mw) {
|
|
2091
|
+
const opts = ctx.input.options;
|
|
2092
|
+
const origin = { model: ctx.model, kind: ctx.kind };
|
|
2093
|
+
const forceOldImage = mw.hasWriteAfter;
|
|
2094
|
+
const cap = {};
|
|
2095
|
+
if (ctx.kind === "put") {
|
|
2096
|
+
await executePutCore(
|
|
2097
|
+
modelClass,
|
|
2098
|
+
ctx.input.item ?? {},
|
|
2099
|
+
opts,
|
|
2100
|
+
(input, send) => runPersist(mw, { Put: input }, origin, cap, (it) => {
|
|
2101
|
+
const put = it.Put;
|
|
2102
|
+
cap.newImage = put.Item;
|
|
2103
|
+
return send(put);
|
|
2104
|
+
}),
|
|
2105
|
+
forceOldImage
|
|
2106
|
+
);
|
|
2107
|
+
return change(cap, true);
|
|
2871
2108
|
}
|
|
2872
|
-
if (
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2109
|
+
if (ctx.kind === "update") {
|
|
2110
|
+
await executeUpdateCore(
|
|
2111
|
+
modelClass,
|
|
2112
|
+
ctx.input.key ?? {},
|
|
2113
|
+
ctx.input.changes ?? {},
|
|
2114
|
+
opts,
|
|
2115
|
+
(input, send) => runPersist(mw, { Update: input }, origin, cap, (it) => {
|
|
2116
|
+
const upd = it.Update;
|
|
2117
|
+
const old = cap.result?.oldItem;
|
|
2118
|
+
cap.newImage = { ...old ?? upd.Key, ...ctx.input.changes ?? {} };
|
|
2119
|
+
return send(upd);
|
|
2120
|
+
}),
|
|
2121
|
+
forceOldImage
|
|
2122
|
+
);
|
|
2123
|
+
return change(cap, true);
|
|
2879
2124
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2125
|
+
await executeDeleteCore(
|
|
2126
|
+
modelClass,
|
|
2127
|
+
ctx.input.key ?? {},
|
|
2128
|
+
opts,
|
|
2129
|
+
(input, send) => runPersist(
|
|
2130
|
+
mw,
|
|
2131
|
+
{ Delete: input },
|
|
2132
|
+
origin,
|
|
2133
|
+
cap,
|
|
2134
|
+
(it) => send(it.Delete)
|
|
2135
|
+
),
|
|
2136
|
+
forceOldImage
|
|
2882
2137
|
);
|
|
2138
|
+
return change(cap, false);
|
|
2883
2139
|
}
|
|
2884
|
-
function
|
|
2885
|
-
return typeof value === "string" ? value : value.path;
|
|
2886
|
-
}
|
|
2887
|
-
function preview(path, n) {
|
|
2888
|
-
return makeProjectionTransform(pathOf(path), "preview", [n]);
|
|
2889
|
-
}
|
|
2890
|
-
function identity(path) {
|
|
2891
|
-
return makeProjectionTransform(pathOf(path), "identity", []);
|
|
2892
|
-
}
|
|
2893
|
-
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
2894
|
-
"graphddb:lifecycleContract"
|
|
2895
|
-
);
|
|
2896
|
-
function isLifecycleContract(value) {
|
|
2897
|
-
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
2898
|
-
}
|
|
2899
|
-
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
2900
|
-
function isEntityWritesDefinition(value) {
|
|
2901
|
-
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
2902
|
-
}
|
|
2903
|
-
function freezeEffects(effects) {
|
|
2140
|
+
function change(cap, withNew) {
|
|
2904
2141
|
const out = {};
|
|
2905
|
-
|
|
2906
|
-
if (
|
|
2907
|
-
if (
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
2142
|
+
const oldImage = cap.result?.oldItem;
|
|
2143
|
+
if (oldImage !== void 0) out.oldImage = oldImage;
|
|
2144
|
+
if (withNew && cap.newImage !== void 0) {
|
|
2145
|
+
out.newImage = cap.newImage;
|
|
2146
|
+
}
|
|
2911
2147
|
return out;
|
|
2912
2148
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
2925
|
-
},
|
|
2926
|
-
putEdge(targetFactory, relationProperty) {
|
|
2927
|
-
return { kind: "putEdge", targetFactory, relationProperty };
|
|
2928
|
-
},
|
|
2929
|
-
deleteEdge(targetFactory, relationProperty) {
|
|
2930
|
-
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
2931
|
-
},
|
|
2932
|
-
dualEdge(forward, inverse) {
|
|
2933
|
-
return {
|
|
2934
|
-
kind: "putEdge",
|
|
2935
|
-
targetFactory: forward.target,
|
|
2936
|
-
relationProperty: forward.relationProperty,
|
|
2937
|
-
inverse: {
|
|
2938
|
-
adjacencyFactory: inverse.adjacency,
|
|
2939
|
-
targetFactory: inverse.target,
|
|
2940
|
-
relationProperty: inverse.relationProperty
|
|
2941
|
-
}
|
|
2942
|
-
};
|
|
2943
|
-
},
|
|
2944
|
-
increment(targetFactory, keys, attribute, amount) {
|
|
2945
|
-
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
2946
|
-
},
|
|
2947
|
-
transform(path, op, ...args) {
|
|
2948
|
-
return makeProjectionTransform(path, op, args);
|
|
2949
|
-
},
|
|
2950
|
-
event(name, payload) {
|
|
2951
|
-
return { kind: "event", name, payload };
|
|
2952
|
-
},
|
|
2953
|
-
idempotentBy(token) {
|
|
2954
|
-
return { kind: "idempotency", token };
|
|
2955
|
-
}
|
|
2956
|
-
};
|
|
2957
|
-
function entityWrites(builder) {
|
|
2958
|
-
const shape = builder(recorder);
|
|
2959
|
-
for (const phase of ["create", "update", "remove"]) {
|
|
2960
|
-
const contract = shape[phase];
|
|
2961
|
-
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
2962
|
-
throw new Error(
|
|
2963
|
-
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
2964
|
-
);
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
2149
|
+
async function runPersist(mw, item, origin, cap, send) {
|
|
2150
|
+
const ctx = mw.persistCtx([item], [origin]);
|
|
2151
|
+
const result = await mw.runPersist(ctx, async (items) => send(items[0]));
|
|
2152
|
+
cap.result = result;
|
|
2153
|
+
return result;
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
// src/operations/put.ts
|
|
2157
|
+
function buildPutInput(modelClass, item, options) {
|
|
2158
|
+
const meta = MetadataRegistry.get(modelClass);
|
|
2159
|
+
if (!meta.primaryKey) {
|
|
2968
2160
|
throw new Error(
|
|
2969
|
-
|
|
2161
|
+
`No primary key defined for '${modelClass.name}'. Ensure the model has a static \`keys = key(...)\` definition.`
|
|
2970
2162
|
);
|
|
2971
2163
|
}
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
}
|
|
2979
|
-
function getEntityWrites(modelClass) {
|
|
2980
|
-
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
2981
|
-
let value;
|
|
2982
|
-
try {
|
|
2983
|
-
value = modelClass[name];
|
|
2984
|
-
} catch {
|
|
2985
|
-
continue;
|
|
2164
|
+
const tableName = TableMapping.resolve(meta.tableName);
|
|
2165
|
+
const serialized = {};
|
|
2166
|
+
for (const fieldMeta of meta.fields) {
|
|
2167
|
+
const value = item[fieldMeta.propertyName];
|
|
2168
|
+
if (value !== void 0) {
|
|
2169
|
+
serialized[fieldMeta.propertyName] = serializeFieldValue(value, fieldMeta);
|
|
2986
2170
|
}
|
|
2987
|
-
if (isEntityWritesDefinition(value)) return value;
|
|
2988
2171
|
}
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
return intent;
|
|
2993
|
-
}
|
|
2994
|
-
|
|
2995
|
-
// src/relation/maintenance-view-adapter.ts
|
|
2996
|
-
function logicalNameOf(meta) {
|
|
2997
|
-
return meta.prefix.endsWith("#") ? meta.prefix.slice(0, -1) : meta.prefix;
|
|
2998
|
-
}
|
|
2999
|
-
function resolveClass(factory) {
|
|
3000
|
-
return factory();
|
|
3001
|
-
}
|
|
3002
|
-
function sourceLogicalName(sourceClass) {
|
|
3003
|
-
const meta = MetadataRegistry.get(sourceClass);
|
|
3004
|
-
return logicalNameOf(meta);
|
|
3005
|
-
}
|
|
3006
|
-
function triggersFor(sourceLogical, on) {
|
|
3007
|
-
return on.map((ev) => `${sourceLogical}.${ev}`);
|
|
3008
|
-
}
|
|
3009
|
-
function assertNoCrossSliceCollision(viewName, decls) {
|
|
3010
|
-
const projectionOwner = /* @__PURE__ */ new Map();
|
|
3011
|
-
const collectionOwner = /* @__PURE__ */ new Map();
|
|
3012
|
-
let keyFieldShape;
|
|
3013
|
-
decls.forEach((d, idx) => {
|
|
3014
|
-
for (const attr of Object.keys(d.project)) {
|
|
3015
|
-
const prev = projectionOwner.get(attr);
|
|
3016
|
-
if (prev !== void 0 && prev !== idx) {
|
|
3017
|
-
throw new Error(
|
|
3018
|
-
`View '${viewName}': projection target '${attr}' is maintained by more than one \`@maintainedFrom\` declaration. A view field must be filled by exactly one source \u2014 the declaration order carries no meaning, so this is ambiguous. Remove '${attr}' from one of the declarations (or split it onto a distinct view field).`
|
|
3019
|
-
);
|
|
3020
|
-
}
|
|
3021
|
-
projectionOwner.set(attr, idx);
|
|
3022
|
-
}
|
|
3023
|
-
if (d.collectionField !== void 0) {
|
|
3024
|
-
const prev = collectionOwner.get(d.collectionField);
|
|
3025
|
-
if (prev !== void 0 && prev !== idx) {
|
|
3026
|
-
throw new Error(
|
|
3027
|
-
`View '${viewName}': collection field '${d.collectionField}' is maintained by more than one \`@maintainedFrom\` declaration. A maintained collection has exactly one source \u2014 split the second source onto a distinct collection field.`
|
|
3028
|
-
);
|
|
3029
|
-
}
|
|
3030
|
-
collectionOwner.set(d.collectionField, idx);
|
|
3031
|
-
}
|
|
3032
|
-
const shape = Object.keys(d.keyBind).slice().sort().join(",");
|
|
3033
|
-
if (keyFieldShape === void 0) {
|
|
3034
|
-
keyFieldShape = shape;
|
|
3035
|
-
} else if (keyFieldShape !== shape) {
|
|
3036
|
-
throw new Error(
|
|
3037
|
-
`View '${viewName}': \`@maintainedFrom\` declarations bind different view-key field SETS ([${keyFieldShape}] vs [${shape}]). All sources must resolve the SAME view row identity \u2014 they must bind the same set of view-key fields (the source field a given key reads from may differ per source, but the key SHAPE may not). A divergent key shape would split the row.`
|
|
3038
|
-
);
|
|
3039
|
-
}
|
|
3040
|
-
});
|
|
3041
|
-
}
|
|
3042
|
-
function viewFromMaintainedFrom(viewClass, meta, kind) {
|
|
3043
|
-
const viewName = viewClass.name ?? logicalNameOf(meta);
|
|
3044
|
-
const decls = meta.maintainedFrom ?? [];
|
|
3045
|
-
const pattern = kind === "sparseView" ? "sparseView" : "materializedView";
|
|
3046
|
-
for (const d of decls) {
|
|
3047
|
-
if (kind === "sparseView" && !d.when) {
|
|
3048
|
-
throw new Error(
|
|
3049
|
-
`View '${viewName}': \`kind: 'sparseView'\` requires every \`@maintainedFrom\` to declare a \`when\` membership predicate (e.g. \`whenMember(source.status, 'eq', 'active')\`) \u2014 the predicate decides PUT (holds) vs DELETE (flips false).`
|
|
3050
|
-
);
|
|
3051
|
-
}
|
|
3052
|
-
if (kind !== "sparseView" && d.when) {
|
|
3053
|
-
throw new Error(
|
|
3054
|
-
`View '${viewName}': a \`@maintainedFrom\` declares a \`when\` membership predicate but the view \`kind\` is '${kind}'. Declare \`kind: 'sparseView'\` to maintain a predicate-gated put/delete row.`
|
|
3055
|
-
);
|
|
3056
|
-
}
|
|
2172
|
+
const keyInput = {};
|
|
2173
|
+
for (const fieldName of meta.primaryKey.inputFieldNames) {
|
|
2174
|
+
keyInput[fieldName] = serialized[fieldName] ?? item[fieldName];
|
|
3057
2175
|
}
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
project: d.project,
|
|
3063
|
-
collectionField: d.collection?.field
|
|
3064
|
-
}))
|
|
2176
|
+
const { pk, sk } = resolveSegmentedKey(
|
|
2177
|
+
meta.primaryKey.segmented,
|
|
2178
|
+
keyInput,
|
|
2179
|
+
`${modelClass.name} primary key`
|
|
3065
2180
|
);
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
if (updateMode !== void 0 && updateMode !== d.updateMode) {
|
|
3071
|
-
throw new Error(
|
|
3072
|
-
`View '${viewName}': conflicting \`updateMode\` across \`@maintainedFrom\` declarations ('${updateMode}' vs '${d.updateMode}'). A view row has a single write path \u2014 declare the same \`updateMode\` on every source.`
|
|
3073
|
-
);
|
|
3074
|
-
}
|
|
3075
|
-
updateMode = d.updateMode;
|
|
3076
|
-
}
|
|
3077
|
-
if (d.consistency !== void 0) {
|
|
3078
|
-
if (consistency !== void 0 && consistency !== d.consistency) {
|
|
3079
|
-
throw new Error(
|
|
3080
|
-
`View '${viewName}': conflicting \`consistency\` across \`@maintainedFrom\` declarations ('${consistency}' vs '${d.consistency}').`
|
|
3081
|
-
);
|
|
3082
|
-
}
|
|
3083
|
-
consistency = d.consistency;
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
const slices = decls.map((d) => sliceFromDeclaration(d));
|
|
3087
|
-
return {
|
|
3088
|
-
name: viewName,
|
|
3089
|
-
viewClass,
|
|
3090
|
-
pattern,
|
|
3091
|
-
slices,
|
|
3092
|
-
...updateMode !== void 0 ? { updateMode } : {},
|
|
3093
|
-
...consistency !== void 0 ? { consistency } : {}
|
|
3094
|
-
};
|
|
3095
|
-
}
|
|
3096
|
-
function sliceFromDeclaration(d) {
|
|
3097
|
-
const sourceClass = resolveClass(d.sourceFactory);
|
|
3098
|
-
const sourceLogical = sourceLogicalName(sourceClass);
|
|
3099
|
-
const collection = d.collection ? {
|
|
3100
|
-
field: d.collection.field,
|
|
3101
|
-
...d.collection.maxItems !== void 0 ? { maxItems: d.collection.maxItems } : {},
|
|
3102
|
-
...d.collection.orderBy !== void 0 ? { orderBy: d.collection.orderBy, orderDir: d.collection.order ?? "DESC" } : {}
|
|
3103
|
-
} : void 0;
|
|
3104
|
-
return {
|
|
3105
|
-
sourceClass,
|
|
3106
|
-
maintainedOn: triggersFor(sourceLogical, d.on),
|
|
3107
|
-
keys: d.keyBind,
|
|
3108
|
-
project: d.project,
|
|
3109
|
-
...collection ? { collection } : {},
|
|
3110
|
-
...d.when ? { predicate: d.when } : {}
|
|
2181
|
+
const dynamoItem = {
|
|
2182
|
+
PK: pk,
|
|
2183
|
+
SK: sk ?? "",
|
|
2184
|
+
...serialized
|
|
3111
2185
|
};
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
for (const rel of ownerMeta.relations) {
|
|
3117
|
-
const v = rel.options?.versioned;
|
|
3118
|
-
if (!v) continue;
|
|
3119
|
-
const targetClass = resolveClass(rel.targetFactory);
|
|
3120
|
-
const targetMeta = MetadataRegistry.get(targetClass);
|
|
3121
|
-
if (!targetMeta.primaryKey) {
|
|
3122
|
-
throw new Error(
|
|
3123
|
-
`Versioned relation '${rel.propertyName}' on '${ownerLogical}': the target model '${targetClass.name}' has no \`key\` \u2014 a versioned latest/history row needs a key to resolve which row to overwrite / append.`
|
|
3124
|
-
);
|
|
2186
|
+
for (const emb of meta.embeddedFields) {
|
|
2187
|
+
const value = item[emb.propertyName];
|
|
2188
|
+
if (value !== void 0) {
|
|
2189
|
+
dynamoItem[emb.propertyName] = value;
|
|
3125
2190
|
}
|
|
3126
|
-
const keyFields = [
|
|
3127
|
-
...segmentFieldNames(targetMeta.primaryKey.segmented.pkSegments),
|
|
3128
|
-
...segmentFieldNames(targetMeta.primaryKey.segmented.skSegments)
|
|
3129
|
-
];
|
|
3130
|
-
rels.push({
|
|
3131
|
-
propertyName: rel.propertyName,
|
|
3132
|
-
mode: v.mode,
|
|
3133
|
-
on: v.on,
|
|
3134
|
-
project: v.project,
|
|
3135
|
-
...v.updateMode !== void 0 ? { updateMode: v.updateMode } : {},
|
|
3136
|
-
...v.consistency !== void 0 ? { consistency: v.consistency } : {},
|
|
3137
|
-
targetClass,
|
|
3138
|
-
targetMeta,
|
|
3139
|
-
keyFields
|
|
3140
|
-
});
|
|
3141
2191
|
}
|
|
3142
|
-
|
|
3143
|
-
const
|
|
3144
|
-
|
|
3145
|
-
const keys = {};
|
|
3146
|
-
for (const f of rel.keyFields) keys[f] = `$.entity.${f}`;
|
|
3147
|
-
const targetName = rel.targetClass.name ?? logicalNameOf(rel.targetMeta);
|
|
3148
|
-
out.push({
|
|
3149
|
-
name: targetName,
|
|
3150
|
-
viewClass: rel.targetClass,
|
|
3151
|
-
pattern: "materializedView",
|
|
3152
|
-
slices: [
|
|
3153
|
-
{
|
|
3154
|
-
sourceClass: ownerClass,
|
|
3155
|
-
maintainedOn: triggersFor(ownerLogical, rel.on),
|
|
3156
|
-
keys,
|
|
3157
|
-
project: rel.project
|
|
3158
|
-
}
|
|
3159
|
-
],
|
|
3160
|
-
...rel.updateMode !== void 0 ? { updateMode: rel.updateMode } : {},
|
|
3161
|
-
...rel.consistency !== void 0 ? { consistency: rel.consistency } : {}
|
|
3162
|
-
});
|
|
2192
|
+
const available = {};
|
|
2193
|
+
for (const fieldName of meta.fields.map((f) => f.propertyName)) {
|
|
2194
|
+
available[fieldName] = serialized[fieldName] ?? item[fieldName];
|
|
3163
2195
|
}
|
|
3164
|
-
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
for (const history of histories) {
|
|
3173
|
-
const discriminators = history.keyFields.filter((f) => !latestKeyFields.has(f));
|
|
3174
|
-
if (discriminators.length === 0) {
|
|
3175
|
-
const latestName = latest.targetClass.name ?? "<latest>";
|
|
3176
|
-
const historyName = history.targetClass.name ?? "<history>";
|
|
3177
|
-
throw new Error(
|
|
3178
|
-
`Versioned relation pair on '${ownerLogical}': the \`versionedHistory\` target '${historyName}' (\`${history.propertyName}\`) is keyed by [${history.keyFields.map((f) => `'${f}'`).join(", ")}], which carries NO version discriminator beyond the \`versionedLatest\` target '${latestName}' (\`${latest.propertyName}\`) keyed by [${[...latestKeyFields].map((f) => `'${f}'`).join(", ")}]. The history row must be keyed by a per-version field (e.g. add a \`version\` segment to '${historyName}'s \`@key\`) so each revision is a NEW row; otherwise every revision overwrites one row (a latest pointer, not an append-only history).`
|
|
3179
|
-
);
|
|
3180
|
-
}
|
|
3181
|
-
}
|
|
2196
|
+
for (const gsi2 of meta.gsiDefinitions) {
|
|
2197
|
+
const { pkAttr, pk: gsiPk, skAttr, sk: gsiSk } = deriveGsiKey(
|
|
2198
|
+
gsi2,
|
|
2199
|
+
available,
|
|
2200
|
+
`${modelClass.name} GSI '${gsi2.indexName}'`
|
|
2201
|
+
);
|
|
2202
|
+
dynamoItem[pkAttr] = gsiPk;
|
|
2203
|
+
dynamoItem[skAttr] = gsiSk;
|
|
3182
2204
|
}
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
const
|
|
3189
|
-
|
|
3190
|
-
|
|
2205
|
+
const putInput = {
|
|
2206
|
+
TableName: tableName,
|
|
2207
|
+
Item: dynamoItem
|
|
2208
|
+
};
|
|
2209
|
+
if (options?.condition) {
|
|
2210
|
+
const condResult = buildConditionExpression(options.condition);
|
|
2211
|
+
putInput.ConditionExpression = condResult.expression;
|
|
2212
|
+
if (Object.keys(condResult.names).length > 0) {
|
|
2213
|
+
putInput.ExpressionAttributeNames = condResult.names;
|
|
3191
2214
|
}
|
|
3192
|
-
if (
|
|
3193
|
-
|
|
2215
|
+
if (Object.keys(condResult.values).length > 0) {
|
|
2216
|
+
putInput.ExpressionAttributeValues = condResult.values;
|
|
3194
2217
|
}
|
|
3195
2218
|
}
|
|
3196
|
-
return
|
|
3197
|
-
}
|
|
3198
|
-
|
|
3199
|
-
// src/relation/maintenance-graph.ts
|
|
3200
|
-
var COUNT_DELTA_FOR_EVENT = {
|
|
3201
|
-
created: 1,
|
|
3202
|
-
removed: -1,
|
|
3203
|
-
updated: 0
|
|
3204
|
-
};
|
|
3205
|
-
var MAINTAIN_EVENTS = ["created", "updated", "removed"];
|
|
3206
|
-
function isPathRooted(value) {
|
|
3207
|
-
return value.startsWith("$.input.") || value.startsWith("$.entity.");
|
|
3208
|
-
}
|
|
3209
|
-
function sourceAttributeOf(path) {
|
|
3210
|
-
const stripped = path.replace(/^\$\.(input|entity)\./, "");
|
|
3211
|
-
return stripped.split(".")[0] ?? stripped;
|
|
3212
|
-
}
|
|
3213
|
-
function normalizeProjectionEntry(value) {
|
|
3214
|
-
if (typeof value === "string") {
|
|
3215
|
-
const path = isPathRooted(value) ? value : `$.entity.${value}`;
|
|
3216
|
-
return identity(path);
|
|
3217
|
-
}
|
|
3218
|
-
return value;
|
|
3219
|
-
}
|
|
3220
|
-
function buildProjectionMap(projection, ownerEntity, relationProperty) {
|
|
3221
|
-
const entries = projection ? Object.entries(projection) : [];
|
|
3222
|
-
if (entries.length === 0) {
|
|
3223
|
-
throw new Error(
|
|
3224
|
-
`Relation '${relationProperty}' on '${ownerEntity}' declares maintenance (\`write.maintainedOn\`) but no \`projection\`: a maintained snapshot / collection with no captured attributes projects nothing. Declare a \`projection\` map (e.g. \`{ postId: 'postId', textPreview: preview('body', 120) }\`).`
|
|
3225
|
-
);
|
|
3226
|
-
}
|
|
3227
|
-
const out = {};
|
|
3228
|
-
for (const [attr, value] of entries) {
|
|
3229
|
-
out[attr] = normalizeProjectionEntry(value);
|
|
3230
|
-
}
|
|
3231
|
-
return out;
|
|
2219
|
+
return putInput;
|
|
3232
2220
|
}
|
|
3233
|
-
function
|
|
3234
|
-
const
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
sourceFields.push(sourceField);
|
|
2221
|
+
async function executePut(modelClass, item, options) {
|
|
2222
|
+
const mw = writeRuntimeFor(options);
|
|
2223
|
+
if (mw) {
|
|
2224
|
+
await executeSingleWriteWithHooks({ kind: "put", modelClass, item, options }, mw);
|
|
2225
|
+
return;
|
|
3239
2226
|
}
|
|
3240
|
-
|
|
2227
|
+
await executePutCore(modelClass, item, options);
|
|
3241
2228
|
}
|
|
3242
|
-
function
|
|
3243
|
-
const
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
}
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
sourceEntity,
|
|
3261
|
-
sourceMeta,
|
|
3262
|
-
destinationKeyFields,
|
|
3263
|
-
keyBindingSourceFields,
|
|
3264
|
-
projection
|
|
3265
|
-
} = args;
|
|
3266
|
-
const sourceAttrs = sourcePayloadAttributes(sourceMeta);
|
|
3267
|
-
try {
|
|
3268
|
-
resolveKey([...destinationKeyFields], ownerMeta);
|
|
3269
|
-
} catch (cause) {
|
|
3270
|
-
throw new Error(
|
|
3271
|
-
`Maintenance declaration for relation '${relationProperty}' on '${ownerEntity}' binds destination-key fields [${destinationKeyFields.map((f) => `'${f}'`).join(", ")}], which cover no access-pattern partition of the maintained entity '${ownerEntity}'. The maintained snapshot / collection row could not be resolved, so the snapshot key would not point at a real target row \u2014 bind the partition-key field(s) of the index the maintained row lives on. (${cause.message})`
|
|
3272
|
-
);
|
|
2229
|
+
async function executePutCore(modelClass, item, options, persistVia, forceOldImage) {
|
|
2230
|
+
const putInput = buildPutInput(modelClass, item, options);
|
|
2231
|
+
const wantsOld = ChangeCaptureRegistry.wantsOldImage(modelClass) || forceOldImage === true;
|
|
2232
|
+
const send = (input) => ClientManager.getExecutor().put(input, {
|
|
2233
|
+
...wantsOld ? { returnOldImage: true } : {},
|
|
2234
|
+
...options?.retry !== void 0 ? { retry: options.retry } : {}
|
|
2235
|
+
});
|
|
2236
|
+
const result = persistVia ? await persistVia(putInput, send) : await send(putInput);
|
|
2237
|
+
if (ChangeCaptureRegistry.hasSubscribers()) {
|
|
2238
|
+
const oldItem = result.oldItem;
|
|
2239
|
+
captureWrite({
|
|
2240
|
+
table: putInput.TableName,
|
|
2241
|
+
model: modelClass.name,
|
|
2242
|
+
op: "put",
|
|
2243
|
+
keys: { pk: String(putInput.Item.PK), sk: String(putInput.Item.SK ?? "") },
|
|
2244
|
+
newItem: putInput.Item,
|
|
2245
|
+
...oldItem ? { oldItem } : {}
|
|
2246
|
+
});
|
|
3273
2247
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
// src/runtime/same-key-collapse.ts
|
|
2251
|
+
function collapseSameKey(items, view) {
|
|
2252
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2253
|
+
for (const item of items) {
|
|
2254
|
+
const sig = view.keySignature(item);
|
|
2255
|
+
const bucket = groups.get(sig);
|
|
2256
|
+
if (bucket === void 0) groups.set(sig, [item]);
|
|
2257
|
+
else bucket.push(item);
|
|
3280
2258
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
2259
|
+
const replacement = /* @__PURE__ */ new Map();
|
|
2260
|
+
const dropped = /* @__PURE__ */ new Set();
|
|
2261
|
+
for (const bucket of groups.values()) {
|
|
2262
|
+
if (bucket.length === 1) continue;
|
|
2263
|
+
const kinds = new Set(bucket.map((it) => view.kind(it)));
|
|
2264
|
+
if (kinds.size === 2 && kinds.has("Delete") && kinds.has("Put")) {
|
|
2265
|
+
for (const it of bucket) dropped.add(it);
|
|
2266
|
+
continue;
|
|
3286
2267
|
}
|
|
3287
|
-
|
|
3288
|
-
if (!sourceAttrs.has(srcAttr)) {
|
|
2268
|
+
if (kinds.size === 2 && kinds.has("Put") && kinds.has("Update")) {
|
|
3289
2269
|
throw new Error(
|
|
3290
|
-
`
|
|
2270
|
+
`Contract runtime: a derived counter (\`increment\`) resolves to the SAME item as the entity write at runtime (the call-time key values made an aliased target coincide with the written row). A Put+Update on one transaction item is unsupported \u2014 DynamoDB rejects touching one key twice, and collapsing it would silently drop the increment. A \`w.increment(...)\` must target a DIFFERENT row (e.g. a parent aggregate's counter); to maintain a counter ON the written entity, write the field as part of the entity itself, not via a derived counter onto its own row.`
|
|
3291
2271
|
);
|
|
3292
2272
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
ownerEntity,
|
|
3298
|
-
aggregateField,
|
|
3299
|
-
ownerMeta,
|
|
3300
|
-
sourceEntity,
|
|
3301
|
-
sourceMeta,
|
|
3302
|
-
destinationKeyFields,
|
|
3303
|
-
keyBindingSourceFields,
|
|
3304
|
-
valueSourceFields
|
|
3305
|
-
} = args;
|
|
3306
|
-
const sourceAttrs = sourcePayloadAttributes(sourceMeta);
|
|
3307
|
-
try {
|
|
3308
|
-
resolveKey([...destinationKeyFields], ownerMeta);
|
|
3309
|
-
} catch (cause) {
|
|
3310
|
-
throw new Error(
|
|
3311
|
-
`Counter aggregate '${aggregateField}' on '${ownerEntity}' binds destination-key fields [${destinationKeyFields.map((f) => `'${f}'`).join(", ")}], which cover no access-pattern partition of the maintained entity '${ownerEntity}'. The counter row could not be resolved \u2014 bind the partition-key field(s) of the index the counter row lives on. (${cause.message})`
|
|
3312
|
-
);
|
|
3313
|
-
}
|
|
3314
|
-
for (const field of keyBindingSourceFields) {
|
|
3315
|
-
if (!sourceAttrs.has(field)) {
|
|
3316
|
-
throw new Error(
|
|
3317
|
-
`Counter aggregate '${aggregateField}' on '${ownerEntity}' reads source field '${field}' to resolve the counter row, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). The destination key could not be built from the source payload \u2014 reject.`
|
|
3318
|
-
);
|
|
2273
|
+
if (kinds.size === 1 && kinds.has("Update")) {
|
|
2274
|
+
replacement.set(bucket[0], view.mergeAdds(bucket));
|
|
2275
|
+
for (const it of bucket.slice(1)) dropped.add(it);
|
|
2276
|
+
continue;
|
|
3319
2277
|
}
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
throw new Error(
|
|
3324
|
-
`Counter aggregate '${aggregateField}' on '${ownerEntity}' tracks \`max('${field}')\`, but the source entity '${sourceEntity}' carries no attribute '${field}' on its payload (available: ${[...sourceAttrs].sort().map((f) => `'${f}'`).join(", ")}). A counter aggregates only the source payload (payload \u540C\u68B1) \u2014 reject.`
|
|
3325
|
-
);
|
|
2278
|
+
if (kinds.size === 1 && (kinds.has("Put") || kinds.has("Delete"))) {
|
|
2279
|
+
for (const it of bucket.slice(1)) dropped.add(it);
|
|
2280
|
+
continue;
|
|
3326
2281
|
}
|
|
2282
|
+
throw new Error(
|
|
2283
|
+
`Contract runtime: ${bucket.length} transaction items resolve to the same physical row with an unsupported op combination (${[...kinds].sort().join("+")}). A TransactWriteItems may not touch one key twice; this collision is not a known collapse shape (self-row Put/Delete de-dup, swap no-op, or symmetric counter ADDs), so it is rejected rather than silently collapsed.`
|
|
2284
|
+
);
|
|
3327
2285
|
}
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
function resolveEntityByName(name, registry) {
|
|
3333
|
-
for (const [klass, meta] of registry) {
|
|
3334
|
-
if (logicalNameOf2(meta) === name) return klass;
|
|
2286
|
+
const out = [];
|
|
2287
|
+
for (const item of items) {
|
|
2288
|
+
if (dropped.has(item)) continue;
|
|
2289
|
+
out.push(replacement.get(item) ?? item);
|
|
3335
2290
|
}
|
|
3336
|
-
return
|
|
2291
|
+
return out;
|
|
3337
2292
|
}
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
targetFactory: ownerFactory,
|
|
3349
|
-
keys,
|
|
3350
|
-
project,
|
|
3351
|
-
...updateMode !== void 0 ? { updateMode } : {},
|
|
3352
|
-
...consistency !== void 0 ? { consistency } : {}
|
|
3353
|
-
};
|
|
3354
|
-
if (relation.type === "hasMany") {
|
|
3355
|
-
const read = relation.options?.read;
|
|
3356
|
-
const order = resolveCollectionOrder(relation, sourceMeta, ownerEntity, project);
|
|
3357
|
-
const collection = {
|
|
3358
|
-
field: relation.propertyName,
|
|
3359
|
-
...read?.maxItems !== void 0 ? { maxItems: read.maxItems } : {},
|
|
3360
|
-
...order !== void 0 ? { orderBy: order.orderBy, orderDir: order.orderDir } : {}
|
|
3361
|
-
};
|
|
3362
|
-
return { kind: "collection", ...base, collection };
|
|
3363
|
-
}
|
|
3364
|
-
return { kind: "snapshot", ...base };
|
|
3365
|
-
}
|
|
3366
|
-
function viewSliceProperty(viewName, sliceIndex, slice) {
|
|
3367
|
-
return slice.collection ? `${viewName}#${slice.collection.field}` : `${viewName}#${sliceIndex}`;
|
|
3368
|
-
}
|
|
3369
|
-
function buildViewEffect(view, slice, trigger) {
|
|
3370
|
-
const targetFactory = () => view.viewClass;
|
|
3371
|
-
const base = {
|
|
3372
|
-
trigger,
|
|
3373
|
-
targetFactory,
|
|
3374
|
-
keys: slice.keys,
|
|
3375
|
-
project: slice.project,
|
|
3376
|
-
...view.updateMode !== void 0 ? { updateMode: view.updateMode } : {},
|
|
3377
|
-
...view.consistency !== void 0 ? { consistency: view.consistency } : {}
|
|
3378
|
-
};
|
|
3379
|
-
if (slice.predicate) {
|
|
3380
|
-
return { kind: "membership", ...base, predicate: slice.predicate };
|
|
3381
|
-
}
|
|
3382
|
-
if (slice.collection) {
|
|
3383
|
-
const collection = {
|
|
3384
|
-
field: slice.collection.field,
|
|
3385
|
-
...slice.collection.maxItems !== void 0 ? { maxItems: slice.collection.maxItems } : {},
|
|
3386
|
-
...slice.collection.orderBy !== void 0 ? {
|
|
3387
|
-
orderBy: slice.collection.orderBy,
|
|
3388
|
-
orderDir: slice.collection.orderDir ?? "DESC"
|
|
3389
|
-
} : {}
|
|
3390
|
-
};
|
|
3391
|
-
return { kind: "collection", ...base, collection };
|
|
3392
|
-
}
|
|
3393
|
-
return { kind: "snapshot", ...base };
|
|
2293
|
+
|
|
2294
|
+
// src/runtime/transaction-commit.ts
|
|
2295
|
+
var MAX_TRANSACT_ITEMS = 25;
|
|
2296
|
+
var execItemCollapseView = {
|
|
2297
|
+
kind: execItemKind,
|
|
2298
|
+
keySignature: execItemKeySignature,
|
|
2299
|
+
mergeAdds: mergeAddUpdates
|
|
2300
|
+
};
|
|
2301
|
+
function collapseSameKeyItems(items) {
|
|
2302
|
+
return collapseSameKey(items, execItemCollapseView);
|
|
3394
2303
|
}
|
|
3395
|
-
function
|
|
3396
|
-
const
|
|
3397
|
-
if (
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
throw new Error(
|
|
3402
|
-
`Relation '${relation.propertyName}' on '${ownerEntity}' declares \`read.order: '${order}'\` for its maintained collection, but its source entity '${logicalNameOf2(sourceMeta)}' has no sort key to order by. A maintained collection orders by the source's sort-key field (the field the live read sorts on); declare a sort key on '${logicalNameOf2(sourceMeta)}' or drop \`read.order\`.`
|
|
2304
|
+
async function commitTransaction(items, options = {}) {
|
|
2305
|
+
const collapsed = collapseSameKeyItems(items);
|
|
2306
|
+
if (collapsed.length === 0) return collapsed;
|
|
2307
|
+
if (collapsed.length > MAX_TRANSACT_ITEMS) {
|
|
2308
|
+
throw options.limitError ? options.limitError(collapsed.length) : new Error(
|
|
2309
|
+
`Transaction exceeds DynamoDB limit of ${MAX_TRANSACT_ITEMS} items`
|
|
3403
2310
|
);
|
|
3404
2311
|
}
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
2312
|
+
const sendBatch = (items2) => ClientManager.getExecutor().transactWrite(
|
|
2313
|
+
items2,
|
|
2314
|
+
options.retry !== void 0 ? { retry: options.retry } : void 0
|
|
2315
|
+
);
|
|
2316
|
+
let committed = collapsed;
|
|
2317
|
+
if (options.middleware) {
|
|
2318
|
+
const { runtime, origins, transaction } = options.middleware;
|
|
2319
|
+
const persistCtx = runtime.persistCtx(collapsed, origins, transaction);
|
|
2320
|
+
await runtime.runPersist(persistCtx, async (items2) => sendBatch(items2));
|
|
2321
|
+
committed = persistCtx.items;
|
|
2322
|
+
} else {
|
|
2323
|
+
await sendBatch(collapsed);
|
|
3409
2324
|
}
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
function buildCounterEffect(aggregate, ownerFactory, trigger, event, keys) {
|
|
3413
|
-
const write = aggregate.options.write;
|
|
3414
|
-
const updateMode = write?.updateMode;
|
|
3415
|
-
const consistency = write?.consistency;
|
|
3416
|
-
const value = aggregate.options.value.op === "count" ? { op: "count" } : { op: "max", field: aggregate.options.value.field };
|
|
3417
|
-
return {
|
|
3418
|
-
kind: "counter",
|
|
3419
|
-
trigger,
|
|
3420
|
-
targetFactory: ownerFactory,
|
|
3421
|
-
keys,
|
|
3422
|
-
attribute: aggregate.propertyName,
|
|
3423
|
-
value,
|
|
3424
|
-
...value.op === "count" ? { delta: COUNT_DELTA_FOR_EVENT[event] } : {},
|
|
3425
|
-
...updateMode !== void 0 ? { updateMode } : {},
|
|
3426
|
-
...consistency !== void 0 ? { consistency } : {}
|
|
3427
|
-
};
|
|
3428
|
-
}
|
|
3429
|
-
function destinationRowKeyOf(ownerEntity, keys) {
|
|
3430
|
-
const parts = Object.entries(keys).map(([field, path]) => `${field}=${path}`).sort();
|
|
3431
|
-
return `${ownerEntity}#${parts.join("&")}`;
|
|
2325
|
+
captureTransactItems(committed, options.modelBySignature ?? EMPTY_MODEL_MAP);
|
|
2326
|
+
return committed;
|
|
3432
2327
|
}
|
|
3433
|
-
|
|
3434
|
-
|
|
2328
|
+
var EMPTY_MODEL_MAP = /* @__PURE__ */ new Map();
|
|
2329
|
+
function captureTransactItems(items, modelBySignature) {
|
|
2330
|
+
if (!ChangeCaptureRegistry.hasSubscribers()) return;
|
|
3435
2331
|
for (const item of items) {
|
|
3436
|
-
if (
|
|
3437
|
-
|
|
3438
|
-
}
|
|
3439
|
-
const WHITE = 0;
|
|
3440
|
-
const GRAY = 1;
|
|
3441
|
-
const BLACK = 2;
|
|
3442
|
-
const color = /* @__PURE__ */ new Map();
|
|
3443
|
-
for (const node of adjacency.keys()) color.set(node, WHITE);
|
|
3444
|
-
for (const start of adjacency.keys()) {
|
|
3445
|
-
if (color.get(start) !== WHITE) continue;
|
|
3446
|
-
const stack = [];
|
|
3447
|
-
const path = [];
|
|
3448
|
-
color.set(start, GRAY);
|
|
3449
|
-
path.push(start);
|
|
3450
|
-
stack.push({ node: start, iter: (adjacency.get(start) ?? /* @__PURE__ */ new Set()).values() });
|
|
3451
|
-
while (stack.length > 0) {
|
|
3452
|
-
const top = stack[stack.length - 1];
|
|
3453
|
-
const next = top.iter.next();
|
|
3454
|
-
if (next.done) {
|
|
3455
|
-
color.set(top.node, BLACK);
|
|
3456
|
-
stack.pop();
|
|
3457
|
-
path.pop();
|
|
3458
|
-
continue;
|
|
3459
|
-
}
|
|
3460
|
-
const child = next.value;
|
|
3461
|
-
const childColor = color.get(child) ?? WHITE;
|
|
3462
|
-
if (childColor === GRAY) {
|
|
3463
|
-
const from = path.indexOf(child);
|
|
3464
|
-
const cycle = [...path.slice(from), child].join(" \u2192 ");
|
|
3465
|
-
throw new Error(
|
|
3466
|
-
`Maintenance dependency cycle detected: ${cycle}. A maintenance trigger chain that loops back on itself is an unbounded cascade \u2014 break the cycle (an entity's maintained shape must not, transitively, be triggered by its own maintenance).`
|
|
3467
|
-
);
|
|
3468
|
-
}
|
|
3469
|
-
if (childColor === WHITE) {
|
|
3470
|
-
color.set(child, GRAY);
|
|
3471
|
-
path.push(child);
|
|
3472
|
-
stack.push({ node: child, iter: (adjacency.get(child) ?? /* @__PURE__ */ new Set()).values() });
|
|
3473
|
-
}
|
|
3474
|
-
}
|
|
2332
|
+
if ("ConditionCheck" in item) continue;
|
|
2333
|
+
captureWrite(captureMetaForItem(item, modelBySignature));
|
|
3475
2334
|
}
|
|
3476
2335
|
}
|
|
3477
|
-
function
|
|
3478
|
-
const
|
|
3479
|
-
const
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
);
|
|
3490
|
-
for (const relation of relations) {
|
|
3491
|
-
const maintainedOn = relation.options?.write?.maintainedOn;
|
|
3492
|
-
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
3493
|
-
const sourceClass = relation.targetFactory();
|
|
3494
|
-
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
3495
|
-
const relationTargetEntity = logicalNameOf2(sourceMeta);
|
|
3496
|
-
const project = buildProjectionMap(
|
|
3497
|
-
relation.options?.projection,
|
|
3498
|
-
ownerEntity,
|
|
3499
|
-
relation.propertyName
|
|
3500
|
-
);
|
|
3501
|
-
const { keys, sourceFields } = buildDestinationKeys(relation.keyBinding);
|
|
3502
|
-
const destinationKeyFields = Object.keys(keys);
|
|
3503
|
-
for (const raw of maintainedOn) {
|
|
3504
|
-
if (!isMaintainTrigger(raw)) {
|
|
3505
|
-
const { event } = parseTrigger(raw);
|
|
3506
|
-
throw new Error(
|
|
3507
|
-
`Maintenance trigger ${JSON.stringify(raw)} on relation '${relation.propertyName}' of '${ownerEntity}' is not a well-formed \`Entity.event\` trigger (event must be one of ${MAINTAIN_EVENTS.map((e) => `'${e}'`).join(" / ")}; got '${event}'). Fix the \`write.maintainedOn\` entry.`
|
|
3508
|
-
);
|
|
3509
|
-
}
|
|
3510
|
-
const trigger = raw;
|
|
3511
|
-
const { entity: triggerEntity } = parseTrigger(trigger);
|
|
3512
|
-
if (!resolveEntityByName(triggerEntity, registry)) {
|
|
3513
|
-
throw new Error(
|
|
3514
|
-
`Maintenance trigger '${trigger}' on relation '${relation.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', which is not a registered model. Declare a model named '${triggerEntity}' or fix the trigger's entity segment.`
|
|
3515
|
-
);
|
|
3516
|
-
}
|
|
3517
|
-
if (triggerEntity !== relationTargetEntity) {
|
|
3518
|
-
throw new Error(
|
|
3519
|
-
`Maintenance trigger '${trigger}' on relation '${relation.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', but the relation's target (the projected source) is '${relationTargetEntity}'. #124 projects only from the relation target's payload (payload \u540C\u68B1); a trigger on a different entity needs a write-time context fetch, which is Phase 2 \u2014 restrict \`maintainedOn\` to '${relationTargetEntity}.*' triggers.`
|
|
3520
|
-
);
|
|
3521
|
-
}
|
|
3522
|
-
assertMaintenanceRoundTrips({
|
|
3523
|
-
ownerEntity,
|
|
3524
|
-
relationProperty: relation.propertyName,
|
|
3525
|
-
ownerMeta,
|
|
3526
|
-
sourceEntity: triggerEntity,
|
|
3527
|
-
sourceMeta,
|
|
3528
|
-
destinationKeyFields,
|
|
3529
|
-
keyBindingSourceFields: sourceFields,
|
|
3530
|
-
projection: project
|
|
3531
|
-
});
|
|
3532
|
-
const effect = buildEffect(
|
|
3533
|
-
relation,
|
|
3534
|
-
() => ownerClass,
|
|
3535
|
-
trigger,
|
|
3536
|
-
keys,
|
|
3537
|
-
project,
|
|
3538
|
-
sourceMeta,
|
|
3539
|
-
ownerEntity
|
|
3540
|
-
);
|
|
3541
|
-
items.push({
|
|
3542
|
-
trigger,
|
|
3543
|
-
sourceEntity: triggerEntity,
|
|
3544
|
-
sourceClass,
|
|
3545
|
-
ownerEntity,
|
|
3546
|
-
ownerClass,
|
|
3547
|
-
relationProperty: relation.propertyName,
|
|
3548
|
-
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
3549
|
-
effect
|
|
3550
|
-
});
|
|
3551
|
-
}
|
|
3552
|
-
}
|
|
3553
|
-
const aggregates = [...ownerMeta.aggregates].sort(
|
|
3554
|
-
(a, b) => a.propertyName < b.propertyName ? -1 : a.propertyName > b.propertyName ? 1 : 0
|
|
3555
|
-
);
|
|
3556
|
-
for (const agg of aggregates) {
|
|
3557
|
-
const maintainedOn = agg.options.write?.maintainedOn;
|
|
3558
|
-
if (!maintainedOn || maintainedOn.length === 0) continue;
|
|
3559
|
-
const sourceClass = agg.targetFactory();
|
|
3560
|
-
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
3561
|
-
const aggSourceEntity = logicalNameOf2(sourceMeta);
|
|
3562
|
-
const { keys, sourceFields } = buildDestinationKeys(agg.keyBinding);
|
|
3563
|
-
const destinationKeyFields = Object.keys(keys);
|
|
3564
|
-
const valueSourceFields = agg.options.value.op === "max" ? [agg.options.value.field] : [];
|
|
3565
|
-
for (const raw of maintainedOn) {
|
|
3566
|
-
if (!isMaintainTrigger(raw)) {
|
|
3567
|
-
const { event: event2 } = parseTrigger(raw);
|
|
3568
|
-
throw new Error(
|
|
3569
|
-
`Maintenance trigger ${JSON.stringify(raw)} on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' is not a well-formed \`Entity.event\` trigger (event must be one of ${MAINTAIN_EVENTS.map((e) => `'${e}'`).join(" / ")}; got '${event2}'). Fix the \`write.maintainedOn\` entry.`
|
|
3570
|
-
);
|
|
3571
|
-
}
|
|
3572
|
-
const trigger = raw;
|
|
3573
|
-
const parsed = parseTrigger(trigger);
|
|
3574
|
-
const triggerEntity = parsed.entity;
|
|
3575
|
-
const event = parsed.event;
|
|
3576
|
-
if (!resolveEntityByName(triggerEntity, registry)) {
|
|
3577
|
-
throw new Error(
|
|
3578
|
-
`Maintenance trigger '${trigger}' on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', which is not a registered model. Declare a model named '${triggerEntity}' or fix the trigger's entity segment.`
|
|
3579
|
-
);
|
|
3580
|
-
}
|
|
3581
|
-
if (triggerEntity !== aggSourceEntity) {
|
|
3582
|
-
throw new Error(
|
|
3583
|
-
`Maintenance trigger '${trigger}' on \`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' names source entity '${triggerEntity}', but the aggregate's source (the entity being counted) is '${aggSourceEntity}'. A counter aggregates only the rows of its own source (payload \u540C\u68B1); a trigger on a different entity is Phase 2 \u2014 restrict \`maintainedOn\` to '${aggSourceEntity}.*' triggers.`
|
|
3584
|
-
);
|
|
3585
|
-
}
|
|
3586
|
-
if (agg.options.value.op === "count" && COUNT_DELTA_FOR_EVENT[event] === 0) {
|
|
3587
|
-
throw new Error(
|
|
3588
|
-
`\`@aggregate\` field '${agg.propertyName}' of '${ownerEntity}' is a \`count()\` counter maintained on '${trigger}', but a 'updated' event does not change a row count (it adds 0). A \`count()\` counter is maintained on 'created' (+1) and 'removed' (-1); drop the 'updated' trigger.`
|
|
3589
|
-
);
|
|
3590
|
-
}
|
|
3591
|
-
assertCounterRoundTrips({
|
|
3592
|
-
ownerEntity,
|
|
3593
|
-
aggregateField: agg.propertyName,
|
|
3594
|
-
ownerMeta,
|
|
3595
|
-
sourceEntity: triggerEntity,
|
|
3596
|
-
sourceMeta,
|
|
3597
|
-
destinationKeyFields,
|
|
3598
|
-
keyBindingSourceFields: sourceFields,
|
|
3599
|
-
valueSourceFields
|
|
3600
|
-
});
|
|
3601
|
-
const effect = buildCounterEffect(agg, () => ownerClass, trigger, event, keys);
|
|
3602
|
-
items.push({
|
|
3603
|
-
trigger,
|
|
3604
|
-
sourceEntity: triggerEntity,
|
|
3605
|
-
sourceClass,
|
|
3606
|
-
ownerEntity,
|
|
3607
|
-
ownerClass,
|
|
3608
|
-
relationProperty: agg.propertyName,
|
|
3609
|
-
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
3610
|
-
effect
|
|
3611
|
-
});
|
|
3612
|
-
}
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
for (const view of [...views].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
3616
|
-
const ownerClass = view.viewClass;
|
|
3617
|
-
const ownerMeta = MetadataRegistry.get(ownerClass);
|
|
3618
|
-
const ownerEntity = logicalNameOf2(ownerMeta);
|
|
3619
|
-
view.slices.forEach((slice, sliceIndex) => {
|
|
3620
|
-
const sourceClass = slice.sourceClass;
|
|
3621
|
-
const sourceMeta = MetadataRegistry.get(sourceClass);
|
|
3622
|
-
const sourceEntity = logicalNameOf2(sourceMeta);
|
|
3623
|
-
const keys = slice.keys;
|
|
3624
|
-
const destinationKeyFields = Object.keys(keys);
|
|
3625
|
-
const keyBindingSourceFields = Object.values(keys).map((p) => sourceAttributeOf(p));
|
|
3626
|
-
for (const trigger of slice.maintainedOn) {
|
|
3627
|
-
const { entity: triggerEntity } = parseTrigger(trigger);
|
|
3628
|
-
if (triggerEntity !== sourceEntity) {
|
|
3629
|
-
throw new Error(
|
|
3630
|
-
`defineView '${view.name}': source slice trigger '${trigger}' names entity '${triggerEntity}', but the slice's source entity is '${sourceEntity}'. A view source projects only from its own payload (payload \u540C\u68B1); a trigger on a different entity needs a write-time context fetch (RFC \xA74.A.6), which is a deferred follow-up \u2014 restrict the slice's \`maintainedOn\` to '${sourceEntity}.*' triggers.`
|
|
3631
|
-
);
|
|
3632
|
-
}
|
|
3633
|
-
assertMaintenanceRoundTrips({
|
|
3634
|
-
ownerEntity,
|
|
3635
|
-
relationProperty: viewSliceProperty(view.name, sliceIndex, slice),
|
|
3636
|
-
ownerMeta,
|
|
3637
|
-
sourceEntity,
|
|
3638
|
-
sourceMeta,
|
|
3639
|
-
destinationKeyFields,
|
|
3640
|
-
keyBindingSourceFields,
|
|
3641
|
-
projection: slice.project
|
|
3642
|
-
});
|
|
3643
|
-
if (slice.predicate) {
|
|
3644
|
-
if (!isPathRooted(slice.predicate.path)) {
|
|
3645
|
-
throw new Error(
|
|
3646
|
-
`defineView '${view.name}': sparse-view predicate path ${JSON.stringify(slice.predicate.path)} is not a payload-rooted source path (\`$.input.*\` / \`$.entity.*\`). The drain evaluates the predicate against the source image \u2014 use a source attribute path.`
|
|
3647
|
-
);
|
|
3648
|
-
}
|
|
3649
|
-
const predAttr = sourceAttributeOf(slice.predicate.path);
|
|
3650
|
-
if (!sourcePayloadAttributes(sourceMeta).has(predAttr)) {
|
|
3651
|
-
throw new Error(
|
|
3652
|
-
`defineView '${view.name}': sparse-view predicate reads source attribute '${predAttr}' (from '${slice.predicate.path}'), but the source entity '${sourceEntity}' carries no such attribute on its payload (available: ${[...sourcePayloadAttributes(sourceMeta)].sort().map((f) => `'${f}'`).join(", ")}). A membership predicate is payload \u540C\u68B1 only \u2014 reject.`
|
|
3653
|
-
);
|
|
3654
|
-
}
|
|
3655
|
-
}
|
|
3656
|
-
const effect = buildViewEffect(view, slice, trigger);
|
|
3657
|
-
items.push({
|
|
3658
|
-
trigger,
|
|
3659
|
-
sourceEntity,
|
|
3660
|
-
sourceClass,
|
|
3661
|
-
ownerEntity,
|
|
3662
|
-
ownerClass,
|
|
3663
|
-
relationProperty: viewSliceProperty(view.name, sliceIndex, slice),
|
|
3664
|
-
destinationRowKey: destinationRowKeyOf(ownerEntity, keys),
|
|
3665
|
-
effect
|
|
3666
|
-
});
|
|
3667
|
-
}
|
|
3668
|
-
});
|
|
2336
|
+
function captureMetaForItem(item, modelBySignature) {
|
|
2337
|
+
const model = modelBySignature.get(execItemKeySignature(item));
|
|
2338
|
+
const labelled = model !== void 0 ? { model } : {};
|
|
2339
|
+
if ("Put" in item) {
|
|
2340
|
+
const Item = item.Put.Item;
|
|
2341
|
+
return {
|
|
2342
|
+
table: item.Put.TableName,
|
|
2343
|
+
...labelled,
|
|
2344
|
+
op: "put",
|
|
2345
|
+
keys: { pk: String(Item.PK), sk: String(Item.SK ?? "") },
|
|
2346
|
+
newItem: Item
|
|
2347
|
+
};
|
|
3669
2348
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
const rowKey = `${item.trigger}\0${item.destinationRowKey}`;
|
|
3678
|
-
const rowBucket = sameRow.get(rowKey);
|
|
3679
|
-
if (rowBucket) rowBucket.push(item);
|
|
3680
|
-
else sameRow.set(rowKey, [item]);
|
|
3681
|
-
}
|
|
3682
|
-
const multiMaintainerTargets = /* @__PURE__ */ new Map();
|
|
3683
|
-
for (const [rowKey, bucket] of sameRow) {
|
|
3684
|
-
if (bucket.length > 1) multiMaintainerTargets.set(rowKey, bucket);
|
|
2349
|
+
if ("Update" in item) {
|
|
2350
|
+
return {
|
|
2351
|
+
table: item.Update.TableName,
|
|
2352
|
+
...labelled,
|
|
2353
|
+
op: "update",
|
|
2354
|
+
keys: { pk: String(item.Update.Key.PK), sk: String(item.Update.Key.SK ?? "") }
|
|
2355
|
+
};
|
|
3685
2356
|
}
|
|
3686
2357
|
return {
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
},
|
|
3692
|
-
multiMaintainerTargets
|
|
2358
|
+
table: item.Delete.TableName,
|
|
2359
|
+
...labelled,
|
|
2360
|
+
op: "delete",
|
|
2361
|
+
keys: { pk: String(item.Delete.Key.PK), sk: String(item.Delete.Key.SK ?? "") }
|
|
3693
2362
|
};
|
|
3694
2363
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
}
|
|
3702
|
-
return select;
|
|
2364
|
+
function execItemKeySignature(item) {
|
|
2365
|
+
const part = (rec) => `${String(rec?.PK ?? "")}#${String(rec?.SK ?? "")}`;
|
|
2366
|
+
if ("Put" in item) return `${item.Put.TableName}#${part(item.Put.Item)}`;
|
|
2367
|
+
if ("Update" in item) return `${item.Update.TableName}#${part(item.Update.Key)}`;
|
|
2368
|
+
if ("Delete" in item) return `${item.Delete.TableName}#${part(item.Delete.Key)}`;
|
|
2369
|
+
return `${item.ConditionCheck.TableName}#${part(item.ConditionCheck.Key)}`;
|
|
3703
2370
|
}
|
|
3704
|
-
function
|
|
3705
|
-
if (
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
return
|
|
2371
|
+
function execItemKind(item) {
|
|
2372
|
+
if ("Put" in item) return "Put";
|
|
2373
|
+
if ("Update" in item) return "Update";
|
|
2374
|
+
if ("Delete" in item) return "Delete";
|
|
2375
|
+
return "ConditionCheck";
|
|
3709
2376
|
}
|
|
3710
|
-
function
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
if (
|
|
3718
|
-
|
|
2377
|
+
function mergeAddUpdates(updates) {
|
|
2378
|
+
const first = updates[0];
|
|
2379
|
+
if (!("Update" in first)) {
|
|
2380
|
+
throw new Error("Contract runtime: mergeAddUpdates received a non-Update item.");
|
|
2381
|
+
}
|
|
2382
|
+
const deltas = /* @__PURE__ */ new Map();
|
|
2383
|
+
for (const it of updates) {
|
|
2384
|
+
if (!("Update" in it)) {
|
|
2385
|
+
throw new Error("Contract runtime: cannot merge a non-Update item into an ADD.");
|
|
2386
|
+
}
|
|
2387
|
+
const names2 = it.Update.ExpressionAttributeNames ?? {};
|
|
2388
|
+
const values2 = it.Update.ExpressionAttributeValues ?? {};
|
|
2389
|
+
for (const [namePlaceholder, attr] of Object.entries(names2)) {
|
|
2390
|
+
const valuePlaceholder = `:${namePlaceholder.slice(1)}`;
|
|
2391
|
+
const delta = values2[valuePlaceholder];
|
|
2392
|
+
if (typeof delta !== "number") {
|
|
2393
|
+
throw new Error(
|
|
2394
|
+
`Contract runtime: cannot merge same-key counter updates \u2014 an ADD delta on '${attr}' is not numeric.`
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
deltas.set(attr, (deltas.get(attr) ?? 0) + delta);
|
|
3719
2398
|
}
|
|
3720
2399
|
}
|
|
3721
|
-
|
|
3722
|
-
}
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
2400
|
+
const names = {};
|
|
2401
|
+
const values = {};
|
|
2402
|
+
const clauses = [];
|
|
2403
|
+
let i = 0;
|
|
2404
|
+
for (const [attr, delta] of deltas) {
|
|
2405
|
+
names[`#a${i}`] = attr;
|
|
2406
|
+
values[`:a${i}`] = delta;
|
|
2407
|
+
clauses.push(`#a${i} :a${i}`);
|
|
2408
|
+
i++;
|
|
3726
2409
|
}
|
|
3727
|
-
const
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
2410
|
+
const merged = {
|
|
2411
|
+
TableName: first.Update.TableName,
|
|
2412
|
+
Key: first.Update.Key,
|
|
2413
|
+
UpdateExpression: `ADD ${clauses.join(", ")}`,
|
|
2414
|
+
ExpressionAttributeNames: names,
|
|
2415
|
+
ExpressionAttributeValues: values
|
|
2416
|
+
};
|
|
2417
|
+
return { Update: merged };
|
|
3731
2418
|
}
|
|
3732
2419
|
|
|
3733
|
-
// src/
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
throw new Error(
|
|
3744
|
-
`DDBModel.subscribe: no model named '${modelName}' is registered. Did you import the model module (so its @model decorator ran) and spell the handler key exactly as the class name?`
|
|
3745
|
-
);
|
|
3746
|
-
}
|
|
3747
|
-
if (!entry.meta.cdcProjected) {
|
|
3748
|
-
throw new Error(
|
|
3749
|
-
`DDBModel.subscribe: model '${modelName}' is not @cdcProjected, so its change events cannot be parsed. Add @cdcProjected() to the model, or remove its handler.`
|
|
3750
|
-
);
|
|
3751
|
-
}
|
|
3752
|
-
routable.set(modelName, {
|
|
3753
|
-
modelClass: entry.cls,
|
|
3754
|
-
metadata: entry.meta,
|
|
3755
|
-
modelName
|
|
3756
|
-
});
|
|
3757
|
-
}
|
|
3758
|
-
return routable;
|
|
2420
|
+
// src/operations/transaction.ts
|
|
2421
|
+
var MODEL_CLASS_KEY = /* @__PURE__ */ Symbol.for("graphddb.modelClass");
|
|
2422
|
+
function attachModelClass(modelStatic, modelClass) {
|
|
2423
|
+
Object.defineProperty(modelStatic, MODEL_CLASS_KEY, {
|
|
2424
|
+
value: modelClass,
|
|
2425
|
+
enumerable: false,
|
|
2426
|
+
configurable: false,
|
|
2427
|
+
writable: false
|
|
2428
|
+
});
|
|
2429
|
+
return modelStatic;
|
|
3759
2430
|
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
modelName,
|
|
3766
|
-
modelClass
|
|
2431
|
+
function resolveModelClass(model) {
|
|
2432
|
+
const modelClass = model[MODEL_CLASS_KEY];
|
|
2433
|
+
if (!modelClass) {
|
|
2434
|
+
throw new Error(
|
|
2435
|
+
"Invalid model reference. Use ModelStatic from DDBModel.asModel()."
|
|
3767
2436
|
);
|
|
3768
|
-
if (oldRecord === null && newRecord === null) continue;
|
|
3769
|
-
const handler = handlers[modelName];
|
|
3770
|
-
await handler(oldRecord, newRecord);
|
|
3771
|
-
return true;
|
|
3772
2437
|
}
|
|
3773
|
-
return
|
|
3774
|
-
}
|
|
3775
|
-
function buildSubscribeHandler(handlers) {
|
|
3776
|
-
const routable = resolveRoutableModels(handlers);
|
|
3777
|
-
return async (batch) => {
|
|
3778
|
-
const batchItemFailures = [];
|
|
3779
|
-
for (const event of batch.records) {
|
|
3780
|
-
try {
|
|
3781
|
-
await dispatchEvent(event, routable, handlers);
|
|
3782
|
-
} catch {
|
|
3783
|
-
batchItemFailures.push(event.sequenceNumber);
|
|
3784
|
-
}
|
|
3785
|
-
}
|
|
3786
|
-
return { batchItemFailures };
|
|
3787
|
-
};
|
|
2438
|
+
return modelClass;
|
|
3788
2439
|
}
|
|
3789
2440
|
|
|
3790
2441
|
export {
|
|
@@ -3793,34 +2444,16 @@ export {
|
|
|
3793
2444
|
pkTemplate,
|
|
3794
2445
|
skTemplate,
|
|
3795
2446
|
resolveSegmentedKey,
|
|
3796
|
-
SELECT_SPEC,
|
|
3797
|
-
isSelectBuilder,
|
|
3798
|
-
normalizeSelectSpec,
|
|
3799
|
-
normalizeTopLevelSelect,
|
|
3800
|
-
buildProject,
|
|
3801
|
-
buildRelation,
|
|
3802
|
-
isInlineSnapshotSpec,
|
|
3803
|
-
detectRelationFields,
|
|
3804
|
-
validateInlineSnapshotSelect,
|
|
3805
|
-
getImplicitKeyFields,
|
|
3806
2447
|
param,
|
|
3807
2448
|
isParam,
|
|
3808
2449
|
BATCH_GET_MAX_KEYS,
|
|
3809
|
-
BATCH_WRITE_MAX_ITEMS,
|
|
3810
2450
|
chunkArray,
|
|
3811
|
-
DynamoExecutor,
|
|
3812
|
-
DEFAULT_MAX_ATTEMPTS,
|
|
3813
|
-
DEFAULT_RETRY_POLICY,
|
|
3814
|
-
isRetryableError,
|
|
3815
|
-
isRetryableTransactionCancellation,
|
|
3816
|
-
RetryingExecutor,
|
|
3817
2451
|
ClientManager,
|
|
3818
2452
|
isRawCondition,
|
|
3819
2453
|
cond,
|
|
3820
2454
|
compileRawCondition,
|
|
3821
2455
|
condParamName,
|
|
3822
2456
|
serializeRawCondition,
|
|
3823
|
-
collectRawConditionParams,
|
|
3824
2457
|
CONDITION_OPERATOR_KEYS,
|
|
3825
2458
|
resolveConditionTree,
|
|
3826
2459
|
buildConditionExpression,
|
|
@@ -3831,7 +2464,6 @@ export {
|
|
|
3831
2464
|
buildReadRuntime,
|
|
3832
2465
|
buildWriteRuntime,
|
|
3833
2466
|
ctxModelFor,
|
|
3834
|
-
buildUpdateExpression,
|
|
3835
2467
|
attachHiddenKey,
|
|
3836
2468
|
buildUpdateInput,
|
|
3837
2469
|
executeUpdate,
|
|
@@ -3840,27 +2472,8 @@ export {
|
|
|
3840
2472
|
buildPutInput,
|
|
3841
2473
|
executePut,
|
|
3842
2474
|
MAX_TRANSACT_ITEMS,
|
|
3843
|
-
collapseSameKeyItems,
|
|
3844
2475
|
commitTransaction,
|
|
3845
2476
|
execItemKeySignature,
|
|
3846
2477
|
attachModelClass,
|
|
3847
|
-
resolveModelClass
|
|
3848
|
-
TransactionContext,
|
|
3849
|
-
executeTransaction,
|
|
3850
|
-
hydrate,
|
|
3851
|
-
maintainTrigger,
|
|
3852
|
-
isMaintainTrigger,
|
|
3853
|
-
preview,
|
|
3854
|
-
identity,
|
|
3855
|
-
LIFECYCLE_CONTRACT_MARKER,
|
|
3856
|
-
isLifecycleContract,
|
|
3857
|
-
ENTITY_WRITES_MARKER,
|
|
3858
|
-
isEntityWritesDefinition,
|
|
3859
|
-
entityWrites,
|
|
3860
|
-
getEntityWrites,
|
|
3861
|
-
lifecyclePhaseForIntent,
|
|
3862
|
-
collectViewDefinitions,
|
|
3863
|
-
buildMaintenanceGraph,
|
|
3864
|
-
parseChange,
|
|
3865
|
-
buildSubscribeHandler
|
|
2478
|
+
resolveModelClass
|
|
3866
2479
|
};
|