graphddb 0.1.0 → 0.2.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 +63 -9
- package/dist/{chunk-6LEHSX45.js → chunk-F27INYI2.js} +2839 -1081
- package/dist/cli.js +28 -5
- package/dist/index.d.ts +59 -3318
- package/dist/index.js +22 -1822
- package/dist/testing/index.d.ts +2 -2
- package/dist/types-D6qLpw2M.d.ts +4514 -0
- package/package.json +7 -2
- package/dist/types-CDrWiPxp.d.ts +0 -1203
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BATCH_GET_MAX_KEYS,
|
|
3
|
+
ChangeCaptureRegistry,
|
|
4
|
+
ClientManager,
|
|
3
5
|
MAX_TRANSACT_ITEMS,
|
|
4
6
|
MetadataRegistry,
|
|
5
7
|
TableMapping,
|
|
8
|
+
attachHiddenKey,
|
|
9
|
+
attachModelClass,
|
|
10
|
+
buildConditionExpression,
|
|
11
|
+
buildDeleteInput,
|
|
12
|
+
buildPutInput,
|
|
13
|
+
buildUpdateInput,
|
|
14
|
+
captureWrite,
|
|
15
|
+
commitTransaction,
|
|
16
|
+
createColumnMap,
|
|
17
|
+
execItemKeySignature,
|
|
18
|
+
executeDelete,
|
|
19
|
+
executePut,
|
|
20
|
+
executeTransaction,
|
|
21
|
+
executeUpdate,
|
|
22
|
+
isColumn,
|
|
6
23
|
pkTemplate,
|
|
7
24
|
resolveKey,
|
|
8
25
|
resolveModelClass,
|
|
26
|
+
resolveSegmentedKey,
|
|
9
27
|
segmentFieldNames,
|
|
28
|
+
serializeFieldValue,
|
|
10
29
|
skTemplate
|
|
11
30
|
} from "./chunk-347U24SB.js";
|
|
12
31
|
|
|
@@ -262,6 +281,1367 @@ function isParam(value) {
|
|
|
262
281
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
|
|
263
282
|
}
|
|
264
283
|
|
|
284
|
+
// src/planner/projection.ts
|
|
285
|
+
function buildProjection(select, additionalFields) {
|
|
286
|
+
const paths = [];
|
|
287
|
+
const names = {};
|
|
288
|
+
let nameCounter = 0;
|
|
289
|
+
function addPath(segments) {
|
|
290
|
+
const aliased = segments.map((seg) => {
|
|
291
|
+
const alias = `#p${nameCounter++}`;
|
|
292
|
+
names[alias] = seg;
|
|
293
|
+
return alias;
|
|
294
|
+
});
|
|
295
|
+
paths.push(aliased.join("."));
|
|
296
|
+
}
|
|
297
|
+
function traverse(selectObj, parentSegments) {
|
|
298
|
+
for (const [fieldName, value] of Object.entries(selectObj)) {
|
|
299
|
+
if (value === true) {
|
|
300
|
+
addPath([...parentSegments, fieldName]);
|
|
301
|
+
} else if (typeof value === "object" && value !== null) {
|
|
302
|
+
if ("select" in value || isSelectBuilder(value)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
traverse(value, [...parentSegments, fieldName]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
traverse(select, []);
|
|
310
|
+
if (additionalFields) {
|
|
311
|
+
for (const fieldName of additionalFields) {
|
|
312
|
+
addPath([fieldName]);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
if (paths.length === 0) {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
projectionExpression: paths.join(", "),
|
|
320
|
+
expressionAttributeNames: names
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/select/cond.ts
|
|
325
|
+
var RAW_COND = /* @__PURE__ */ Symbol.for("graphddb.rawCond");
|
|
326
|
+
function isRawCondition(value) {
|
|
327
|
+
return typeof value === "object" && value !== null && value[RAW_COND] === true;
|
|
328
|
+
}
|
|
329
|
+
function cond(template, ...parts) {
|
|
330
|
+
return {
|
|
331
|
+
[RAW_COND]: true,
|
|
332
|
+
template,
|
|
333
|
+
parts
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function compileRawCondition(raw, allocName, allocValue) {
|
|
337
|
+
let out = raw.template[0];
|
|
338
|
+
for (let i = 0; i < raw.parts.length; i++) {
|
|
339
|
+
const part = raw.parts[i];
|
|
340
|
+
if (isColumn(part)) {
|
|
341
|
+
out += allocName(part.name);
|
|
342
|
+
} else {
|
|
343
|
+
out += allocValue(part);
|
|
344
|
+
}
|
|
345
|
+
out += raw.template[i + 1];
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/expression/filter-expression.ts
|
|
351
|
+
var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
352
|
+
var OPERATOR_KEYS = /* @__PURE__ */ new Set([
|
|
353
|
+
"eq",
|
|
354
|
+
"ne",
|
|
355
|
+
"gt",
|
|
356
|
+
"ge",
|
|
357
|
+
"lt",
|
|
358
|
+
"le",
|
|
359
|
+
"between",
|
|
360
|
+
"in",
|
|
361
|
+
"beginsWith",
|
|
362
|
+
"contains",
|
|
363
|
+
"notContains",
|
|
364
|
+
"attributeExists",
|
|
365
|
+
"attributeType",
|
|
366
|
+
"size"
|
|
367
|
+
]);
|
|
368
|
+
function nameAlias(ctx, column) {
|
|
369
|
+
for (const [alias2, col] of Object.entries(ctx.names)) {
|
|
370
|
+
if (col === column) return alias2;
|
|
371
|
+
}
|
|
372
|
+
const alias = `#f${ctx.nameCounter.n++}`;
|
|
373
|
+
ctx.names[alias] = column;
|
|
374
|
+
return alias;
|
|
375
|
+
}
|
|
376
|
+
function valueAlias(ctx, field, raw) {
|
|
377
|
+
const alias = `:vf${ctx.valueCounter.n++}`;
|
|
378
|
+
const fieldMeta = ctx.fieldMap.get(field);
|
|
379
|
+
ctx.values[alias] = fieldMeta ? serializeFieldValue(raw, fieldMeta) : raw;
|
|
380
|
+
return alias;
|
|
381
|
+
}
|
|
382
|
+
function compileField(ctx, field, condition) {
|
|
383
|
+
const nAlias = nameAlias(ctx, field);
|
|
384
|
+
if (!isOperatorObject(condition)) {
|
|
385
|
+
const vAlias = valueAlias(ctx, field, condition);
|
|
386
|
+
return `${nAlias} = ${vAlias}`;
|
|
387
|
+
}
|
|
388
|
+
const ops = condition;
|
|
389
|
+
const clauses = [];
|
|
390
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
391
|
+
switch (op) {
|
|
392
|
+
case "eq":
|
|
393
|
+
clauses.push(`${nAlias} = ${valueAlias(ctx, field, value)}`);
|
|
394
|
+
break;
|
|
395
|
+
case "ne":
|
|
396
|
+
clauses.push(`${nAlias} <> ${valueAlias(ctx, field, value)}`);
|
|
397
|
+
break;
|
|
398
|
+
case "gt":
|
|
399
|
+
clauses.push(`${nAlias} > ${valueAlias(ctx, field, value)}`);
|
|
400
|
+
break;
|
|
401
|
+
case "ge":
|
|
402
|
+
clauses.push(`${nAlias} >= ${valueAlias(ctx, field, value)}`);
|
|
403
|
+
break;
|
|
404
|
+
case "lt":
|
|
405
|
+
clauses.push(`${nAlias} < ${valueAlias(ctx, field, value)}`);
|
|
406
|
+
break;
|
|
407
|
+
case "le":
|
|
408
|
+
clauses.push(`${nAlias} <= ${valueAlias(ctx, field, value)}`);
|
|
409
|
+
break;
|
|
410
|
+
case "between": {
|
|
411
|
+
const [lo, hi] = value;
|
|
412
|
+
clauses.push(
|
|
413
|
+
`${nAlias} BETWEEN ${valueAlias(ctx, field, lo)} AND ${valueAlias(ctx, field, hi)}`
|
|
414
|
+
);
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "in": {
|
|
418
|
+
const arr = value;
|
|
419
|
+
const aliases = arr.map((v) => valueAlias(ctx, field, v));
|
|
420
|
+
clauses.push(`${nAlias} IN (${aliases.join(", ")})`);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case "beginsWith":
|
|
424
|
+
clauses.push(`begins_with(${nAlias}, ${valueAlias(ctx, field, value)})`);
|
|
425
|
+
break;
|
|
426
|
+
case "contains":
|
|
427
|
+
clauses.push(`contains(${nAlias}, ${valueAlias(ctx, field, value)})`);
|
|
428
|
+
break;
|
|
429
|
+
case "notContains":
|
|
430
|
+
clauses.push(
|
|
431
|
+
`NOT contains(${nAlias}, ${valueAlias(ctx, field, value)})`
|
|
432
|
+
);
|
|
433
|
+
break;
|
|
434
|
+
case "attributeExists":
|
|
435
|
+
clauses.push(
|
|
436
|
+
value === false ? `attribute_not_exists(${nAlias})` : `attribute_exists(${nAlias})`
|
|
437
|
+
);
|
|
438
|
+
break;
|
|
439
|
+
case "attributeType":
|
|
440
|
+
clauses.push(
|
|
441
|
+
`attribute_type(${nAlias}, ${valueAlias(ctx, field, value)})`
|
|
442
|
+
);
|
|
443
|
+
break;
|
|
444
|
+
case "size":
|
|
445
|
+
clauses.push(`size(${nAlias}) = ${valueAlias(ctx, field, value)}`);
|
|
446
|
+
break;
|
|
447
|
+
default:
|
|
448
|
+
throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return joinAnd(clauses);
|
|
452
|
+
}
|
|
453
|
+
function isOperatorObject(value) {
|
|
454
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array || value instanceof Set) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
const keys = Object.keys(value);
|
|
458
|
+
if (keys.length === 0) return false;
|
|
459
|
+
return keys.every((k) => OPERATOR_KEYS.has(k));
|
|
460
|
+
}
|
|
461
|
+
function joinAnd(clauses) {
|
|
462
|
+
if (clauses.length === 1) return clauses[0];
|
|
463
|
+
return clauses.map((c) => wrap(c)).join(" AND ");
|
|
464
|
+
}
|
|
465
|
+
function wrap(expr) {
|
|
466
|
+
if (isAlreadyWrapped(expr)) return expr;
|
|
467
|
+
if (expr.includes(" AND ") || expr.includes(" OR ")) {
|
|
468
|
+
return `(${expr})`;
|
|
469
|
+
}
|
|
470
|
+
return expr;
|
|
471
|
+
}
|
|
472
|
+
function isAlreadyWrapped(expr) {
|
|
473
|
+
if (!expr.startsWith("(") || !expr.endsWith(")")) return false;
|
|
474
|
+
let depth = 0;
|
|
475
|
+
for (let i = 0; i < expr.length; i++) {
|
|
476
|
+
if (expr[i] === "(") depth++;
|
|
477
|
+
else if (expr[i] === ")") {
|
|
478
|
+
depth--;
|
|
479
|
+
if (depth === 0 && i < expr.length - 1) return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return depth === 0;
|
|
483
|
+
}
|
|
484
|
+
function compileRaw(ctx, raw) {
|
|
485
|
+
return compileRawCondition(
|
|
486
|
+
raw,
|
|
487
|
+
(column) => nameAlias(ctx, column),
|
|
488
|
+
(value) => {
|
|
489
|
+
const alias = `:vf${ctx.valueCounter.n++}`;
|
|
490
|
+
ctx.values[alias] = value;
|
|
491
|
+
return alias;
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
function compileNode(ctx, node) {
|
|
496
|
+
if (isRawCondition(node)) {
|
|
497
|
+
return compileRaw(ctx, node);
|
|
498
|
+
}
|
|
499
|
+
const clauses = [];
|
|
500
|
+
for (const [key, value] of Object.entries(node)) {
|
|
501
|
+
if (value === void 0) continue;
|
|
502
|
+
if (LOGICAL_KEYS.has(key)) {
|
|
503
|
+
if (key === "and" || key === "or") {
|
|
504
|
+
const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
|
|
505
|
+
if (parts.length === 0) continue;
|
|
506
|
+
if (parts.length === 1) {
|
|
507
|
+
clauses.push(parts[0]);
|
|
508
|
+
} else {
|
|
509
|
+
const sep = key === "and" ? " AND " : " OR ";
|
|
510
|
+
const joined = parts.map((p) => wrap(p)).join(sep);
|
|
511
|
+
clauses.push(`(${joined})`);
|
|
512
|
+
}
|
|
513
|
+
} else {
|
|
514
|
+
const inner = compileNode(ctx, value);
|
|
515
|
+
if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
|
|
516
|
+
}
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
const clause = compileField(ctx, key, value);
|
|
520
|
+
if (clause.length > 0) clauses.push(clause);
|
|
521
|
+
}
|
|
522
|
+
return joinAnd(clauses);
|
|
523
|
+
}
|
|
524
|
+
function compileFilterExpression(filter, metadata) {
|
|
525
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
526
|
+
return void 0;
|
|
527
|
+
}
|
|
528
|
+
const ctx = {
|
|
529
|
+
names: {},
|
|
530
|
+
values: {},
|
|
531
|
+
fieldMap: new Map(metadata.fields.map((f) => [f.propertyName, f])),
|
|
532
|
+
nameCounter: { n: 0 },
|
|
533
|
+
valueCounter: { n: 0 }
|
|
534
|
+
};
|
|
535
|
+
const expression = compileNode(ctx, filter);
|
|
536
|
+
if (expression.length === 0) {
|
|
537
|
+
return void 0;
|
|
538
|
+
}
|
|
539
|
+
return {
|
|
540
|
+
filterExpression: expression,
|
|
541
|
+
expressionAttributeNames: ctx.names,
|
|
542
|
+
expressionAttributeValues: ctx.values
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function evaluateFilter(item, filter) {
|
|
546
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
547
|
+
if (value === void 0) continue;
|
|
548
|
+
if (key === "and") {
|
|
549
|
+
if (!value.every((s) => evaluateFilter(item, s)))
|
|
550
|
+
return false;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (key === "or") {
|
|
554
|
+
if (!value.some((s) => evaluateFilter(item, s)))
|
|
555
|
+
return false;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (key === "not") {
|
|
559
|
+
if (evaluateFilter(item, value)) return false;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (!evaluateFieldCondition(item[key], item, key, value)) {
|
|
563
|
+
return false;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
function cmp(a, b) {
|
|
569
|
+
if (a instanceof Date && b instanceof Date) {
|
|
570
|
+
return a.getTime() - b.getTime();
|
|
571
|
+
}
|
|
572
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
573
|
+
if (typeof a === "string" && typeof b === "string")
|
|
574
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
575
|
+
if (a === b) return 0;
|
|
576
|
+
return a < b ? -1 : 1;
|
|
577
|
+
}
|
|
578
|
+
function eq(a, b) {
|
|
579
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
580
|
+
return a === b;
|
|
581
|
+
}
|
|
582
|
+
function evaluateFieldCondition(actual, item, field, condition) {
|
|
583
|
+
if (!isOperatorObject(condition)) {
|
|
584
|
+
return eq(actual, condition);
|
|
585
|
+
}
|
|
586
|
+
const ops = condition;
|
|
587
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
588
|
+
switch (op) {
|
|
589
|
+
case "eq":
|
|
590
|
+
if (!eq(actual, value)) return false;
|
|
591
|
+
break;
|
|
592
|
+
case "ne":
|
|
593
|
+
if (eq(actual, value)) return false;
|
|
594
|
+
break;
|
|
595
|
+
case "gt":
|
|
596
|
+
if (!(cmp(actual, value) > 0)) return false;
|
|
597
|
+
break;
|
|
598
|
+
case "ge":
|
|
599
|
+
if (!(cmp(actual, value) >= 0)) return false;
|
|
600
|
+
break;
|
|
601
|
+
case "lt":
|
|
602
|
+
if (!(cmp(actual, value) < 0)) return false;
|
|
603
|
+
break;
|
|
604
|
+
case "le":
|
|
605
|
+
if (!(cmp(actual, value) <= 0)) return false;
|
|
606
|
+
break;
|
|
607
|
+
case "between": {
|
|
608
|
+
const [lo, hi] = value;
|
|
609
|
+
if (!(cmp(actual, lo) >= 0 && cmp(actual, hi) <= 0)) return false;
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
case "in":
|
|
613
|
+
if (!value.some((v) => eq(actual, v))) return false;
|
|
614
|
+
break;
|
|
615
|
+
case "beginsWith":
|
|
616
|
+
if (typeof actual !== "string" || !actual.startsWith(value))
|
|
617
|
+
return false;
|
|
618
|
+
break;
|
|
619
|
+
case "contains":
|
|
620
|
+
if (typeof actual !== "string" || !actual.includes(value))
|
|
621
|
+
return false;
|
|
622
|
+
break;
|
|
623
|
+
case "notContains":
|
|
624
|
+
if (typeof actual === "string" && actual.includes(value))
|
|
625
|
+
return false;
|
|
626
|
+
break;
|
|
627
|
+
case "attributeExists": {
|
|
628
|
+
const exists = field in item && actual !== void 0 && actual !== null;
|
|
629
|
+
if (value === false ? exists : !exists) return false;
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
case "attributeType":
|
|
633
|
+
break;
|
|
634
|
+
case "size": {
|
|
635
|
+
const len = typeof actual === "string" || Array.isArray(actual) ? actual.length : void 0;
|
|
636
|
+
if (len !== value) return false;
|
|
637
|
+
break;
|
|
638
|
+
}
|
|
639
|
+
default:
|
|
640
|
+
throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/planner/planner.ts
|
|
647
|
+
function plan(metadata, input) {
|
|
648
|
+
const queryFields = Object.keys(input.key);
|
|
649
|
+
const resolved = resolveKey(queryFields, metadata);
|
|
650
|
+
if (input.consistentRead && resolved.type === "gsi") {
|
|
651
|
+
throw new Error(
|
|
652
|
+
"consistentRead is not supported for GSI queries. Use a primary key query instead."
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
656
|
+
const built = buildKeyCondition(metadata, resolved, input.key);
|
|
657
|
+
const keyCondition = built.keyCondition;
|
|
658
|
+
const additionalProjectionFields = input.updatable ? Array.from(
|
|
659
|
+
/* @__PURE__ */ new Set([...input.additionalProjectionFields ?? [], "PK", "SK"])
|
|
660
|
+
) : input.additionalProjectionFields;
|
|
661
|
+
const projection = buildProjection(input.select, additionalProjectionFields);
|
|
662
|
+
const compiledFilter = compileFilterExpression(input.filter, metadata);
|
|
663
|
+
const isGetItem = resolved.type === "pk" && !resolved.partial && built.skValue !== void 0 && // GetItem does not support FilterExpression — fall back to Query when a
|
|
664
|
+
// server-side filter is requested so the condition can be applied.
|
|
665
|
+
!compiledFilter;
|
|
666
|
+
const projectionFields2 = projection ? {
|
|
667
|
+
projectionExpression: projection.projectionExpression,
|
|
668
|
+
expressionAttributeNames: projection.expressionAttributeNames
|
|
669
|
+
} : {};
|
|
670
|
+
const filterFields = compiledFilter ? {
|
|
671
|
+
filterExpression: compiledFilter.filterExpression,
|
|
672
|
+
filterExpressionAttributeNames: compiledFilter.expressionAttributeNames,
|
|
673
|
+
filterExpressionAttributeValues: compiledFilter.expressionAttributeValues
|
|
674
|
+
} : {};
|
|
675
|
+
const consistentRead = input.consistentRead && resolved.type === "pk" ? { consistentRead: true } : {};
|
|
676
|
+
let op;
|
|
677
|
+
if (isGetItem) {
|
|
678
|
+
op = {
|
|
679
|
+
type: "GetItem",
|
|
680
|
+
tableName,
|
|
681
|
+
keyCondition,
|
|
682
|
+
...projectionFields2,
|
|
683
|
+
...consistentRead
|
|
684
|
+
};
|
|
685
|
+
} else {
|
|
686
|
+
const queryOp = {
|
|
687
|
+
type: "Query",
|
|
688
|
+
tableName,
|
|
689
|
+
keyCondition,
|
|
690
|
+
scanIndexForward: (input.order ?? "ASC") === "ASC",
|
|
691
|
+
...projectionFields2,
|
|
692
|
+
...filterFields,
|
|
693
|
+
...consistentRead
|
|
694
|
+
};
|
|
695
|
+
if (resolved.type === "gsi") {
|
|
696
|
+
queryOp.indexName = resolved.indexName;
|
|
697
|
+
}
|
|
698
|
+
if (built.rangeCondition) {
|
|
699
|
+
queryOp.rangeCondition = built.rangeCondition;
|
|
700
|
+
}
|
|
701
|
+
if (input.limit != null) {
|
|
702
|
+
queryOp.limit = input.limit;
|
|
703
|
+
}
|
|
704
|
+
if (input.after) {
|
|
705
|
+
queryOp.exclusiveStartKey = input.after;
|
|
706
|
+
}
|
|
707
|
+
op = queryOp;
|
|
708
|
+
}
|
|
709
|
+
return { operations: [op] };
|
|
710
|
+
}
|
|
711
|
+
function buildKeyCondition(metadata, resolved, queryKey) {
|
|
712
|
+
if (resolved.type === "pk") {
|
|
713
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
714
|
+
return buildFromSegments(
|
|
715
|
+
metadata.primaryKey.segmented,
|
|
716
|
+
metadata.primaryKey.inputFieldNames,
|
|
717
|
+
queryKey,
|
|
718
|
+
"PK",
|
|
719
|
+
"SK"
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
const gsiDef = metadata.gsiDefinitions.find(
|
|
723
|
+
(g) => g.indexName === resolved.indexName
|
|
724
|
+
);
|
|
725
|
+
if (!gsiDef) throw new Error(`GSI '${resolved.indexName}' not found`);
|
|
726
|
+
return buildFromSegments(
|
|
727
|
+
gsiDef.segmented,
|
|
728
|
+
gsiDef.inputFieldNames,
|
|
729
|
+
queryKey,
|
|
730
|
+
`${gsiDef.indexName}PK`,
|
|
731
|
+
`${gsiDef.indexName}SK`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
function buildFromSegments(segmented, inputFieldNames, queryKey, pkAttr, skAttr) {
|
|
735
|
+
const keyInput = {};
|
|
736
|
+
for (const fieldName of inputFieldNames) {
|
|
737
|
+
keyInput[fieldName] = queryKey[fieldName];
|
|
738
|
+
}
|
|
739
|
+
const { pk, sk, skTruncated } = resolveSegmentedKey(segmented, keyInput);
|
|
740
|
+
const keyCondition = { [pkAttr]: pk };
|
|
741
|
+
if (sk === void 0) {
|
|
742
|
+
return { keyCondition };
|
|
743
|
+
}
|
|
744
|
+
if (skTruncated) {
|
|
745
|
+
return {
|
|
746
|
+
keyCondition,
|
|
747
|
+
rangeCondition: { operator: "begins_with", key: skAttr, value: sk }
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
keyCondition[skAttr] = sk;
|
|
751
|
+
return { keyCondition, skValue: sk };
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// src/executor/executor.ts
|
|
755
|
+
async function execute(operation) {
|
|
756
|
+
return ClientManager.getExecutor().execute(operation);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// src/hydrator/hydrator.ts
|
|
760
|
+
function hydrate(rawItems, select, metadata, updatable = false) {
|
|
761
|
+
return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
|
|
762
|
+
}
|
|
763
|
+
function hydrateItem(raw, select, metadata, updatable = false) {
|
|
764
|
+
const fieldMap = new Map(
|
|
765
|
+
metadata.fields.map((f) => [f.propertyName, f])
|
|
766
|
+
);
|
|
767
|
+
const embeddedMap = new Map(
|
|
768
|
+
metadata.embeddedFields.map((e) => [e.propertyName, e])
|
|
769
|
+
);
|
|
770
|
+
const result = {};
|
|
771
|
+
for (const [fieldName, selectValue] of Object.entries(select)) {
|
|
772
|
+
if (selectValue === true) {
|
|
773
|
+
const value = raw[fieldName];
|
|
774
|
+
if (value !== void 0) {
|
|
775
|
+
const fieldMeta = fieldMap.get(fieldName);
|
|
776
|
+
result[fieldName] = deserializeValue(value, fieldMeta);
|
|
777
|
+
}
|
|
778
|
+
} else if (typeof selectValue === "object" && selectValue !== null) {
|
|
779
|
+
if ("select" in selectValue || isSelectBuilder(selectValue)) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
const rawEmb = raw[fieldName];
|
|
783
|
+
if (rawEmb && typeof rawEmb === "object") {
|
|
784
|
+
result[fieldName] = hydrateEmbedded(
|
|
785
|
+
rawEmb,
|
|
786
|
+
selectValue,
|
|
787
|
+
metadata,
|
|
788
|
+
fieldName
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (updatable) {
|
|
794
|
+
attachHiddenKey(result, raw);
|
|
795
|
+
}
|
|
796
|
+
return result;
|
|
797
|
+
}
|
|
798
|
+
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
799
|
+
const result = {};
|
|
800
|
+
for (const [key, selectValue] of Object.entries(select)) {
|
|
801
|
+
if (selectValue === true) {
|
|
802
|
+
if (raw[key] !== void 0) {
|
|
803
|
+
result[key] = raw[key];
|
|
804
|
+
}
|
|
805
|
+
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
806
|
+
const nested = raw[key];
|
|
807
|
+
if (nested && typeof nested === "object") {
|
|
808
|
+
result[key] = hydrateEmbedded(
|
|
809
|
+
nested,
|
|
810
|
+
selectValue,
|
|
811
|
+
_metadata,
|
|
812
|
+
key
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return result;
|
|
818
|
+
}
|
|
819
|
+
function deserializeValue(value, fieldMeta) {
|
|
820
|
+
if (!fieldMeta) return value;
|
|
821
|
+
if (fieldMeta.options?.deserialize) {
|
|
822
|
+
return fieldMeta.options.deserialize(value);
|
|
823
|
+
}
|
|
824
|
+
if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
|
|
825
|
+
return new Date(value);
|
|
826
|
+
}
|
|
827
|
+
if (fieldMeta.options?.format === "date" && typeof value === "string") {
|
|
828
|
+
return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
|
|
829
|
+
}
|
|
830
|
+
return value;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/pagination/cursor.ts
|
|
834
|
+
function encodeCursor(lastEvaluatedKey) {
|
|
835
|
+
const json = JSON.stringify(lastEvaluatedKey);
|
|
836
|
+
const bytes = new TextEncoder().encode(json);
|
|
837
|
+
let binary = "";
|
|
838
|
+
for (const b of bytes) {
|
|
839
|
+
binary += String.fromCharCode(b);
|
|
840
|
+
}
|
|
841
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
842
|
+
}
|
|
843
|
+
function decodeCursor(cursor) {
|
|
844
|
+
let base64 = cursor.replace(/-/g, "+").replace(/_/g, "/");
|
|
845
|
+
while (base64.length % 4 !== 0) {
|
|
846
|
+
base64 += "=";
|
|
847
|
+
}
|
|
848
|
+
const binary = atob(base64);
|
|
849
|
+
const bytes = new Uint8Array(binary.length);
|
|
850
|
+
for (let i = 0; i < binary.length; i++) {
|
|
851
|
+
bytes[i] = binary.charCodeAt(i);
|
|
852
|
+
}
|
|
853
|
+
const json = new TextDecoder().decode(bytes);
|
|
854
|
+
return JSON.parse(json);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/operations/list.ts
|
|
858
|
+
async function executeListInternal(modelClass, key, selectSpec, options = {}) {
|
|
859
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
860
|
+
const normalized = normalizeTopLevelSelect(selectSpec);
|
|
861
|
+
const select = normalized.select;
|
|
862
|
+
const filter = options.filter ?? normalized.filter;
|
|
863
|
+
const after = options.after ?? normalized.after;
|
|
864
|
+
const limit = options.limit ?? normalized.limit;
|
|
865
|
+
const order = options.order ?? normalized.order;
|
|
866
|
+
const exclusiveStartKey = after ? decodeCursor(after) : void 0;
|
|
867
|
+
const implicitKeys = getImplicitKeyFields(select, metadata);
|
|
868
|
+
const executionPlan = plan(metadata, {
|
|
869
|
+
key,
|
|
870
|
+
select,
|
|
871
|
+
limit,
|
|
872
|
+
after: exclusiveStartKey,
|
|
873
|
+
order,
|
|
874
|
+
filter,
|
|
875
|
+
additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
|
|
876
|
+
updatable: options.updatable ?? false
|
|
877
|
+
});
|
|
878
|
+
const operation = executionPlan.operations[0];
|
|
879
|
+
const result = await execute(operation);
|
|
880
|
+
const rawItems = result.items;
|
|
881
|
+
const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
|
|
882
|
+
const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
|
|
883
|
+
return { items: hydrated, rawItems, cursor };
|
|
884
|
+
}
|
|
885
|
+
async function executeList(modelClass, key, selectSpec, options = {}) {
|
|
886
|
+
const { items, cursor } = await executeListInternal(
|
|
887
|
+
modelClass,
|
|
888
|
+
key,
|
|
889
|
+
selectSpec,
|
|
890
|
+
options
|
|
891
|
+
);
|
|
892
|
+
return { items, cursor };
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// src/planner/relation-planner.ts
|
|
896
|
+
function buildRelationQueryKey(rel, rawParentItem) {
|
|
897
|
+
const queryKey = {};
|
|
898
|
+
for (const [targetField, sourceField] of Object.entries(rel.keyBinding)) {
|
|
899
|
+
queryKey[targetField] = rawParentItem[sourceField];
|
|
900
|
+
}
|
|
901
|
+
return queryKey;
|
|
902
|
+
}
|
|
903
|
+
function serializeQueryKey(queryKey) {
|
|
904
|
+
const entries = Object.entries(queryKey).sort(([a], [b]) => a.localeCompare(b));
|
|
905
|
+
return JSON.stringify(entries);
|
|
906
|
+
}
|
|
907
|
+
function planGetItemForQueryKey(metadata, queryKey, select, additionalProjectionFields) {
|
|
908
|
+
const executionPlan = plan(metadata, {
|
|
909
|
+
key: queryKey,
|
|
910
|
+
select,
|
|
911
|
+
additionalProjectionFields
|
|
912
|
+
});
|
|
913
|
+
const operation = executionPlan.operations[0];
|
|
914
|
+
if (operation.type !== "GetItem") {
|
|
915
|
+
throw new Error(
|
|
916
|
+
`Relation target requires GetItem access pattern, got ${operation.type}`
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
return operation;
|
|
920
|
+
}
|
|
921
|
+
function planBatchGetForQueryKeys(metadata, queryKeys, select, additionalProjectionFields) {
|
|
922
|
+
if (queryKeys.length === 0) {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
const keyFields = [
|
|
926
|
+
...new Set(queryKeys.flatMap((queryKey) => Object.keys(queryKey)))
|
|
927
|
+
].filter((field) => !(field in select && select[field] === true));
|
|
928
|
+
const projectionFields2 = [
|
|
929
|
+
.../* @__PURE__ */ new Set([
|
|
930
|
+
...(additionalProjectionFields ?? []).filter(
|
|
931
|
+
(field) => !(field in select && select[field] === true)
|
|
932
|
+
),
|
|
933
|
+
...keyFields
|
|
934
|
+
])
|
|
935
|
+
];
|
|
936
|
+
const samplePlan = planGetItemForQueryKey(
|
|
937
|
+
metadata,
|
|
938
|
+
queryKeys[0],
|
|
939
|
+
select,
|
|
940
|
+
projectionFields2.length > 0 ? projectionFields2 : void 0
|
|
941
|
+
);
|
|
942
|
+
const uniqueKeys = dedupeDynamoKeys(
|
|
943
|
+
queryKeys.map(
|
|
944
|
+
(queryKey) => planGetItemForQueryKey(
|
|
945
|
+
metadata,
|
|
946
|
+
queryKey,
|
|
947
|
+
select,
|
|
948
|
+
projectionFields2.length > 0 ? projectionFields2 : void 0
|
|
949
|
+
).keyCondition
|
|
950
|
+
)
|
|
951
|
+
);
|
|
952
|
+
const operations = [];
|
|
953
|
+
for (let i = 0; i < uniqueKeys.length; i += BATCH_GET_MAX_KEYS) {
|
|
954
|
+
operations.push({
|
|
955
|
+
type: "BatchGetItem",
|
|
956
|
+
tableName: samplePlan.tableName,
|
|
957
|
+
keys: uniqueKeys.slice(i, i + BATCH_GET_MAX_KEYS),
|
|
958
|
+
...samplePlan.projectionExpression ? { projectionExpression: samplePlan.projectionExpression } : {},
|
|
959
|
+
...samplePlan.expressionAttributeNames ? { expressionAttributeNames: samplePlan.expressionAttributeNames } : {},
|
|
960
|
+
...samplePlan.consistentRead ? { consistentRead: true } : {}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
return operations;
|
|
964
|
+
}
|
|
965
|
+
function dedupeDynamoKeys(keys) {
|
|
966
|
+
const seen = /* @__PURE__ */ new Set();
|
|
967
|
+
const result = [];
|
|
968
|
+
for (const key of keys) {
|
|
969
|
+
const serialized = JSON.stringify(
|
|
970
|
+
Object.entries(key).sort(([a], [b]) => a.localeCompare(b))
|
|
971
|
+
);
|
|
972
|
+
if (seen.has(serialized)) continue;
|
|
973
|
+
seen.add(serialized);
|
|
974
|
+
result.push(key);
|
|
975
|
+
}
|
|
976
|
+
return result;
|
|
977
|
+
}
|
|
978
|
+
function planRelationOperations(metadata, select, context = { estimatedParentCount: 1 }) {
|
|
979
|
+
const operations = [];
|
|
980
|
+
const relations = detectRelationFields(select, metadata);
|
|
981
|
+
for (const rel of relations) {
|
|
982
|
+
const selectSpec = normalizeSelectSpec(select[rel.propertyName]);
|
|
983
|
+
const targetClass = rel.targetFactory();
|
|
984
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
985
|
+
if (rel.type === "hasMany") {
|
|
986
|
+
const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
|
|
987
|
+
const tableName = TableMapping.resolve(targetMeta.tableName);
|
|
988
|
+
const listPlan = plan(targetMeta, {
|
|
989
|
+
key: buildPlaceholderHasManyKey(rel, targetMeta),
|
|
990
|
+
select: selectSpec.select,
|
|
991
|
+
limit
|
|
992
|
+
});
|
|
993
|
+
operations.push(...listPlan.operations);
|
|
994
|
+
operations.push(
|
|
995
|
+
...planRelationOperations(targetMeta, selectSpec.select, {
|
|
996
|
+
estimatedParentCount: limit
|
|
997
|
+
})
|
|
998
|
+
);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const estimatedCount = context.estimatedParentCount ?? 1;
|
|
1002
|
+
operations.push(
|
|
1003
|
+
...planBatchGetForExplain(targetMeta, selectSpec.select, estimatedCount)
|
|
1004
|
+
);
|
|
1005
|
+
operations.push(
|
|
1006
|
+
...planRelationOperations(targetMeta, selectSpec.select, {
|
|
1007
|
+
estimatedParentCount: 1
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
return operations;
|
|
1012
|
+
}
|
|
1013
|
+
function buildPlaceholderHasManyKey(rel, targetMeta) {
|
|
1014
|
+
const queryKey = {};
|
|
1015
|
+
for (const targetField of Object.keys(rel.keyBinding)) {
|
|
1016
|
+
queryKey[targetField] = `__${targetField}__`;
|
|
1017
|
+
}
|
|
1018
|
+
if (targetMeta.primaryKey) {
|
|
1019
|
+
for (const fieldName of targetMeta.primaryKey.inputFieldNames) {
|
|
1020
|
+
if (!(fieldName in queryKey)) {
|
|
1021
|
+
queryKey[fieldName] = `__${fieldName}__`;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return queryKey;
|
|
1026
|
+
}
|
|
1027
|
+
function planBatchGetForExplain(metadata, select, estimatedKeyCount) {
|
|
1028
|
+
const placeholderKeys = Array.from(
|
|
1029
|
+
{ length: estimatedKeyCount },
|
|
1030
|
+
(_, index) => buildExplainPlaceholderKey(metadata, index)
|
|
1031
|
+
);
|
|
1032
|
+
return planBatchGetForQueryKeys(metadata, placeholderKeys, select);
|
|
1033
|
+
}
|
|
1034
|
+
function buildExplainPlaceholderKey(metadata, index) {
|
|
1035
|
+
const queryKey = {};
|
|
1036
|
+
if (metadata.primaryKey) {
|
|
1037
|
+
for (const fieldName of metadata.primaryKey.inputFieldNames) {
|
|
1038
|
+
queryKey[fieldName] = `__explain_${fieldName}_${index}__`;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return queryKey;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// src/relation/concurrency.ts
|
|
1045
|
+
var RELATION_TRAVERSAL_CONCURRENCY = 16;
|
|
1046
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
1047
|
+
if (items.length === 0) {
|
|
1048
|
+
return [];
|
|
1049
|
+
}
|
|
1050
|
+
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
1051
|
+
if (effectiveLimit >= items.length) {
|
|
1052
|
+
return Promise.all(items.map((item, index) => worker(item, index)));
|
|
1053
|
+
}
|
|
1054
|
+
const results = new Array(items.length);
|
|
1055
|
+
let nextIndex = 0;
|
|
1056
|
+
async function runWorker() {
|
|
1057
|
+
while (true) {
|
|
1058
|
+
const index = nextIndex++;
|
|
1059
|
+
if (index >= items.length) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
results[index] = await worker(items[index], index);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
const runners = [];
|
|
1066
|
+
for (let i = 0; i < effectiveLimit; i++) {
|
|
1067
|
+
runners.push(runWorker());
|
|
1068
|
+
}
|
|
1069
|
+
await Promise.all(runners);
|
|
1070
|
+
return results;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// src/relation/execution-plan.ts
|
|
1074
|
+
function relationResultPaths(select, metadata, parentPath = "$") {
|
|
1075
|
+
const out = [];
|
|
1076
|
+
const relations = detectRelationFields(select, metadata);
|
|
1077
|
+
for (const rel of [...relations].sort(
|
|
1078
|
+
(a, b) => a.propertyName.localeCompare(b.propertyName)
|
|
1079
|
+
)) {
|
|
1080
|
+
const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
|
|
1081
|
+
const targetMeta = MetadataRegistry.get(rel.targetFactory());
|
|
1082
|
+
const childPath = `${parentPath}.${rel.propertyName}`;
|
|
1083
|
+
const isMany = rel.type === "hasMany";
|
|
1084
|
+
const resultPath = isMany ? `${childPath}.items` : childPath;
|
|
1085
|
+
out.push({
|
|
1086
|
+
propertyName: rel.propertyName,
|
|
1087
|
+
parentPath,
|
|
1088
|
+
resultPath,
|
|
1089
|
+
isMany
|
|
1090
|
+
});
|
|
1091
|
+
out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
|
|
1092
|
+
}
|
|
1093
|
+
return out;
|
|
1094
|
+
}
|
|
1095
|
+
function parentResultPath(resultPath) {
|
|
1096
|
+
if (!resultPath.startsWith("$.")) return "$";
|
|
1097
|
+
const tokens = resultPath.slice(2).split(".");
|
|
1098
|
+
if (tokens[tokens.length - 1] === "items") tokens.pop();
|
|
1099
|
+
tokens.pop();
|
|
1100
|
+
return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
|
|
1101
|
+
}
|
|
1102
|
+
function deriveExecutionPlan(resultPaths) {
|
|
1103
|
+
if (resultPaths.length <= 1) return void 0;
|
|
1104
|
+
const indexByPath = /* @__PURE__ */ new Map();
|
|
1105
|
+
resultPaths.forEach((path, i) => {
|
|
1106
|
+
indexByPath.set(path, i);
|
|
1107
|
+
});
|
|
1108
|
+
const stageOf = new Array(resultPaths.length);
|
|
1109
|
+
for (let i = 0; i < resultPaths.length; i++) {
|
|
1110
|
+
if (resultPaths[i] === "$") {
|
|
1111
|
+
stageOf[i] = 0;
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
const parentPath = parentResultPath(resultPaths[i]);
|
|
1115
|
+
const parentIndex = indexByPath.get(parentPath);
|
|
1116
|
+
const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
|
|
1117
|
+
stageOf[i] = parentStage + 1;
|
|
1118
|
+
}
|
|
1119
|
+
const stageCount = Math.max(...stageOf) + 1;
|
|
1120
|
+
const groups = Array.from({ length: stageCount }, () => []);
|
|
1121
|
+
for (let i = 0; i < resultPaths.length; i++) {
|
|
1122
|
+
groups[stageOf[i]].push(i);
|
|
1123
|
+
}
|
|
1124
|
+
for (const g of groups) g.sort((a, b) => a - b);
|
|
1125
|
+
return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1126
|
+
}
|
|
1127
|
+
function buildRelationExecutionPlan(select, metadata) {
|
|
1128
|
+
const relationPaths = relationResultPaths(select, metadata);
|
|
1129
|
+
if (relationPaths.length === 0) return void 0;
|
|
1130
|
+
const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
|
|
1131
|
+
const plan2 = deriveExecutionPlan(resultPaths);
|
|
1132
|
+
if (plan2 === void 0) return void 0;
|
|
1133
|
+
return { plan: plan2, resultPaths };
|
|
1134
|
+
}
|
|
1135
|
+
function deriveCompositionPlan(composeCount) {
|
|
1136
|
+
if (composeCount <= 0) {
|
|
1137
|
+
return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1138
|
+
}
|
|
1139
|
+
const stage = Array.from({ length: composeCount }, (_unused, i) => i);
|
|
1140
|
+
return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1141
|
+
}
|
|
1142
|
+
function stageOfResultPath(resolved, resultPath) {
|
|
1143
|
+
const index = resolved.resultPaths.indexOf(resultPath);
|
|
1144
|
+
if (index < 0) return void 0;
|
|
1145
|
+
for (let s = 0; s < resolved.plan.groups.length; s++) {
|
|
1146
|
+
if (resolved.plan.groups[s].includes(index)) return s;
|
|
1147
|
+
}
|
|
1148
|
+
return void 0;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// src/relation/traversal.ts
|
|
1152
|
+
function evaluateFilterClientSide(item, filter, _targetMeta) {
|
|
1153
|
+
return evaluateFilter(item, filter);
|
|
1154
|
+
}
|
|
1155
|
+
function relationSpec(value) {
|
|
1156
|
+
return normalizeSelectSpec(value);
|
|
1157
|
+
}
|
|
1158
|
+
function validateDepth(select, metadata, maxDepth, currentDepth = 1) {
|
|
1159
|
+
for (const [fieldName, value] of Object.entries(select)) {
|
|
1160
|
+
if (typeof value === "object" && value !== null && ("select" in value || isSelectBuilder(value))) {
|
|
1161
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
1162
|
+
if (!rel) continue;
|
|
1163
|
+
if (currentDepth > maxDepth) {
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
`Relation traversal depth exceeded: '${fieldName}' at depth ${currentDepth} exceeds maxDepth ${maxDepth}. Set maxDepth: ${currentDepth} to allow this.`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
const targetClass = rel.targetFactory();
|
|
1169
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1170
|
+
const innerSelect = relationSpec(value).select;
|
|
1171
|
+
validateDepth(innerSelect, targetMeta, maxDepth, currentDepth + 1);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function relationResultPath(parentPath, rel) {
|
|
1176
|
+
const base = `${parentPath}.${rel.propertyName}`;
|
|
1177
|
+
return rel.type === "hasMany" ? `${base}.items` : base;
|
|
1178
|
+
}
|
|
1179
|
+
function stageRelations(relations, parentPath, plan2) {
|
|
1180
|
+
if (plan2 === void 0 || relations.length === 0) {
|
|
1181
|
+
return relations.length > 0 ? { stages: [[...relations]], concurrency: RELATION_TRAVERSAL_CONCURRENCY } : { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1182
|
+
}
|
|
1183
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
1184
|
+
const unstaged = [];
|
|
1185
|
+
for (const rel of relations) {
|
|
1186
|
+
const stage = stageOfResultPath(plan2, relationResultPath(parentPath, rel));
|
|
1187
|
+
if (stage === void 0) {
|
|
1188
|
+
unstaged.push(rel);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
const bucket = byStage.get(stage);
|
|
1192
|
+
if (bucket) bucket.push(rel);
|
|
1193
|
+
else byStage.set(stage, [rel]);
|
|
1194
|
+
}
|
|
1195
|
+
const stages = [...byStage.keys()].sort((a, b) => a - b).map((s) => byStage.get(s));
|
|
1196
|
+
if (unstaged.length > 0) stages.push(unstaged);
|
|
1197
|
+
return { stages, concurrency: plan2.plan.concurrency };
|
|
1198
|
+
}
|
|
1199
|
+
async function runStaged(relations, parentPath, plan2, worker) {
|
|
1200
|
+
const { stages, concurrency } = stageRelations(relations, parentPath, plan2);
|
|
1201
|
+
for (const stage of stages) {
|
|
1202
|
+
await mapWithConcurrency(stage, concurrency, (rel) => worker(rel));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
function hasCompleteQueryKey(queryKey) {
|
|
1206
|
+
return Object.values(queryKey).every((value) => value != null);
|
|
1207
|
+
}
|
|
1208
|
+
function matchesQueryKey(item, queryKey) {
|
|
1209
|
+
return Object.entries(queryKey).every(
|
|
1210
|
+
([field, value]) => item[field] === value
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, targetMeta, options, currentDepth, parentPath) {
|
|
1214
|
+
const results = rawParentItems.map(
|
|
1215
|
+
() => null
|
|
1216
|
+
);
|
|
1217
|
+
const queryKeys = [];
|
|
1218
|
+
const parentIndexes = [];
|
|
1219
|
+
for (let i = 0; i < rawParentItems.length; i++) {
|
|
1220
|
+
const queryKey = buildRelationQueryKey(rel, rawParentItems[i]);
|
|
1221
|
+
if (!hasCompleteQueryKey(queryKey)) {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
queryKeys.push(queryKey);
|
|
1225
|
+
parentIndexes.push(i);
|
|
1226
|
+
}
|
|
1227
|
+
if (queryKeys.length === 0) {
|
|
1228
|
+
return results;
|
|
1229
|
+
}
|
|
1230
|
+
const implicitKeys = getImplicitKeyFields(selectSpec.select, targetMeta);
|
|
1231
|
+
const projectionExtras = options.updatable ? [...implicitKeys, "PK", "SK"] : implicitKeys;
|
|
1232
|
+
const batchOps = planBatchGetForQueryKeys(
|
|
1233
|
+
targetMeta,
|
|
1234
|
+
queryKeys,
|
|
1235
|
+
selectSpec.select,
|
|
1236
|
+
projectionExtras.length > 0 ? projectionExtras : void 0
|
|
1237
|
+
);
|
|
1238
|
+
const executor = ClientManager.getExecutor();
|
|
1239
|
+
const queryKeyToRaw = /* @__PURE__ */ new Map();
|
|
1240
|
+
for (const operation of batchOps) {
|
|
1241
|
+
const batchResult = await executor.batchGet(operation);
|
|
1242
|
+
for (const item of batchResult.items) {
|
|
1243
|
+
for (const queryKey of queryKeys) {
|
|
1244
|
+
if (!matchesQueryKey(item, queryKey)) {
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
queryKeyToRaw.set(serializeQueryKey(queryKey), item);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
const nestedRelations = detectRelationFields(selectSpec.select, targetMeta);
|
|
1252
|
+
const ownPath = relationResultPath(parentPath, rel);
|
|
1253
|
+
await mapWithConcurrency(
|
|
1254
|
+
queryKeys,
|
|
1255
|
+
options.plan?.plan.concurrency ?? RELATION_TRAVERSAL_CONCURRENCY,
|
|
1256
|
+
async (queryKey, j) => {
|
|
1257
|
+
const parentIndex = parentIndexes[j];
|
|
1258
|
+
const raw = queryKeyToRaw.get(serializeQueryKey(queryKey));
|
|
1259
|
+
if (!raw) {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const updatable = options.updatable ?? false;
|
|
1263
|
+
let resolvedItem = hydrate([raw], selectSpec.select, targetMeta, updatable)[0] ?? null;
|
|
1264
|
+
if (resolvedItem && nestedRelations.length > 0) {
|
|
1265
|
+
resolvedItem = await resolveRelations(
|
|
1266
|
+
resolvedItem,
|
|
1267
|
+
raw,
|
|
1268
|
+
selectSpec.select,
|
|
1269
|
+
targetMeta,
|
|
1270
|
+
options,
|
|
1271
|
+
currentDepth + 1,
|
|
1272
|
+
ownPath
|
|
1273
|
+
);
|
|
1274
|
+
if (resolvedItem && updatable) {
|
|
1275
|
+
attachHiddenKey(resolvedItem, raw);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (resolvedItem && !passesPostLoad(resolvedItem, selectSpec, targetMeta)) {
|
|
1279
|
+
results[parentIndex] = null;
|
|
1280
|
+
} else {
|
|
1281
|
+
results[parentIndex] = resolvedItem;
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
);
|
|
1285
|
+
return results;
|
|
1286
|
+
}
|
|
1287
|
+
function passesPostLoad(item, spec, targetMeta) {
|
|
1288
|
+
if (spec.filter && !evaluateFilterClientSide(item, spec.filter, targetMeta)) {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
async function resolveSingleValueRelationsBatch(parentItems, rawParentItems, select, metadata, options, currentDepth, parentPath) {
|
|
1294
|
+
const results = parentItems.map((item, i) => {
|
|
1295
|
+
const copy = { ...item };
|
|
1296
|
+
if (options.updatable) {
|
|
1297
|
+
attachHiddenKey(copy, rawParentItems[i]);
|
|
1298
|
+
}
|
|
1299
|
+
return copy;
|
|
1300
|
+
});
|
|
1301
|
+
const relations = detectRelationFields(select, metadata).filter(
|
|
1302
|
+
(rel) => rel.type === "belongsTo" || rel.type === "hasOne"
|
|
1303
|
+
);
|
|
1304
|
+
await runStaged(relations, parentPath, options.plan, async (rel) => {
|
|
1305
|
+
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1306
|
+
const targetClass = rel.targetFactory();
|
|
1307
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1308
|
+
const batchResults = await fetchBelongsToHasOneBatch(
|
|
1309
|
+
rel,
|
|
1310
|
+
rawParentItems,
|
|
1311
|
+
selectSpec,
|
|
1312
|
+
targetMeta,
|
|
1313
|
+
options,
|
|
1314
|
+
currentDepth,
|
|
1315
|
+
parentPath
|
|
1316
|
+
);
|
|
1317
|
+
for (let i = 0; i < results.length; i++) {
|
|
1318
|
+
results[i][rel.propertyName] = batchResults[i];
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
return results;
|
|
1322
|
+
}
|
|
1323
|
+
async function resolveRelations(parentItem, rawParentItem, select, metadata, options, currentDepth = 1, currentPath = "$") {
|
|
1324
|
+
const result = { ...parentItem };
|
|
1325
|
+
const relations = detectRelationFields(select, metadata);
|
|
1326
|
+
const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
|
|
1327
|
+
const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
|
|
1328
|
+
await runStaged(relations, currentPath, opts.plan, async (rel) => {
|
|
1329
|
+
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1330
|
+
const ownPath = relationResultPath(currentPath, rel);
|
|
1331
|
+
if (rel.type === "hasMany") {
|
|
1332
|
+
const targetClass2 = rel.targetFactory();
|
|
1333
|
+
const queryKey = buildRelationQueryKey(rel, rawParentItem);
|
|
1334
|
+
const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
|
|
1335
|
+
const order = selectSpec.order ?? rel.options?.order ?? "ASC";
|
|
1336
|
+
const listResult = await executeListInternal(targetClass2, queryKey, selectSpec.select, {
|
|
1337
|
+
limit,
|
|
1338
|
+
after: selectSpec.after,
|
|
1339
|
+
order,
|
|
1340
|
+
filter: selectSpec.filter,
|
|
1341
|
+
updatable: opts.updatable
|
|
1342
|
+
});
|
|
1343
|
+
let items = listResult.items;
|
|
1344
|
+
const rawItems = listResult.rawItems;
|
|
1345
|
+
const targetMeta2 = MetadataRegistry.get(targetClass2);
|
|
1346
|
+
const nestedRelations = detectRelationFields(selectSpec.select, targetMeta2);
|
|
1347
|
+
if (nestedRelations.length > 0) {
|
|
1348
|
+
items = await resolveSingleValueRelationsBatch(
|
|
1349
|
+
items,
|
|
1350
|
+
rawItems,
|
|
1351
|
+
selectSpec.select,
|
|
1352
|
+
targetMeta2,
|
|
1353
|
+
opts,
|
|
1354
|
+
currentDepth + 1,
|
|
1355
|
+
ownPath
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
result[rel.propertyName] = {
|
|
1359
|
+
items,
|
|
1360
|
+
cursor: listResult.cursor
|
|
1361
|
+
};
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
const targetClass = rel.targetFactory();
|
|
1365
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1366
|
+
const [resolvedItem] = await fetchBelongsToHasOneBatch(
|
|
1367
|
+
rel,
|
|
1368
|
+
[rawParentItem],
|
|
1369
|
+
selectSpec,
|
|
1370
|
+
targetMeta,
|
|
1371
|
+
opts,
|
|
1372
|
+
currentDepth,
|
|
1373
|
+
currentPath
|
|
1374
|
+
);
|
|
1375
|
+
result[rel.propertyName] = resolvedItem;
|
|
1376
|
+
});
|
|
1377
|
+
return result;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// src/operations/query.ts
|
|
1381
|
+
async function executeQuery(modelClass, key, selectSpec, options) {
|
|
1382
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1383
|
+
const queryFields = Object.keys(key);
|
|
1384
|
+
const resolved = resolveKey(queryFields, metadata);
|
|
1385
|
+
if (resolved.type === "gsi" && !resolved.unique) {
|
|
1386
|
+
throw new Error(
|
|
1387
|
+
`Cannot use query() with non-unique GSI '${resolved.indexName}'. Use list() instead.`
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
const normalized = normalizeTopLevelSelect(selectSpec);
|
|
1391
|
+
const select = normalized.select;
|
|
1392
|
+
const filter = normalized.filter;
|
|
1393
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
1394
|
+
const relations = detectRelationFields(select, metadata);
|
|
1395
|
+
if (relations.length > 0) {
|
|
1396
|
+
validateDepth(select, metadata, maxDepth);
|
|
1397
|
+
}
|
|
1398
|
+
const implicitKeys = getImplicitKeyFields(select, metadata);
|
|
1399
|
+
const updatable = options?.updatable ?? false;
|
|
1400
|
+
const executionPlan = plan(metadata, {
|
|
1401
|
+
key,
|
|
1402
|
+
select,
|
|
1403
|
+
filter,
|
|
1404
|
+
consistentRead: options?.consistentRead,
|
|
1405
|
+
additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
|
|
1406
|
+
updatable
|
|
1407
|
+
});
|
|
1408
|
+
const operation = executionPlan.operations[0];
|
|
1409
|
+
const canParallelizeRelations = relations.length > 0 && relations.every(
|
|
1410
|
+
(rel) => Object.values(rel.keyBinding).every(
|
|
1411
|
+
(sourceField) => key[sourceField] != null
|
|
1412
|
+
)
|
|
1413
|
+
);
|
|
1414
|
+
const parentPromise = execute(operation);
|
|
1415
|
+
const relationPromise = canParallelizeRelations ? resolveRelations({}, key, select, metadata, { maxDepth, updatable }, 1) : null;
|
|
1416
|
+
let result;
|
|
1417
|
+
try {
|
|
1418
|
+
result = await parentPromise;
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
if (relationPromise) await relationPromise.catch(() => void 0);
|
|
1421
|
+
throw err;
|
|
1422
|
+
}
|
|
1423
|
+
if (result.items.length === 0) {
|
|
1424
|
+
if (relationPromise) await relationPromise.catch(() => void 0);
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
const rawItem = result.items[0];
|
|
1428
|
+
const hydrated = hydrate(result.items, select, metadata, updatable);
|
|
1429
|
+
let hydratedItem = hydrated[0] ?? null;
|
|
1430
|
+
if (hydratedItem && relations.length > 0) {
|
|
1431
|
+
if (relationPromise) {
|
|
1432
|
+
const resolvedRelations = await relationPromise;
|
|
1433
|
+
for (const rel of relations) {
|
|
1434
|
+
hydratedItem[rel.propertyName] = resolvedRelations[rel.propertyName];
|
|
1435
|
+
}
|
|
1436
|
+
} else {
|
|
1437
|
+
hydratedItem = await resolveRelations(
|
|
1438
|
+
hydratedItem,
|
|
1439
|
+
rawItem,
|
|
1440
|
+
select,
|
|
1441
|
+
metadata,
|
|
1442
|
+
{ maxDepth, updatable },
|
|
1443
|
+
1
|
|
1444
|
+
);
|
|
1445
|
+
if (hydratedItem && updatable) {
|
|
1446
|
+
attachHiddenKey(hydratedItem, rawItem);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (options?.hydrate) {
|
|
1451
|
+
return options.hydrate(hydratedItem);
|
|
1452
|
+
}
|
|
1453
|
+
return hydratedItem;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/operations/explain.ts
|
|
1457
|
+
function executeExplain(modelClass, key, options) {
|
|
1458
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1459
|
+
const exclusiveStartKey = options?.after ? decodeCursor(options.after) : void 0;
|
|
1460
|
+
const normalized = normalizeTopLevelSelect(options?.select);
|
|
1461
|
+
const select = normalized.select ?? {};
|
|
1462
|
+
const filter = options?.filter ?? normalized.filter;
|
|
1463
|
+
const mainPlan = plan(metadata, {
|
|
1464
|
+
key,
|
|
1465
|
+
select,
|
|
1466
|
+
limit: options?.limit,
|
|
1467
|
+
after: exclusiveStartKey,
|
|
1468
|
+
order: options?.order,
|
|
1469
|
+
consistentRead: options?.consistentRead,
|
|
1470
|
+
filter
|
|
1471
|
+
});
|
|
1472
|
+
const relationOps = planRelationOperations(metadata, select, {
|
|
1473
|
+
estimatedParentCount: 1
|
|
1474
|
+
});
|
|
1475
|
+
return {
|
|
1476
|
+
operations: [...mainPlan.operations, ...relationOps]
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// src/operations/batch.ts
|
|
1481
|
+
var BatchGetResult = class {
|
|
1482
|
+
groups;
|
|
1483
|
+
constructor(groups) {
|
|
1484
|
+
this.groups = groups;
|
|
1485
|
+
}
|
|
1486
|
+
get(model) {
|
|
1487
|
+
const modelClass = resolveModelClass(model);
|
|
1488
|
+
return this.groups.get(modelClass) ?? [];
|
|
1489
|
+
}
|
|
1490
|
+
};
|
|
1491
|
+
function buildScalarSelect(metadata) {
|
|
1492
|
+
const select = {};
|
|
1493
|
+
for (const field of metadata.fields) {
|
|
1494
|
+
select[field.propertyName] = true;
|
|
1495
|
+
}
|
|
1496
|
+
return select;
|
|
1497
|
+
}
|
|
1498
|
+
function buildDynamoKey(modelClass, keyObj) {
|
|
1499
|
+
const deleteInput = buildDeleteInput(modelClass, keyObj);
|
|
1500
|
+
return deleteInput.Key;
|
|
1501
|
+
}
|
|
1502
|
+
function hydrateBatchItems(rawItems, metadata) {
|
|
1503
|
+
if (rawItems.length === 0) {
|
|
1504
|
+
return [];
|
|
1505
|
+
}
|
|
1506
|
+
const select = buildScalarSelect(metadata);
|
|
1507
|
+
const hydrated = hydrate(rawItems, select, metadata);
|
|
1508
|
+
if (metadata.embeddedFields.length === 0) {
|
|
1509
|
+
return hydrated;
|
|
1510
|
+
}
|
|
1511
|
+
return hydrated.map((item, index) => {
|
|
1512
|
+
const raw = rawItems[index];
|
|
1513
|
+
const merged = { ...item };
|
|
1514
|
+
for (const emb of metadata.embeddedFields) {
|
|
1515
|
+
const rawEmb = raw?.[emb.propertyName];
|
|
1516
|
+
if (rawEmb !== void 0 && rawEmb !== null) {
|
|
1517
|
+
merged[emb.propertyName] = rawEmb;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return merged;
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
async function executeBatchGetForTable(tableName, entries) {
|
|
1524
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1525
|
+
if (entries.length === 0) {
|
|
1526
|
+
return groups;
|
|
1527
|
+
}
|
|
1528
|
+
const keys = entries.map((entry) => entry.dynamoKey);
|
|
1529
|
+
const { items: rawItems } = await ClientManager.getExecutor().batchGet({
|
|
1530
|
+
tableName,
|
|
1531
|
+
keys
|
|
1532
|
+
});
|
|
1533
|
+
const entriesByModel = /* @__PURE__ */ new Map();
|
|
1534
|
+
for (const entry of entries) {
|
|
1535
|
+
const existing = entriesByModel.get(entry.modelClass) ?? [];
|
|
1536
|
+
existing.push(entry);
|
|
1537
|
+
entriesByModel.set(entry.modelClass, existing);
|
|
1538
|
+
}
|
|
1539
|
+
for (const [modelClass, modelEntries] of entriesByModel) {
|
|
1540
|
+
const metadata = modelEntries[0].metadata;
|
|
1541
|
+
const expectedKeys = new Set(
|
|
1542
|
+
modelEntries.map(
|
|
1543
|
+
(entry) => `${String(entry.dynamoKey.PK)}::${String(entry.dynamoKey.SK)}`
|
|
1544
|
+
)
|
|
1545
|
+
);
|
|
1546
|
+
const modelRawItems = rawItems.filter(
|
|
1547
|
+
(item) => expectedKeys.has(`${String(item.PK)}::${String(item.SK)}`)
|
|
1548
|
+
);
|
|
1549
|
+
groups.set(modelClass, hydrateBatchItems(modelRawItems, metadata));
|
|
1550
|
+
}
|
|
1551
|
+
return groups;
|
|
1552
|
+
}
|
|
1553
|
+
async function executeBatchGet(requests) {
|
|
1554
|
+
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const request of requests) {
|
|
1556
|
+
const modelClass = resolveModelClass(request.model);
|
|
1557
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1558
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1559
|
+
for (const keyObj of request.keys) {
|
|
1560
|
+
const entry = {
|
|
1561
|
+
modelClass,
|
|
1562
|
+
metadata,
|
|
1563
|
+
dynamoKey: buildDynamoKey(modelClass, keyObj)
|
|
1564
|
+
};
|
|
1565
|
+
const existing = entriesByTable.get(tableName) ?? [];
|
|
1566
|
+
existing.push(entry);
|
|
1567
|
+
entriesByTable.set(tableName, existing);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
const mergedGroups = /* @__PURE__ */ new Map();
|
|
1571
|
+
for (const [, entries] of entriesByTable) {
|
|
1572
|
+
const tableName = TableMapping.resolve(entries[0].metadata.tableName);
|
|
1573
|
+
const tableGroups = await executeBatchGetForTable(tableName, entries);
|
|
1574
|
+
for (const [modelClass, items] of tableGroups) {
|
|
1575
|
+
const existing = mergedGroups.get(modelClass) ?? [];
|
|
1576
|
+
mergedGroups.set(modelClass, existing.concat(items));
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return new BatchGetResult(mergedGroups);
|
|
1580
|
+
}
|
|
1581
|
+
function toExecItem(entry) {
|
|
1582
|
+
if (entry.request.type === "put") {
|
|
1583
|
+
return { type: "put", item: entry.request.putInput.Item };
|
|
1584
|
+
}
|
|
1585
|
+
return { type: "delete", key: entry.request.deleteInput.Key };
|
|
1586
|
+
}
|
|
1587
|
+
async function executeBatchWrite(requests) {
|
|
1588
|
+
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1589
|
+
for (const request of requests) {
|
|
1590
|
+
const modelClass = resolveModelClass(request.model);
|
|
1591
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1592
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1593
|
+
if (request.type === "put") {
|
|
1594
|
+
const putInput = buildPutInput(modelClass, request.item, request.options);
|
|
1595
|
+
const entry2 = {
|
|
1596
|
+
tableName,
|
|
1597
|
+
modelName: modelClass.name,
|
|
1598
|
+
request: { type: "put", putInput }
|
|
1599
|
+
};
|
|
1600
|
+
const existing2 = entriesByTable.get(tableName) ?? [];
|
|
1601
|
+
existing2.push(entry2);
|
|
1602
|
+
entriesByTable.set(tableName, existing2);
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const deleteInput = buildDeleteInput(modelClass, request.key, request.options);
|
|
1606
|
+
const entry = {
|
|
1607
|
+
tableName,
|
|
1608
|
+
modelName: modelClass.name,
|
|
1609
|
+
request: { type: "delete", deleteInput }
|
|
1610
|
+
};
|
|
1611
|
+
const existing = entriesByTable.get(tableName) ?? [];
|
|
1612
|
+
existing.push(entry);
|
|
1613
|
+
entriesByTable.set(tableName, existing);
|
|
1614
|
+
}
|
|
1615
|
+
const executor = ClientManager.getExecutor();
|
|
1616
|
+
for (const [tableName, entries] of entriesByTable) {
|
|
1617
|
+
await executor.batchWrite(tableName, entries.map(toExecItem));
|
|
1618
|
+
}
|
|
1619
|
+
if (ChangeCaptureRegistry.hasSubscribers()) {
|
|
1620
|
+
for (const [, entries] of entriesByTable) {
|
|
1621
|
+
for (const entry of entries) {
|
|
1622
|
+
if (entry.request.type === "put") {
|
|
1623
|
+
const item = entry.request.putInput.Item;
|
|
1624
|
+
captureWrite({
|
|
1625
|
+
table: entry.tableName,
|
|
1626
|
+
...entry.modelName ? { model: entry.modelName } : {},
|
|
1627
|
+
op: "put",
|
|
1628
|
+
keys: { pk: String(item.PK), sk: String(item.SK ?? "") },
|
|
1629
|
+
newItem: item
|
|
1630
|
+
});
|
|
1631
|
+
} else {
|
|
1632
|
+
const dkey = entry.request.deleteInput.Key;
|
|
1633
|
+
captureWrite({
|
|
1634
|
+
table: entry.tableName,
|
|
1635
|
+
...entry.modelName ? { model: entry.modelName } : {},
|
|
1636
|
+
op: "delete",
|
|
1637
|
+
keys: { pk: String(dkey.PK), sk: String(dkey.SK ?? "") }
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
265
1645
|
// src/define/entity-writes.ts
|
|
266
1646
|
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
267
1647
|
"graphddb:lifecycleContract"
|
|
@@ -269,86 +1649,1087 @@ var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
|
269
1649
|
function isLifecycleContract(value) {
|
|
270
1650
|
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
271
1651
|
}
|
|
272
|
-
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
273
|
-
function isEntityWritesDefinition(value) {
|
|
274
|
-
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
1652
|
+
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
1653
|
+
function isEntityWritesDefinition(value) {
|
|
1654
|
+
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
1655
|
+
}
|
|
1656
|
+
function freezeEffects(effects) {
|
|
1657
|
+
const out = {};
|
|
1658
|
+
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
1659
|
+
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
1660
|
+
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
1661
|
+
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
1662
|
+
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
1663
|
+
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
1664
|
+
return out;
|
|
1665
|
+
}
|
|
1666
|
+
var recorder = {
|
|
1667
|
+
lifecycle(effects = {}) {
|
|
1668
|
+
return {
|
|
1669
|
+
[LIFECYCLE_CONTRACT_MARKER]: true,
|
|
1670
|
+
effects: freezeEffects(effects)
|
|
1671
|
+
};
|
|
1672
|
+
},
|
|
1673
|
+
exists(targetFactory, keys) {
|
|
1674
|
+
return { kind: "requires", targetFactory, keys };
|
|
1675
|
+
},
|
|
1676
|
+
unique(spec) {
|
|
1677
|
+
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
1678
|
+
},
|
|
1679
|
+
putEdge(targetFactory, relationProperty) {
|
|
1680
|
+
return { kind: "putEdge", targetFactory, relationProperty };
|
|
1681
|
+
},
|
|
1682
|
+
deleteEdge(targetFactory, relationProperty) {
|
|
1683
|
+
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
1684
|
+
},
|
|
1685
|
+
increment(targetFactory, keys, attribute, amount) {
|
|
1686
|
+
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
1687
|
+
},
|
|
1688
|
+
event(name, payload) {
|
|
1689
|
+
return { kind: "event", name, payload };
|
|
1690
|
+
},
|
|
1691
|
+
idempotentBy(token) {
|
|
1692
|
+
return { kind: "idempotency", token };
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
function entityWrites(builder) {
|
|
1696
|
+
const shape = builder(recorder);
|
|
1697
|
+
for (const phase of ["create", "update", "remove"]) {
|
|
1698
|
+
const contract = shape[phase];
|
|
1699
|
+
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
1700
|
+
throw new Error(
|
|
1701
|
+
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
1706
|
+
throw new Error(
|
|
1707
|
+
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
return {
|
|
1711
|
+
[ENTITY_WRITES_MARKER]: true,
|
|
1712
|
+
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
1713
|
+
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
1714
|
+
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
function getEntityWrites(modelClass) {
|
|
1718
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
1719
|
+
let value;
|
|
1720
|
+
try {
|
|
1721
|
+
value = modelClass[name];
|
|
1722
|
+
} catch {
|
|
1723
|
+
continue;
|
|
1724
|
+
}
|
|
1725
|
+
if (isEntityWritesDefinition(value)) return value;
|
|
1726
|
+
}
|
|
1727
|
+
return void 0;
|
|
1728
|
+
}
|
|
1729
|
+
function lifecyclePhaseForIntent(intent) {
|
|
1730
|
+
return intent;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// src/relation/edge-write.ts
|
|
1734
|
+
var OLD_VALUE_NAMESPACE = "old";
|
|
1735
|
+
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
1736
|
+
function isEdgeWritesDefinition(value) {
|
|
1737
|
+
return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
|
|
1738
|
+
}
|
|
1739
|
+
function declaration(target, relationProperty) {
|
|
1740
|
+
return { targetFactory: target, relationProperty };
|
|
1741
|
+
}
|
|
1742
|
+
var recorder2 = {
|
|
1743
|
+
putEdge: declaration,
|
|
1744
|
+
deleteEdge: declaration,
|
|
1745
|
+
updateKeyChangeEdge: declaration
|
|
1746
|
+
};
|
|
1747
|
+
function edgeWrites(builder) {
|
|
1748
|
+
const edges = builder(recorder2);
|
|
1749
|
+
if (edges.length === 0) {
|
|
1750
|
+
throw new Error(
|
|
1751
|
+
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
return { [EDGE_WRITES_MARKER]: true, edges };
|
|
1755
|
+
}
|
|
1756
|
+
function edgeKeyFields(segmented) {
|
|
1757
|
+
return [
|
|
1758
|
+
...segmentFieldNames(segmented.pkSegments),
|
|
1759
|
+
...segmentFieldNames(segmented.skSegments)
|
|
1760
|
+
];
|
|
1761
|
+
}
|
|
1762
|
+
function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
1763
|
+
const targetClass = decl.targetFactory();
|
|
1764
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1765
|
+
const relation = targetMeta.relations.find(
|
|
1766
|
+
(r) => r.propertyName === decl.relationProperty
|
|
1767
|
+
);
|
|
1768
|
+
if (!relation) {
|
|
1769
|
+
throw new Error(
|
|
1770
|
+
`Edge write declaration references relation '${decl.relationProperty}' on '${targetClass.name}', which has no such relation. Declare the edge with the read-side relation property it materializes.`
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
const boundFields = Object.keys(relation.keyBinding);
|
|
1774
|
+
for (const targetField of boundFields) {
|
|
1775
|
+
if (!edgeFields.has(targetField)) {
|
|
1776
|
+
throw new Error(
|
|
1777
|
+
`Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds on field '${targetField}', which is not a key field of the adjacency entity '${entity.tableName}' edge (key fields: ${[...edgeFields].map((f) => `'${f}'`).join(", ")}). The written row would not be reachable by the relation's read \u2014 declare the edge on the entity whose key carries the relation's bound field.`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
try {
|
|
1782
|
+
resolveKey(boundFields, entity);
|
|
1783
|
+
} catch (cause) {
|
|
1784
|
+
throw new Error(
|
|
1785
|
+
`Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds [${boundFields.map((f) => `'${f}'`).join(", ")}], which does not cover any access-pattern partition of the adjacency entity '${entity.tableName}' edge (base key [${edgeKeyFields(entity.primaryKey.segmented).map((f) => `'${f}'`).join(", ")}]). The relation's read could not resolve a partition for the written row, so the edge would be unreadable \u2014 bind every partition-key field of the index the relation reads through. (${cause.message})`
|
|
1786
|
+
);
|
|
1787
|
+
}
|
|
1788
|
+
return relation;
|
|
1789
|
+
}
|
|
1790
|
+
function templateRecord(fields) {
|
|
1791
|
+
const out = {};
|
|
1792
|
+
for (const field of [...fields].sort()) {
|
|
1793
|
+
out[field] = `{${field}}`;
|
|
1794
|
+
}
|
|
1795
|
+
return out;
|
|
1796
|
+
}
|
|
1797
|
+
function keyConditionTemplate(segmented, namespace) {
|
|
1798
|
+
const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
|
|
1799
|
+
const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
|
|
1800
|
+
const out = { PK: pk };
|
|
1801
|
+
if (sk !== void 0) out.SK = sk;
|
|
1802
|
+
return out;
|
|
1803
|
+
}
|
|
1804
|
+
function putItemTemplate(edgeFields) {
|
|
1805
|
+
return templateRecord(edgeFields);
|
|
1806
|
+
}
|
|
1807
|
+
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
1808
|
+
const entity = MetadataRegistry.get(adjacencyClass);
|
|
1809
|
+
if (!entity.primaryKey) {
|
|
1810
|
+
throw new Error(
|
|
1811
|
+
`Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
const segmented = entity.primaryKey.segmented;
|
|
1815
|
+
const edgeFields = edgeKeyFields(segmented);
|
|
1816
|
+
assertRelationRoundTrips(entity, new Set(edgeFields), decl);
|
|
1817
|
+
const tableName = TableMapping.resolve(entity.tableName);
|
|
1818
|
+
const entityName = adjacencyClass.name;
|
|
1819
|
+
const put = {
|
|
1820
|
+
type: "Put",
|
|
1821
|
+
tableName,
|
|
1822
|
+
entity: entityName,
|
|
1823
|
+
item: putItemTemplate(edgeFields)
|
|
1824
|
+
};
|
|
1825
|
+
const deleteNew = {
|
|
1826
|
+
type: "Delete",
|
|
1827
|
+
tableName,
|
|
1828
|
+
entity: entityName,
|
|
1829
|
+
keyCondition: keyConditionTemplate(segmented)
|
|
1830
|
+
};
|
|
1831
|
+
const deleteOld = {
|
|
1832
|
+
type: "Delete",
|
|
1833
|
+
tableName,
|
|
1834
|
+
entity: entityName,
|
|
1835
|
+
keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
|
|
1836
|
+
};
|
|
1837
|
+
switch (lifecycle) {
|
|
1838
|
+
case "onCreate":
|
|
1839
|
+
return [put];
|
|
1840
|
+
case "onDelete":
|
|
1841
|
+
return [deleteNew];
|
|
1842
|
+
case "onUpdateKeyChange":
|
|
1843
|
+
return [deleteOld, put];
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
|
|
1847
|
+
const items = [];
|
|
1848
|
+
for (const decl of writes.edges) {
|
|
1849
|
+
items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
|
|
1850
|
+
}
|
|
1851
|
+
return items;
|
|
1852
|
+
}
|
|
1853
|
+
function getEdgeWrites(modelClass) {
|
|
1854
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
1855
|
+
let value;
|
|
1856
|
+
try {
|
|
1857
|
+
value = modelClass[name];
|
|
1858
|
+
} catch {
|
|
1859
|
+
continue;
|
|
1860
|
+
}
|
|
1861
|
+
if (isEdgeWritesDefinition(value)) return value;
|
|
1862
|
+
}
|
|
1863
|
+
return void 0;
|
|
1864
|
+
}
|
|
1865
|
+
function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
1866
|
+
const writes = getEdgeWrites(modelClass);
|
|
1867
|
+
if (!writes) {
|
|
1868
|
+
throw new Error(
|
|
1869
|
+
`Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
|
|
1870
|
+
);
|
|
1871
|
+
}
|
|
1872
|
+
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
// src/runtime/command-runtime.ts
|
|
1876
|
+
function resolveCommandMethod(contract, methodName) {
|
|
1877
|
+
const method = contract.methods[methodName];
|
|
1878
|
+
if (method === void 0) {
|
|
1879
|
+
throw new Error(
|
|
1880
|
+
`Contract runtime: command method '${methodName}' does not exist on this contract. Available methods: ${Object.keys(contract.methods).sort().join(", ") || "(none)"}.`
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
return method;
|
|
1884
|
+
}
|
|
1885
|
+
function modelClassOf(op) {
|
|
1886
|
+
return op.entity.modelClass;
|
|
1887
|
+
}
|
|
1888
|
+
function modelStaticOf(op) {
|
|
1889
|
+
return op.entity.modelClass.asModel();
|
|
1890
|
+
}
|
|
1891
|
+
function renderLeaf(leaf, key, params, context) {
|
|
1892
|
+
if (isContractParamRef(leaf)) {
|
|
1893
|
+
if (!(leaf.field in params)) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`Contract runtime: ${context} references params field '${leaf.field}', which was not supplied. Pass it in the method's params argument.`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
return params[leaf.field];
|
|
1899
|
+
}
|
|
1900
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
1901
|
+
if (leaf.field in key) return key[leaf.field];
|
|
1902
|
+
if (leaf.field in params) return params[leaf.field];
|
|
1903
|
+
throw new Error(
|
|
1904
|
+
`Contract runtime: ${context} references key field '${leaf.field}', which was not supplied. Pass it in the method's key or params argument.`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
return leaf;
|
|
1908
|
+
}
|
|
1909
|
+
function renderRecord(structure, key, params, context) {
|
|
1910
|
+
const out = {};
|
|
1911
|
+
if (structure === void 0) return out;
|
|
1912
|
+
for (const [field, leaf] of Object.entries(structure)) {
|
|
1913
|
+
out[field] = renderLeaf(leaf, key, params, `${context} field '${field}'`);
|
|
1914
|
+
}
|
|
1915
|
+
return out;
|
|
1916
|
+
}
|
|
1917
|
+
function renderCondition(condition, key, params, context) {
|
|
1918
|
+
if (condition === void 0) return void 0;
|
|
1919
|
+
if (typeof condition === "object" && condition !== null) {
|
|
1920
|
+
const obj = condition;
|
|
1921
|
+
if (obj.notExists === true) return { notExists: true };
|
|
1922
|
+
if (typeof obj.attributeExists === "string") {
|
|
1923
|
+
return { attributeExists: obj.attributeExists };
|
|
1924
|
+
}
|
|
1925
|
+
if (typeof obj.attributeNotExists === "string") {
|
|
1926
|
+
return { attributeNotExists: obj.attributeNotExists };
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return renderRecord(
|
|
1930
|
+
condition,
|
|
1931
|
+
key,
|
|
1932
|
+
params,
|
|
1933
|
+
`${context} condition`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
function renderWrite(op, key, params, contextLabel) {
|
|
1937
|
+
if (!isContractKeyRef(op.keys) && op.operation !== "put") {
|
|
1938
|
+
throw new Error(
|
|
1939
|
+
`Contract runtime: ${contextLabel} did not pass the contract key directly into its '${op.operation}' key position. A command method must call \`Model.${op.operation}(keys, \u2026)\` with the key whole.`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
const base = {
|
|
1943
|
+
operation: op.operation,
|
|
1944
|
+
modelClass: modelClassOf(op),
|
|
1945
|
+
modelStatic: modelStaticOf(op),
|
|
1946
|
+
key,
|
|
1947
|
+
condition: renderCondition(op.condition, key, params, contextLabel)
|
|
1948
|
+
};
|
|
1949
|
+
if (op.operation === "put") {
|
|
1950
|
+
return { ...base, item: renderRecord(op.item, key, params, `${contextLabel} item`) };
|
|
1951
|
+
}
|
|
1952
|
+
if (op.operation === "update") {
|
|
1953
|
+
return { ...base, changes: renderRecord(op.changes, key, params, `${contextLabel} changes`) };
|
|
1954
|
+
}
|
|
1955
|
+
return base;
|
|
1956
|
+
}
|
|
1957
|
+
async function executeSingleWrite(w) {
|
|
1958
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
1959
|
+
if (w.operation === "put") {
|
|
1960
|
+
await executePut(w.modelClass, w.item ?? {}, options);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (w.operation === "update") {
|
|
1964
|
+
await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
await executeDelete(w.modelClass, w.key, options);
|
|
275
1968
|
}
|
|
276
|
-
function
|
|
1969
|
+
async function readBackProjection(write, returnSelection) {
|
|
1970
|
+
const select = {};
|
|
1971
|
+
for (const field of Object.keys(returnSelection)) {
|
|
1972
|
+
if (returnSelection[field] === true) select[field] = true;
|
|
1973
|
+
}
|
|
1974
|
+
const key = readBackKey(write);
|
|
1975
|
+
return executeQuery(write.modelClass, key, select, { consistentRead: true });
|
|
1976
|
+
}
|
|
1977
|
+
function readBackKey(write) {
|
|
1978
|
+
if (write.operation !== "put") return write.key;
|
|
1979
|
+
const metadata = MetadataRegistry.get(write.modelClass);
|
|
1980
|
+
const item = write.item ?? {};
|
|
1981
|
+
const key = {};
|
|
1982
|
+
const fields = metadata.primaryKey ? metadata.primaryKey.inputFieldNames : Object.keys(item);
|
|
1983
|
+
for (const field of fields) {
|
|
1984
|
+
if (field in item) key[field] = item[field];
|
|
1985
|
+
}
|
|
1986
|
+
return key;
|
|
1987
|
+
}
|
|
1988
|
+
function renderConditionCheck(check, key, params, contextLabel) {
|
|
1989
|
+
const modelClass = check.entity.modelClass;
|
|
1990
|
+
const referencedKey = {};
|
|
1991
|
+
for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
|
|
1992
|
+
if (inputField in params) {
|
|
1993
|
+
referencedKey[targetField] = params[inputField];
|
|
1994
|
+
} else if (inputField in key) {
|
|
1995
|
+
referencedKey[targetField] = key[inputField];
|
|
1996
|
+
} else {
|
|
1997
|
+
throw new Error(
|
|
1998
|
+
`Contract runtime: ${contextLabel} asserts '${check.entity.name}' exists, keyed by input field '${inputField}', which was not supplied. Pass it in the method's params (or key) argument.`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2003
|
+
modelClass,
|
|
2004
|
+
referencedKey
|
|
2005
|
+
);
|
|
2006
|
+
const cond2 = buildConditionExpression({ attributeExists: "PK" });
|
|
2007
|
+
const checkInput = {
|
|
2008
|
+
TableName,
|
|
2009
|
+
Key,
|
|
2010
|
+
ConditionExpression: cond2.expression
|
|
2011
|
+
};
|
|
2012
|
+
if (Object.keys(cond2.names).length > 0) checkInput.ExpressionAttributeNames = cond2.names;
|
|
2013
|
+
if (Object.keys(cond2.values).length > 0) checkInput.ExpressionAttributeValues = cond2.values;
|
|
2014
|
+
return checkInput;
|
|
2015
|
+
}
|
|
2016
|
+
var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
|
|
2017
|
+
function renderTemplate(template, key, params, contextLabel) {
|
|
2018
|
+
const resolveToken = (token) => {
|
|
2019
|
+
if (token in params) return params[token];
|
|
2020
|
+
if (token in key) return key[token];
|
|
2021
|
+
throw new Error(
|
|
2022
|
+
`Contract runtime: ${contextLabel} references input field '${token}', which was not supplied. Pass it in the method's params (or key) argument` + (token.startsWith("old.") ? ` \u2014 an \`old.*\` token is the pre-mutation value of a foreign key on an onUpdateKeyChange (pass it as \`{ '${token}': \u2026 }\`).` : `.`)
|
|
2023
|
+
);
|
|
2024
|
+
};
|
|
2025
|
+
const whole = /^\{([^{}]+)\}$/.exec(template);
|
|
2026
|
+
if (whole !== null) return resolveToken(whole[1]);
|
|
2027
|
+
return template.replace(RUNTIME_TOKEN_RE, (_m, token) => String(resolveToken(token)));
|
|
2028
|
+
}
|
|
2029
|
+
function renderTemplateRecord(record, key, params, contextLabel) {
|
|
277
2030
|
const out = {};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
282
|
-
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
283
|
-
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
2031
|
+
for (const [field, template] of Object.entries(record ?? {})) {
|
|
2032
|
+
out[field] = renderTemplate(template, key, params, contextLabel);
|
|
2033
|
+
}
|
|
284
2034
|
return out;
|
|
285
2035
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
2036
|
+
function renderEdgeWriteItems(edge, key, params, contextLabel, modelBySignature) {
|
|
2037
|
+
const adjacency = edge.entity.modelClass;
|
|
2038
|
+
return edge.items.map((item) => {
|
|
2039
|
+
if (item.type === "Put") {
|
|
2040
|
+
const rendered = renderTemplateRecord(item.item, key, params, `${contextLabel} edge Put`);
|
|
2041
|
+
const out2 = { Put: buildPutInput(adjacency, rendered) };
|
|
2042
|
+
modelBySignature?.set(execItemKeySignature(out2), adjacency.name);
|
|
2043
|
+
return out2;
|
|
2044
|
+
}
|
|
2045
|
+
const renderedKey = renderTemplateRecord(
|
|
2046
|
+
item.keyCondition,
|
|
2047
|
+
key,
|
|
2048
|
+
params,
|
|
2049
|
+
`${contextLabel} edge Delete`
|
|
2050
|
+
);
|
|
2051
|
+
const del = {
|
|
2052
|
+
TableName: TableMapping.resolve(item.tableName),
|
|
2053
|
+
Key: renderedKey
|
|
291
2054
|
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
304
|
-
},
|
|
305
|
-
increment(targetFactory, keys, attribute, amount) {
|
|
306
|
-
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
307
|
-
},
|
|
308
|
-
event(name, payload) {
|
|
309
|
-
return { kind: "event", name, payload };
|
|
310
|
-
},
|
|
311
|
-
idempotentBy(token) {
|
|
312
|
-
return { kind: "idempotency", token };
|
|
2055
|
+
const out = { Delete: del };
|
|
2056
|
+
modelBySignature?.set(execItemKeySignature(out), adjacency.name);
|
|
2057
|
+
return out;
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
function renderDerivedUpdate(update, key, params, contextLabel, modelBySignature) {
|
|
2061
|
+
const metadata = MetadataRegistry.get(update.entity.modelClass);
|
|
2062
|
+
if (!metadata.primaryKey) {
|
|
2063
|
+
throw new Error(
|
|
2064
|
+
`Contract runtime: ${contextLabel} derives a counter update on '${update.entity.name}', which has no primary key.`
|
|
2065
|
+
);
|
|
313
2066
|
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
2067
|
+
const keyInput = {};
|
|
2068
|
+
for (const [targetField, inputField] of Object.entries(update.keyBinding)) {
|
|
2069
|
+
keyInput[targetField] = renderTemplate(
|
|
2070
|
+
`{${inputField}}`,
|
|
2071
|
+
key,
|
|
2072
|
+
params,
|
|
2073
|
+
`${contextLabel} derived-update key`
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2077
|
+
update.entity.modelClass,
|
|
2078
|
+
keyInput
|
|
2079
|
+
);
|
|
2080
|
+
const updateInput = {
|
|
2081
|
+
TableName,
|
|
2082
|
+
Key,
|
|
2083
|
+
UpdateExpression: "ADD #a0 :a0",
|
|
2084
|
+
ExpressionAttributeNames: { "#a0": update.attribute },
|
|
2085
|
+
ExpressionAttributeValues: { ":a0": update.amount }
|
|
2086
|
+
};
|
|
2087
|
+
const out = { Update: updateInput };
|
|
2088
|
+
modelBySignature?.set(execItemKeySignature(out), update.entity.modelClass.name);
|
|
2089
|
+
return out;
|
|
2090
|
+
}
|
|
2091
|
+
function renderUniqueGuardItems(guard, key, params, contextLabel) {
|
|
2092
|
+
return guard.items.map((item) => {
|
|
2093
|
+
if (item.type === "Put") {
|
|
2094
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} guard Put`);
|
|
2095
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2096
|
+
const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
|
|
2097
|
+
put.ConditionExpression = cond2.expression;
|
|
2098
|
+
if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
|
|
2099
|
+
if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
|
|
2100
|
+
return { Put: put };
|
|
2101
|
+
}
|
|
2102
|
+
const Key = renderTemplateRecord(item.keyCondition, key, params, `${contextLabel} guard Delete`);
|
|
2103
|
+
const del = { TableName: TableMapping.resolve(item.tableName), Key };
|
|
2104
|
+
return { Delete: del };
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
function renderOutboxEventItems(event, key, params, contextLabel) {
|
|
2108
|
+
return event.items.map((item) => {
|
|
2109
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} outbox Put`);
|
|
2110
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2111
|
+
return { Put: put };
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
2115
|
+
return guard.items.map((item) => {
|
|
2116
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
|
|
2117
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2118
|
+
const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
|
|
2119
|
+
put.ConditionExpression = cond2.expression;
|
|
2120
|
+
if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
|
|
2121
|
+
if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
|
|
2122
|
+
return { Put: put };
|
|
2123
|
+
});
|
|
2124
|
+
}
|
|
2125
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map()) {
|
|
2126
|
+
const writeItems = writes.map((w) => {
|
|
2127
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2128
|
+
let item;
|
|
2129
|
+
if (w.operation === "put") {
|
|
2130
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
2131
|
+
} else if (w.operation === "update") {
|
|
2132
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
2133
|
+
} else {
|
|
2134
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
2135
|
+
}
|
|
2136
|
+
modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
|
|
2137
|
+
return item;
|
|
2138
|
+
});
|
|
2139
|
+
const checkItems = checks.map((c) => ({ ConditionCheck: c.check }));
|
|
2140
|
+
await commitTransaction(
|
|
2141
|
+
[
|
|
2142
|
+
...writeItems,
|
|
2143
|
+
...edgeItems,
|
|
2144
|
+
...updateItems,
|
|
2145
|
+
...guardItemsRaw,
|
|
2146
|
+
...outboxItems,
|
|
2147
|
+
...idempotencyItems,
|
|
2148
|
+
...checkItems
|
|
2149
|
+
],
|
|
2150
|
+
{
|
|
2151
|
+
modelBySignature,
|
|
2152
|
+
limitError: (count) => new Error(
|
|
2153
|
+
`Contract runtime: command method '${methodName}' composes ${count} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split. Reduce the fragment / effect count.`
|
|
2154
|
+
)
|
|
2155
|
+
}
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
async function executeTransactWrites(writes, methodName) {
|
|
2159
|
+
if (writes.length === 0) return;
|
|
2160
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2161
|
+
const transactItems = writes.map((w) => {
|
|
2162
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2163
|
+
let item;
|
|
2164
|
+
if (w.operation === "put") {
|
|
2165
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
2166
|
+
} else if (w.operation === "update") {
|
|
2167
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
2168
|
+
} else {
|
|
2169
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
2170
|
+
}
|
|
2171
|
+
modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
|
|
2172
|
+
return item;
|
|
2173
|
+
});
|
|
2174
|
+
await commitTransaction(transactItems, {
|
|
2175
|
+
modelBySignature,
|
|
2176
|
+
limitError: (count) => new Error(
|
|
2177
|
+
`Contract runtime: command method '${methodName}' resolves an array of ${count} keys to a 'transact' (TransactWriteItems) batch, but DynamoDB caps a transaction at ${MAX_TRANSACT_ITEMS} items and a transaction is atomic \u2014 it cannot be split across requests without breaking atomicity. Pass \u2264${MAX_TRANSACT_ITEMS} keys, or declare \`batch: 'batchWrite'\` (a non-atomic BatchWriteItem, chunked automatically) if atomicity is not required.`
|
|
2178
|
+
)
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
async function executeParallelWrites(writes) {
|
|
2182
|
+
if (writes.length === 0) return [];
|
|
2183
|
+
const coalescible = writes.every(
|
|
2184
|
+
(w) => w.condition === void 0 && (w.operation === "put" || w.operation === "delete")
|
|
2185
|
+
);
|
|
2186
|
+
if (coalescible) {
|
|
2187
|
+
const requests = writes.map(
|
|
2188
|
+
(w) => w.operation === "put" ? { type: "put", model: w.modelStatic, item: w.item ?? {} } : { type: "delete", model: w.modelStatic, key: w.key }
|
|
2189
|
+
);
|
|
2190
|
+
await executeBatchWrite(requests);
|
|
2191
|
+
return writes.map(() => ({ ok: true }));
|
|
2192
|
+
}
|
|
2193
|
+
return Promise.all(
|
|
2194
|
+
writes.map(async (w) => {
|
|
2195
|
+
try {
|
|
2196
|
+
await executeSingleWrite(w);
|
|
2197
|
+
return { ok: true };
|
|
2198
|
+
} catch (e) {
|
|
2199
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2200
|
+
}
|
|
2201
|
+
})
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}) {
|
|
2205
|
+
const method = resolveCommandMethod(contract, methodName);
|
|
2206
|
+
if (Array.isArray(keyOrKeys)) {
|
|
2207
|
+
const keys = keyOrKeys;
|
|
2208
|
+
if (method.ops !== void 0 && method.ops.length > 1) {
|
|
320
2209
|
throw new Error(
|
|
321
|
-
`
|
|
2210
|
+
`Contract runtime: command method '${methodName}' is a multi-fragment mutation (it composes ${method.ops.length} fragments into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
const hasDerivedEffects2 = method.conditionChecks !== void 0 && method.conditionChecks.length > 0 || method.edgeWrites !== void 0 && method.edgeWrites.length > 0 || method.derivedUpdates !== void 0 && method.derivedUpdates.length > 0 || method.uniqueGuards !== void 0 && method.uniqueGuards.length > 0 || method.outboxEvents !== void 0 && method.outboxEvents.length > 0 || method.idempotencyGuard !== void 0;
|
|
2214
|
+
if (hasDerivedEffects2) {
|
|
2215
|
+
throw new Error(
|
|
2216
|
+
`Contract runtime: command method '${methodName}' carries derived effects (referential integrity / edge writes / derived counters / uniqueness guards / outbox events / idempotency guard \u2014 it composes its write + those items into one atomic TransactWriteItems) and accepts a single key only. Call it once per target, not with a key array.`
|
|
322
2217
|
);
|
|
323
2218
|
}
|
|
2219
|
+
const writes = keys.map(
|
|
2220
|
+
(key2, i) => renderWrite(method.op, key2, params, `command method '${methodName}' (key #${i})`)
|
|
2221
|
+
);
|
|
2222
|
+
if (method.mode === "parallel") {
|
|
2223
|
+
return { results: await executeParallelWrites(writes) };
|
|
2224
|
+
}
|
|
2225
|
+
await executeTransactWrites(writes, methodName);
|
|
2226
|
+
return;
|
|
324
2227
|
}
|
|
325
|
-
|
|
2228
|
+
const key = keyOrKeys;
|
|
2229
|
+
const conditionChecks = method.conditionChecks ?? [];
|
|
2230
|
+
const edgeWrites2 = method.edgeWrites ?? [];
|
|
2231
|
+
const derivedUpdates = method.derivedUpdates ?? [];
|
|
2232
|
+
const uniqueGuards = method.uniqueGuards ?? [];
|
|
2233
|
+
const outboxEvents = method.outboxEvents ?? [];
|
|
2234
|
+
const idempotencyGuard = method.idempotencyGuard;
|
|
2235
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
|
|
2236
|
+
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2237
|
+
if (isMultiFragment || hasDerivedEffects) {
|
|
2238
|
+
const ops = method.ops ?? [method.op];
|
|
2239
|
+
const writes = ops.map(
|
|
2240
|
+
(opn, i) => renderWrite(opn, key, params, `command method '${methodName}' (fragment #${i})`)
|
|
2241
|
+
);
|
|
2242
|
+
if (hasDerivedEffects) {
|
|
2243
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2244
|
+
const edgeItems = edgeWrites2.flatMap(
|
|
2245
|
+
(edge, i) => renderEdgeWriteItems(
|
|
2246
|
+
edge,
|
|
2247
|
+
key,
|
|
2248
|
+
params,
|
|
2249
|
+
`command method '${methodName}' (edge #${i})`,
|
|
2250
|
+
modelBySignature
|
|
2251
|
+
)
|
|
2252
|
+
);
|
|
2253
|
+
const updateItems = derivedUpdates.map(
|
|
2254
|
+
(update, i) => renderDerivedUpdate(
|
|
2255
|
+
update,
|
|
2256
|
+
key,
|
|
2257
|
+
params,
|
|
2258
|
+
`command method '${methodName}' (derive #${i})`,
|
|
2259
|
+
modelBySignature
|
|
2260
|
+
)
|
|
2261
|
+
);
|
|
2262
|
+
const guardItems = uniqueGuards.flatMap(
|
|
2263
|
+
(guard, i) => renderUniqueGuardItems(guard, key, params, `command method '${methodName}' (unique #${i})`)
|
|
2264
|
+
);
|
|
2265
|
+
const outboxItems = outboxEvents.flatMap(
|
|
2266
|
+
(event, i) => renderOutboxEventItems(event, key, params, `command method '${methodName}' (emit #${i})`)
|
|
2267
|
+
);
|
|
2268
|
+
const idempotencyItems = idempotencyGuard !== void 0 ? renderIdempotencyGuardItems(
|
|
2269
|
+
idempotencyGuard,
|
|
2270
|
+
key,
|
|
2271
|
+
params,
|
|
2272
|
+
`command method '${methodName}' (idempotency)`
|
|
2273
|
+
) : [];
|
|
2274
|
+
const checks = conditionChecks.map((check, i) => ({
|
|
2275
|
+
check: renderConditionCheck(
|
|
2276
|
+
check,
|
|
2277
|
+
key,
|
|
2278
|
+
params,
|
|
2279
|
+
`command method '${methodName}' (requires #${i})`
|
|
2280
|
+
)
|
|
2281
|
+
}));
|
|
2282
|
+
await executeReferentialTransaction(
|
|
2283
|
+
writes,
|
|
2284
|
+
edgeItems,
|
|
2285
|
+
updateItems,
|
|
2286
|
+
guardItems,
|
|
2287
|
+
outboxItems,
|
|
2288
|
+
idempotencyItems,
|
|
2289
|
+
checks,
|
|
2290
|
+
methodName,
|
|
2291
|
+
modelBySignature
|
|
2292
|
+
);
|
|
2293
|
+
} else {
|
|
2294
|
+
await executeTransactWrites(writes, methodName);
|
|
2295
|
+
}
|
|
2296
|
+
if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
|
|
2297
|
+
return readBackProjection(writes[0], method.returnSelection);
|
|
2298
|
+
}
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
const write = renderWrite(method.op, key, params, `command method '${methodName}'`);
|
|
2302
|
+
await executeSingleWrite(write);
|
|
2303
|
+
if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
|
|
2304
|
+
return readBackProjection(write, method.returnSelection);
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
function renderCompiledOp(op, ctx) {
|
|
2308
|
+
const fragment = op.fragment;
|
|
2309
|
+
const baseOp = op.condition !== void 0 ? withCondition(fragment.op, op.condition) : fragment.op;
|
|
2310
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2311
|
+
const write = renderWrite(baseOp, op.params, op.params, ctx);
|
|
2312
|
+
const edgeItems = [];
|
|
2313
|
+
const updateItems = [];
|
|
2314
|
+
const guardItems = [];
|
|
2315
|
+
const outboxItems = [];
|
|
2316
|
+
const idempotencyItems = [];
|
|
2317
|
+
const checkItems = [];
|
|
2318
|
+
for (const edge of fragment.edgeWrites ?? []) {
|
|
2319
|
+
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
2320
|
+
}
|
|
2321
|
+
for (const u of fragment.derivedUpdates ?? []) {
|
|
2322
|
+
updateItems.push(renderDerivedUpdate(u, op.params, op.params, `${ctx} derive`, modelBySignature));
|
|
2323
|
+
}
|
|
2324
|
+
for (const g of fragment.uniqueGuards ?? []) {
|
|
2325
|
+
guardItems.push(...renderUniqueGuardItems(g, op.params, op.params, `${ctx} unique`));
|
|
2326
|
+
}
|
|
2327
|
+
for (const e of fragment.outboxEvents ?? []) {
|
|
2328
|
+
outboxItems.push(...renderOutboxEventItems(e, op.params, op.params, `${ctx} emit`));
|
|
2329
|
+
}
|
|
2330
|
+
if (fragment.idempotencyGuard !== void 0) {
|
|
2331
|
+
idempotencyItems.push(
|
|
2332
|
+
...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
for (const c of fragment.conditionChecks ?? []) {
|
|
2336
|
+
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
2337
|
+
}
|
|
2338
|
+
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
|
|
2339
|
+
return {
|
|
2340
|
+
write,
|
|
2341
|
+
edgeItems,
|
|
2342
|
+
updateItems,
|
|
2343
|
+
guardItems,
|
|
2344
|
+
outboxItems,
|
|
2345
|
+
idempotencyItems,
|
|
2346
|
+
checkItems,
|
|
2347
|
+
modelBySignature,
|
|
2348
|
+
hasDerivedEffects
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
function readBackForCompiledOp(op, write) {
|
|
2352
|
+
const sel = op.result?.select;
|
|
2353
|
+
if (sel === void 0 || Object.keys(sel).length === 0) return void 0;
|
|
2354
|
+
const key = {};
|
|
2355
|
+
for (const f of op.fragment.keyFields) {
|
|
2356
|
+
if (f in op.params) key[f] = op.params[f];
|
|
2357
|
+
}
|
|
2358
|
+
return executeQuery(write.modelClass, key, sel, {
|
|
2359
|
+
consistentRead: op.result?.options?.consistentRead ?? true,
|
|
2360
|
+
maxDepth: op.result?.options?.maxDepth
|
|
2361
|
+
});
|
|
2362
|
+
}
|
|
2363
|
+
async function executeCompiledWriteOp(ops, options) {
|
|
2364
|
+
const label = options.label;
|
|
2365
|
+
const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
|
|
2366
|
+
const rendered = renderedOps.map((r) => r.write);
|
|
2367
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2368
|
+
const edgeItems = [];
|
|
2369
|
+
const updateItems = [];
|
|
2370
|
+
const guardItems = [];
|
|
2371
|
+
const outboxItems = [];
|
|
2372
|
+
const idempotencyItems = [];
|
|
2373
|
+
const checkItems = [];
|
|
2374
|
+
for (const r of renderedOps) {
|
|
2375
|
+
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
2376
|
+
edgeItems.push(...r.edgeItems);
|
|
2377
|
+
updateItems.push(...r.updateItems);
|
|
2378
|
+
guardItems.push(...r.guardItems);
|
|
2379
|
+
outboxItems.push(...r.outboxItems);
|
|
2380
|
+
idempotencyItems.push(...r.idempotencyItems);
|
|
2381
|
+
checkItems.push(...r.checkItems);
|
|
2382
|
+
}
|
|
2383
|
+
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
2384
|
+
if (rendered.length === 1 && !hasDerivedEffects) {
|
|
2385
|
+
await executeSingleWrite(rendered[0]);
|
|
2386
|
+
} else {
|
|
2387
|
+
await executeReferentialTransaction(
|
|
2388
|
+
rendered,
|
|
2389
|
+
edgeItems,
|
|
2390
|
+
updateItems,
|
|
2391
|
+
guardItems,
|
|
2392
|
+
outboxItems,
|
|
2393
|
+
idempotencyItems,
|
|
2394
|
+
checkItems,
|
|
2395
|
+
label,
|
|
2396
|
+
modelBySignature
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
const readBacks = await Promise.all(
|
|
2400
|
+
ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
|
|
2401
|
+
);
|
|
2402
|
+
return { readBacks };
|
|
2403
|
+
}
|
|
2404
|
+
async function executeCompiledParallelWrites(ops, options) {
|
|
2405
|
+
const label = options.label;
|
|
2406
|
+
const results = new Array(ops.length);
|
|
2407
|
+
const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
|
|
2408
|
+
const coalescibleIdx = [];
|
|
2409
|
+
const individualIdx = [];
|
|
2410
|
+
renderedOps.forEach((r, i) => {
|
|
2411
|
+
const w = r.write;
|
|
2412
|
+
if (!r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
|
|
2413
|
+
coalescibleIdx.push(i);
|
|
2414
|
+
} else {
|
|
2415
|
+
individualIdx.push(i);
|
|
2416
|
+
}
|
|
2417
|
+
});
|
|
2418
|
+
await Promise.all([
|
|
2419
|
+
// The coalescible group → ONE BatchWriteItem (chunked, UnprocessedItems retry) via
|
|
2420
|
+
// the shared public-layer coalescing. On success every op reports `{ ok: true }`
|
|
2421
|
+
// plus its read-back; a BatchWriteItem request failure fails the whole group.
|
|
2422
|
+
(async () => {
|
|
2423
|
+
if (coalescibleIdx.length === 0) return;
|
|
2424
|
+
const writes = coalescibleIdx.map((i) => renderedOps[i].write);
|
|
2425
|
+
try {
|
|
2426
|
+
await executeParallelWrites(writes);
|
|
2427
|
+
await Promise.all(
|
|
2428
|
+
coalescibleIdx.map(async (i) => {
|
|
2429
|
+
const value = await readBackForCompiledOp(ops[i], renderedOps[i].write);
|
|
2430
|
+
results[i] = { ok: true, value };
|
|
2431
|
+
})
|
|
2432
|
+
);
|
|
2433
|
+
} catch (e) {
|
|
2434
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
2435
|
+
for (const i of coalescibleIdx) results[i] = { ok: false, error };
|
|
2436
|
+
}
|
|
2437
|
+
})(),
|
|
2438
|
+
// Each individual op → its own single-op atomic execution (condition gate / derived
|
|
2439
|
+
// effects / read-back preserved), concurrently and with per-op partial success.
|
|
2440
|
+
...individualIdx.map(async (i) => {
|
|
2441
|
+
try {
|
|
2442
|
+
const { readBacks } = await executeCompiledWriteOp([ops[i]], {
|
|
2443
|
+
label: `${label} (op #${i})`
|
|
2444
|
+
});
|
|
2445
|
+
results[i] = { ok: true, value: readBacks[0] };
|
|
2446
|
+
} catch (e) {
|
|
2447
|
+
results[i] = { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
|
|
2448
|
+
}
|
|
2449
|
+
})
|
|
2450
|
+
]);
|
|
2451
|
+
return results;
|
|
2452
|
+
}
|
|
2453
|
+
function withCondition(op, condition) {
|
|
2454
|
+
if (op.condition === void 0) return { ...op, condition };
|
|
2455
|
+
const base = op.condition;
|
|
2456
|
+
if (base.notExists === true && Object.keys(condition).length > 0) {
|
|
326
2457
|
throw new Error(
|
|
327
|
-
|
|
2458
|
+
`DDBModel.mutate: a \`condition\` (equality write gate) on a 'create' is not supported \u2014 a create already guards with attribute_not_exists(PK). Use an 'update' with a condition, or drop the condition.`
|
|
2459
|
+
);
|
|
2460
|
+
}
|
|
2461
|
+
return { ...op, condition };
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
// src/runtime/envelope.ts
|
|
2465
|
+
async function executeReadEnvelope(envelope) {
|
|
2466
|
+
const aliases = Object.keys(envelope);
|
|
2467
|
+
const results = await Promise.all(
|
|
2468
|
+
aliases.map((alias) => executeReadRoute(alias, envelope[alias]))
|
|
2469
|
+
);
|
|
2470
|
+
const out = {};
|
|
2471
|
+
aliases.forEach((alias, i) => {
|
|
2472
|
+
out[alias] = results[i];
|
|
2473
|
+
});
|
|
2474
|
+
return out;
|
|
2475
|
+
}
|
|
2476
|
+
async function executeReadRoute(alias, route) {
|
|
2477
|
+
if (route === null || typeof route !== "object") {
|
|
2478
|
+
throw new Error(
|
|
2479
|
+
`DDBModel.query: the descriptor for alias '${alias}' is not a read descriptor (\`{ query | list: Model, key, select, options? }\`).`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
const isQuery = route.query !== void 0;
|
|
2483
|
+
const isList = route.list !== void 0;
|
|
2484
|
+
if (isQuery === isList) {
|
|
2485
|
+
throw new Error(
|
|
2486
|
+
`DDBModel.query: the descriptor for alias '${alias}' must carry exactly one of \`query\` or \`list\` (the target model). It declared ${isQuery && isList ? "both" : "neither"}.`
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
if (route.key === void 0 || route.select === void 0) {
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
`DDBModel.query: the '${isQuery ? "query" : "list"}' descriptor for alias '${alias}' must declare both \`key\` and \`select\`.`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
const modelClass = resolveModelClass(
|
|
2495
|
+
resolveModelStatic(isQuery ? route.query : route.list, alias)
|
|
2496
|
+
);
|
|
2497
|
+
const opt = route.options ?? {};
|
|
2498
|
+
if (isQuery) {
|
|
2499
|
+
return executeQuery(modelClass, route.key, route.select, {
|
|
2500
|
+
consistentRead: opt.consistentRead,
|
|
2501
|
+
maxDepth: opt.maxDepth
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
return executeList(modelClass, route.key, route.select, {
|
|
2505
|
+
limit: opt.limit,
|
|
2506
|
+
after: opt.after,
|
|
2507
|
+
order: opt.order,
|
|
2508
|
+
filter: opt.filter
|
|
2509
|
+
});
|
|
2510
|
+
}
|
|
2511
|
+
var INTENT_KEYS = ["create", "update", "remove"];
|
|
2512
|
+
async function executeWriteEnvelope(envelope, options = {}) {
|
|
2513
|
+
const mode = options.mode ?? "transaction";
|
|
2514
|
+
const aliases = Object.keys(envelope);
|
|
2515
|
+
if (aliases.length === 0) {
|
|
2516
|
+
throw new Error(
|
|
2517
|
+
"DDBModel.mutate: the write envelope is empty. Declare at least one `{ alias: { create | update | remove: Model, key, input? } }` operation."
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
|
|
2521
|
+
if (mode === "transaction") {
|
|
2522
|
+
return executeTransactionMode(ops);
|
|
2523
|
+
}
|
|
2524
|
+
return executeParallelMode(ops);
|
|
2525
|
+
}
|
|
2526
|
+
function normalizeWriteOp(alias, d) {
|
|
2527
|
+
if (d === null || typeof d !== "object") {
|
|
2528
|
+
throw new Error(
|
|
2529
|
+
`DDBModel.mutate: the descriptor for alias '${alias}' is not a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result? }\`).`
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
const present = INTENT_KEYS.filter((k) => d[k] !== void 0);
|
|
2533
|
+
if (present.length !== 1) {
|
|
2534
|
+
throw new Error(
|
|
2535
|
+
`DDBModel.mutate: the descriptor for alias '${alias}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
|
|
2536
|
+
);
|
|
2537
|
+
}
|
|
2538
|
+
const intent = present[0];
|
|
2539
|
+
if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
|
|
2540
|
+
throw new Error(
|
|
2541
|
+
`DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object (or a non-empty array of key objects) binding the target's primary-key fields.`
|
|
328
2542
|
);
|
|
329
2543
|
}
|
|
2544
|
+
const model = resolveModelStatic(d[intent], alias);
|
|
2545
|
+
const input = d.input ?? {};
|
|
2546
|
+
const base = { alias, intent, model, input, condition: d.condition, result: d.result };
|
|
2547
|
+
if (Array.isArray(d.key)) {
|
|
2548
|
+
const keys = d.key;
|
|
2549
|
+
if (keys.length === 0) {
|
|
2550
|
+
throw new Error(
|
|
2551
|
+
`DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' declared an EMPTY key array. Pass at least one key, or omit the operation.`
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
return keys.map((key, bulkIndex) => {
|
|
2555
|
+
if (key === null || typeof key !== "object") {
|
|
2556
|
+
throw new Error(
|
|
2557
|
+
`DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' key #${bulkIndex} is not a key object.`
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
return { ...base, key, bulkIndex };
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
return [{ ...base, key: d.key }];
|
|
2564
|
+
}
|
|
2565
|
+
function compileWriteOp(op) {
|
|
2566
|
+
const keyFieldNames2 = Object.keys(op.key);
|
|
2567
|
+
const inputFieldNames = Object.keys(op.input);
|
|
2568
|
+
const model = op.model;
|
|
2569
|
+
const body = ($) => {
|
|
2570
|
+
const descriptor = {
|
|
2571
|
+
[op.intent]: (() => model),
|
|
2572
|
+
key: mapToRefs($, keyFieldNames2),
|
|
2573
|
+
...inputFieldNames.length > 0 ? { input: mapToRefs($, inputFieldNames) } : {}
|
|
2574
|
+
};
|
|
2575
|
+
const map = { [op.alias]: descriptor };
|
|
2576
|
+
return map;
|
|
2577
|
+
};
|
|
2578
|
+
const plan2 = mutation(body);
|
|
2579
|
+
if (!isCommandPlan(plan2)) {
|
|
2580
|
+
throw new Error("DDBModel.mutate: internal error building the mutation plan.");
|
|
2581
|
+
}
|
|
2582
|
+
const compiled = compileMutationPlan(plan2);
|
|
2583
|
+
const fragment = compiled.fragments[0];
|
|
2584
|
+
const params = { ...op.input, ...op.key };
|
|
330
2585
|
return {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
2586
|
+
fragment,
|
|
2587
|
+
params,
|
|
2588
|
+
condition: op.condition,
|
|
2589
|
+
result: op.result
|
|
335
2590
|
};
|
|
336
2591
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
2592
|
+
function mapToRefs($, fields) {
|
|
2593
|
+
const out = {};
|
|
2594
|
+
for (const f of fields) out[f] = $[f];
|
|
2595
|
+
return out;
|
|
2596
|
+
}
|
|
2597
|
+
async function executeTransactionMode(ops) {
|
|
2598
|
+
const compiled = ops.map(compileWriteOp);
|
|
2599
|
+
const { readBacks } = await executeCompiledWriteOp(compiled, {
|
|
2600
|
+
label: "DDBModel.mutate"
|
|
2601
|
+
});
|
|
2602
|
+
const out = {};
|
|
2603
|
+
ops.forEach((op, i) => {
|
|
2604
|
+
if (op.result === void 0) return;
|
|
2605
|
+
if (op.bulkIndex === void 0) {
|
|
2606
|
+
out[op.alias] = readBacks[i];
|
|
2607
|
+
} else {
|
|
2608
|
+
const arr = out[op.alias] ?? [];
|
|
2609
|
+
arr[op.bulkIndex] = readBacks[i];
|
|
2610
|
+
out[op.alias] = arr;
|
|
344
2611
|
}
|
|
345
|
-
|
|
2612
|
+
});
|
|
2613
|
+
return out;
|
|
2614
|
+
}
|
|
2615
|
+
async function executeParallelMode(ops) {
|
|
2616
|
+
const compiled = ops.map(compileWriteOp);
|
|
2617
|
+
const settled = await executeCompiledParallelWrites(compiled, {
|
|
2618
|
+
label: "DDBModel.mutate"
|
|
2619
|
+
});
|
|
2620
|
+
const out = {};
|
|
2621
|
+
ops.forEach((op, i) => {
|
|
2622
|
+
const res = settled[i];
|
|
2623
|
+
const mapped = res.ok ? { ok: true, value: op.result !== void 0 ? res.value : void 0 } : { ok: false, error: res.error };
|
|
2624
|
+
if (op.bulkIndex === void 0) {
|
|
2625
|
+
out[op.alias] = mapped;
|
|
2626
|
+
} else {
|
|
2627
|
+
const arr = out[op.alias] ?? [];
|
|
2628
|
+
arr[op.bulkIndex] = mapped;
|
|
2629
|
+
out[op.alias] = arr;
|
|
2630
|
+
}
|
|
2631
|
+
});
|
|
2632
|
+
return out;
|
|
2633
|
+
}
|
|
2634
|
+
function resolveModelStatic(ref, alias) {
|
|
2635
|
+
if (ref === null || ref === void 0) {
|
|
2636
|
+
throw new Error(`DDBModel: the descriptor for alias '${alias}' names no target model.`);
|
|
2637
|
+
}
|
|
2638
|
+
if (typeof ref === "function") {
|
|
2639
|
+
const asModel = ref.asModel;
|
|
2640
|
+
if (typeof asModel === "function") {
|
|
2641
|
+
return asModel.call(ref);
|
|
2642
|
+
}
|
|
2643
|
+
return resolveModelStatic(ref(), alias);
|
|
2644
|
+
}
|
|
2645
|
+
return ref;
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
// src/model/DDBModel.ts
|
|
2649
|
+
var DDBModel = class {
|
|
2650
|
+
static setClient(client) {
|
|
2651
|
+
ClientManager.setClient(client);
|
|
2652
|
+
}
|
|
2653
|
+
static setTableMapping(mapping) {
|
|
2654
|
+
TableMapping.set(mapping);
|
|
2655
|
+
}
|
|
2656
|
+
static async transaction(fn) {
|
|
2657
|
+
await executeTransaction(fn);
|
|
2658
|
+
}
|
|
2659
|
+
/**
|
|
2660
|
+
* In-process multi-route READ (issue #101 — the unified envelope). Each alias
|
|
2661
|
+
* route (`{ query | list: Model, key, select, options? }`) runs **independently
|
|
2662
|
+
* and in parallel**; the result is an object keyed by the input aliases. A
|
|
2663
|
+
* `query` route delegates to the single-item read path, a `list` route to the
|
|
2664
|
+
* partition read path. Cross-route consistency is NOT guaranteed.
|
|
2665
|
+
*/
|
|
2666
|
+
static async query(envelope) {
|
|
2667
|
+
return executeReadEnvelope(envelope);
|
|
2668
|
+
}
|
|
2669
|
+
static mutate(envelope, options) {
|
|
2670
|
+
return executeWriteEnvelope(envelope, options);
|
|
2671
|
+
}
|
|
2672
|
+
static async batchGet(requests) {
|
|
2673
|
+
return executeBatchGet(requests);
|
|
2674
|
+
}
|
|
2675
|
+
static async batchWrite(requests) {
|
|
2676
|
+
return executeBatchWrite(requests);
|
|
2677
|
+
}
|
|
2678
|
+
static asModel() {
|
|
2679
|
+
const modelClass = this;
|
|
2680
|
+
const modelStatic = {
|
|
2681
|
+
col: createColumnMap(),
|
|
2682
|
+
project(select) {
|
|
2683
|
+
return buildProject(select);
|
|
2684
|
+
},
|
|
2685
|
+
relation(select) {
|
|
2686
|
+
return buildRelation(select);
|
|
2687
|
+
},
|
|
2688
|
+
// The public `query` is overloaded (plain / updatable / hydrate, the last
|
|
2689
|
+
// introducing a fresh return-type parameter `R`, issue #53). An overloaded
|
|
2690
|
+
// method on an object literal cannot contextually infer its single
|
|
2691
|
+
// implementation signature, so the implementation is written with explicit
|
|
2692
|
+
// runtime-facing parameter types and assigned through the interface's
|
|
2693
|
+
// method type — the public overloads (the only caller-visible surface)
|
|
2694
|
+
// are untouched.
|
|
2695
|
+
query: ((key, select, options) => {
|
|
2696
|
+
return executeQuery(modelClass, key, select, options);
|
|
2697
|
+
}),
|
|
2698
|
+
list: ((key, select, options) => {
|
|
2699
|
+
return executeList(modelClass, key, select, options);
|
|
2700
|
+
}),
|
|
2701
|
+
explain(key, options) {
|
|
2702
|
+
return executeExplain(
|
|
2703
|
+
modelClass,
|
|
2704
|
+
key,
|
|
2705
|
+
options
|
|
2706
|
+
);
|
|
2707
|
+
},
|
|
2708
|
+
async putItem(item, options) {
|
|
2709
|
+
await executePut(
|
|
2710
|
+
modelClass,
|
|
2711
|
+
item,
|
|
2712
|
+
options
|
|
2713
|
+
);
|
|
2714
|
+
},
|
|
2715
|
+
async updateItem(entity, changes, options) {
|
|
2716
|
+
await executeUpdate(
|
|
2717
|
+
modelClass,
|
|
2718
|
+
entity,
|
|
2719
|
+
changes,
|
|
2720
|
+
options
|
|
2721
|
+
);
|
|
2722
|
+
},
|
|
2723
|
+
async deleteItem(key, options) {
|
|
2724
|
+
await executeDelete(modelClass, key, options);
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
return attachModelClass(
|
|
2728
|
+
modelStatic,
|
|
2729
|
+
modelClass
|
|
2730
|
+
);
|
|
346
2731
|
}
|
|
347
|
-
|
|
348
|
-
}
|
|
349
|
-
function lifecyclePhaseForIntent(intent) {
|
|
350
|
-
return intent;
|
|
351
|
-
}
|
|
2732
|
+
};
|
|
352
2733
|
|
|
353
2734
|
// src/define/mutation.ts
|
|
354
2735
|
var INPUT_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationInputRef");
|
|
@@ -376,19 +2757,32 @@ function parseEntityRef(leaf) {
|
|
|
376
2757
|
path: leaf
|
|
377
2758
|
};
|
|
378
2759
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
2760
|
+
var ALIAS_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationAliasFieldRef");
|
|
2761
|
+
function isAliasFieldRef(value) {
|
|
2762
|
+
return typeof value === "object" && value !== null && value[ALIAS_FIELD_REF_BRAND] === true;
|
|
2763
|
+
}
|
|
2764
|
+
function makeRootRef(root) {
|
|
2765
|
+
const token = `{${root}}`;
|
|
2766
|
+
const origin = `the placeholder '${root}' (\`$.${root}\`)`;
|
|
2767
|
+
const aliasFieldCache = /* @__PURE__ */ new Map();
|
|
382
2768
|
const target = /* @__PURE__ */ Object.create(null);
|
|
383
2769
|
const proxy = new Proxy(target, {
|
|
384
2770
|
get(_t, prop) {
|
|
385
2771
|
if (prop === INPUT_REF_BRAND) return true;
|
|
386
|
-
if (prop === "field") return
|
|
2772
|
+
if (prop === "field") return root;
|
|
387
2773
|
if (prop === "token") return token;
|
|
388
2774
|
if (typeof prop === "symbol") return void 0;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
2775
|
+
if (prop === "toString" || prop === "valueOf" || prop === "constructor") {
|
|
2776
|
+
throw new Error(
|
|
2777
|
+
`mutation: unsupported operation '${prop}' on ${origin}. A descriptor \`key\` / \`input\` value only supports a direct \`$.field\` input reference, a \`$.alias.field\` cross-fragment reference, or a concrete literal \u2014 never a transform or coercion.`
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
let ref = aliasFieldCache.get(prop);
|
|
2781
|
+
if (!ref) {
|
|
2782
|
+
ref = { [ALIAS_FIELD_REF_BRAND]: true, alias: root, field: prop };
|
|
2783
|
+
aliasFieldCache.set(prop, ref);
|
|
2784
|
+
}
|
|
2785
|
+
return ref;
|
|
392
2786
|
},
|
|
393
2787
|
set() {
|
|
394
2788
|
throw new Error(`mutation: cannot assign on ${origin}.`);
|
|
@@ -401,16 +2795,16 @@ function makeInputProxy() {
|
|
|
401
2795
|
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
402
2796
|
get(_t, prop) {
|
|
403
2797
|
if (typeof prop === "symbol") return void 0;
|
|
404
|
-
const
|
|
405
|
-
let ref = cache.get(
|
|
2798
|
+
const root = prop;
|
|
2799
|
+
let ref = cache.get(root);
|
|
406
2800
|
if (!ref) {
|
|
407
|
-
ref =
|
|
408
|
-
cache.set(
|
|
2801
|
+
ref = makeRootRef(root);
|
|
2802
|
+
cache.set(root, ref);
|
|
409
2803
|
}
|
|
410
2804
|
return ref;
|
|
411
2805
|
},
|
|
412
2806
|
set() {
|
|
413
|
-
throw new Error("mutation: cannot assign to the `$`
|
|
2807
|
+
throw new Error("mutation: cannot assign to the `$` placeholder proxy.");
|
|
414
2808
|
}
|
|
415
2809
|
});
|
|
416
2810
|
}
|
|
@@ -418,10 +2812,40 @@ var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
|
|
|
418
2812
|
function isMutationFragment(value) {
|
|
419
2813
|
return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
|
|
420
2814
|
}
|
|
421
|
-
|
|
422
|
-
|
|
2815
|
+
var INTENT_KEYS2 = ["create", "update", "remove"];
|
|
2816
|
+
function resolveDescriptorTarget(descriptor, alias) {
|
|
2817
|
+
const present = INTENT_KEYS2.filter((k) => descriptor[k] !== void 0);
|
|
2818
|
+
if (present.length === 0) {
|
|
2819
|
+
throw new Error(
|
|
2820
|
+
`mutation: the descriptor for alias '${alias}' has no intent key. Each descriptor must carry exactly one of \`create\` / \`update\` / \`remove\` whose value is the target model (or a \`() => Model\` thunk), e.g. \`{ create: GroupMembership, key, input }\`.`
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
if (present.length > 1) {
|
|
2824
|
+
throw new Error(
|
|
2825
|
+
`mutation: the descriptor for alias '${alias}' declares multiple intent keys (${present.join(", ")}). A descriptor is exactly one write \u2014 declare a single \`create\` / \`update\` / \`remove\`.`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
const intent = present[0];
|
|
2829
|
+
const ref = descriptor[intent];
|
|
2830
|
+
const resolved = resolveModelRef(ref, intent, alias);
|
|
423
2831
|
const modelClass = resolveModelClass(resolved);
|
|
424
|
-
return { name: modelClass.name, modelClass };
|
|
2832
|
+
return { intent, entity: { name: modelClass.name, modelClass } };
|
|
2833
|
+
}
|
|
2834
|
+
function resolveModelRef(ref, intent, alias) {
|
|
2835
|
+
if (ref === null || ref === void 0) {
|
|
2836
|
+
throw new Error(
|
|
2837
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' names no target model.`
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
if (typeof ref === "function") {
|
|
2841
|
+
const proto = ref.prototype;
|
|
2842
|
+
const isModelClass = proto instanceof DDBModel || typeof ref === "function" && ref.prototype instanceof DDBModel;
|
|
2843
|
+
if (isModelClass) {
|
|
2844
|
+
return ref;
|
|
2845
|
+
}
|
|
2846
|
+
return ref();
|
|
2847
|
+
}
|
|
2848
|
+
return ref;
|
|
425
2849
|
}
|
|
426
2850
|
function assertFaithfulInput(input, intent, entityName) {
|
|
427
2851
|
if (input === null || typeof input !== "object") {
|
|
@@ -430,9 +2854,9 @@ function assertFaithfulInput(input, intent, entityName) {
|
|
|
430
2854
|
);
|
|
431
2855
|
}
|
|
432
2856
|
for (const [field, leaf] of Object.entries(input)) {
|
|
433
|
-
if (!isMutationInputRef(leaf) && !isConcreteLiteral(leaf)) {
|
|
2857
|
+
if (!isMutationInputRef(leaf) && !isAliasFieldRef(leaf) && !isConcreteLiteral(leaf)) {
|
|
434
2858
|
throw new Error(
|
|
435
|
-
`mutation: the '${intent}' fragment on '${entityName}' binds model field '${field}' to a value that is neither a \`$.field\` input reference nor a concrete literal. A fragment input is declarative \u2014 only
|
|
2859
|
+
`mutation: the '${intent}' fragment on '${entityName}' binds model field '${field}' to a value that is neither a \`$.field\` input reference, a \`$.alias.field\` cross-fragment reference, nor a concrete literal. A fragment input is declarative \u2014 only \`$.field\` / \`$.alias.field\` references or literals are allowed, never a computed value.`
|
|
436
2860
|
);
|
|
437
2861
|
}
|
|
438
2862
|
}
|
|
@@ -442,10 +2866,9 @@ function isConcreteLiteral(value) {
|
|
|
442
2866
|
const t = typeof value;
|
|
443
2867
|
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
444
2868
|
}
|
|
445
|
-
function makeFragment(intent,
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
if (options.use !== void 0 && !isEntityWritesDefinition(options.use)) {
|
|
2869
|
+
function makeFragment(intent, entity, input, use) {
|
|
2870
|
+
const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
|
|
2871
|
+
if (use !== void 0 && !isEntityWritesDefinition(use)) {
|
|
449
2872
|
throw new Error(
|
|
450
2873
|
`mutation: the '${intent}' fragment on '${entity.name}' was given a \`use:\` that is not an \`entityWrites(...)\` save contract. \`use:\` takes the WHOLE \`writes\` set (an \`entityWrites(...)\` value), never a single lifecycle (e.g. \`Model.writes.create\`) \u2014 the fragment's intent already selects the lifecycle.`
|
|
451
2874
|
);
|
|
@@ -454,308 +2877,121 @@ function makeFragment(intent, target, options) {
|
|
|
454
2877
|
[FRAGMENT_BRAND]: true,
|
|
455
2878
|
intent,
|
|
456
2879
|
entity,
|
|
457
|
-
input,
|
|
458
|
-
...
|
|
2880
|
+
input: checked,
|
|
2881
|
+
...use !== void 0 ? { use } : {}
|
|
459
2882
|
};
|
|
460
2883
|
}
|
|
461
|
-
var mutationRecorder = {
|
|
462
|
-
create: (target, options) => makeFragment("create", target, options),
|
|
463
|
-
update: (target, options) => makeFragment("update", target, options),
|
|
464
|
-
remove: (target, options) => makeFragment("remove", target, options)
|
|
465
|
-
};
|
|
466
2884
|
var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
|
|
467
2885
|
function isCommandPlan(value) {
|
|
468
2886
|
return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
|
|
469
2887
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const $ = makeInputProxy();
|
|
475
|
-
const result = body(mutationRecorder, $);
|
|
476
|
-
if (!Array.isArray(result)) {
|
|
477
|
-
throw new Error(
|
|
478
|
-
`mutation '${name}': the body must RETURN an array of write fragments (e.g. \`(m, $) => [ m.create(() => Model, { input }) ]\`). The fragments are declarations composed at compile time, not executed calls.`
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
if (result.length === 0) {
|
|
482
|
-
throw new Error(
|
|
483
|
-
`mutation '${name}': the body returned an empty fragment list. A mutation must declare at least one write fragment.`
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
result.forEach((fragment, i) => {
|
|
487
|
-
if (!isMutationFragment(fragment)) {
|
|
488
|
-
throw new Error(
|
|
489
|
-
`mutation '${name}': fragment #${i} is not a write fragment. Each list element must be an \`m.create\` / \`m.update\` / \`m.remove\` declaration.`
|
|
490
|
-
);
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
return { [COMMAND_PLAN_BRAND]: true, name, fragments: result };
|
|
494
|
-
}
|
|
495
|
-
var definePlan = mutation;
|
|
496
|
-
|
|
497
|
-
// src/relation/edge-write.ts
|
|
498
|
-
var OLD_VALUE_NAMESPACE = "old";
|
|
499
|
-
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
500
|
-
function isEdgeWritesDefinition(value) {
|
|
501
|
-
return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
|
|
502
|
-
}
|
|
503
|
-
function declaration(target, relationProperty) {
|
|
504
|
-
return { targetFactory: target, relationProperty };
|
|
505
|
-
}
|
|
506
|
-
var recorder2 = {
|
|
507
|
-
putEdge: declaration,
|
|
508
|
-
deleteEdge: declaration,
|
|
509
|
-
updateKeyChangeEdge: declaration
|
|
510
|
-
};
|
|
511
|
-
function edgeWrites(builder) {
|
|
512
|
-
const edges = builder(recorder2);
|
|
513
|
-
if (edges.length === 0) {
|
|
514
|
-
throw new Error(
|
|
515
|
-
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
516
|
-
);
|
|
517
|
-
}
|
|
518
|
-
return { [EDGE_WRITES_MARKER]: true, edges };
|
|
519
|
-
}
|
|
520
|
-
function edgeKeyFields(segmented) {
|
|
521
|
-
return [
|
|
522
|
-
...segmentFieldNames(segmented.pkSegments),
|
|
523
|
-
...segmentFieldNames(segmented.skSegments)
|
|
524
|
-
];
|
|
525
|
-
}
|
|
526
|
-
function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
527
|
-
const targetClass = decl.targetFactory();
|
|
528
|
-
const targetMeta = MetadataRegistry.get(targetClass);
|
|
529
|
-
const relation = targetMeta.relations.find(
|
|
530
|
-
(r) => r.propertyName === decl.relationProperty
|
|
531
|
-
);
|
|
532
|
-
if (!relation) {
|
|
2888
|
+
function mergeBindings(descriptor, intent, alias) {
|
|
2889
|
+
const merged = {};
|
|
2890
|
+
const key = descriptor.key;
|
|
2891
|
+
if (key === void 0 || key === null || typeof key !== "object") {
|
|
533
2892
|
throw new Error(
|
|
534
|
-
`
|
|
2893
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object binding the target's primary-key fields (\`{ field: $.field, \u2026 }\`).`
|
|
535
2894
|
);
|
|
536
2895
|
}
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
2896
|
+
for (const [field, leaf] of Object.entries(key)) merged[field] = leaf;
|
|
2897
|
+
const input = descriptor.input;
|
|
2898
|
+
if (input !== void 0 && input !== null) {
|
|
2899
|
+
if (typeof input !== "object") {
|
|
540
2900
|
throw new Error(
|
|
541
|
-
`
|
|
2901
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' has a non-object \`input\`.`
|
|
542
2902
|
);
|
|
543
2903
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
`Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds [${boundFields.map((f) => `'${f}'`).join(", ")}], which does not cover any access-pattern partition of the adjacency entity '${entity.tableName}' edge (base key [${edgeKeyFields(entity.primaryKey.segmented).map((f) => `'${f}'`).join(", ")}]). The relation's read could not resolve a partition for the written row, so the edge would be unreadable \u2014 bind every partition-key field of the index the relation reads through. (${cause.message})`
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
return relation;
|
|
553
|
-
}
|
|
554
|
-
function templateRecord(fields) {
|
|
555
|
-
const out = {};
|
|
556
|
-
for (const field of [...fields].sort()) {
|
|
557
|
-
out[field] = `{${field}}`;
|
|
558
|
-
}
|
|
559
|
-
return out;
|
|
560
|
-
}
|
|
561
|
-
function keyConditionTemplate(segmented, namespace) {
|
|
562
|
-
const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
|
|
563
|
-
const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
|
|
564
|
-
const out = { PK: pk };
|
|
565
|
-
if (sk !== void 0) out.SK = sk;
|
|
566
|
-
return out;
|
|
567
|
-
}
|
|
568
|
-
function putItemTemplate(edgeFields) {
|
|
569
|
-
return templateRecord(edgeFields);
|
|
570
|
-
}
|
|
571
|
-
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
572
|
-
const entity = MetadataRegistry.get(adjacencyClass);
|
|
573
|
-
if (!entity.primaryKey) {
|
|
574
|
-
throw new Error(
|
|
575
|
-
`Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
const segmented = entity.primaryKey.segmented;
|
|
579
|
-
const edgeFields = edgeKeyFields(segmented);
|
|
580
|
-
assertRelationRoundTrips(entity, new Set(edgeFields), decl);
|
|
581
|
-
const tableName = TableMapping.resolve(entity.tableName);
|
|
582
|
-
const entityName = adjacencyClass.name;
|
|
583
|
-
const put = {
|
|
584
|
-
type: "Put",
|
|
585
|
-
tableName,
|
|
586
|
-
entity: entityName,
|
|
587
|
-
item: putItemTemplate(edgeFields)
|
|
588
|
-
};
|
|
589
|
-
const deleteNew = {
|
|
590
|
-
type: "Delete",
|
|
591
|
-
tableName,
|
|
592
|
-
entity: entityName,
|
|
593
|
-
keyCondition: keyConditionTemplate(segmented)
|
|
594
|
-
};
|
|
595
|
-
const deleteOld = {
|
|
596
|
-
type: "Delete",
|
|
597
|
-
tableName,
|
|
598
|
-
entity: entityName,
|
|
599
|
-
keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
|
|
600
|
-
};
|
|
601
|
-
switch (lifecycle) {
|
|
602
|
-
case "onCreate":
|
|
603
|
-
return [put];
|
|
604
|
-
case "onDelete":
|
|
605
|
-
return [deleteNew];
|
|
606
|
-
case "onUpdateKeyChange":
|
|
607
|
-
return [deleteOld, put];
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
|
|
611
|
-
const items = [];
|
|
612
|
-
for (const decl of writes.edges) {
|
|
613
|
-
items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
|
|
614
|
-
}
|
|
615
|
-
return items;
|
|
616
|
-
}
|
|
617
|
-
function getEdgeWrites(modelClass) {
|
|
618
|
-
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
619
|
-
let value;
|
|
620
|
-
try {
|
|
621
|
-
value = modelClass[name];
|
|
622
|
-
} catch {
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
if (isEdgeWritesDefinition(value)) return value;
|
|
626
|
-
}
|
|
627
|
-
return void 0;
|
|
628
|
-
}
|
|
629
|
-
function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
630
|
-
const writes = getEdgeWrites(modelClass);
|
|
631
|
-
if (!writes) {
|
|
632
|
-
throw new Error(
|
|
633
|
-
`Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
// src/define/contract.ts
|
|
640
|
-
function isCommandBatchMode(value) {
|
|
641
|
-
return value === "transact" || value === "batchWrite";
|
|
642
|
-
}
|
|
643
|
-
var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
|
|
644
|
-
var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
|
|
645
|
-
function isContractKeyRef(value) {
|
|
646
|
-
return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
|
|
647
|
-
}
|
|
648
|
-
function mintContractKeyFieldRef(field) {
|
|
649
|
-
return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
|
|
650
|
-
}
|
|
651
|
-
function wholeKeysSentinel() {
|
|
652
|
-
return { [KEY_REF_BRAND]: true };
|
|
653
|
-
}
|
|
654
|
-
function isContractKeyFieldRef(value) {
|
|
655
|
-
return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
|
|
656
|
-
}
|
|
657
|
-
function makeKeyFieldRef(field, marker, onCoerce) {
|
|
658
|
-
const token = `{key.${field}}`;
|
|
659
|
-
const origin = `keys field '${field}'`;
|
|
660
|
-
const target = /* @__PURE__ */ Object.create(null);
|
|
661
|
-
const proxy = new Proxy(target, {
|
|
662
|
-
get(_t, prop) {
|
|
663
|
-
if (prop === KEY_FIELD_REF_BRAND) return true;
|
|
664
|
-
if (prop === "field") return field;
|
|
665
|
-
if (prop === "token") return token;
|
|
666
|
-
if (prop === Symbol.toPrimitive) {
|
|
667
|
-
return (hint) => {
|
|
668
|
-
onCoerce();
|
|
669
|
-
return hint === "number" ? NaN : marker;
|
|
670
|
-
};
|
|
671
|
-
}
|
|
672
|
-
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
673
|
-
return () => {
|
|
674
|
-
onCoerce();
|
|
675
|
-
return marker;
|
|
676
|
-
};
|
|
677
|
-
}
|
|
678
|
-
if (prop === "valueOf") {
|
|
679
|
-
return () => {
|
|
680
|
-
onCoerce();
|
|
681
|
-
return NaN;
|
|
682
|
-
};
|
|
2904
|
+
for (const [field, leaf] of Object.entries(input)) {
|
|
2905
|
+
if (field in merged) {
|
|
2906
|
+
throw new Error(
|
|
2907
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' binds field '${field}' in both \`key\` and \`input\`. A primary-key field belongs in \`key\` only.`
|
|
2908
|
+
);
|
|
683
2909
|
}
|
|
684
|
-
|
|
685
|
-
const name = String(prop);
|
|
686
|
-
throw new Error(
|
|
687
|
-
`publicQueryModel/publicCommandModel: unsupported operation '${name}' on ${origin}. A contract method body only supports a *direct* key-field reference (\`keys.field\`) or the whole \`keys\` passed into a model op's key position; any transform, indexing, or coercion on a key field is not representable as a declarative contract method.`
|
|
688
|
-
);
|
|
689
|
-
},
|
|
690
|
-
set() {
|
|
691
|
-
throw new Error(`publicQueryModel/publicCommandModel: cannot assign on ${origin}.`);
|
|
692
|
-
},
|
|
693
|
-
defineProperty() {
|
|
694
|
-
throw new Error(
|
|
695
|
-
`publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
|
|
696
|
-
);
|
|
697
|
-
},
|
|
698
|
-
deleteProperty() {
|
|
699
|
-
throw new Error(`publicQueryModel/publicCommandModel: cannot delete on ${origin}.`);
|
|
2910
|
+
merged[field] = leaf;
|
|
700
2911
|
}
|
|
701
|
-
}
|
|
702
|
-
return
|
|
2912
|
+
}
|
|
2913
|
+
return merged;
|
|
703
2914
|
}
|
|
704
|
-
function
|
|
705
|
-
const
|
|
706
|
-
const
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
if (
|
|
710
|
-
if (prop === Symbol.toPrimitive) {
|
|
711
|
-
return (hint) => {
|
|
712
|
-
onCoerce();
|
|
713
|
-
return hint === "number" ? NaN : "[contract key]";
|
|
714
|
-
};
|
|
715
|
-
}
|
|
716
|
-
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
717
|
-
return () => {
|
|
718
|
-
onCoerce();
|
|
719
|
-
return "[contract key]";
|
|
720
|
-
};
|
|
721
|
-
}
|
|
722
|
-
if (prop === "valueOf") {
|
|
723
|
-
return () => {
|
|
724
|
-
onCoerce();
|
|
725
|
-
return NaN;
|
|
726
|
-
};
|
|
727
|
-
}
|
|
728
|
-
if (prop === Symbol.iterator || prop === Symbol.asyncIterator || prop === "then" || prop === "constructor" || prop === "prototype" || typeof prop === "symbol" && typeof Symbol.for === "function" && (prop === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || prop.description === "nodejs.util.inspect.custom")) {
|
|
729
|
-
return void 0;
|
|
730
|
-
}
|
|
731
|
-
if (typeof prop === "symbol") {
|
|
2915
|
+
function resolveAliasRefs(input, aliasToIndex, alias) {
|
|
2916
|
+
const out = {};
|
|
2917
|
+
for (const [field, leaf] of Object.entries(input)) {
|
|
2918
|
+
if (isAliasFieldRef(leaf)) {
|
|
2919
|
+
const index = aliasToIndex.get(leaf.alias);
|
|
2920
|
+
if (index === void 0) {
|
|
732
2921
|
throw new Error(
|
|
733
|
-
|
|
2922
|
+
`mutation: the descriptor for alias '${alias}' binds '${field}' to \`$.${leaf.alias}.${leaf.field}\`, but no descriptor is aliased '${leaf.alias}'. A cross-fragment reference must name another alias in the same mutation map.`
|
|
734
2923
|
);
|
|
735
2924
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
throw new Error(
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
2925
|
+
out[field] = `$.entity[${index}].${leaf.field}`;
|
|
2926
|
+
} else {
|
|
2927
|
+
out[field] = leaf;
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
return out;
|
|
2931
|
+
}
|
|
2932
|
+
function mutation(nameOrBody, maybeBody) {
|
|
2933
|
+
let name;
|
|
2934
|
+
let body;
|
|
2935
|
+
if (typeof nameOrBody === "string") {
|
|
2936
|
+
if (maybeBody === void 0) {
|
|
2937
|
+
throw new Error("mutation: a name was given but no body function.");
|
|
2938
|
+
}
|
|
2939
|
+
name = nameOrBody;
|
|
2940
|
+
body = maybeBody;
|
|
2941
|
+
} else {
|
|
2942
|
+
body = nameOrBody;
|
|
2943
|
+
name = "mutation";
|
|
2944
|
+
}
|
|
2945
|
+
if (typeof body !== "function") {
|
|
2946
|
+
throw new Error(
|
|
2947
|
+
"mutation: the body must be a function `$ => ({ alias: descriptor, \u2026 })` returning an alias \u2192 write-descriptor map."
|
|
2948
|
+
);
|
|
2949
|
+
}
|
|
2950
|
+
const $ = makeInputProxy();
|
|
2951
|
+
const map = body($);
|
|
2952
|
+
if (map === null || typeof map !== "object" || Array.isArray(map)) {
|
|
2953
|
+
throw new Error(
|
|
2954
|
+
`mutation '${name}': the body must RETURN an alias \u2192 descriptor object (e.g. \`$ => ({ membership: { create: Model, key, input } })\`). Note the \`({ \u2026 })\` parentheses \u2014 an arrow returning an object literal needs them.`
|
|
2955
|
+
);
|
|
2956
|
+
}
|
|
2957
|
+
const aliases = Object.keys(map);
|
|
2958
|
+
if (aliases.length === 0) {
|
|
2959
|
+
throw new Error(
|
|
2960
|
+
`mutation '${name}': the body returned an empty descriptor map. A mutation must declare at least one write descriptor.`
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
const aliasToIndex = /* @__PURE__ */ new Map();
|
|
2964
|
+
aliases.forEach((alias, i) => aliasToIndex.set(alias, i));
|
|
2965
|
+
const fragments = aliases.map((alias) => {
|
|
2966
|
+
const descriptor = map[alias];
|
|
2967
|
+
if (descriptor === null || typeof descriptor !== "object") {
|
|
753
2968
|
throw new Error(
|
|
754
|
-
|
|
2969
|
+
`mutation '${name}': the value for alias '${alias}' is not a write descriptor (an object \`{ create | update | remove: Model, key, input? }\`).`
|
|
755
2970
|
);
|
|
756
2971
|
}
|
|
2972
|
+
const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
|
|
2973
|
+
const merged = mergeBindings(descriptor, intent, alias);
|
|
2974
|
+
const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
|
|
2975
|
+
return makeFragment(intent, entity, resolved, descriptor.use);
|
|
757
2976
|
});
|
|
758
|
-
return
|
|
2977
|
+
return { [COMMAND_PLAN_BRAND]: true, name, fragments };
|
|
2978
|
+
}
|
|
2979
|
+
var definePlan = mutation;
|
|
2980
|
+
|
|
2981
|
+
// src/define/contract.ts
|
|
2982
|
+
var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
|
|
2983
|
+
var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
|
|
2984
|
+
function isContractKeyRef(value) {
|
|
2985
|
+
return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
|
|
2986
|
+
}
|
|
2987
|
+
function mintContractKeyFieldRef(field) {
|
|
2988
|
+
return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
|
|
2989
|
+
}
|
|
2990
|
+
function wholeKeysSentinel() {
|
|
2991
|
+
return { [KEY_REF_BRAND]: true };
|
|
2992
|
+
}
|
|
2993
|
+
function isContractKeyFieldRef(value) {
|
|
2994
|
+
return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
|
|
759
2995
|
}
|
|
760
2996
|
var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
|
|
761
2997
|
function isContractParamRef(value) {
|
|
@@ -764,103 +3000,6 @@ function isContractParamRef(value) {
|
|
|
764
3000
|
function mintContractParamRef(field) {
|
|
765
3001
|
return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
|
|
766
3002
|
}
|
|
767
|
-
var PARAM_MARKER_TAGS = [
|
|
768
|
-
"Bxqv",
|
|
769
|
-
"Ymnoprstuwy0123456789WIDEMARKERBODYFILLERY"
|
|
770
|
-
];
|
|
771
|
-
var PARAM_PASS_COUNT = PARAM_MARKER_TAGS.length;
|
|
772
|
-
function paramMarker(pass, field) {
|
|
773
|
-
const tag = PARAM_MARKER_TAGS[pass];
|
|
774
|
-
return `${tag}::${field}::${tag}`;
|
|
775
|
-
}
|
|
776
|
-
function makeParamFieldRef(field, marker, onCoerce) {
|
|
777
|
-
const token = `{${field}}`;
|
|
778
|
-
const origin = `params field '${field}'`;
|
|
779
|
-
const target = /* @__PURE__ */ Object.create(null);
|
|
780
|
-
const proxy = new Proxy(target, {
|
|
781
|
-
get(_t, prop) {
|
|
782
|
-
if (prop === PARAM_REF_BRAND) return true;
|
|
783
|
-
if (prop === "token") return token;
|
|
784
|
-
if (prop === "field") return field;
|
|
785
|
-
if (prop === Symbol.toPrimitive) {
|
|
786
|
-
return (hint) => {
|
|
787
|
-
onCoerce();
|
|
788
|
-
return hint === "number" ? NaN : marker;
|
|
789
|
-
};
|
|
790
|
-
}
|
|
791
|
-
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
792
|
-
return () => {
|
|
793
|
-
onCoerce();
|
|
794
|
-
return marker;
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
if (prop === "valueOf") {
|
|
798
|
-
return () => {
|
|
799
|
-
onCoerce();
|
|
800
|
-
return NaN;
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
if (typeof prop === "symbol") return void 0;
|
|
804
|
-
const name = String(prop);
|
|
805
|
-
throw new Error(
|
|
806
|
-
`publicQueryModel/publicCommandModel: unsupported operation '${name}' on ${origin}. A contract method body only supports a *direct* field reference (\`params.field\`) or a concrete literal at a value position; any string transform, property access (e.g. \`.length\`, \`[i]\`, \`.toUpperCase()\`), arithmetic, or value branching on a parameter is not representable as a declarative contract method.`
|
|
807
|
-
);
|
|
808
|
-
},
|
|
809
|
-
set() {
|
|
810
|
-
throw new Error(
|
|
811
|
-
`publicQueryModel/publicCommandModel: cannot assign on ${origin}.`
|
|
812
|
-
);
|
|
813
|
-
},
|
|
814
|
-
defineProperty() {
|
|
815
|
-
throw new Error(
|
|
816
|
-
`publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
|
|
817
|
-
);
|
|
818
|
-
},
|
|
819
|
-
deleteProperty() {
|
|
820
|
-
throw new Error(
|
|
821
|
-
`publicQueryModel/publicCommandModel: cannot delete on ${origin}.`
|
|
822
|
-
);
|
|
823
|
-
}
|
|
824
|
-
});
|
|
825
|
-
return proxy;
|
|
826
|
-
}
|
|
827
|
-
function makeParamsProxy(pass, onAccess, onCoerce) {
|
|
828
|
-
const cache = /* @__PURE__ */ new Map();
|
|
829
|
-
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
830
|
-
get(_t, prop) {
|
|
831
|
-
if (typeof prop === "symbol") {
|
|
832
|
-
if (prop === Symbol.iterator || prop === Symbol.asyncIterator || prop === Symbol.toStringTag || typeof Symbol.for === "function" && (prop === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || prop.description === "nodejs.util.inspect.custom")) {
|
|
833
|
-
return void 0;
|
|
834
|
-
}
|
|
835
|
-
throw new Error(
|
|
836
|
-
"publicQueryModel/publicCommandModel: unsupported symbol access on `params`. Reference only declared param fields directly; any JS transform / branching / coercion on a parameter is not representable as a declarative contract method."
|
|
837
|
-
);
|
|
838
|
-
}
|
|
839
|
-
onAccess(prop);
|
|
840
|
-
let ref = cache.get(prop);
|
|
841
|
-
if (!ref) {
|
|
842
|
-
ref = makeParamFieldRef(prop, paramMarker(pass, prop), () => onCoerce(prop));
|
|
843
|
-
cache.set(prop, ref);
|
|
844
|
-
}
|
|
845
|
-
return ref;
|
|
846
|
-
},
|
|
847
|
-
set() {
|
|
848
|
-
throw new Error(
|
|
849
|
-
"publicQueryModel/publicCommandModel: cannot assign to a field of `params`."
|
|
850
|
-
);
|
|
851
|
-
},
|
|
852
|
-
defineProperty() {
|
|
853
|
-
throw new Error(
|
|
854
|
-
"publicQueryModel/publicCommandModel: cannot define a property on `params`."
|
|
855
|
-
);
|
|
856
|
-
},
|
|
857
|
-
deleteProperty() {
|
|
858
|
-
throw new Error(
|
|
859
|
-
"publicQueryModel/publicCommandModel: cannot delete a field of `params`."
|
|
860
|
-
);
|
|
861
|
-
}
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
3003
|
var FROM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractFromRef");
|
|
865
3004
|
var COMPOSE_NODE_BRAND = /* @__PURE__ */ Symbol("graphddb:contractComposeNode");
|
|
866
3005
|
function isContractFromRef(value) {
|
|
@@ -925,69 +3064,10 @@ function entityRef(model) {
|
|
|
925
3064
|
const modelClass = resolveModelClass(model);
|
|
926
3065
|
return { name: modelClass.name, modelClass };
|
|
927
3066
|
}
|
|
928
|
-
var PUT_HAS_NO_KEY = { [KEY_REF_BRAND]: true };
|
|
929
|
-
function assertFaithfulKeys(keys, op) {
|
|
930
|
-
if (isContractKeyRef(keys)) return keys;
|
|
931
|
-
if (keys !== null && typeof keys === "object" && !isContractKeyFieldRef(keys)) {
|
|
932
|
-
const record = keys;
|
|
933
|
-
for (const [field, leaf] of Object.entries(record)) {
|
|
934
|
-
if (!isContractKeyFieldRef(leaf) && !isConcreteLiteral2(leaf)) {
|
|
935
|
-
throw new Error(
|
|
936
|
-
`publicQueryModel/publicCommandModel: the \`${op}\` operation's key field '${field}' is neither a direct \`keys.${field}\` reference nor a concrete literal. A rebuilt partition key may only place \`keys.<field>\` references or literals at its leaves; a transformed value is not representable as a declarative contract method.`
|
|
937
|
-
);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
return record;
|
|
941
|
-
}
|
|
942
|
-
throw new Error(
|
|
943
|
-
`publicQueryModel/publicCommandModel: the \`${op}\` operation must receive the method's \`keys\` argument *directly* in its key position, or a partition key rebuilt from \`keys.<field>\` references. The value passed is not a valid contract key \u2014 it was transformed, coerced, or replaced, which is not representable as a declarative contract method.`
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
3067
|
function isConcreteLiteral2(value) {
|
|
947
3068
|
const t = typeof value;
|
|
948
3069
|
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
949
3070
|
}
|
|
950
|
-
var activeSink = null;
|
|
951
|
-
function recordContractOp(operation, model, args) {
|
|
952
|
-
const entity = entityRef(model);
|
|
953
|
-
let keys;
|
|
954
|
-
if (operation === "put") {
|
|
955
|
-
keys = PUT_HAS_NO_KEY;
|
|
956
|
-
} else {
|
|
957
|
-
keys = assertFaithfulKeys(args.keys, operation);
|
|
958
|
-
}
|
|
959
|
-
const { select, compose } = extractCompose(args.select, operation, entity.name);
|
|
960
|
-
const batch = args.batch === void 0 || args.batch === null ? void 0 : args.batch;
|
|
961
|
-
const op = {
|
|
962
|
-
__isContractMethodOp: true,
|
|
963
|
-
entity,
|
|
964
|
-
operation,
|
|
965
|
-
keys,
|
|
966
|
-
...select !== void 0 ? { select } : {},
|
|
967
|
-
...args.changes !== void 0 ? { changes: args.changes } : {},
|
|
968
|
-
...args.item !== void 0 ? { item: args.item } : {},
|
|
969
|
-
...args.condition !== void 0 ? { condition: args.condition } : {},
|
|
970
|
-
// Captured raw (cast through the field type); validated in the factory.
|
|
971
|
-
...batch !== void 0 ? { batch } : {},
|
|
972
|
-
...compose.length > 0 ? { compose } : {}
|
|
973
|
-
};
|
|
974
|
-
activeSink.push(op);
|
|
975
|
-
return op;
|
|
976
|
-
}
|
|
977
|
-
function assertBatchModeDeclaration(name, op) {
|
|
978
|
-
const batch = op.batch;
|
|
979
|
-
if (batch === void 0) return;
|
|
980
|
-
if (!isCommandBatchMode(batch)) {
|
|
981
|
-
throw new Error(
|
|
982
|
-
`publicCommandModel: method '${name}' declared an invalid \`batch\` mode \u2014 \`batch\` must be 'transact' (atomic TransactWriteItems, \u226425, condition-capable) or 'batchWrite' (non-atomic BatchWriteItem, no conditions), but received ${JSON.stringify(batch)}.`
|
|
983
|
-
);
|
|
984
|
-
}
|
|
985
|
-
if (batch === "batchWrite" && op.condition !== void 0) {
|
|
986
|
-
throw new Error(
|
|
987
|
-
`publicCommandModel: method '${name}' declares a 'batchWrite' batch mode but also a write condition \u2014 DynamoDB's BatchWriteItem has no ConditionExpression and cannot carry one. Declare \`batch: 'transact'\` (an atomic TransactWriteItems, which supports the \`{ notExists }\` / equality condition subset) for a conditional batched write.`
|
|
988
|
-
);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
3071
|
function extractCompose(select, operation, entityName) {
|
|
992
3072
|
if (select === void 0) return { select: void 0, compose: [] };
|
|
993
3073
|
const projection = {};
|
|
@@ -996,234 +3076,15 @@ function extractCompose(select, operation, entityName) {
|
|
|
996
3076
|
if (isContractComposeNode(value)) {
|
|
997
3077
|
if (operation !== "query" && operation !== "list") {
|
|
998
3078
|
throw new Error(
|
|
999
|
-
`publicQueryModel: an External Query composition (\`query(...)\`) at select property '${field}' is only valid on a read; the recorded op on '${entityName}' is a '${operation}'.`
|
|
1000
|
-
);
|
|
1001
|
-
}
|
|
1002
|
-
compose.push({ as: field, method: value.method, bind: value.bind });
|
|
1003
|
-
continue;
|
|
1004
|
-
}
|
|
1005
|
-
projection[field] = value;
|
|
1006
|
-
}
|
|
1007
|
-
return { select: projection, compose };
|
|
1008
|
-
}
|
|
1009
|
-
function isRecordingContractMethod() {
|
|
1010
|
-
return activeSink !== null;
|
|
1011
|
-
}
|
|
1012
|
-
function runMethodPass(body, pass, accessed, coerced, keyCoerced) {
|
|
1013
|
-
const keys = makeKeySentinel(
|
|
1014
|
-
pass,
|
|
1015
|
-
() => {
|
|
1016
|
-
},
|
|
1017
|
-
() => {
|
|
1018
|
-
keyCoerced.value = true;
|
|
1019
|
-
}
|
|
1020
|
-
);
|
|
1021
|
-
const params = makeParamsProxy(
|
|
1022
|
-
pass,
|
|
1023
|
-
(field) => accessed.add(field),
|
|
1024
|
-
(field) => coerced.add(field)
|
|
1025
|
-
);
|
|
1026
|
-
const sink = [];
|
|
1027
|
-
const previous = activeSink;
|
|
1028
|
-
activeSink = sink;
|
|
1029
|
-
try {
|
|
1030
|
-
body(keys, params);
|
|
1031
|
-
} finally {
|
|
1032
|
-
activeSink = previous;
|
|
1033
|
-
}
|
|
1034
|
-
if (sink.length === 0) {
|
|
1035
|
-
throw new Error(
|
|
1036
|
-
"publicQueryModel/publicCommandModel: a method body must resolve to exactly one model operation (e.g. `return Model.query(keys, select, params)`). No model operation was recorded \u2014 the body did not call `Model.query` / `Model.list` / `Model.put` / `Model.update` / `Model.delete`."
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
if (sink.length > 1) {
|
|
1040
|
-
throw new Error(
|
|
1041
|
-
`publicQueryModel/publicCommandModel: a method body must resolve to exactly one model operation, but it recorded ${sink.length}. A contract method is a single declarative operation; multiple writes belong in a transaction (defineTransaction), and branching across operations is not representable as a declarative contract method.`
|
|
1042
|
-
);
|
|
1043
|
-
}
|
|
1044
|
-
return sink[0];
|
|
1045
|
-
}
|
|
1046
|
-
function verifyOps(a, b, surfaced) {
|
|
1047
|
-
if (a.operation !== b.operation) {
|
|
1048
|
-
throw new Error(
|
|
1049
|
-
"publicQueryModel/publicCommandModel: the method body is not deterministic (it resolved to different operations across evaluation passes). A contract method must be a pure, declarative description of a single operation."
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
if (a.entity.name !== b.entity.name) {
|
|
1053
|
-
throw new Error(
|
|
1054
|
-
"publicQueryModel/publicCommandModel: the method body targeted different models across evaluation passes; it must be a pure, declarative description."
|
|
1055
|
-
);
|
|
1056
|
-
}
|
|
1057
|
-
verifyKeySlot(a.keys, b.keys, `${a.operation} key`);
|
|
1058
|
-
verifyRecord(a.select, b.select, surfaced, `${a.operation} select`);
|
|
1059
|
-
verifyRecord(a.changes, b.changes, surfaced, `${a.operation} changes`);
|
|
1060
|
-
verifyRecord(a.item, b.item, surfaced, `${a.operation} item`);
|
|
1061
|
-
verifyRecord(
|
|
1062
|
-
a.condition,
|
|
1063
|
-
b.condition,
|
|
1064
|
-
surfaced,
|
|
1065
|
-
`${a.operation} condition`
|
|
1066
|
-
);
|
|
1067
|
-
verifyCompose(a.compose, b.compose, `${a.operation} compose`);
|
|
1068
|
-
if (a.batch !== b.batch) {
|
|
1069
|
-
throw new Error(
|
|
1070
|
-
`publicCommandModel: the method body declared a different \`batch\` mode across evaluation passes ('${String(a.batch)}' vs '${String(b.batch)}'); the batched-write mode must be a fixed literal declaration.`
|
|
1071
|
-
);
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
function verifyCompose(a, b, context) {
|
|
1075
|
-
const ca = a ?? [];
|
|
1076
|
-
const cb = b ?? [];
|
|
1077
|
-
const byAsA = new Map(ca.map((c) => [c.as, c]));
|
|
1078
|
-
const byAsB = new Map(cb.map((c) => [c.as, c]));
|
|
1079
|
-
if (byAsA.size !== ca.length || byAsB.size !== cb.length) {
|
|
1080
|
-
throw new Error(
|
|
1081
|
-
`publicQueryModel: ${context} declares two compositions at the same select property; each \`query(...)\` must attach to a distinct property.`
|
|
1082
|
-
);
|
|
1083
|
-
}
|
|
1084
|
-
const keysA = [...byAsA.keys()].sort();
|
|
1085
|
-
const keysB = [...byAsB.keys()].sort();
|
|
1086
|
-
if (keysA.length !== keysB.length || keysA.some((x, i) => x !== keysB[i])) {
|
|
1087
|
-
throw new Error(
|
|
1088
|
-
`publicQueryModel: ${context} property set diverged between evaluation passes; the method body must declare a fixed set of compositions.`
|
|
1089
|
-
);
|
|
1090
|
-
}
|
|
1091
|
-
for (const as of keysA) {
|
|
1092
|
-
const na = byAsA.get(as);
|
|
1093
|
-
const nb = byAsB.get(as);
|
|
1094
|
-
if (na.method !== nb.method) {
|
|
1095
|
-
throw new Error(
|
|
1096
|
-
`publicQueryModel: ${context} '${as}' references a different contract method across evaluation passes; a composition must point at a single, stable contract method.`
|
|
1097
|
-
);
|
|
1098
|
-
}
|
|
1099
|
-
const fieldsA = Object.keys(na.bind).sort();
|
|
1100
|
-
const fieldsB = Object.keys(nb.bind).sort();
|
|
1101
|
-
if (fieldsA.length !== fieldsB.length || fieldsA.some((x, i) => x !== fieldsB[i])) {
|
|
1102
|
-
throw new Error(
|
|
1103
|
-
`publicQueryModel: ${context} '${as}' binds a different set of child key fields across evaluation passes; the binding must be a fixed declarative map.`
|
|
1104
|
-
);
|
|
1105
|
-
}
|
|
1106
|
-
for (const field of fieldsA) {
|
|
1107
|
-
if (na.bind[field].path !== nb.bind[field].path) {
|
|
1108
|
-
throw new Error(
|
|
1109
|
-
`publicQueryModel: ${context} '${as}' binding '${field}' resolves to a different \`from\` path across evaluation passes; the binding must be a literal declarative path.`
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
function verifyKeySlot(a, b, context) {
|
|
1116
|
-
const wholeA = isContractKeyRef(a);
|
|
1117
|
-
const wholeB = isContractKeyRef(b);
|
|
1118
|
-
if (wholeA && wholeB) return;
|
|
1119
|
-
if (wholeA !== wholeB) {
|
|
1120
|
-
throw new Error(
|
|
1121
|
-
`publicQueryModel/publicCommandModel: ${context} is the whole \`keys\` in one evaluation pass but a rebuilt key in the other; the method body must be a pure, declarative description.`
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
verifyRecord(
|
|
1125
|
-
a,
|
|
1126
|
-
b,
|
|
1127
|
-
/* @__PURE__ */ new Set(),
|
|
1128
|
-
context
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
function verifyRecord(a, b, surfaced, context) {
|
|
1132
|
-
if (a === void 0 && b === void 0) return;
|
|
1133
|
-
if (a === void 0 || b === void 0) {
|
|
1134
|
-
throw new Error(
|
|
1135
|
-
`publicQueryModel/publicCommandModel: ${context} present in one evaluation pass but not the other; the method body must be a pure, declarative description.`
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
const ka = Object.keys(a).sort();
|
|
1139
|
-
const kb = Object.keys(b).sort();
|
|
1140
|
-
if (ka.length !== kb.length || ka.some((x, i) => x !== kb[i])) {
|
|
1141
|
-
throw new Error(
|
|
1142
|
-
`publicQueryModel/publicCommandModel: ${context} field set diverged between evaluation passes; the method body must be a pure, declarative description.`
|
|
1143
|
-
);
|
|
1144
|
-
}
|
|
1145
|
-
for (const field of ka) {
|
|
1146
|
-
verifyLeaf(a[field], b[field], surfaced, `${context} field '${field}'`);
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
function refTokenOf(value) {
|
|
1150
|
-
if (isContractParamRef(value)) return value.token;
|
|
1151
|
-
if (isContractKeyFieldRef(value)) return value.token;
|
|
1152
|
-
return void 0;
|
|
1153
|
-
}
|
|
1154
|
-
function verifyLeaf(va, vb, surfaced, context) {
|
|
1155
|
-
const ra = refTokenOf(va);
|
|
1156
|
-
const rb = refTokenOf(vb);
|
|
1157
|
-
if (ra !== void 0 || rb !== void 0) {
|
|
1158
|
-
if (ra === void 0 || rb === void 0) {
|
|
1159
|
-
throw new Error(
|
|
1160
|
-
`publicQueryModel/publicCommandModel: ${context} is a field reference in one evaluation pass but a transformed value in the other \u2014 a coercion / transform escape was applied. Use a direct \`params.field\` / \`keys.field\` reference or a concrete literal.`
|
|
1161
|
-
);
|
|
1162
|
-
}
|
|
1163
|
-
if (ra !== rb) {
|
|
1164
|
-
throw new Error(
|
|
1165
|
-
`publicQueryModel/publicCommandModel: ${context} resolves to different field references across evaluation passes \u2014 the value is not a stable, declarative reference.`
|
|
1166
|
-
);
|
|
1167
|
-
}
|
|
1168
|
-
if (isContractParamRef(va)) surfaced.add(ra);
|
|
1169
|
-
return;
|
|
1170
|
-
}
|
|
1171
|
-
const na = normalizeLiteral(va);
|
|
1172
|
-
const nb = normalizeLiteral(vb);
|
|
1173
|
-
if (na !== nb) {
|
|
1174
|
-
throw new Error(
|
|
1175
|
-
`publicQueryModel/publicCommandModel: ${context} is not a stable concrete literal \u2014 evaluating the method body twice produced different values ('${na}' vs '${nb}'). A parameter was coerced or transformed into the value (e.g. \`String(params.x).toUpperCase()\`, \`\${params.x}-suffix\`, or arithmetic). Only a direct field reference or a concrete literal is allowed at a value position.`
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
function normalizeLiteral(value) {
|
|
1180
|
-
if (value === null) return "t:null";
|
|
1181
|
-
if (value === void 0) return "t:undef";
|
|
1182
|
-
if (value instanceof Date) return `t:date:${value.toISOString()}`;
|
|
1183
|
-
const t = typeof value;
|
|
1184
|
-
if (t === "string") return `t:s:${value}`;
|
|
1185
|
-
if (t === "boolean") return `t:b:${String(value)}`;
|
|
1186
|
-
if (t === "number") {
|
|
1187
|
-
if (Number.isNaN(value)) {
|
|
1188
|
-
throw new Error(
|
|
1189
|
-
"publicQueryModel/publicCommandModel: a value leaf coerced to NaN, which means arithmetic was performed on a parameter. Only direct field references or concrete literals are allowed at value positions."
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
return `t:n:${String(value)}`;
|
|
1193
|
-
}
|
|
1194
|
-
throw new Error(
|
|
1195
|
-
`publicQueryModel/publicCommandModel: a value leaf has unsupported type '${t}'. Only a direct \`params.field\` reference or a concrete literal (string / number / boolean / Date) is allowed at a value position.`
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
function resolveMethodOp(name, body) {
|
|
1199
|
-
const accessed = /* @__PURE__ */ new Set();
|
|
1200
|
-
const coerced = /* @__PURE__ */ new Set();
|
|
1201
|
-
const keyCoerced = { value: false };
|
|
1202
|
-
const ops = [];
|
|
1203
|
-
for (let pass = 0; pass < PARAM_PASS_COUNT; pass++) {
|
|
1204
|
-
ops.push(runMethodPass(body, pass, accessed, coerced, keyCoerced));
|
|
1205
|
-
}
|
|
1206
|
-
if (keyCoerced.value) {
|
|
1207
|
-
throw new Error(
|
|
1208
|
-
`publicQueryModel/publicCommandModel: method '${name}' coerces its \`keys\` argument to a primitive (e.g. via \`\${keys}\`, \`String(keys)\`, string concatenation, or a comparison). \`keys\` may only be passed *directly* into a model operation's key position; coercing or transforming it is not representable as a declarative contract method.`
|
|
1209
|
-
);
|
|
1210
|
-
}
|
|
1211
|
-
if (coerced.size > 0) {
|
|
1212
|
-
const field = [...coerced][0];
|
|
1213
|
-
throw new Error(
|
|
1214
|
-
`publicQueryModel/publicCommandModel: method '${name}' coerces params field '${field}' to a primitive (e.g. via \`String(params.${field})\`, a template literal \`\${\u2026}\`, string concatenation, or a comparison) and then consumes it in a transform or branch \u2014 for example \`String(params.${field}).slice(0, n)\` or \`\${params.${field}}-suffix\`. A contract method only supports a *direct* field reference or a concrete literal at a value position; a coerced / transformed parameter is not representable as a declarative contract method.`
|
|
1215
|
-
);
|
|
1216
|
-
}
|
|
1217
|
-
const surfaced = /* @__PURE__ */ new Set();
|
|
1218
|
-
verifyOps(ops[0], ops[1], surfaced);
|
|
1219
|
-
for (const field of accessed) {
|
|
1220
|
-
if (!surfaced.has(`{${field}}`)) {
|
|
1221
|
-
throw new Error(
|
|
1222
|
-
`publicQueryModel/publicCommandModel: method '${name}' accesses params field '${field}' but it never appears as a field reference in the recorded operation. This happens when a parameter is consumed by a value branch (e.g. \`params.${field} === 'x' ? \u2026 : \u2026\`) or a string transform instead of being referenced directly. A contract method only supports direct \`params.field\` references or concrete literals at value positions.`
|
|
1223
|
-
);
|
|
3079
|
+
`publicQueryModel: an External Query composition (\`query(...)\`) at select property '${field}' is only valid on a read; the recorded op on '${entityName}' is a '${operation}'.`
|
|
3080
|
+
);
|
|
3081
|
+
}
|
|
3082
|
+
compose.push({ as: field, method: value.method, bind: value.bind });
|
|
3083
|
+
continue;
|
|
1224
3084
|
}
|
|
3085
|
+
projection[field] = value;
|
|
1225
3086
|
}
|
|
1226
|
-
return
|
|
3087
|
+
return { select: projection, compose };
|
|
1227
3088
|
}
|
|
1228
3089
|
function deriveReadFacts(op) {
|
|
1229
3090
|
if (op.operation === "list") {
|
|
@@ -1247,103 +3108,32 @@ function deriveReadFacts(op) {
|
|
|
1247
3108
|
}
|
|
1248
3109
|
return { resolution: "point", inputArity: "either" };
|
|
1249
3110
|
}
|
|
1250
|
-
function assertDeclaredArity(name, declared, resolution, derived) {
|
|
1251
|
-
if (declared === void 0) return;
|
|
1252
|
-
if (declared === derived) return;
|
|
1253
|
-
const hint = resolution === "range" ? `it resolved to a 'range' read (a partition \`Query\`), whose arity is always 'single' \u2014 use \`range(body)\`, not \`point(...)\`.` : derived === "single" ? `it resolved to a non-coalescible unique-GSI 'point' read (a per-key GSI \`Query\`; \`BatchGetItem\` cannot read a GSI), whose arity is 'single' \u2014 use \`point(body, { coalesce: false })\`.` : `it resolved to a coalescible base-table 'point' read (a key array \u2192 one \`BatchGetItem\`), whose arity is 'either' \u2014 use \`point(body)\` (\`coalesce\` defaults to \`true\`).`;
|
|
1254
|
-
throw new Error(
|
|
1255
|
-
`publicQueryModel: method '${name}' declared input arity '${declared}', but ${hint} The declared arity is threaded into the type-level call surface, so it must match the resolved access pattern.`
|
|
1256
|
-
);
|
|
1257
|
-
}
|
|
1258
|
-
var ARITY_TAG = "__contractArity";
|
|
1259
|
-
function tagArity(arity, body) {
|
|
1260
|
-
if (typeof body !== "function") {
|
|
1261
|
-
throw new Error(
|
|
1262
|
-
`publicQueryModel: point()/range() must wrap a method body function (\`(keys, params) => Model.query(...) | Model.list(...)\`), received ${body === null ? "null" : typeof body}.`
|
|
1263
|
-
);
|
|
1264
|
-
}
|
|
1265
|
-
Object.defineProperty(body, ARITY_TAG, { value: arity, enumerable: false });
|
|
1266
|
-
return body;
|
|
1267
|
-
}
|
|
1268
|
-
function declaredArityOf(body) {
|
|
1269
|
-
if (typeof body !== "function" && typeof body !== "object") return void 0;
|
|
1270
|
-
if (body === null) return void 0;
|
|
1271
|
-
const tag = body[ARITY_TAG];
|
|
1272
|
-
return tag === "single" || tag === "either" || tag === "array" ? tag : void 0;
|
|
1273
|
-
}
|
|
1274
|
-
function point(body, options) {
|
|
1275
|
-
const arity = options?.coalesce === false ? "single" : "either";
|
|
1276
|
-
return tagArity(arity, body);
|
|
1277
|
-
}
|
|
1278
|
-
function range(body) {
|
|
1279
|
-
return tagArity("single", body);
|
|
1280
|
-
}
|
|
1281
3111
|
function isQueryModelContract(value) {
|
|
1282
3112
|
return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "query";
|
|
1283
3113
|
}
|
|
1284
3114
|
function isCommandModelContract(value) {
|
|
1285
3115
|
return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "command";
|
|
1286
3116
|
}
|
|
1287
|
-
function publicQueryModel(
|
|
1288
|
-
|
|
1289
|
-
return (methods) => buildQueryContract(methods, keyFields);
|
|
1290
|
-
}
|
|
1291
|
-
function normalizeFactoryArgs(factory, modelOrKeyFields, maybeKeyFields) {
|
|
1292
|
-
let model;
|
|
1293
|
-
let keyFields;
|
|
1294
|
-
if (Array.isArray(modelOrKeyFields)) {
|
|
1295
|
-
keyFields = modelOrKeyFields;
|
|
1296
|
-
} else if (modelOrKeyFields !== void 0) {
|
|
1297
|
-
model = modelOrKeyFields;
|
|
1298
|
-
keyFields = maybeKeyFields;
|
|
1299
|
-
}
|
|
1300
|
-
if (model !== void 0) resolveModelClass(model);
|
|
1301
|
-
if (keyFields !== void 0) assertKeyFieldList(factory, keyFields);
|
|
1302
|
-
return { keyFields };
|
|
1303
|
-
}
|
|
1304
|
-
function assertKeyFieldList(factory, keyFields) {
|
|
1305
|
-
if (keyFields.length === 0) {
|
|
1306
|
-
throw new Error(
|
|
1307
|
-
`${factory}: the explicit Key-field list is empty. Supply at least one Key field (e.g. \`['email']\`), or omit the list to default to the model's primary key.`
|
|
1308
|
-
);
|
|
1309
|
-
}
|
|
1310
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1311
|
-
for (const field of keyFields) {
|
|
1312
|
-
if (typeof field !== "string" || field.length === 0) {
|
|
1313
|
-
throw new Error(
|
|
1314
|
-
`${factory}: an explicit Key field must be a non-empty string, but the list ${JSON.stringify(keyFields)} contains ${JSON.stringify(field)}.`
|
|
1315
|
-
);
|
|
1316
|
-
}
|
|
1317
|
-
if (seen.has(field)) {
|
|
1318
|
-
throw new Error(
|
|
1319
|
-
`${factory}: the explicit Key-field list ${JSON.stringify(keyFields)} repeats the field '${field}'; each Key field must appear once.`
|
|
1320
|
-
);
|
|
1321
|
-
}
|
|
1322
|
-
seen.add(field);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
function withKeyFields(op, keyFields) {
|
|
1326
|
-
if (keyFields === void 0) return op;
|
|
1327
|
-
if (op.operation === "put") return op;
|
|
1328
|
-
if (!isContractKeyRef(op.keys)) return op;
|
|
1329
|
-
return { ...op, keyFields };
|
|
3117
|
+
function publicQueryModel(methods) {
|
|
3118
|
+
return buildQueryContract(methods);
|
|
1330
3119
|
}
|
|
1331
|
-
function buildQueryContract(methods
|
|
3120
|
+
function buildQueryContract(methods) {
|
|
1332
3121
|
const resolved = {};
|
|
1333
3122
|
for (const [name, body] of Object.entries(methods)) {
|
|
1334
|
-
|
|
3123
|
+
let op;
|
|
3124
|
+
if (isPublicReadDescriptor(body)) {
|
|
3125
|
+
op = readDescriptorToOp(name, body);
|
|
3126
|
+
} else {
|
|
1335
3127
|
throw new Error(
|
|
1336
|
-
`publicQueryModel: method '${name}' must be a
|
|
3128
|
+
`publicQueryModel: method '${name}' must be a read descriptor (\`{ query | list: Model, key, select, options? }\`). The legacy (keys, params) => Model.query(...) | Model.list(...) closure form has been removed (#101).`
|
|
1337
3129
|
);
|
|
1338
3130
|
}
|
|
1339
|
-
const op = withKeyFields(resolveMethodOp(name, body), keyFields);
|
|
1340
3131
|
if (op.operation !== "query" && op.operation !== "list") {
|
|
1341
3132
|
throw new Error(
|
|
1342
3133
|
`publicQueryModel: method '${name}' resolves to a '${op.operation}' (write) operation; a query model may only resolve to a read (\`Model.query\` / \`Model.list\`). Use publicCommandModel for writes.`
|
|
1343
3134
|
);
|
|
1344
3135
|
}
|
|
1345
3136
|
const { resolution, inputArity } = deriveReadFacts(op);
|
|
1346
|
-
assertDeclaredArity(name, declaredArityOf(body), resolution, inputArity);
|
|
1347
3137
|
resolved[name] = {
|
|
1348
3138
|
__methodKind: "query",
|
|
1349
3139
|
op,
|
|
@@ -1356,115 +3146,61 @@ function buildQueryContract(methods, keyFields) {
|
|
|
1356
3146
|
for (const spec of Object.values(resolved)) methodSpecToContract.set(spec, contract);
|
|
1357
3147
|
return contract;
|
|
1358
3148
|
}
|
|
1359
|
-
function publicCommandModel(
|
|
1360
|
-
|
|
1361
|
-
return (methods) => buildCommandContract(methods, keyFields);
|
|
3149
|
+
function publicCommandModel(methods) {
|
|
3150
|
+
return buildCommandContract(methods);
|
|
1362
3151
|
}
|
|
1363
|
-
function buildCommandContract(methods
|
|
3152
|
+
function buildCommandContract(methods) {
|
|
1364
3153
|
const resolved = {};
|
|
1365
3154
|
for (const [name, body] of Object.entries(methods)) {
|
|
1366
|
-
if (
|
|
1367
|
-
|
|
3155
|
+
if (isPublicWriteDescriptor(body)) {
|
|
3156
|
+
const { plan: plan2, select, condition, mode } = writeDescriptorToPlan(name, body);
|
|
3157
|
+
resolved[name] = finalizeDescriptorCommand(name, plan2, select, condition, mode);
|
|
1368
3158
|
continue;
|
|
1369
3159
|
}
|
|
1370
|
-
if (
|
|
1371
|
-
|
|
1372
|
-
|
|
3160
|
+
if (isPublicComposeDescriptor(body)) {
|
|
3161
|
+
const select = body.result !== void 0 && body.result.select !== void 0 ? body.result.select : void 0;
|
|
3162
|
+
resolved[name] = finalizeDescriptorCommand(
|
|
3163
|
+
name,
|
|
3164
|
+
body.command,
|
|
3165
|
+
select,
|
|
3166
|
+
void 0,
|
|
3167
|
+
body.mode ?? "transaction"
|
|
1373
3168
|
);
|
|
3169
|
+
continue;
|
|
1374
3170
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
3171
|
+
if (isCommandPlan(body)) {
|
|
3172
|
+
resolved[name] = finalizeDescriptorCommand(
|
|
3173
|
+
name,
|
|
3174
|
+
body,
|
|
3175
|
+
void 0,
|
|
3176
|
+
void 0,
|
|
3177
|
+
"transaction"
|
|
1379
3178
|
);
|
|
3179
|
+
continue;
|
|
1380
3180
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
op,
|
|
1385
|
-
// A single key → one write op; a key array → a batched write. Both forms
|
|
1386
|
-
// are point writes against known keys, so the accepted arity is 'either'.
|
|
1387
|
-
inputArity: "either",
|
|
1388
|
-
result: commandResultOf(op),
|
|
1389
|
-
...op.batch !== void 0 ? { batch: op.batch } : {}
|
|
1390
|
-
};
|
|
3181
|
+
throw new Error(
|
|
3182
|
+
`publicCommandModel: method '${name}' must be a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result?, mode? }\`), a composite \`{ command: mutation(...), input?, result?, mode? }\` descriptor, or a bare \`mutation($ => ({ \u2026 }))\` plan. The legacy closure form (\`(keys, params) => Model.putItem(...)\`) and the \`command(...).plan(...)\` builder have been removed (#101).`
|
|
3183
|
+
);
|
|
1391
3184
|
}
|
|
1392
3185
|
return { __isContract: true, kind: "command", methods: resolved };
|
|
1393
3186
|
}
|
|
1394
|
-
function commandResultOf(op) {
|
|
1395
|
-
return op.operation === "update" ? "entity" : "void";
|
|
1396
|
-
}
|
|
1397
3187
|
var PLANNED_COMMAND_BRAND = /* @__PURE__ */ Symbol("graphddb:plannedCommandMethod");
|
|
1398
3188
|
function isPlannedCommandMethod(value) {
|
|
1399
3189
|
return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
|
|
1400
3190
|
}
|
|
1401
|
-
function
|
|
1402
|
-
const
|
|
1403
|
-
|
|
1404
|
-
throw new Error(
|
|
1405
|
-
"command({ input, select }): `input` must be a record of `param.*` placeholders (e.g. `{ userId: param.string(), title: param.string() }`)."
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
for (const [field, p] of Object.entries(input)) {
|
|
1409
|
-
if (!isParam(p)) {
|
|
1410
|
-
throw new Error(
|
|
1411
|
-
`command({ input }): input field '${field}' must be a \`param.*\` placeholder (e.g. \`param.string()\`), not ${JSON.stringify(p)}.`
|
|
1412
|
-
);
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
const select = spec.select;
|
|
1416
|
-
if (select !== void 0) {
|
|
1417
|
-
if (typeof select !== "object" || select === null) {
|
|
1418
|
-
throw new Error(
|
|
1419
|
-
"command({ select }): `select` must be a boolean field map (e.g. `{ postId: true, title: true }`), or omitted for no return projection."
|
|
1420
|
-
);
|
|
1421
|
-
}
|
|
1422
|
-
for (const [field, flag] of Object.entries(select)) {
|
|
1423
|
-
if (typeof flag !== "boolean") {
|
|
1424
|
-
throw new Error(
|
|
1425
|
-
`command({ select }): return projection field '${field}' must be a boolean flag (\`true\` / \`false\`), not ${JSON.stringify(flag)}.`
|
|
1426
|
-
);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
return {
|
|
1431
|
-
plan(plan) {
|
|
1432
|
-
if (!isCommandPlan(plan)) {
|
|
1433
|
-
throw new Error(
|
|
1434
|
-
"command(...).plan(...): expected a `mutation(...)` / `definePlan(...)` CommandPlan (the internal write-plan composition), but received a non-plan value."
|
|
1435
|
-
);
|
|
1436
|
-
}
|
|
1437
|
-
return {
|
|
1438
|
-
[PLANNED_COMMAND_BRAND]: true,
|
|
1439
|
-
plan,
|
|
1440
|
-
input,
|
|
1441
|
-
select
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
};
|
|
1445
|
-
}
|
|
1446
|
-
function buildPlannedCommandMethod(name, planned, keyFields) {
|
|
1447
|
-
const plan = compileMutationPlan(planned.plan);
|
|
1448
|
-
const primary = plan.fragments[0];
|
|
1449
|
-
if (keyFields !== void 0) {
|
|
1450
|
-
const want = [...keyFields].sort().join(",");
|
|
1451
|
-
const got = [...primary.keyFields].sort().join(",");
|
|
1452
|
-
if (want !== got) {
|
|
1453
|
-
throw new Error(
|
|
1454
|
-
`publicCommandModel: planned method '${name}' writes '${primary.op.entity.name}' keyed by its primary key [${primary.keyFields.join(", ")}], but the factory was given an explicit Key-field list [${keyFields.join(", ")}]. A mutation-derived command is keyed by the written entity's primary key; omit the explicit list or match it.`
|
|
1455
|
-
);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
3191
|
+
function buildPlannedCommandMethod(name, planned) {
|
|
3192
|
+
const plan2 = compileMutationPlan(planned.plan);
|
|
3193
|
+
const primary = plan2.fragments[0];
|
|
1458
3194
|
const stamp = (fragment) => fragment.op.operation === "put" ? fragment.op : { ...fragment.op, keyFields: fragment.keyFields };
|
|
1459
3195
|
const op = stamp(primary);
|
|
1460
3196
|
const returnSelection = normalizeReturnSelection(planned.select);
|
|
1461
|
-
const ops =
|
|
1462
|
-
const conditionChecks =
|
|
1463
|
-
const edgeWrites2 =
|
|
1464
|
-
const derivedUpdates =
|
|
1465
|
-
const uniqueGuards =
|
|
1466
|
-
const outboxEvents =
|
|
1467
|
-
const idempotencyGuards =
|
|
3197
|
+
const ops = plan2.fragments.length > 1 ? plan2.fragments.map(stamp) : void 0;
|
|
3198
|
+
const conditionChecks = plan2.fragments.flatMap((f) => f.conditionChecks ?? []);
|
|
3199
|
+
const edgeWrites2 = plan2.fragments.flatMap((f) => f.edgeWrites ?? []);
|
|
3200
|
+
const derivedUpdates = plan2.fragments.flatMap((f) => f.derivedUpdates ?? []);
|
|
3201
|
+
const uniqueGuards = plan2.fragments.flatMap((f) => f.uniqueGuards ?? []);
|
|
3202
|
+
const outboxEvents = plan2.fragments.flatMap((f) => f.outboxEvents ?? []);
|
|
3203
|
+
const idempotencyGuards = plan2.fragments.map((f) => f.idempotencyGuard).filter((g) => g !== void 0);
|
|
1468
3204
|
if (idempotencyGuards.length > 1) {
|
|
1469
3205
|
throw new Error(
|
|
1470
3206
|
`publicCommandModel: planned method '${name}' declares an idempotency guard on more than one fragment. Idempotency is a single command-level client token (it keys the whole atomic write); declare \`w.idempotentBy(...)\` on at most one fragment's lifecycle.`
|
|
@@ -1488,6 +3224,21 @@ function buildPlannedCommandMethod(name, planned, keyFields) {
|
|
|
1488
3224
|
...returnSelection !== void 0 ? { returnSelection } : {}
|
|
1489
3225
|
};
|
|
1490
3226
|
}
|
|
3227
|
+
function finalizeDescriptorCommand(name, plan2, select, condition, mode) {
|
|
3228
|
+
const planned = {
|
|
3229
|
+
[PLANNED_COMMAND_BRAND]: true,
|
|
3230
|
+
plan: plan2,
|
|
3231
|
+
input: {},
|
|
3232
|
+
select
|
|
3233
|
+
};
|
|
3234
|
+
const built = buildPlannedCommandMethod(name, planned);
|
|
3235
|
+
const op = condition !== void 0 ? { ...built.op, condition } : built.op;
|
|
3236
|
+
return {
|
|
3237
|
+
...built,
|
|
3238
|
+
op,
|
|
3239
|
+
mode
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
1491
3242
|
function normalizeReturnSelection(select) {
|
|
1492
3243
|
if (select === void 0) return void 0;
|
|
1493
3244
|
const out = {};
|
|
@@ -1496,6 +3247,117 @@ function normalizeReturnSelection(select) {
|
|
|
1496
3247
|
}
|
|
1497
3248
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
1498
3249
|
}
|
|
3250
|
+
var READ_INTENT_KEYS = ["query", "list"];
|
|
3251
|
+
var WRITE_INTENT_KEYS = ["create", "update", "remove"];
|
|
3252
|
+
function isPublicComposeDescriptor(value) {
|
|
3253
|
+
return typeof value === "object" && value !== null && isCommandPlan(value.command);
|
|
3254
|
+
}
|
|
3255
|
+
function isPublicReadDescriptor(value) {
|
|
3256
|
+
return typeof value === "object" && value !== null && READ_INTENT_KEYS.some((k) => value[k] !== void 0);
|
|
3257
|
+
}
|
|
3258
|
+
function isPublicWriteDescriptor(value) {
|
|
3259
|
+
return typeof value === "object" && value !== null && WRITE_INTENT_KEYS.some((k) => value[k] !== void 0);
|
|
3260
|
+
}
|
|
3261
|
+
function resolveDescriptorModel(ref, name) {
|
|
3262
|
+
if (ref === null || ref === void 0) {
|
|
3263
|
+
throw new Error(`publicQueryModel/publicCommandModel: method '${name}' names no target model.`);
|
|
3264
|
+
}
|
|
3265
|
+
if (typeof ref === "function") {
|
|
3266
|
+
const asModel = ref.asModel;
|
|
3267
|
+
if (typeof asModel === "function") return asModel.call(ref);
|
|
3268
|
+
return resolveDescriptorModel(ref(), name);
|
|
3269
|
+
}
|
|
3270
|
+
return ref;
|
|
3271
|
+
}
|
|
3272
|
+
function descriptorKeyRecord(record, name, what) {
|
|
3273
|
+
const out = {};
|
|
3274
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
3275
|
+
if (isParam(leaf)) {
|
|
3276
|
+
out[field] = mintContractKeyFieldRef(field);
|
|
3277
|
+
} else if (isConcreteLiteral2(leaf)) {
|
|
3278
|
+
out[field] = leaf;
|
|
3279
|
+
} else {
|
|
3280
|
+
throw new Error(
|
|
3281
|
+
`publicQueryModel/publicCommandModel: method '${name}' ${what} field '${field}' must be a \`param.*\` placeholder or a concrete literal, not ${JSON.stringify(leaf)}.`
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
return out;
|
|
3286
|
+
}
|
|
3287
|
+
function descriptorConditionRecord(record, name) {
|
|
3288
|
+
const out = {};
|
|
3289
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
3290
|
+
if (isParam(leaf)) {
|
|
3291
|
+
out[field] = mintContractParamRef(field);
|
|
3292
|
+
} else {
|
|
3293
|
+
out[field] = leaf;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
return out;
|
|
3297
|
+
}
|
|
3298
|
+
function readDescriptorToOp(name, d) {
|
|
3299
|
+
const isQuery = d.query !== void 0;
|
|
3300
|
+
const isList = d.list !== void 0;
|
|
3301
|
+
if (isQuery === isList) {
|
|
3302
|
+
throw new Error(
|
|
3303
|
+
`publicQueryModel: method '${name}' must carry exactly one of \`query\` or \`list\` (the target model); it declared ${isQuery && isList ? "both" : "neither"}.`
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
if (d.key === void 0 || d.select === void 0) {
|
|
3307
|
+
throw new Error(
|
|
3308
|
+
`publicQueryModel: the '${isQuery ? "query" : "list"}' descriptor for method '${name}' must declare both \`key\` and \`select\`.`
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
const model = resolveDescriptorModel(isQuery ? d.query : d.list, name);
|
|
3312
|
+
const entity = entityRef(model);
|
|
3313
|
+
const operation = isQuery ? "query" : "list";
|
|
3314
|
+
const keyFieldNames2 = Object.keys(d.key);
|
|
3315
|
+
const keyRecord = descriptorKeyRecord(d.key, name, `${operation} key`);
|
|
3316
|
+
const { select, compose } = extractCompose({ ...d.select }, operation, entity.name);
|
|
3317
|
+
const keys = isQuery ? wholeKeysSentinel() : keyRecord;
|
|
3318
|
+
return {
|
|
3319
|
+
__isContractMethodOp: true,
|
|
3320
|
+
entity,
|
|
3321
|
+
operation,
|
|
3322
|
+
keys,
|
|
3323
|
+
...isQuery ? { keyFields: keyFieldNames2 } : {},
|
|
3324
|
+
...select !== void 0 ? { select } : {},
|
|
3325
|
+
...compose.length > 0 ? { compose } : {}
|
|
3326
|
+
};
|
|
3327
|
+
}
|
|
3328
|
+
function writeDescriptorToPlan(name, d) {
|
|
3329
|
+
const present = WRITE_INTENT_KEYS.filter((k) => d[k] !== void 0);
|
|
3330
|
+
if (present.length !== 1) {
|
|
3331
|
+
throw new Error(
|
|
3332
|
+
`publicCommandModel: method '${name}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
const intent = present[0];
|
|
3336
|
+
if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
|
|
3337
|
+
throw new Error(
|
|
3338
|
+
`publicCommandModel: the '${intent}' descriptor for method '${name}' must declare a \`key\` object binding the target's primary-key fields.`
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
const model = resolveDescriptorModel(d[intent], name);
|
|
3342
|
+
const keyFieldNames2 = Object.keys(d.key);
|
|
3343
|
+
const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
|
|
3344
|
+
const plan2 = mutation(name, ($) => {
|
|
3345
|
+
const refMap = (fields) => {
|
|
3346
|
+
const out = {};
|
|
3347
|
+
for (const f of fields) out[f] = $[f];
|
|
3348
|
+
return out;
|
|
3349
|
+
};
|
|
3350
|
+
const descriptor = {
|
|
3351
|
+
[intent]: (() => model),
|
|
3352
|
+
key: refMap(keyFieldNames2),
|
|
3353
|
+
...inputFieldNames.length > 0 ? { input: refMap(inputFieldNames) } : {}
|
|
3354
|
+
};
|
|
3355
|
+
return { [name]: descriptor };
|
|
3356
|
+
});
|
|
3357
|
+
const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
|
|
3358
|
+
const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
|
|
3359
|
+
return { plan: plan2, select, condition, mode: d.mode ?? "transaction" };
|
|
3360
|
+
}
|
|
1499
3361
|
|
|
1500
3362
|
// src/spec/mutation-command.ts
|
|
1501
3363
|
var INTENT_OPERATION = {
|
|
@@ -1928,18 +3790,18 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
1928
3790
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
|
|
1929
3791
|
};
|
|
1930
3792
|
}
|
|
1931
|
-
function compileSingleFragmentPlan(
|
|
1932
|
-
if (!isCommandPlan(
|
|
3793
|
+
function compileSingleFragmentPlan(plan2) {
|
|
3794
|
+
if (!isCommandPlan(plan2)) {
|
|
1933
3795
|
throw new Error(
|
|
1934
3796
|
"mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
|
|
1935
3797
|
);
|
|
1936
3798
|
}
|
|
1937
|
-
if (
|
|
3799
|
+
if (plan2.fragments.length !== 1) {
|
|
1938
3800
|
throw new Error(
|
|
1939
|
-
`mutation '${
|
|
3801
|
+
`mutation '${plan2.name}': compileSingleFragmentPlan expects exactly one fragment, but the plan declares ${plan2.fragments.length}. Use compileMutationPlan for the N-fragment atomic merge (issue #90).`
|
|
1940
3802
|
);
|
|
1941
3803
|
}
|
|
1942
|
-
return compileFragment(
|
|
3804
|
+
return compileFragment(plan2.fragments[0], 0, null);
|
|
1943
3805
|
}
|
|
1944
3806
|
var MAX_TRANSACT_COMPOSE_ITEMS = 25;
|
|
1945
3807
|
function keySignature(fragment) {
|
|
@@ -1963,26 +3825,26 @@ function keyLeafToken(leaf) {
|
|
|
1963
3825
|
if (isContractParamRef(leaf)) return `input:${leaf.field}`;
|
|
1964
3826
|
return `literal:${JSON.stringify(leaf)}`;
|
|
1965
3827
|
}
|
|
1966
|
-
function compileMutationPlan(
|
|
1967
|
-
if (!isCommandPlan(
|
|
3828
|
+
function compileMutationPlan(plan2) {
|
|
3829
|
+
if (!isCommandPlan(plan2)) {
|
|
1968
3830
|
throw new Error(
|
|
1969
3831
|
"mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
|
|
1970
3832
|
);
|
|
1971
3833
|
}
|
|
1972
|
-
const fragments =
|
|
3834
|
+
const fragments = plan2.fragments;
|
|
1973
3835
|
const compiled = [];
|
|
1974
3836
|
for (let i = 0; i < fragments.length; i++) {
|
|
1975
3837
|
const resolveEntityRef = (ref, consumerIndex, consumerField) => {
|
|
1976
3838
|
if (ref.fragmentIndex >= consumerIndex) {
|
|
1977
3839
|
throw new Error(
|
|
1978
|
-
`mutation '${
|
|
3840
|
+
`mutation '${plan2.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', a CIRCULAR cross-fragment dependency \u2014 a fragment may only reference a value produced by an EARLIER fragment (a lower index), never itself or a later one. A \`TransactWriteItems\` is atomic and cannot read one item's value while writing another, so the reference graph must be acyclic.`
|
|
1979
3841
|
);
|
|
1980
3842
|
}
|
|
1981
3843
|
const producer = compiled[ref.fragmentIndex];
|
|
1982
3844
|
const resolved = producedFieldLeaf(producer, ref.field);
|
|
1983
3845
|
if (resolved === void 0) {
|
|
1984
3846
|
throw new Error(
|
|
1985
|
-
`mutation '${
|
|
3847
|
+
`mutation '${plan2.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', but fragment #${ref.fragmentIndex} ('${producer.op.entity.name}') does not write a field '${ref.field}'. A cross-fragment reference must name a field the producing fragment binds in its \`input\`.`
|
|
1986
3848
|
);
|
|
1987
3849
|
}
|
|
1988
3850
|
return resolved;
|
|
@@ -1995,17 +3857,17 @@ function compileMutationPlan(plan) {
|
|
|
1995
3857
|
const prior = byKey.get(sig);
|
|
1996
3858
|
if (prior !== void 0) {
|
|
1997
3859
|
throw new Error(
|
|
1998
|
-
`mutation '${
|
|
3860
|
+
`mutation '${plan2.name}': fragments #${prior} and #${i} both write the same item ('${compiled[i].op.entity.name}', key ${describeKey(compiled[i])}) in one \`TransactWriteItems\`. DynamoDB rejects a transaction that operates on the same primary key more than once \u2014 split the conflicting writes into separate mutations, or merge them into a single fragment.`
|
|
1999
3861
|
);
|
|
2000
3862
|
}
|
|
2001
3863
|
byKey.set(sig, i);
|
|
2002
3864
|
}
|
|
2003
3865
|
if (compiled.length > MAX_TRANSACT_COMPOSE_ITEMS) {
|
|
2004
3866
|
throw new Error(
|
|
2005
|
-
`mutation '${
|
|
3867
|
+
`mutation '${plan2.name}': composes ${compiled.length} write fragments into one \`TransactWriteItems\`, but DynamoDB caps a transaction at ${MAX_TRANSACT_COMPOSE_ITEMS} items and a transaction is atomic \u2014 it cannot be split across requests without breaking atomicity. Reduce the fragment count to \u2264${MAX_TRANSACT_COMPOSE_ITEMS}.`
|
|
2006
3868
|
);
|
|
2007
3869
|
}
|
|
2008
|
-
return { name:
|
|
3870
|
+
return { name: plan2.name, fragments: compiled };
|
|
2009
3871
|
}
|
|
2010
3872
|
function producedFieldLeaf(producer, field) {
|
|
2011
3873
|
const op = producer.op;
|
|
@@ -2029,7 +3891,7 @@ function describeKey(fragment) {
|
|
|
2029
3891
|
}
|
|
2030
3892
|
|
|
2031
3893
|
// src/spec/guard.ts
|
|
2032
|
-
function conditionInputToSpec(condition, context,
|
|
3894
|
+
function conditionInputToSpec(condition, context, renderLeaf2) {
|
|
2033
3895
|
if (condition === void 0 || condition === null) return void 0;
|
|
2034
3896
|
if (typeof condition !== "object") {
|
|
2035
3897
|
throw new Error(
|
|
@@ -2056,7 +3918,7 @@ function conditionInputToSpec(condition, context, renderLeaf) {
|
|
|
2056
3918
|
}
|
|
2057
3919
|
const fields = {};
|
|
2058
3920
|
for (const key of Object.keys(obj).sort()) {
|
|
2059
|
-
fields[key] =
|
|
3921
|
+
fields[key] = renderLeaf2(key, obj[key]);
|
|
2060
3922
|
}
|
|
2061
3923
|
return { kind: "equals", fields };
|
|
2062
3924
|
}
|
|
@@ -2500,17 +4362,17 @@ function verifyWrite(instrs, surfaced) {
|
|
|
2500
4362
|
"defineTransaction: write operation diverged between evaluation passes; the callback must be a pure, declarative description of the writes."
|
|
2501
4363
|
);
|
|
2502
4364
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
4365
|
+
verifyRecord(instrs.map((x) => x.item), surfaced, `${op} item`);
|
|
4366
|
+
verifyRecord(instrs.map((x) => x.key), surfaced, `${op} key`);
|
|
4367
|
+
verifyRecord(instrs.map((x) => x.changes), surfaced, `${op} changes`);
|
|
4368
|
+
verifyRecord(
|
|
2507
4369
|
instrs.map((x) => x.condition),
|
|
2508
4370
|
surfaced,
|
|
2509
4371
|
`${op} condition`
|
|
2510
4372
|
);
|
|
2511
4373
|
verifyWhen(instrs.map((x) => x.when), surfaced, `${op} when`);
|
|
2512
4374
|
}
|
|
2513
|
-
function
|
|
4375
|
+
function verifyRecord(records, surfaced, context) {
|
|
2514
4376
|
const present = records.filter((r) => r !== void 0);
|
|
2515
4377
|
if (present.length === 0) return;
|
|
2516
4378
|
if (present.length !== records.length) {
|
|
@@ -2528,7 +4390,7 @@ function verifyRecord2(records, surfaced, context) {
|
|
|
2528
4390
|
}
|
|
2529
4391
|
}
|
|
2530
4392
|
for (const field of keys) {
|
|
2531
|
-
|
|
4393
|
+
verifyLeaf(
|
|
2532
4394
|
present.map((r) => r[field]),
|
|
2533
4395
|
surfaced,
|
|
2534
4396
|
`${context} field '${field}'`
|
|
@@ -2546,9 +4408,9 @@ function verifyWhen(whens, surfaced, context) {
|
|
|
2546
4408
|
for (const w of present) {
|
|
2547
4409
|
if (isTransactionRef(w.left)) surfaced.add(w.left.token);
|
|
2548
4410
|
}
|
|
2549
|
-
|
|
4411
|
+
verifyLeaf(present.map((w) => w.right), surfaced, `${context} right-hand value`);
|
|
2550
4412
|
}
|
|
2551
|
-
function
|
|
4413
|
+
function verifyLeaf(values, surfaced, context) {
|
|
2552
4414
|
const refTokens = values.map((v) => isTransactionRef(v) ? v.token : void 0);
|
|
2553
4415
|
const anyRef = refTokens.some((t) => t !== void 0);
|
|
2554
4416
|
if (anyRef) {
|
|
@@ -2566,7 +4428,7 @@ function verifyLeaf2(values, surfaced, context) {
|
|
|
2566
4428
|
surfaced.add(token);
|
|
2567
4429
|
return;
|
|
2568
4430
|
}
|
|
2569
|
-
const norm = values.map(
|
|
4431
|
+
const norm = values.map(normalizeLiteral);
|
|
2570
4432
|
for (let i = 1; i < norm.length; i++) {
|
|
2571
4433
|
if (norm[i] !== norm[0]) {
|
|
2572
4434
|
throw new Error(
|
|
@@ -2575,7 +4437,7 @@ function verifyLeaf2(values, surfaced, context) {
|
|
|
2575
4437
|
}
|
|
2576
4438
|
}
|
|
2577
4439
|
}
|
|
2578
|
-
function
|
|
4440
|
+
function normalizeLiteral(value) {
|
|
2579
4441
|
if (value === null) return "t:null";
|
|
2580
4442
|
if (value instanceof Date) return `t:date:${value.toISOString()}`;
|
|
2581
4443
|
const t = typeof value;
|
|
@@ -2866,113 +4728,6 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
|
|
|
2866
4728
|
throw new Error(first.message + suffix);
|
|
2867
4729
|
}
|
|
2868
4730
|
|
|
2869
|
-
// src/relation/concurrency.ts
|
|
2870
|
-
var RELATION_TRAVERSAL_CONCURRENCY = 16;
|
|
2871
|
-
async function mapWithConcurrency(items, limit, worker) {
|
|
2872
|
-
if (items.length === 0) {
|
|
2873
|
-
return [];
|
|
2874
|
-
}
|
|
2875
|
-
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
2876
|
-
if (effectiveLimit >= items.length) {
|
|
2877
|
-
return Promise.all(items.map((item, index) => worker(item, index)));
|
|
2878
|
-
}
|
|
2879
|
-
const results = new Array(items.length);
|
|
2880
|
-
let nextIndex = 0;
|
|
2881
|
-
async function runWorker() {
|
|
2882
|
-
while (true) {
|
|
2883
|
-
const index = nextIndex++;
|
|
2884
|
-
if (index >= items.length) {
|
|
2885
|
-
return;
|
|
2886
|
-
}
|
|
2887
|
-
results[index] = await worker(items[index], index);
|
|
2888
|
-
}
|
|
2889
|
-
}
|
|
2890
|
-
const runners = [];
|
|
2891
|
-
for (let i = 0; i < effectiveLimit; i++) {
|
|
2892
|
-
runners.push(runWorker());
|
|
2893
|
-
}
|
|
2894
|
-
await Promise.all(runners);
|
|
2895
|
-
return results;
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
// src/relation/execution-plan.ts
|
|
2899
|
-
function relationResultPaths(select, metadata, parentPath = "$") {
|
|
2900
|
-
const out = [];
|
|
2901
|
-
const relations = detectRelationFields(select, metadata);
|
|
2902
|
-
for (const rel of [...relations].sort(
|
|
2903
|
-
(a, b) => a.propertyName.localeCompare(b.propertyName)
|
|
2904
|
-
)) {
|
|
2905
|
-
const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
|
|
2906
|
-
const targetMeta = MetadataRegistry.get(rel.targetFactory());
|
|
2907
|
-
const childPath = `${parentPath}.${rel.propertyName}`;
|
|
2908
|
-
const isMany = rel.type === "hasMany";
|
|
2909
|
-
const resultPath = isMany ? `${childPath}.items` : childPath;
|
|
2910
|
-
out.push({
|
|
2911
|
-
propertyName: rel.propertyName,
|
|
2912
|
-
parentPath,
|
|
2913
|
-
resultPath,
|
|
2914
|
-
isMany
|
|
2915
|
-
});
|
|
2916
|
-
out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
|
|
2917
|
-
}
|
|
2918
|
-
return out;
|
|
2919
|
-
}
|
|
2920
|
-
function parentResultPath(resultPath) {
|
|
2921
|
-
if (!resultPath.startsWith("$.")) return "$";
|
|
2922
|
-
const tokens = resultPath.slice(2).split(".");
|
|
2923
|
-
if (tokens[tokens.length - 1] === "items") tokens.pop();
|
|
2924
|
-
tokens.pop();
|
|
2925
|
-
return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
|
|
2926
|
-
}
|
|
2927
|
-
function deriveExecutionPlan(resultPaths) {
|
|
2928
|
-
if (resultPaths.length <= 1) return void 0;
|
|
2929
|
-
const indexByPath = /* @__PURE__ */ new Map();
|
|
2930
|
-
resultPaths.forEach((path, i) => {
|
|
2931
|
-
indexByPath.set(path, i);
|
|
2932
|
-
});
|
|
2933
|
-
const stageOf = new Array(resultPaths.length);
|
|
2934
|
-
for (let i = 0; i < resultPaths.length; i++) {
|
|
2935
|
-
if (resultPaths[i] === "$") {
|
|
2936
|
-
stageOf[i] = 0;
|
|
2937
|
-
continue;
|
|
2938
|
-
}
|
|
2939
|
-
const parentPath = parentResultPath(resultPaths[i]);
|
|
2940
|
-
const parentIndex = indexByPath.get(parentPath);
|
|
2941
|
-
const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
|
|
2942
|
-
stageOf[i] = parentStage + 1;
|
|
2943
|
-
}
|
|
2944
|
-
const stageCount = Math.max(...stageOf) + 1;
|
|
2945
|
-
const groups = Array.from({ length: stageCount }, () => []);
|
|
2946
|
-
for (let i = 0; i < resultPaths.length; i++) {
|
|
2947
|
-
groups[stageOf[i]].push(i);
|
|
2948
|
-
}
|
|
2949
|
-
for (const g of groups) g.sort((a, b) => a - b);
|
|
2950
|
-
return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
2951
|
-
}
|
|
2952
|
-
function buildRelationExecutionPlan(select, metadata) {
|
|
2953
|
-
const relationPaths = relationResultPaths(select, metadata);
|
|
2954
|
-
if (relationPaths.length === 0) return void 0;
|
|
2955
|
-
const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
|
|
2956
|
-
const plan = deriveExecutionPlan(resultPaths);
|
|
2957
|
-
if (plan === void 0) return void 0;
|
|
2958
|
-
return { plan, resultPaths };
|
|
2959
|
-
}
|
|
2960
|
-
function deriveCompositionPlan(composeCount) {
|
|
2961
|
-
if (composeCount <= 0) {
|
|
2962
|
-
return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
2963
|
-
}
|
|
2964
|
-
const stage = Array.from({ length: composeCount }, (_unused, i) => i);
|
|
2965
|
-
return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
2966
|
-
}
|
|
2967
|
-
function stageOfResultPath(resolved, resultPath) {
|
|
2968
|
-
const index = resolved.resultPaths.indexOf(resultPath);
|
|
2969
|
-
if (index < 0) return void 0;
|
|
2970
|
-
for (let s = 0; s < resolved.plan.groups.length; s++) {
|
|
2971
|
-
if (resolved.plan.groups[s].includes(index)) return s;
|
|
2972
|
-
}
|
|
2973
|
-
return void 0;
|
|
2974
|
-
}
|
|
2975
|
-
|
|
2976
4731
|
// src/spec/contract-boundary-check.ts
|
|
2977
4732
|
function buildOwnershipIndex(contexts) {
|
|
2978
4733
|
const modelOwner = /* @__PURE__ */ new Map();
|
|
@@ -3202,12 +4957,12 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
|
|
|
3202
4957
|
const keyCondition2 = {
|
|
3203
4958
|
PK: pk2
|
|
3204
4959
|
};
|
|
3205
|
-
const
|
|
4960
|
+
const range2 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
|
|
3206
4961
|
return {
|
|
3207
4962
|
type: "Query",
|
|
3208
4963
|
tableName,
|
|
3209
4964
|
keyCondition: keyCondition2,
|
|
3210
|
-
...
|
|
4965
|
+
...range2 ? { rangeCondition: range2 } : {},
|
|
3211
4966
|
projection: projectionFields(select),
|
|
3212
4967
|
limit,
|
|
3213
4968
|
resultPath,
|
|
@@ -3223,13 +4978,13 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
|
|
|
3223
4978
|
resolved.partial
|
|
3224
4979
|
);
|
|
3225
4980
|
const keyCondition = { [`${gsi.indexName}PK`]: pk };
|
|
3226
|
-
const
|
|
4981
|
+
const range = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
|
|
3227
4982
|
return {
|
|
3228
4983
|
type: "Query",
|
|
3229
4984
|
tableName,
|
|
3230
4985
|
indexName: gsi.indexName,
|
|
3231
4986
|
keyCondition,
|
|
3232
|
-
...
|
|
4987
|
+
...range ? { rangeCondition: range } : {},
|
|
3233
4988
|
projection: projectionFields(select),
|
|
3234
4989
|
limit,
|
|
3235
4990
|
resultPath,
|
|
@@ -3933,15 +5688,9 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
3933
5688
|
}
|
|
3934
5689
|
return { params: sortedParams(params), items, maxItems: items.length };
|
|
3935
5690
|
}
|
|
3936
|
-
function
|
|
3937
|
-
if (mode ===
|
|
3938
|
-
|
|
3939
|
-
if (commandSpec.condition !== void 0) {
|
|
3940
|
-
throw new Error(
|
|
3941
|
-
`Contract '${contractName}.${methodName}': a 'batchWrite' batched form cannot carry a write condition \u2014 DynamoDB's BatchWriteItem has no ConditionExpression. Declare \`batch: 'transact'\` for a conditional batch.`
|
|
3942
|
-
);
|
|
3943
|
-
}
|
|
3944
|
-
return { mode: "batchWrite", operation: opName };
|
|
5691
|
+
function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
|
|
5692
|
+
if (mode === "parallel") {
|
|
5693
|
+
return { mode: "parallel", operation: opName };
|
|
3945
5694
|
}
|
|
3946
5695
|
const txName = batchTxName(contractName, methodName);
|
|
3947
5696
|
transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
|
|
@@ -4076,15 +5825,15 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
4076
5825
|
returnSelection
|
|
4077
5826
|
);
|
|
4078
5827
|
}
|
|
4079
|
-
const batch =
|
|
4080
|
-
method.
|
|
5828
|
+
const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
|
|
5829
|
+
method.mode,
|
|
4081
5830
|
op,
|
|
4082
5831
|
commandSpec,
|
|
4083
5832
|
contractName,
|
|
4084
5833
|
methodName,
|
|
4085
5834
|
opName,
|
|
4086
5835
|
transactionSpecs
|
|
4087
|
-
);
|
|
5836
|
+
) : void 0;
|
|
4088
5837
|
const single = promotedToTransaction ? { mode: "transaction", transaction: opName } : { mode: "op", operation: opName };
|
|
4089
5838
|
methods[methodName] = {
|
|
4090
5839
|
inputArity: method.inputArity,
|
|
@@ -4197,13 +5946,31 @@ export {
|
|
|
4197
5946
|
buildManifest,
|
|
4198
5947
|
isSelectBuilder,
|
|
4199
5948
|
normalizeSelectSpec,
|
|
4200
|
-
normalizeTopLevelSelect,
|
|
4201
|
-
buildProject,
|
|
4202
|
-
buildRelation,
|
|
4203
5949
|
detectRelationFields,
|
|
4204
5950
|
getImplicitKeyFields,
|
|
4205
5951
|
param,
|
|
4206
5952
|
isParam,
|
|
5953
|
+
buildProjection,
|
|
5954
|
+
cond,
|
|
5955
|
+
compileFilterExpression,
|
|
5956
|
+
evaluateFilter,
|
|
5957
|
+
plan,
|
|
5958
|
+
execute,
|
|
5959
|
+
hydrate,
|
|
5960
|
+
encodeCursor,
|
|
5961
|
+
decodeCursor,
|
|
5962
|
+
executeListInternal,
|
|
5963
|
+
executeList,
|
|
5964
|
+
RELATION_TRAVERSAL_CONCURRENCY,
|
|
5965
|
+
mapWithConcurrency,
|
|
5966
|
+
deriveCompositionPlan,
|
|
5967
|
+
validateDepth,
|
|
5968
|
+
resolveRelations,
|
|
5969
|
+
executeQuery,
|
|
5970
|
+
executeExplain,
|
|
5971
|
+
BatchGetResult,
|
|
5972
|
+
executeBatchGet,
|
|
5973
|
+
executeBatchWrite,
|
|
4207
5974
|
LIFECYCLE_CONTRACT_MARKER,
|
|
4208
5975
|
isLifecycleContract,
|
|
4209
5976
|
ENTITY_WRITES_MARKER,
|
|
@@ -4211,11 +5978,6 @@ export {
|
|
|
4211
5978
|
entityWrites,
|
|
4212
5979
|
getEntityWrites,
|
|
4213
5980
|
lifecyclePhaseForIntent,
|
|
4214
|
-
isMutationInputRef,
|
|
4215
|
-
isMutationFragment,
|
|
4216
|
-
isCommandPlan,
|
|
4217
|
-
mutation,
|
|
4218
|
-
definePlan,
|
|
4219
5981
|
OLD_VALUE_NAMESPACE,
|
|
4220
5982
|
EDGE_WRITES_MARKER,
|
|
4221
5983
|
isEdgeWritesDefinition,
|
|
@@ -4228,7 +5990,13 @@ export {
|
|
|
4228
5990
|
compileFragment,
|
|
4229
5991
|
compileSingleFragmentPlan,
|
|
4230
5992
|
compileMutationPlan,
|
|
4231
|
-
|
|
5993
|
+
executeCommandMethod,
|
|
5994
|
+
DDBModel,
|
|
5995
|
+
isMutationInputRef,
|
|
5996
|
+
isMutationFragment,
|
|
5997
|
+
isCommandPlan,
|
|
5998
|
+
mutation,
|
|
5999
|
+
definePlan,
|
|
4232
6000
|
isContractKeyRef,
|
|
4233
6001
|
mintContractKeyFieldRef,
|
|
4234
6002
|
wholeKeysSentinel,
|
|
@@ -4240,21 +6008,11 @@ export {
|
|
|
4240
6008
|
from,
|
|
4241
6009
|
query,
|
|
4242
6010
|
contractOfMethodSpec,
|
|
4243
|
-
recordContractOp,
|
|
4244
|
-
isRecordingContractMethod,
|
|
4245
|
-
point,
|
|
4246
|
-
range,
|
|
4247
6011
|
isQueryModelContract,
|
|
4248
6012
|
isCommandModelContract,
|
|
4249
6013
|
publicQueryModel,
|
|
4250
6014
|
publicCommandModel,
|
|
4251
6015
|
isPlannedCommandMethod,
|
|
4252
|
-
command,
|
|
4253
|
-
RELATION_TRAVERSAL_CONCURRENCY,
|
|
4254
|
-
mapWithConcurrency,
|
|
4255
|
-
buildRelationExecutionPlan,
|
|
4256
|
-
deriveCompositionPlan,
|
|
4257
|
-
stageOfResultPath,
|
|
4258
6016
|
assertSupportedCondition,
|
|
4259
6017
|
assertJsonSerializable,
|
|
4260
6018
|
assertBundleSerializable,
|