graphddb 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -11
- package/dist/{chunk-6LEHSX45.js → chunk-4B4MUPUJ.js} +3037 -1143
- package/dist/{chunk-347U24SB.js → chunk-PWV7JDMR.js} +353 -37
- package/dist/{chunk-UNRQ5YJT.js → chunk-SBNP62H7.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +61 -3334
- package/dist/index.js +187 -1881
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/types-DPJ4tPjX.d.ts +4607 -0
- package/package.json +1 -1
- package/dist/types-CDrWiPxp.d.ts +0 -1203
|
@@ -1,14 +1,38 @@
|
|
|
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
|
+
compileRawCondition,
|
|
17
|
+
createColumnMap,
|
|
18
|
+
execItemKeySignature,
|
|
19
|
+
executeDelete,
|
|
20
|
+
executePut,
|
|
21
|
+
executeTransaction,
|
|
22
|
+
executeUpdate,
|
|
23
|
+
isColumn,
|
|
24
|
+
isParam,
|
|
25
|
+
isRawCondition,
|
|
26
|
+
param,
|
|
6
27
|
pkTemplate,
|
|
7
28
|
resolveKey,
|
|
8
29
|
resolveModelClass,
|
|
30
|
+
resolveSegmentedKey,
|
|
9
31
|
segmentFieldNames,
|
|
32
|
+
serializeFieldValue,
|
|
33
|
+
serializeRawCondition,
|
|
10
34
|
skTemplate
|
|
11
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-PWV7JDMR.js";
|
|
12
36
|
|
|
13
37
|
// src/spec/types.ts
|
|
14
38
|
var SPEC_VERSION = "1.0";
|
|
@@ -217,305 +241,1451 @@ function getImplicitKeyFields(select, metadata) {
|
|
|
217
241
|
return [...needed];
|
|
218
242
|
}
|
|
219
243
|
|
|
220
|
-
// src/
|
|
221
|
-
function
|
|
222
|
-
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return makeParam("literal", values);
|
|
245
|
-
},
|
|
246
|
-
/**
|
|
247
|
-
* A placeholder for an **array** parameter whose elements have the given field
|
|
248
|
-
* shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
|
|
249
|
-
* each element's fields to `{item.<field>}` templates.
|
|
250
|
-
*
|
|
251
|
-
* @example
|
|
252
|
-
* ```ts
|
|
253
|
-
* param.array({ userId: param.string(), role: param.string() });
|
|
254
|
-
* // Param<{ userId: string; role: string }[]>
|
|
255
|
-
* ```
|
|
256
|
-
*/
|
|
257
|
-
array(element) {
|
|
258
|
-
return { kind: "array", element };
|
|
244
|
+
// src/planner/projection.ts
|
|
245
|
+
function buildProjection(select, additionalFields) {
|
|
246
|
+
const paths = [];
|
|
247
|
+
const names = {};
|
|
248
|
+
let nameCounter = 0;
|
|
249
|
+
function addPath(segments) {
|
|
250
|
+
const aliased = segments.map((seg) => {
|
|
251
|
+
const alias = `#p${nameCounter++}`;
|
|
252
|
+
names[alias] = seg;
|
|
253
|
+
return alias;
|
|
254
|
+
});
|
|
255
|
+
paths.push(aliased.join("."));
|
|
256
|
+
}
|
|
257
|
+
function traverse(selectObj, parentSegments) {
|
|
258
|
+
for (const [fieldName, value] of Object.entries(selectObj)) {
|
|
259
|
+
if (value === true) {
|
|
260
|
+
addPath([...parentSegments, fieldName]);
|
|
261
|
+
} else if (typeof value === "object" && value !== null) {
|
|
262
|
+
if ("select" in value || isSelectBuilder(value)) {
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
traverse(value, [...parentSegments, fieldName]);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
259
268
|
}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
269
|
+
traverse(select, []);
|
|
270
|
+
if (additionalFields) {
|
|
271
|
+
for (const fieldName of additionalFields) {
|
|
272
|
+
addPath([fieldName]);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (paths.length === 0) {
|
|
276
|
+
return void 0;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
projectionExpression: paths.join(", "),
|
|
280
|
+
expressionAttributeNames: names
|
|
281
|
+
};
|
|
263
282
|
}
|
|
264
283
|
|
|
265
|
-
// src/
|
|
266
|
-
var
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
284
|
+
// src/expression/filter-expression.ts
|
|
285
|
+
var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
286
|
+
var OPERATOR_KEYS = /* @__PURE__ */ new Set([
|
|
287
|
+
"eq",
|
|
288
|
+
"ne",
|
|
289
|
+
"gt",
|
|
290
|
+
"ge",
|
|
291
|
+
"lt",
|
|
292
|
+
"le",
|
|
293
|
+
"between",
|
|
294
|
+
"in",
|
|
295
|
+
"beginsWith",
|
|
296
|
+
"contains",
|
|
297
|
+
"notContains",
|
|
298
|
+
"attributeExists",
|
|
299
|
+
"attributeType",
|
|
300
|
+
"size"
|
|
301
|
+
]);
|
|
302
|
+
function nameAlias(ctx, column) {
|
|
303
|
+
for (const [alias2, col] of Object.entries(ctx.names)) {
|
|
304
|
+
if (col === column) return alias2;
|
|
305
|
+
}
|
|
306
|
+
const alias = `#f${ctx.nameCounter.n++}`;
|
|
307
|
+
ctx.names[alias] = column;
|
|
308
|
+
return alias;
|
|
309
|
+
}
|
|
310
|
+
function valueAlias(ctx, field, raw) {
|
|
311
|
+
const alias = `:vf${ctx.valueCounter.n++}`;
|
|
312
|
+
const fieldMeta = ctx.fieldMap.get(field);
|
|
313
|
+
ctx.values[alias] = fieldMeta ? serializeFieldValue(raw, fieldMeta) : raw;
|
|
314
|
+
return alias;
|
|
315
|
+
}
|
|
316
|
+
function compileField(ctx, field, condition) {
|
|
317
|
+
const nAlias = nameAlias(ctx, field);
|
|
318
|
+
if (!isOperatorObject(condition)) {
|
|
319
|
+
const vAlias = valueAlias(ctx, field, condition);
|
|
320
|
+
return `${nAlias} = ${vAlias}`;
|
|
321
|
+
}
|
|
322
|
+
const ops = condition;
|
|
323
|
+
const clauses = [];
|
|
324
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
325
|
+
switch (op) {
|
|
326
|
+
case "eq":
|
|
327
|
+
clauses.push(`${nAlias} = ${valueAlias(ctx, field, value)}`);
|
|
328
|
+
break;
|
|
329
|
+
case "ne":
|
|
330
|
+
clauses.push(`${nAlias} <> ${valueAlias(ctx, field, value)}`);
|
|
331
|
+
break;
|
|
332
|
+
case "gt":
|
|
333
|
+
clauses.push(`${nAlias} > ${valueAlias(ctx, field, value)}`);
|
|
334
|
+
break;
|
|
335
|
+
case "ge":
|
|
336
|
+
clauses.push(`${nAlias} >= ${valueAlias(ctx, field, value)}`);
|
|
337
|
+
break;
|
|
338
|
+
case "lt":
|
|
339
|
+
clauses.push(`${nAlias} < ${valueAlias(ctx, field, value)}`);
|
|
340
|
+
break;
|
|
341
|
+
case "le":
|
|
342
|
+
clauses.push(`${nAlias} <= ${valueAlias(ctx, field, value)}`);
|
|
343
|
+
break;
|
|
344
|
+
case "between": {
|
|
345
|
+
const [lo, hi] = value;
|
|
346
|
+
clauses.push(
|
|
347
|
+
`${nAlias} BETWEEN ${valueAlias(ctx, field, lo)} AND ${valueAlias(ctx, field, hi)}`
|
|
348
|
+
);
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "in": {
|
|
352
|
+
const arr = value;
|
|
353
|
+
const aliases = arr.map((v) => valueAlias(ctx, field, v));
|
|
354
|
+
clauses.push(`${nAlias} IN (${aliases.join(", ")})`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
case "beginsWith":
|
|
358
|
+
clauses.push(`begins_with(${nAlias}, ${valueAlias(ctx, field, value)})`);
|
|
359
|
+
break;
|
|
360
|
+
case "contains":
|
|
361
|
+
clauses.push(`contains(${nAlias}, ${valueAlias(ctx, field, value)})`);
|
|
362
|
+
break;
|
|
363
|
+
case "notContains":
|
|
364
|
+
clauses.push(
|
|
365
|
+
`NOT contains(${nAlias}, ${valueAlias(ctx, field, value)})`
|
|
366
|
+
);
|
|
367
|
+
break;
|
|
368
|
+
case "attributeExists":
|
|
369
|
+
clauses.push(
|
|
370
|
+
value === false ? `attribute_not_exists(${nAlias})` : `attribute_exists(${nAlias})`
|
|
371
|
+
);
|
|
372
|
+
break;
|
|
373
|
+
case "attributeType":
|
|
374
|
+
clauses.push(
|
|
375
|
+
`attribute_type(${nAlias}, ${valueAlias(ctx, field, value)})`
|
|
376
|
+
);
|
|
377
|
+
break;
|
|
378
|
+
case "size":
|
|
379
|
+
clauses.push(`size(${nAlias}) = ${valueAlias(ctx, field, value)}`);
|
|
380
|
+
break;
|
|
381
|
+
default:
|
|
382
|
+
throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return joinAnd(clauses);
|
|
271
386
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
387
|
+
function isOperatorObject(value) {
|
|
388
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array || value instanceof Set) {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
const keys = Object.keys(value);
|
|
392
|
+
if (keys.length === 0) return false;
|
|
393
|
+
return keys.every((k) => OPERATOR_KEYS.has(k));
|
|
275
394
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
280
|
-
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
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;
|
|
284
|
-
return out;
|
|
395
|
+
function joinAnd(clauses) {
|
|
396
|
+
if (clauses.length === 1) return clauses[0];
|
|
397
|
+
return clauses.map((c) => wrap(c)).join(" AND ");
|
|
285
398
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
effects: freezeEffects(effects)
|
|
291
|
-
};
|
|
292
|
-
},
|
|
293
|
-
exists(targetFactory, keys) {
|
|
294
|
-
return { kind: "requires", targetFactory, keys };
|
|
295
|
-
},
|
|
296
|
-
unique(spec) {
|
|
297
|
-
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
298
|
-
},
|
|
299
|
-
putEdge(targetFactory, relationProperty) {
|
|
300
|
-
return { kind: "putEdge", targetFactory, relationProperty };
|
|
301
|
-
},
|
|
302
|
-
deleteEdge(targetFactory, relationProperty) {
|
|
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 };
|
|
399
|
+
function wrap(expr) {
|
|
400
|
+
if (isAlreadyWrapped(expr)) return expr;
|
|
401
|
+
if (expr.includes(" AND ") || expr.includes(" OR ")) {
|
|
402
|
+
return `(${expr})`;
|
|
313
403
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
404
|
+
return expr;
|
|
405
|
+
}
|
|
406
|
+
function isAlreadyWrapped(expr) {
|
|
407
|
+
if (!expr.startsWith("(") || !expr.endsWith(")")) return false;
|
|
408
|
+
let depth = 0;
|
|
409
|
+
for (let i = 0; i < expr.length; i++) {
|
|
410
|
+
if (expr[i] === "(") depth++;
|
|
411
|
+
else if (expr[i] === ")") {
|
|
412
|
+
depth--;
|
|
413
|
+
if (depth === 0 && i < expr.length - 1) return false;
|
|
323
414
|
}
|
|
324
415
|
}
|
|
325
|
-
|
|
326
|
-
throw new Error(
|
|
327
|
-
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
328
|
-
);
|
|
329
|
-
}
|
|
330
|
-
return {
|
|
331
|
-
[ENTITY_WRITES_MARKER]: true,
|
|
332
|
-
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
333
|
-
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
334
|
-
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
335
|
-
};
|
|
416
|
+
return depth === 0;
|
|
336
417
|
}
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
418
|
+
function compileRaw(ctx, raw) {
|
|
419
|
+
return compileRawCondition(
|
|
420
|
+
raw,
|
|
421
|
+
(column) => nameAlias(ctx, column),
|
|
422
|
+
(value) => {
|
|
423
|
+
const alias = `:vf${ctx.valueCounter.n++}`;
|
|
424
|
+
ctx.values[alias] = value;
|
|
425
|
+
return alias;
|
|
426
|
+
}
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
function compileNode(ctx, node) {
|
|
430
|
+
if (isRawCondition(node)) {
|
|
431
|
+
return compileRaw(ctx, node);
|
|
432
|
+
}
|
|
433
|
+
const clauses = [];
|
|
434
|
+
for (const [key, value] of Object.entries(node)) {
|
|
435
|
+
if (value === void 0) continue;
|
|
436
|
+
if (LOGICAL_KEYS.has(key)) {
|
|
437
|
+
if (key === "and" || key === "or") {
|
|
438
|
+
const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
|
|
439
|
+
if (parts.length === 0) continue;
|
|
440
|
+
if (parts.length === 1) {
|
|
441
|
+
clauses.push(parts[0]);
|
|
442
|
+
} else {
|
|
443
|
+
const sep = key === "and" ? " AND " : " OR ";
|
|
444
|
+
const joined = parts.map((p) => wrap(p)).join(sep);
|
|
445
|
+
clauses.push(`(${joined})`);
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
const inner = compileNode(ctx, value);
|
|
449
|
+
if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
|
|
450
|
+
}
|
|
343
451
|
continue;
|
|
344
452
|
}
|
|
345
|
-
|
|
453
|
+
const clause = compileField(ctx, key, value);
|
|
454
|
+
if (clause.length > 0) clauses.push(clause);
|
|
346
455
|
}
|
|
347
|
-
return
|
|
348
|
-
}
|
|
349
|
-
function lifecyclePhaseForIntent(intent) {
|
|
350
|
-
return intent;
|
|
456
|
+
return joinAnd(clauses);
|
|
351
457
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
if (
|
|
365
|
-
if (ENTITY_REF_PREFIX_RE.test(leaf)) {
|
|
366
|
-
throw new Error(
|
|
367
|
-
`mutation: the input value ${JSON.stringify(leaf)} looks like a cross-fragment reference but is malformed. The only supported form is \`$.entity[<index>].<field>\` (e.g. \`$.entity[0].postId\`) \u2014 a single dotted field on a 0-based, non-negative fragment index. Fix the path or, if you meant a literal string, it must not begin with \`$.entity\`.`
|
|
368
|
-
);
|
|
369
|
-
}
|
|
458
|
+
function compileFilterExpression(filter, metadata) {
|
|
459
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
460
|
+
return void 0;
|
|
461
|
+
}
|
|
462
|
+
const ctx = {
|
|
463
|
+
names: {},
|
|
464
|
+
values: {},
|
|
465
|
+
fieldMap: new Map(metadata.fields.map((f) => [f.propertyName, f])),
|
|
466
|
+
nameCounter: { n: 0 },
|
|
467
|
+
valueCounter: { n: 0 }
|
|
468
|
+
};
|
|
469
|
+
const expression = compileNode(ctx, filter);
|
|
470
|
+
if (expression.length === 0) {
|
|
370
471
|
return void 0;
|
|
371
472
|
}
|
|
372
473
|
return {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
path: leaf
|
|
474
|
+
filterExpression: expression,
|
|
475
|
+
expressionAttributeNames: ctx.names,
|
|
476
|
+
expressionAttributeValues: ctx.values
|
|
377
477
|
};
|
|
378
478
|
}
|
|
379
|
-
function
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (prop === "field") return field;
|
|
387
|
-
if (prop === "token") return token;
|
|
388
|
-
if (typeof prop === "symbol") return void 0;
|
|
389
|
-
throw new Error(
|
|
390
|
-
`mutation: unsupported operation '${String(prop)}' on ${origin}. A fragment \`input\` binding only supports a *direct* \`$.field\` reference or a concrete literal \u2014 any transform, indexing, coercion, or concatenation on an input field is not representable as a declarative mutation.`
|
|
391
|
-
);
|
|
392
|
-
},
|
|
393
|
-
set() {
|
|
394
|
-
throw new Error(`mutation: cannot assign on ${origin}.`);
|
|
479
|
+
function evaluateFilter(item, filter) {
|
|
480
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
481
|
+
if (value === void 0) continue;
|
|
482
|
+
if (key === "and") {
|
|
483
|
+
if (!value.every((s) => evaluateFilter(item, s)))
|
|
484
|
+
return false;
|
|
485
|
+
continue;
|
|
395
486
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
487
|
+
if (key === "or") {
|
|
488
|
+
if (!value.some((s) => evaluateFilter(item, s)))
|
|
489
|
+
return false;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (key === "not") {
|
|
493
|
+
if (evaluateFilter(item, value)) return false;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (!evaluateFieldCondition(item[key], item, key, value)) {
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
function cmp(a, b) {
|
|
503
|
+
if (a instanceof Date && b instanceof Date) {
|
|
504
|
+
return a.getTime() - b.getTime();
|
|
505
|
+
}
|
|
506
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
507
|
+
if (typeof a === "string" && typeof b === "string")
|
|
508
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
509
|
+
if (a === b) return 0;
|
|
510
|
+
return a < b ? -1 : 1;
|
|
511
|
+
}
|
|
512
|
+
function eq(a, b) {
|
|
513
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
514
|
+
return a === b;
|
|
515
|
+
}
|
|
516
|
+
function evaluateFieldCondition(actual, item, field, condition) {
|
|
517
|
+
if (!isOperatorObject(condition)) {
|
|
518
|
+
return eq(actual, condition);
|
|
519
|
+
}
|
|
520
|
+
const ops = condition;
|
|
521
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
522
|
+
switch (op) {
|
|
523
|
+
case "eq":
|
|
524
|
+
if (!eq(actual, value)) return false;
|
|
525
|
+
break;
|
|
526
|
+
case "ne":
|
|
527
|
+
if (eq(actual, value)) return false;
|
|
528
|
+
break;
|
|
529
|
+
case "gt":
|
|
530
|
+
if (!(cmp(actual, value) > 0)) return false;
|
|
531
|
+
break;
|
|
532
|
+
case "ge":
|
|
533
|
+
if (!(cmp(actual, value) >= 0)) return false;
|
|
534
|
+
break;
|
|
535
|
+
case "lt":
|
|
536
|
+
if (!(cmp(actual, value) < 0)) return false;
|
|
537
|
+
break;
|
|
538
|
+
case "le":
|
|
539
|
+
if (!(cmp(actual, value) <= 0)) return false;
|
|
540
|
+
break;
|
|
541
|
+
case "between": {
|
|
542
|
+
const [lo, hi] = value;
|
|
543
|
+
if (!(cmp(actual, lo) >= 0 && cmp(actual, hi) <= 0)) return false;
|
|
544
|
+
break;
|
|
409
545
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
546
|
+
case "in":
|
|
547
|
+
if (!value.some((v) => eq(actual, v))) return false;
|
|
548
|
+
break;
|
|
549
|
+
case "beginsWith":
|
|
550
|
+
if (typeof actual !== "string" || !actual.startsWith(value))
|
|
551
|
+
return false;
|
|
552
|
+
break;
|
|
553
|
+
case "contains":
|
|
554
|
+
if (typeof actual !== "string" || !actual.includes(value))
|
|
555
|
+
return false;
|
|
556
|
+
break;
|
|
557
|
+
case "notContains":
|
|
558
|
+
if (typeof actual === "string" && actual.includes(value))
|
|
559
|
+
return false;
|
|
560
|
+
break;
|
|
561
|
+
case "attributeExists": {
|
|
562
|
+
const exists = field in item && actual !== void 0 && actual !== null;
|
|
563
|
+
if (value === false ? exists : !exists) return false;
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "attributeType":
|
|
567
|
+
break;
|
|
568
|
+
case "size": {
|
|
569
|
+
const len = typeof actual === "string" || Array.isArray(actual) ? actual.length : void 0;
|
|
570
|
+
if (len !== value) return false;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
default:
|
|
574
|
+
throw new Error(`Unknown filter operator '${op}' on field '${field}'`);
|
|
414
575
|
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
|
|
418
|
-
function isMutationFragment(value) {
|
|
419
|
-
return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
|
|
420
|
-
}
|
|
421
|
-
function targetEntityRef(target) {
|
|
422
|
-
const resolved = target();
|
|
423
|
-
const modelClass = resolveModelClass(resolved);
|
|
424
|
-
return { name: modelClass.name, modelClass };
|
|
576
|
+
}
|
|
577
|
+
return true;
|
|
425
578
|
}
|
|
426
|
-
|
|
427
|
-
|
|
579
|
+
|
|
580
|
+
// src/planner/planner.ts
|
|
581
|
+
function plan(metadata, input) {
|
|
582
|
+
const queryFields = Object.keys(input.key);
|
|
583
|
+
const resolved = resolveKey(queryFields, metadata);
|
|
584
|
+
if (input.consistentRead && resolved.type === "gsi") {
|
|
428
585
|
throw new Error(
|
|
429
|
-
|
|
586
|
+
"consistentRead is not supported for GSI queries. Use a primary key query instead."
|
|
430
587
|
);
|
|
431
588
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
589
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
590
|
+
const built = buildKeyCondition(metadata, resolved, input.key);
|
|
591
|
+
const keyCondition = built.keyCondition;
|
|
592
|
+
const additionalProjectionFields = input.updatable ? Array.from(
|
|
593
|
+
/* @__PURE__ */ new Set([...input.additionalProjectionFields ?? [], "PK", "SK"])
|
|
594
|
+
) : input.additionalProjectionFields;
|
|
595
|
+
const projection = buildProjection(input.select, additionalProjectionFields);
|
|
596
|
+
const compiledFilter = compileFilterExpression(input.filter, metadata);
|
|
597
|
+
const isGetItem = resolved.type === "pk" && !resolved.partial && built.skValue !== void 0 && // GetItem does not support FilterExpression — fall back to Query when a
|
|
598
|
+
// server-side filter is requested so the condition can be applied.
|
|
599
|
+
!compiledFilter;
|
|
600
|
+
const projectionFields2 = projection ? {
|
|
601
|
+
projectionExpression: projection.projectionExpression,
|
|
602
|
+
expressionAttributeNames: projection.expressionAttributeNames
|
|
603
|
+
} : {};
|
|
604
|
+
const filterFields = compiledFilter ? {
|
|
605
|
+
filterExpression: compiledFilter.filterExpression,
|
|
606
|
+
filterExpressionAttributeNames: compiledFilter.expressionAttributeNames,
|
|
607
|
+
filterExpressionAttributeValues: compiledFilter.expressionAttributeValues
|
|
608
|
+
} : {};
|
|
609
|
+
const consistentRead = input.consistentRead && resolved.type === "pk" ? { consistentRead: true } : {};
|
|
610
|
+
let op;
|
|
611
|
+
if (isGetItem) {
|
|
612
|
+
op = {
|
|
613
|
+
type: "GetItem",
|
|
614
|
+
tableName,
|
|
615
|
+
keyCondition,
|
|
616
|
+
...projectionFields2,
|
|
617
|
+
...consistentRead
|
|
618
|
+
};
|
|
619
|
+
} else {
|
|
620
|
+
const queryOp = {
|
|
621
|
+
type: "Query",
|
|
622
|
+
tableName,
|
|
623
|
+
keyCondition,
|
|
624
|
+
scanIndexForward: (input.order ?? "ASC") === "ASC",
|
|
625
|
+
...projectionFields2,
|
|
626
|
+
...filterFields,
|
|
627
|
+
...consistentRead
|
|
628
|
+
};
|
|
629
|
+
if (resolved.type === "gsi") {
|
|
630
|
+
queryOp.indexName = resolved.indexName;
|
|
631
|
+
}
|
|
632
|
+
if (built.rangeCondition) {
|
|
633
|
+
queryOp.rangeCondition = built.rangeCondition;
|
|
634
|
+
}
|
|
635
|
+
if (input.limit != null) {
|
|
636
|
+
queryOp.limit = input.limit;
|
|
637
|
+
}
|
|
638
|
+
if (input.after) {
|
|
639
|
+
queryOp.exclusiveStartKey = input.after;
|
|
437
640
|
}
|
|
641
|
+
op = queryOp;
|
|
438
642
|
}
|
|
439
|
-
return
|
|
440
|
-
}
|
|
441
|
-
function isConcreteLiteral(value) {
|
|
442
|
-
const t = typeof value;
|
|
443
|
-
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
643
|
+
return { operations: [op] };
|
|
444
644
|
}
|
|
445
|
-
function
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
645
|
+
function buildKeyCondition(metadata, resolved, queryKey) {
|
|
646
|
+
if (resolved.type === "pk") {
|
|
647
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
648
|
+
return buildFromSegments(
|
|
649
|
+
metadata.primaryKey.segmented,
|
|
650
|
+
metadata.primaryKey.inputFieldNames,
|
|
651
|
+
queryKey,
|
|
652
|
+
"PK",
|
|
653
|
+
"SK"
|
|
451
654
|
);
|
|
452
655
|
}
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
remove: (target, options) => makeFragment("remove", target, options)
|
|
465
|
-
};
|
|
466
|
-
var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
|
|
467
|
-
function isCommandPlan(value) {
|
|
468
|
-
return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
|
|
656
|
+
const gsiDef = metadata.gsiDefinitions.find(
|
|
657
|
+
(g) => g.indexName === resolved.indexName
|
|
658
|
+
);
|
|
659
|
+
if (!gsiDef) throw new Error(`GSI '${resolved.indexName}' not found`);
|
|
660
|
+
return buildFromSegments(
|
|
661
|
+
gsiDef.segmented,
|
|
662
|
+
gsiDef.inputFieldNames,
|
|
663
|
+
queryKey,
|
|
664
|
+
`${gsiDef.indexName}PK`,
|
|
665
|
+
`${gsiDef.indexName}SK`
|
|
666
|
+
);
|
|
469
667
|
}
|
|
470
|
-
function
|
|
471
|
-
|
|
472
|
-
|
|
668
|
+
function buildFromSegments(segmented, inputFieldNames, queryKey, pkAttr, skAttr) {
|
|
669
|
+
const keyInput = {};
|
|
670
|
+
for (const fieldName of inputFieldNames) {
|
|
671
|
+
keyInput[fieldName] = queryKey[fieldName];
|
|
473
672
|
}
|
|
474
|
-
const
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
477
|
-
|
|
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
|
-
);
|
|
673
|
+
const { pk, sk, skTruncated } = resolveSegmentedKey(segmented, keyInput);
|
|
674
|
+
const keyCondition = { [pkAttr]: pk };
|
|
675
|
+
if (sk === void 0) {
|
|
676
|
+
return { keyCondition };
|
|
480
677
|
}
|
|
481
|
-
if (
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
678
|
+
if (skTruncated) {
|
|
679
|
+
return {
|
|
680
|
+
keyCondition,
|
|
681
|
+
rangeCondition: { operator: "begins_with", key: skAttr, value: sk }
|
|
682
|
+
};
|
|
485
683
|
}
|
|
486
|
-
|
|
487
|
-
|
|
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 };
|
|
684
|
+
keyCondition[skAttr] = sk;
|
|
685
|
+
return { keyCondition, skValue: sk };
|
|
494
686
|
}
|
|
495
|
-
var definePlan = mutation;
|
|
496
687
|
|
|
497
|
-
// src/
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
function isEdgeWritesDefinition(value) {
|
|
501
|
-
return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
|
|
688
|
+
// src/executor/executor.ts
|
|
689
|
+
async function execute(operation) {
|
|
690
|
+
return ClientManager.getExecutor().execute(operation);
|
|
502
691
|
}
|
|
503
|
-
|
|
504
|
-
|
|
692
|
+
|
|
693
|
+
// src/hydrator/hydrator.ts
|
|
694
|
+
function hydrate(rawItems, select, metadata, updatable = false) {
|
|
695
|
+
return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
|
|
505
696
|
}
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
697
|
+
function hydrateItem(raw, select, metadata, updatable = false) {
|
|
698
|
+
const fieldMap = new Map(
|
|
699
|
+
metadata.fields.map((f) => [f.propertyName, f])
|
|
700
|
+
);
|
|
701
|
+
const embeddedMap = new Map(
|
|
702
|
+
metadata.embeddedFields.map((e) => [e.propertyName, e])
|
|
703
|
+
);
|
|
704
|
+
const result = {};
|
|
705
|
+
for (const [fieldName, selectValue] of Object.entries(select)) {
|
|
706
|
+
if (selectValue === true) {
|
|
707
|
+
const value = raw[fieldName];
|
|
708
|
+
if (value !== void 0) {
|
|
709
|
+
const fieldMeta = fieldMap.get(fieldName);
|
|
710
|
+
result[fieldName] = deserializeValue(value, fieldMeta);
|
|
711
|
+
}
|
|
712
|
+
} else if (typeof selectValue === "object" && selectValue !== null) {
|
|
713
|
+
if ("select" in selectValue || isSelectBuilder(selectValue)) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
const rawEmb = raw[fieldName];
|
|
717
|
+
if (rawEmb && typeof rawEmb === "object") {
|
|
718
|
+
result[fieldName] = hydrateEmbedded(
|
|
719
|
+
rawEmb,
|
|
720
|
+
selectValue,
|
|
721
|
+
metadata,
|
|
722
|
+
fieldName
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (updatable) {
|
|
728
|
+
attachHiddenKey(result, raw);
|
|
729
|
+
}
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
733
|
+
const result = {};
|
|
734
|
+
for (const [key, selectValue] of Object.entries(select)) {
|
|
735
|
+
if (selectValue === true) {
|
|
736
|
+
if (raw[key] !== void 0) {
|
|
737
|
+
result[key] = raw[key];
|
|
738
|
+
}
|
|
739
|
+
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
740
|
+
const nested = raw[key];
|
|
741
|
+
if (nested && typeof nested === "object") {
|
|
742
|
+
result[key] = hydrateEmbedded(
|
|
743
|
+
nested,
|
|
744
|
+
selectValue,
|
|
745
|
+
_metadata,
|
|
746
|
+
key
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
return result;
|
|
752
|
+
}
|
|
753
|
+
function deserializeValue(value, fieldMeta) {
|
|
754
|
+
if (!fieldMeta) return value;
|
|
755
|
+
if (fieldMeta.options?.deserialize) {
|
|
756
|
+
return fieldMeta.options.deserialize(value);
|
|
757
|
+
}
|
|
758
|
+
if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
|
|
759
|
+
return new Date(value);
|
|
760
|
+
}
|
|
761
|
+
if (fieldMeta.options?.format === "date" && typeof value === "string") {
|
|
762
|
+
return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
|
|
763
|
+
}
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/pagination/cursor.ts
|
|
768
|
+
function encodeCursor(lastEvaluatedKey) {
|
|
769
|
+
const json = JSON.stringify(lastEvaluatedKey);
|
|
770
|
+
const bytes = new TextEncoder().encode(json);
|
|
771
|
+
let binary = "";
|
|
772
|
+
for (const b of bytes) {
|
|
773
|
+
binary += String.fromCharCode(b);
|
|
774
|
+
}
|
|
775
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
776
|
+
}
|
|
777
|
+
function decodeCursor(cursor) {
|
|
778
|
+
let base64 = cursor.replace(/-/g, "+").replace(/_/g, "/");
|
|
779
|
+
while (base64.length % 4 !== 0) {
|
|
780
|
+
base64 += "=";
|
|
781
|
+
}
|
|
782
|
+
const binary = atob(base64);
|
|
783
|
+
const bytes = new Uint8Array(binary.length);
|
|
784
|
+
for (let i = 0; i < binary.length; i++) {
|
|
785
|
+
bytes[i] = binary.charCodeAt(i);
|
|
786
|
+
}
|
|
787
|
+
const json = new TextDecoder().decode(bytes);
|
|
788
|
+
return JSON.parse(json);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/operations/list.ts
|
|
792
|
+
async function executeListInternal(modelClass, key, selectSpec, options = {}) {
|
|
793
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
794
|
+
const normalized = normalizeTopLevelSelect(selectSpec);
|
|
795
|
+
const select = normalized.select;
|
|
796
|
+
const filter = options.filter ?? normalized.filter;
|
|
797
|
+
const after = options.after ?? normalized.after;
|
|
798
|
+
const limit = options.limit ?? normalized.limit;
|
|
799
|
+
const order = options.order ?? normalized.order;
|
|
800
|
+
const exclusiveStartKey = after ? decodeCursor(after) : void 0;
|
|
801
|
+
const implicitKeys = getImplicitKeyFields(select, metadata);
|
|
802
|
+
const executionPlan = plan(metadata, {
|
|
803
|
+
key,
|
|
804
|
+
select,
|
|
805
|
+
limit,
|
|
806
|
+
after: exclusiveStartKey,
|
|
807
|
+
order,
|
|
808
|
+
filter,
|
|
809
|
+
additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
|
|
810
|
+
updatable: options.updatable ?? false
|
|
811
|
+
});
|
|
812
|
+
const operation = executionPlan.operations[0];
|
|
813
|
+
const result = await execute(operation);
|
|
814
|
+
const rawItems = result.items;
|
|
815
|
+
const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
|
|
816
|
+
const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
|
|
817
|
+
return { items: hydrated, rawItems, cursor };
|
|
818
|
+
}
|
|
819
|
+
async function executeList(modelClass, key, selectSpec, options = {}) {
|
|
820
|
+
const { items, cursor } = await executeListInternal(
|
|
821
|
+
modelClass,
|
|
822
|
+
key,
|
|
823
|
+
selectSpec,
|
|
824
|
+
options
|
|
825
|
+
);
|
|
826
|
+
return { items, cursor };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// src/planner/relation-planner.ts
|
|
830
|
+
function buildRelationQueryKey(rel, rawParentItem) {
|
|
831
|
+
const queryKey = {};
|
|
832
|
+
for (const [targetField, sourceField] of Object.entries(rel.keyBinding)) {
|
|
833
|
+
queryKey[targetField] = rawParentItem[sourceField];
|
|
834
|
+
}
|
|
835
|
+
return queryKey;
|
|
836
|
+
}
|
|
837
|
+
function serializeQueryKey(queryKey) {
|
|
838
|
+
const entries = Object.entries(queryKey).sort(([a], [b]) => a.localeCompare(b));
|
|
839
|
+
return JSON.stringify(entries);
|
|
840
|
+
}
|
|
841
|
+
function planGetItemForQueryKey(metadata, queryKey, select, additionalProjectionFields) {
|
|
842
|
+
const executionPlan = plan(metadata, {
|
|
843
|
+
key: queryKey,
|
|
844
|
+
select,
|
|
845
|
+
additionalProjectionFields
|
|
846
|
+
});
|
|
847
|
+
const operation = executionPlan.operations[0];
|
|
848
|
+
if (operation.type !== "GetItem") {
|
|
849
|
+
throw new Error(
|
|
850
|
+
`Relation target requires GetItem access pattern, got ${operation.type}`
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
return operation;
|
|
854
|
+
}
|
|
855
|
+
function planBatchGetForQueryKeys(metadata, queryKeys, select, additionalProjectionFields) {
|
|
856
|
+
if (queryKeys.length === 0) {
|
|
857
|
+
return [];
|
|
858
|
+
}
|
|
859
|
+
const keyFields = [
|
|
860
|
+
...new Set(queryKeys.flatMap((queryKey) => Object.keys(queryKey)))
|
|
861
|
+
].filter((field) => !(field in select && select[field] === true));
|
|
862
|
+
const projectionFields2 = [
|
|
863
|
+
.../* @__PURE__ */ new Set([
|
|
864
|
+
...(additionalProjectionFields ?? []).filter(
|
|
865
|
+
(field) => !(field in select && select[field] === true)
|
|
866
|
+
),
|
|
867
|
+
...keyFields
|
|
868
|
+
])
|
|
869
|
+
];
|
|
870
|
+
const samplePlan = planGetItemForQueryKey(
|
|
871
|
+
metadata,
|
|
872
|
+
queryKeys[0],
|
|
873
|
+
select,
|
|
874
|
+
projectionFields2.length > 0 ? projectionFields2 : void 0
|
|
875
|
+
);
|
|
876
|
+
const uniqueKeys = dedupeDynamoKeys(
|
|
877
|
+
queryKeys.map(
|
|
878
|
+
(queryKey) => planGetItemForQueryKey(
|
|
879
|
+
metadata,
|
|
880
|
+
queryKey,
|
|
881
|
+
select,
|
|
882
|
+
projectionFields2.length > 0 ? projectionFields2 : void 0
|
|
883
|
+
).keyCondition
|
|
884
|
+
)
|
|
885
|
+
);
|
|
886
|
+
const operations = [];
|
|
887
|
+
for (let i = 0; i < uniqueKeys.length; i += BATCH_GET_MAX_KEYS) {
|
|
888
|
+
operations.push({
|
|
889
|
+
type: "BatchGetItem",
|
|
890
|
+
tableName: samplePlan.tableName,
|
|
891
|
+
keys: uniqueKeys.slice(i, i + BATCH_GET_MAX_KEYS),
|
|
892
|
+
...samplePlan.projectionExpression ? { projectionExpression: samplePlan.projectionExpression } : {},
|
|
893
|
+
...samplePlan.expressionAttributeNames ? { expressionAttributeNames: samplePlan.expressionAttributeNames } : {},
|
|
894
|
+
...samplePlan.consistentRead ? { consistentRead: true } : {}
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
return operations;
|
|
898
|
+
}
|
|
899
|
+
function dedupeDynamoKeys(keys) {
|
|
900
|
+
const seen = /* @__PURE__ */ new Set();
|
|
901
|
+
const result = [];
|
|
902
|
+
for (const key of keys) {
|
|
903
|
+
const serialized = JSON.stringify(
|
|
904
|
+
Object.entries(key).sort(([a], [b]) => a.localeCompare(b))
|
|
905
|
+
);
|
|
906
|
+
if (seen.has(serialized)) continue;
|
|
907
|
+
seen.add(serialized);
|
|
908
|
+
result.push(key);
|
|
909
|
+
}
|
|
910
|
+
return result;
|
|
911
|
+
}
|
|
912
|
+
function planRelationOperations(metadata, select, context = { estimatedParentCount: 1 }) {
|
|
913
|
+
const operations = [];
|
|
914
|
+
const relations = detectRelationFields(select, metadata);
|
|
915
|
+
for (const rel of relations) {
|
|
916
|
+
const selectSpec = normalizeSelectSpec(select[rel.propertyName]);
|
|
917
|
+
const targetClass = rel.targetFactory();
|
|
918
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
919
|
+
if (rel.type === "hasMany") {
|
|
920
|
+
const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
|
|
921
|
+
const tableName = TableMapping.resolve(targetMeta.tableName);
|
|
922
|
+
const listPlan = plan(targetMeta, {
|
|
923
|
+
key: buildPlaceholderHasManyKey(rel, targetMeta),
|
|
924
|
+
select: selectSpec.select,
|
|
925
|
+
limit
|
|
926
|
+
});
|
|
927
|
+
operations.push(...listPlan.operations);
|
|
928
|
+
operations.push(
|
|
929
|
+
...planRelationOperations(targetMeta, selectSpec.select, {
|
|
930
|
+
estimatedParentCount: limit
|
|
931
|
+
})
|
|
932
|
+
);
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const estimatedCount = context.estimatedParentCount ?? 1;
|
|
936
|
+
operations.push(
|
|
937
|
+
...planBatchGetForExplain(targetMeta, selectSpec.select, estimatedCount)
|
|
938
|
+
);
|
|
939
|
+
operations.push(
|
|
940
|
+
...planRelationOperations(targetMeta, selectSpec.select, {
|
|
941
|
+
estimatedParentCount: 1
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return operations;
|
|
946
|
+
}
|
|
947
|
+
function buildPlaceholderHasManyKey(rel, targetMeta) {
|
|
948
|
+
const queryKey = {};
|
|
949
|
+
for (const targetField of Object.keys(rel.keyBinding)) {
|
|
950
|
+
queryKey[targetField] = `__${targetField}__`;
|
|
951
|
+
}
|
|
952
|
+
if (targetMeta.primaryKey) {
|
|
953
|
+
for (const fieldName of targetMeta.primaryKey.inputFieldNames) {
|
|
954
|
+
if (!(fieldName in queryKey)) {
|
|
955
|
+
queryKey[fieldName] = `__${fieldName}__`;
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return queryKey;
|
|
960
|
+
}
|
|
961
|
+
function planBatchGetForExplain(metadata, select, estimatedKeyCount) {
|
|
962
|
+
const placeholderKeys = Array.from(
|
|
963
|
+
{ length: estimatedKeyCount },
|
|
964
|
+
(_, index) => buildExplainPlaceholderKey(metadata, index)
|
|
965
|
+
);
|
|
966
|
+
return planBatchGetForQueryKeys(metadata, placeholderKeys, select);
|
|
967
|
+
}
|
|
968
|
+
function buildExplainPlaceholderKey(metadata, index) {
|
|
969
|
+
const queryKey = {};
|
|
970
|
+
if (metadata.primaryKey) {
|
|
971
|
+
for (const fieldName of metadata.primaryKey.inputFieldNames) {
|
|
972
|
+
queryKey[fieldName] = `__explain_${fieldName}_${index}__`;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return queryKey;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// src/relation/concurrency.ts
|
|
979
|
+
var RELATION_TRAVERSAL_CONCURRENCY = 16;
|
|
980
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
981
|
+
if (items.length === 0) {
|
|
982
|
+
return [];
|
|
983
|
+
}
|
|
984
|
+
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
985
|
+
if (effectiveLimit >= items.length) {
|
|
986
|
+
return Promise.all(items.map((item, index) => worker(item, index)));
|
|
987
|
+
}
|
|
988
|
+
const results = new Array(items.length);
|
|
989
|
+
let nextIndex = 0;
|
|
990
|
+
async function runWorker() {
|
|
991
|
+
while (true) {
|
|
992
|
+
const index = nextIndex++;
|
|
993
|
+
if (index >= items.length) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
results[index] = await worker(items[index], index);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const runners = [];
|
|
1000
|
+
for (let i = 0; i < effectiveLimit; i++) {
|
|
1001
|
+
runners.push(runWorker());
|
|
1002
|
+
}
|
|
1003
|
+
await Promise.all(runners);
|
|
1004
|
+
return results;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/relation/execution-plan.ts
|
|
1008
|
+
function relationResultPaths(select, metadata, parentPath = "$") {
|
|
1009
|
+
const out = [];
|
|
1010
|
+
const relations = detectRelationFields(select, metadata);
|
|
1011
|
+
for (const rel of [...relations].sort(
|
|
1012
|
+
(a, b) => a.propertyName.localeCompare(b.propertyName)
|
|
1013
|
+
)) {
|
|
1014
|
+
const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
|
|
1015
|
+
const targetMeta = MetadataRegistry.get(rel.targetFactory());
|
|
1016
|
+
const childPath = `${parentPath}.${rel.propertyName}`;
|
|
1017
|
+
const isMany = rel.type === "hasMany";
|
|
1018
|
+
const resultPath = isMany ? `${childPath}.items` : childPath;
|
|
1019
|
+
out.push({
|
|
1020
|
+
propertyName: rel.propertyName,
|
|
1021
|
+
parentPath,
|
|
1022
|
+
resultPath,
|
|
1023
|
+
isMany
|
|
1024
|
+
});
|
|
1025
|
+
out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
|
|
1026
|
+
}
|
|
1027
|
+
return out;
|
|
1028
|
+
}
|
|
1029
|
+
function parentResultPath(resultPath) {
|
|
1030
|
+
if (!resultPath.startsWith("$.")) return "$";
|
|
1031
|
+
const tokens = resultPath.slice(2).split(".");
|
|
1032
|
+
if (tokens[tokens.length - 1] === "items") tokens.pop();
|
|
1033
|
+
tokens.pop();
|
|
1034
|
+
return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
|
|
1035
|
+
}
|
|
1036
|
+
function deriveExecutionPlan(resultPaths) {
|
|
1037
|
+
if (resultPaths.length <= 1) return void 0;
|
|
1038
|
+
const indexByPath = /* @__PURE__ */ new Map();
|
|
1039
|
+
resultPaths.forEach((path, i) => {
|
|
1040
|
+
indexByPath.set(path, i);
|
|
1041
|
+
});
|
|
1042
|
+
const stageOf = new Array(resultPaths.length);
|
|
1043
|
+
for (let i = 0; i < resultPaths.length; i++) {
|
|
1044
|
+
if (resultPaths[i] === "$") {
|
|
1045
|
+
stageOf[i] = 0;
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
const parentPath = parentResultPath(resultPaths[i]);
|
|
1049
|
+
const parentIndex = indexByPath.get(parentPath);
|
|
1050
|
+
const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
|
|
1051
|
+
stageOf[i] = parentStage + 1;
|
|
1052
|
+
}
|
|
1053
|
+
const stageCount = Math.max(...stageOf) + 1;
|
|
1054
|
+
const groups = Array.from({ length: stageCount }, () => []);
|
|
1055
|
+
for (let i = 0; i < resultPaths.length; i++) {
|
|
1056
|
+
groups[stageOf[i]].push(i);
|
|
1057
|
+
}
|
|
1058
|
+
for (const g of groups) g.sort((a, b) => a - b);
|
|
1059
|
+
return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1060
|
+
}
|
|
1061
|
+
function buildRelationExecutionPlan(select, metadata) {
|
|
1062
|
+
const relationPaths = relationResultPaths(select, metadata);
|
|
1063
|
+
if (relationPaths.length === 0) return void 0;
|
|
1064
|
+
const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
|
|
1065
|
+
const plan2 = deriveExecutionPlan(resultPaths);
|
|
1066
|
+
if (plan2 === void 0) return void 0;
|
|
1067
|
+
return { plan: plan2, resultPaths };
|
|
1068
|
+
}
|
|
1069
|
+
function deriveCompositionPlan(composeCount) {
|
|
1070
|
+
if (composeCount <= 0) {
|
|
1071
|
+
return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1072
|
+
}
|
|
1073
|
+
const stage = Array.from({ length: composeCount }, (_unused, i) => i);
|
|
1074
|
+
return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1075
|
+
}
|
|
1076
|
+
function stageOfResultPath(resolved, resultPath) {
|
|
1077
|
+
const index = resolved.resultPaths.indexOf(resultPath);
|
|
1078
|
+
if (index < 0) return void 0;
|
|
1079
|
+
for (let s = 0; s < resolved.plan.groups.length; s++) {
|
|
1080
|
+
if (resolved.plan.groups[s].includes(index)) return s;
|
|
1081
|
+
}
|
|
1082
|
+
return void 0;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/relation/traversal.ts
|
|
1086
|
+
function evaluateFilterClientSide(item, filter, _targetMeta) {
|
|
1087
|
+
return evaluateFilter(item, filter);
|
|
1088
|
+
}
|
|
1089
|
+
function relationSpec(value) {
|
|
1090
|
+
return normalizeSelectSpec(value);
|
|
1091
|
+
}
|
|
1092
|
+
function validateDepth(select, metadata, maxDepth, currentDepth = 1) {
|
|
1093
|
+
for (const [fieldName, value] of Object.entries(select)) {
|
|
1094
|
+
if (typeof value === "object" && value !== null && ("select" in value || isSelectBuilder(value))) {
|
|
1095
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
1096
|
+
if (!rel) continue;
|
|
1097
|
+
if (currentDepth > maxDepth) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Relation traversal depth exceeded: '${fieldName}' at depth ${currentDepth} exceeds maxDepth ${maxDepth}. Set maxDepth: ${currentDepth} to allow this.`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const targetClass = rel.targetFactory();
|
|
1103
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1104
|
+
const innerSelect = relationSpec(value).select;
|
|
1105
|
+
validateDepth(innerSelect, targetMeta, maxDepth, currentDepth + 1);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
function relationResultPath(parentPath, rel) {
|
|
1110
|
+
const base = `${parentPath}.${rel.propertyName}`;
|
|
1111
|
+
return rel.type === "hasMany" ? `${base}.items` : base;
|
|
1112
|
+
}
|
|
1113
|
+
function stageRelations(relations, parentPath, plan2) {
|
|
1114
|
+
if (plan2 === void 0 || relations.length === 0) {
|
|
1115
|
+
return relations.length > 0 ? { stages: [[...relations]], concurrency: RELATION_TRAVERSAL_CONCURRENCY } : { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1116
|
+
}
|
|
1117
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
1118
|
+
const unstaged = [];
|
|
1119
|
+
for (const rel of relations) {
|
|
1120
|
+
const stage = stageOfResultPath(plan2, relationResultPath(parentPath, rel));
|
|
1121
|
+
if (stage === void 0) {
|
|
1122
|
+
unstaged.push(rel);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
const bucket = byStage.get(stage);
|
|
1126
|
+
if (bucket) bucket.push(rel);
|
|
1127
|
+
else byStage.set(stage, [rel]);
|
|
1128
|
+
}
|
|
1129
|
+
const stages = [...byStage.keys()].sort((a, b) => a - b).map((s) => byStage.get(s));
|
|
1130
|
+
if (unstaged.length > 0) stages.push(unstaged);
|
|
1131
|
+
return { stages, concurrency: plan2.plan.concurrency };
|
|
1132
|
+
}
|
|
1133
|
+
async function runStaged(relations, parentPath, plan2, worker) {
|
|
1134
|
+
const { stages, concurrency } = stageRelations(relations, parentPath, plan2);
|
|
1135
|
+
for (const stage of stages) {
|
|
1136
|
+
await mapWithConcurrency(stage, concurrency, (rel) => worker(rel));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
function hasCompleteQueryKey(queryKey) {
|
|
1140
|
+
return Object.values(queryKey).every((value) => value != null);
|
|
1141
|
+
}
|
|
1142
|
+
function matchesQueryKey(item, queryKey) {
|
|
1143
|
+
return Object.entries(queryKey).every(
|
|
1144
|
+
([field, value]) => item[field] === value
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, targetMeta, options, currentDepth, parentPath) {
|
|
1148
|
+
const results = rawParentItems.map(
|
|
1149
|
+
() => null
|
|
1150
|
+
);
|
|
1151
|
+
const queryKeys = [];
|
|
1152
|
+
const parentIndexes = [];
|
|
1153
|
+
for (let i = 0; i < rawParentItems.length; i++) {
|
|
1154
|
+
const queryKey = buildRelationQueryKey(rel, rawParentItems[i]);
|
|
1155
|
+
if (!hasCompleteQueryKey(queryKey)) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
queryKeys.push(queryKey);
|
|
1159
|
+
parentIndexes.push(i);
|
|
1160
|
+
}
|
|
1161
|
+
if (queryKeys.length === 0) {
|
|
1162
|
+
return results;
|
|
1163
|
+
}
|
|
1164
|
+
const implicitKeys = getImplicitKeyFields(selectSpec.select, targetMeta);
|
|
1165
|
+
const projectionExtras = options.updatable ? [...implicitKeys, "PK", "SK"] : implicitKeys;
|
|
1166
|
+
const batchOps = planBatchGetForQueryKeys(
|
|
1167
|
+
targetMeta,
|
|
1168
|
+
queryKeys,
|
|
1169
|
+
selectSpec.select,
|
|
1170
|
+
projectionExtras.length > 0 ? projectionExtras : void 0
|
|
1171
|
+
);
|
|
1172
|
+
const executor = ClientManager.getExecutor();
|
|
1173
|
+
const queryKeyToRaw = /* @__PURE__ */ new Map();
|
|
1174
|
+
for (const operation of batchOps) {
|
|
1175
|
+
const batchResult = await executor.batchGet(operation);
|
|
1176
|
+
for (const item of batchResult.items) {
|
|
1177
|
+
for (const queryKey of queryKeys) {
|
|
1178
|
+
if (!matchesQueryKey(item, queryKey)) {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
queryKeyToRaw.set(serializeQueryKey(queryKey), item);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
const nestedRelations = detectRelationFields(selectSpec.select, targetMeta);
|
|
1186
|
+
const ownPath = relationResultPath(parentPath, rel);
|
|
1187
|
+
await mapWithConcurrency(
|
|
1188
|
+
queryKeys,
|
|
1189
|
+
options.plan?.plan.concurrency ?? RELATION_TRAVERSAL_CONCURRENCY,
|
|
1190
|
+
async (queryKey, j) => {
|
|
1191
|
+
const parentIndex = parentIndexes[j];
|
|
1192
|
+
const raw = queryKeyToRaw.get(serializeQueryKey(queryKey));
|
|
1193
|
+
if (!raw) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const updatable = options.updatable ?? false;
|
|
1197
|
+
let resolvedItem = hydrate([raw], selectSpec.select, targetMeta, updatable)[0] ?? null;
|
|
1198
|
+
if (resolvedItem && nestedRelations.length > 0) {
|
|
1199
|
+
resolvedItem = await resolveRelations(
|
|
1200
|
+
resolvedItem,
|
|
1201
|
+
raw,
|
|
1202
|
+
selectSpec.select,
|
|
1203
|
+
targetMeta,
|
|
1204
|
+
options,
|
|
1205
|
+
currentDepth + 1,
|
|
1206
|
+
ownPath
|
|
1207
|
+
);
|
|
1208
|
+
if (resolvedItem && updatable) {
|
|
1209
|
+
attachHiddenKey(resolvedItem, raw);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (resolvedItem && !passesPostLoad(resolvedItem, selectSpec, targetMeta)) {
|
|
1213
|
+
results[parentIndex] = null;
|
|
1214
|
+
} else {
|
|
1215
|
+
results[parentIndex] = resolvedItem;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
);
|
|
1219
|
+
return results;
|
|
1220
|
+
}
|
|
1221
|
+
function passesPostLoad(item, spec, targetMeta) {
|
|
1222
|
+
if (spec.filter && !evaluateFilterClientSide(item, spec.filter, targetMeta)) {
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
async function resolveSingleValueRelationsBatch(parentItems, rawParentItems, select, metadata, options, currentDepth, parentPath) {
|
|
1228
|
+
const results = parentItems.map((item, i) => {
|
|
1229
|
+
const copy = { ...item };
|
|
1230
|
+
if (options.updatable) {
|
|
1231
|
+
attachHiddenKey(copy, rawParentItems[i]);
|
|
1232
|
+
}
|
|
1233
|
+
return copy;
|
|
1234
|
+
});
|
|
1235
|
+
const relations = detectRelationFields(select, metadata).filter(
|
|
1236
|
+
(rel) => rel.type === "belongsTo" || rel.type === "hasOne"
|
|
1237
|
+
);
|
|
1238
|
+
await runStaged(relations, parentPath, options.plan, async (rel) => {
|
|
1239
|
+
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1240
|
+
const targetClass = rel.targetFactory();
|
|
1241
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1242
|
+
const batchResults = await fetchBelongsToHasOneBatch(
|
|
1243
|
+
rel,
|
|
1244
|
+
rawParentItems,
|
|
1245
|
+
selectSpec,
|
|
1246
|
+
targetMeta,
|
|
1247
|
+
options,
|
|
1248
|
+
currentDepth,
|
|
1249
|
+
parentPath
|
|
1250
|
+
);
|
|
1251
|
+
for (let i = 0; i < results.length; i++) {
|
|
1252
|
+
results[i][rel.propertyName] = batchResults[i];
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
return results;
|
|
1256
|
+
}
|
|
1257
|
+
async function resolveRelations(parentItem, rawParentItem, select, metadata, options, currentDepth = 1, currentPath = "$") {
|
|
1258
|
+
const result = { ...parentItem };
|
|
1259
|
+
const relations = detectRelationFields(select, metadata);
|
|
1260
|
+
const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
|
|
1261
|
+
const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
|
|
1262
|
+
await runStaged(relations, currentPath, opts.plan, async (rel) => {
|
|
1263
|
+
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1264
|
+
const ownPath = relationResultPath(currentPath, rel);
|
|
1265
|
+
if (rel.type === "hasMany") {
|
|
1266
|
+
const targetClass2 = rel.targetFactory();
|
|
1267
|
+
const queryKey = buildRelationQueryKey(rel, rawParentItem);
|
|
1268
|
+
const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
|
|
1269
|
+
const order = selectSpec.order ?? rel.options?.order ?? "ASC";
|
|
1270
|
+
const listResult = await executeListInternal(targetClass2, queryKey, selectSpec.select, {
|
|
1271
|
+
limit,
|
|
1272
|
+
after: selectSpec.after,
|
|
1273
|
+
order,
|
|
1274
|
+
filter: selectSpec.filter,
|
|
1275
|
+
updatable: opts.updatable
|
|
1276
|
+
});
|
|
1277
|
+
let items = listResult.items;
|
|
1278
|
+
const rawItems = listResult.rawItems;
|
|
1279
|
+
const targetMeta2 = MetadataRegistry.get(targetClass2);
|
|
1280
|
+
const nestedRelations = detectRelationFields(selectSpec.select, targetMeta2);
|
|
1281
|
+
if (nestedRelations.length > 0) {
|
|
1282
|
+
items = await resolveSingleValueRelationsBatch(
|
|
1283
|
+
items,
|
|
1284
|
+
rawItems,
|
|
1285
|
+
selectSpec.select,
|
|
1286
|
+
targetMeta2,
|
|
1287
|
+
opts,
|
|
1288
|
+
currentDepth + 1,
|
|
1289
|
+
ownPath
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
result[rel.propertyName] = {
|
|
1293
|
+
items,
|
|
1294
|
+
cursor: listResult.cursor
|
|
1295
|
+
};
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const targetClass = rel.targetFactory();
|
|
1299
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1300
|
+
const [resolvedItem] = await fetchBelongsToHasOneBatch(
|
|
1301
|
+
rel,
|
|
1302
|
+
[rawParentItem],
|
|
1303
|
+
selectSpec,
|
|
1304
|
+
targetMeta,
|
|
1305
|
+
opts,
|
|
1306
|
+
currentDepth,
|
|
1307
|
+
currentPath
|
|
1308
|
+
);
|
|
1309
|
+
result[rel.propertyName] = resolvedItem;
|
|
1310
|
+
});
|
|
1311
|
+
return result;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/operations/query.ts
|
|
1315
|
+
async function executeQuery(modelClass, key, selectSpec, options) {
|
|
1316
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1317
|
+
const queryFields = Object.keys(key);
|
|
1318
|
+
const resolved = resolveKey(queryFields, metadata);
|
|
1319
|
+
if (resolved.type === "gsi" && !resolved.unique) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
`Cannot use query() with non-unique GSI '${resolved.indexName}'. Use list() instead.`
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
const normalized = normalizeTopLevelSelect(selectSpec);
|
|
1325
|
+
const select = normalized.select;
|
|
1326
|
+
const filter = normalized.filter;
|
|
1327
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
1328
|
+
const relations = detectRelationFields(select, metadata);
|
|
1329
|
+
if (relations.length > 0) {
|
|
1330
|
+
validateDepth(select, metadata, maxDepth);
|
|
1331
|
+
}
|
|
1332
|
+
const implicitKeys = getImplicitKeyFields(select, metadata);
|
|
1333
|
+
const updatable = options?.updatable ?? false;
|
|
1334
|
+
const executionPlan = plan(metadata, {
|
|
1335
|
+
key,
|
|
1336
|
+
select,
|
|
1337
|
+
filter,
|
|
1338
|
+
consistentRead: options?.consistentRead,
|
|
1339
|
+
additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
|
|
1340
|
+
updatable
|
|
1341
|
+
});
|
|
1342
|
+
const operation = executionPlan.operations[0];
|
|
1343
|
+
const canParallelizeRelations = relations.length > 0 && relations.every(
|
|
1344
|
+
(rel) => Object.values(rel.keyBinding).every(
|
|
1345
|
+
(sourceField) => key[sourceField] != null
|
|
1346
|
+
)
|
|
1347
|
+
);
|
|
1348
|
+
const parentPromise = execute(operation);
|
|
1349
|
+
const relationPromise = canParallelizeRelations ? resolveRelations({}, key, select, metadata, { maxDepth, updatable }, 1) : null;
|
|
1350
|
+
let result;
|
|
1351
|
+
try {
|
|
1352
|
+
result = await parentPromise;
|
|
1353
|
+
} catch (err) {
|
|
1354
|
+
if (relationPromise) await relationPromise.catch(() => void 0);
|
|
1355
|
+
throw err;
|
|
1356
|
+
}
|
|
1357
|
+
if (result.items.length === 0) {
|
|
1358
|
+
if (relationPromise) await relationPromise.catch(() => void 0);
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
const rawItem = result.items[0];
|
|
1362
|
+
const hydrated = hydrate(result.items, select, metadata, updatable);
|
|
1363
|
+
let hydratedItem = hydrated[0] ?? null;
|
|
1364
|
+
if (hydratedItem && relations.length > 0) {
|
|
1365
|
+
if (relationPromise) {
|
|
1366
|
+
const resolvedRelations = await relationPromise;
|
|
1367
|
+
for (const rel of relations) {
|
|
1368
|
+
hydratedItem[rel.propertyName] = resolvedRelations[rel.propertyName];
|
|
1369
|
+
}
|
|
1370
|
+
} else {
|
|
1371
|
+
hydratedItem = await resolveRelations(
|
|
1372
|
+
hydratedItem,
|
|
1373
|
+
rawItem,
|
|
1374
|
+
select,
|
|
1375
|
+
metadata,
|
|
1376
|
+
{ maxDepth, updatable },
|
|
1377
|
+
1
|
|
1378
|
+
);
|
|
1379
|
+
if (hydratedItem && updatable) {
|
|
1380
|
+
attachHiddenKey(hydratedItem, rawItem);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
if (options?.hydrate) {
|
|
1385
|
+
return options.hydrate(hydratedItem);
|
|
1386
|
+
}
|
|
1387
|
+
return hydratedItem;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// src/operations/explain.ts
|
|
1391
|
+
function executeExplain(modelClass, key, options) {
|
|
1392
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1393
|
+
const exclusiveStartKey = options?.after ? decodeCursor(options.after) : void 0;
|
|
1394
|
+
const normalized = normalizeTopLevelSelect(options?.select);
|
|
1395
|
+
const select = normalized.select ?? {};
|
|
1396
|
+
const filter = options?.filter ?? normalized.filter;
|
|
1397
|
+
const mainPlan = plan(metadata, {
|
|
1398
|
+
key,
|
|
1399
|
+
select,
|
|
1400
|
+
limit: options?.limit,
|
|
1401
|
+
after: exclusiveStartKey,
|
|
1402
|
+
order: options?.order,
|
|
1403
|
+
consistentRead: options?.consistentRead,
|
|
1404
|
+
filter
|
|
1405
|
+
});
|
|
1406
|
+
const relationOps = planRelationOperations(metadata, select, {
|
|
1407
|
+
estimatedParentCount: 1
|
|
1408
|
+
});
|
|
1409
|
+
return {
|
|
1410
|
+
operations: [...mainPlan.operations, ...relationOps]
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/operations/batch.ts
|
|
1415
|
+
var BatchGetResult = class {
|
|
1416
|
+
groups;
|
|
1417
|
+
constructor(groups) {
|
|
1418
|
+
this.groups = groups;
|
|
1419
|
+
}
|
|
1420
|
+
get(model) {
|
|
1421
|
+
const modelClass = resolveModelClass(model);
|
|
1422
|
+
return this.groups.get(modelClass) ?? [];
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
function buildScalarSelect(metadata) {
|
|
1426
|
+
const select = {};
|
|
1427
|
+
for (const field of metadata.fields) {
|
|
1428
|
+
select[field.propertyName] = true;
|
|
1429
|
+
}
|
|
1430
|
+
return select;
|
|
1431
|
+
}
|
|
1432
|
+
function buildDynamoKey(modelClass, keyObj) {
|
|
1433
|
+
const deleteInput = buildDeleteInput(modelClass, keyObj);
|
|
1434
|
+
return deleteInput.Key;
|
|
1435
|
+
}
|
|
1436
|
+
function hydrateBatchItems(rawItems, metadata) {
|
|
1437
|
+
if (rawItems.length === 0) {
|
|
1438
|
+
return [];
|
|
1439
|
+
}
|
|
1440
|
+
const select = buildScalarSelect(metadata);
|
|
1441
|
+
const hydrated = hydrate(rawItems, select, metadata);
|
|
1442
|
+
if (metadata.embeddedFields.length === 0) {
|
|
1443
|
+
return hydrated;
|
|
1444
|
+
}
|
|
1445
|
+
return hydrated.map((item, index) => {
|
|
1446
|
+
const raw = rawItems[index];
|
|
1447
|
+
const merged = { ...item };
|
|
1448
|
+
for (const emb of metadata.embeddedFields) {
|
|
1449
|
+
const rawEmb = raw?.[emb.propertyName];
|
|
1450
|
+
if (rawEmb !== void 0 && rawEmb !== null) {
|
|
1451
|
+
merged[emb.propertyName] = rawEmb;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
return merged;
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
async function executeBatchGetForTable(tableName, entries) {
|
|
1458
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1459
|
+
if (entries.length === 0) {
|
|
1460
|
+
return groups;
|
|
1461
|
+
}
|
|
1462
|
+
const keys = entries.map((entry) => entry.dynamoKey);
|
|
1463
|
+
const { items: rawItems } = await ClientManager.getExecutor().batchGet({
|
|
1464
|
+
tableName,
|
|
1465
|
+
keys
|
|
1466
|
+
});
|
|
1467
|
+
const entriesByModel = /* @__PURE__ */ new Map();
|
|
1468
|
+
for (const entry of entries) {
|
|
1469
|
+
const existing = entriesByModel.get(entry.modelClass) ?? [];
|
|
1470
|
+
existing.push(entry);
|
|
1471
|
+
entriesByModel.set(entry.modelClass, existing);
|
|
1472
|
+
}
|
|
1473
|
+
for (const [modelClass, modelEntries] of entriesByModel) {
|
|
1474
|
+
const metadata = modelEntries[0].metadata;
|
|
1475
|
+
const expectedKeys = new Set(
|
|
1476
|
+
modelEntries.map(
|
|
1477
|
+
(entry) => `${String(entry.dynamoKey.PK)}::${String(entry.dynamoKey.SK)}`
|
|
1478
|
+
)
|
|
1479
|
+
);
|
|
1480
|
+
const modelRawItems = rawItems.filter(
|
|
1481
|
+
(item) => expectedKeys.has(`${String(item.PK)}::${String(item.SK)}`)
|
|
1482
|
+
);
|
|
1483
|
+
groups.set(modelClass, hydrateBatchItems(modelRawItems, metadata));
|
|
1484
|
+
}
|
|
1485
|
+
return groups;
|
|
1486
|
+
}
|
|
1487
|
+
async function executeBatchGet(requests) {
|
|
1488
|
+
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1489
|
+
for (const request of requests) {
|
|
1490
|
+
const modelClass = resolveModelClass(request.model);
|
|
1491
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1492
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1493
|
+
for (const keyObj of request.keys) {
|
|
1494
|
+
const entry = {
|
|
1495
|
+
modelClass,
|
|
1496
|
+
metadata,
|
|
1497
|
+
dynamoKey: buildDynamoKey(modelClass, keyObj)
|
|
1498
|
+
};
|
|
1499
|
+
const existing = entriesByTable.get(tableName) ?? [];
|
|
1500
|
+
existing.push(entry);
|
|
1501
|
+
entriesByTable.set(tableName, existing);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
const mergedGroups = /* @__PURE__ */ new Map();
|
|
1505
|
+
for (const [, entries] of entriesByTable) {
|
|
1506
|
+
const tableName = TableMapping.resolve(entries[0].metadata.tableName);
|
|
1507
|
+
const tableGroups = await executeBatchGetForTable(tableName, entries);
|
|
1508
|
+
for (const [modelClass, items] of tableGroups) {
|
|
1509
|
+
const existing = mergedGroups.get(modelClass) ?? [];
|
|
1510
|
+
mergedGroups.set(modelClass, existing.concat(items));
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
return new BatchGetResult(mergedGroups);
|
|
1514
|
+
}
|
|
1515
|
+
function toExecItem(entry) {
|
|
1516
|
+
if (entry.request.type === "put") {
|
|
1517
|
+
return { type: "put", item: entry.request.putInput.Item };
|
|
1518
|
+
}
|
|
1519
|
+
return { type: "delete", key: entry.request.deleteInput.Key };
|
|
1520
|
+
}
|
|
1521
|
+
async function executeBatchWrite(requests) {
|
|
1522
|
+
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const request of requests) {
|
|
1524
|
+
const modelClass = resolveModelClass(request.model);
|
|
1525
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1526
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1527
|
+
if (request.type === "put") {
|
|
1528
|
+
const putInput = buildPutInput(modelClass, request.item, request.options);
|
|
1529
|
+
const entry2 = {
|
|
1530
|
+
tableName,
|
|
1531
|
+
modelName: modelClass.name,
|
|
1532
|
+
request: { type: "put", putInput }
|
|
1533
|
+
};
|
|
1534
|
+
const existing2 = entriesByTable.get(tableName) ?? [];
|
|
1535
|
+
existing2.push(entry2);
|
|
1536
|
+
entriesByTable.set(tableName, existing2);
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
const deleteInput = buildDeleteInput(modelClass, request.key, request.options);
|
|
1540
|
+
const entry = {
|
|
1541
|
+
tableName,
|
|
1542
|
+
modelName: modelClass.name,
|
|
1543
|
+
request: { type: "delete", deleteInput }
|
|
1544
|
+
};
|
|
1545
|
+
const existing = entriesByTable.get(tableName) ?? [];
|
|
1546
|
+
existing.push(entry);
|
|
1547
|
+
entriesByTable.set(tableName, existing);
|
|
1548
|
+
}
|
|
1549
|
+
const executor = ClientManager.getExecutor();
|
|
1550
|
+
for (const [tableName, entries] of entriesByTable) {
|
|
1551
|
+
await executor.batchWrite(tableName, entries.map(toExecItem));
|
|
1552
|
+
}
|
|
1553
|
+
if (ChangeCaptureRegistry.hasSubscribers()) {
|
|
1554
|
+
for (const [, entries] of entriesByTable) {
|
|
1555
|
+
for (const entry of entries) {
|
|
1556
|
+
if (entry.request.type === "put") {
|
|
1557
|
+
const item = entry.request.putInput.Item;
|
|
1558
|
+
captureWrite({
|
|
1559
|
+
table: entry.tableName,
|
|
1560
|
+
...entry.modelName ? { model: entry.modelName } : {},
|
|
1561
|
+
op: "put",
|
|
1562
|
+
keys: { pk: String(item.PK), sk: String(item.SK ?? "") },
|
|
1563
|
+
newItem: item
|
|
1564
|
+
});
|
|
1565
|
+
} else {
|
|
1566
|
+
const dkey = entry.request.deleteInput.Key;
|
|
1567
|
+
captureWrite({
|
|
1568
|
+
table: entry.tableName,
|
|
1569
|
+
...entry.modelName ? { model: entry.modelName } : {},
|
|
1570
|
+
op: "delete",
|
|
1571
|
+
keys: { pk: String(dkey.PK), sk: String(dkey.SK ?? "") }
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// src/define/entity-writes.ts
|
|
1580
|
+
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
1581
|
+
"graphddb:lifecycleContract"
|
|
1582
|
+
);
|
|
1583
|
+
function isLifecycleContract(value) {
|
|
1584
|
+
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
1585
|
+
}
|
|
1586
|
+
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
1587
|
+
function isEntityWritesDefinition(value) {
|
|
1588
|
+
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
1589
|
+
}
|
|
1590
|
+
function freezeEffects(effects) {
|
|
1591
|
+
const out = {};
|
|
1592
|
+
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
1593
|
+
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
1594
|
+
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
1595
|
+
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
1596
|
+
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
1597
|
+
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
1598
|
+
return out;
|
|
1599
|
+
}
|
|
1600
|
+
var recorder = {
|
|
1601
|
+
lifecycle(effects = {}) {
|
|
1602
|
+
return {
|
|
1603
|
+
[LIFECYCLE_CONTRACT_MARKER]: true,
|
|
1604
|
+
effects: freezeEffects(effects)
|
|
1605
|
+
};
|
|
1606
|
+
},
|
|
1607
|
+
exists(targetFactory, keys) {
|
|
1608
|
+
return { kind: "requires", targetFactory, keys };
|
|
1609
|
+
},
|
|
1610
|
+
unique(spec) {
|
|
1611
|
+
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
1612
|
+
},
|
|
1613
|
+
putEdge(targetFactory, relationProperty) {
|
|
1614
|
+
return { kind: "putEdge", targetFactory, relationProperty };
|
|
1615
|
+
},
|
|
1616
|
+
deleteEdge(targetFactory, relationProperty) {
|
|
1617
|
+
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
1618
|
+
},
|
|
1619
|
+
increment(targetFactory, keys, attribute, amount) {
|
|
1620
|
+
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
1621
|
+
},
|
|
1622
|
+
event(name, payload) {
|
|
1623
|
+
return { kind: "event", name, payload };
|
|
1624
|
+
},
|
|
1625
|
+
idempotentBy(token) {
|
|
1626
|
+
return { kind: "idempotency", token };
|
|
1627
|
+
}
|
|
1628
|
+
};
|
|
1629
|
+
function entityWrites(builder) {
|
|
1630
|
+
const shape = builder(recorder);
|
|
1631
|
+
for (const phase of ["create", "update", "remove"]) {
|
|
1632
|
+
const contract = shape[phase];
|
|
1633
|
+
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
1634
|
+
throw new Error(
|
|
1635
|
+
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
1640
|
+
throw new Error(
|
|
1641
|
+
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
[ENTITY_WRITES_MARKER]: true,
|
|
1646
|
+
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
1647
|
+
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
1648
|
+
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
function getEntityWrites(modelClass) {
|
|
1652
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
1653
|
+
let value;
|
|
1654
|
+
try {
|
|
1655
|
+
value = modelClass[name];
|
|
1656
|
+
} catch {
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
if (isEntityWritesDefinition(value)) return value;
|
|
1660
|
+
}
|
|
1661
|
+
return void 0;
|
|
1662
|
+
}
|
|
1663
|
+
function lifecyclePhaseForIntent(intent) {
|
|
1664
|
+
return intent;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
// src/relation/edge-write.ts
|
|
1668
|
+
var OLD_VALUE_NAMESPACE = "old";
|
|
1669
|
+
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
1670
|
+
function isEdgeWritesDefinition(value) {
|
|
1671
|
+
return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
|
|
1672
|
+
}
|
|
1673
|
+
function declaration(target, relationProperty) {
|
|
1674
|
+
return { targetFactory: target, relationProperty };
|
|
1675
|
+
}
|
|
1676
|
+
var recorder2 = {
|
|
1677
|
+
putEdge: declaration,
|
|
1678
|
+
deleteEdge: declaration,
|
|
1679
|
+
updateKeyChangeEdge: declaration
|
|
1680
|
+
};
|
|
1681
|
+
function edgeWrites(builder) {
|
|
1682
|
+
const edges = builder(recorder2);
|
|
1683
|
+
if (edges.length === 0) {
|
|
1684
|
+
throw new Error(
|
|
1685
|
+
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
return { [EDGE_WRITES_MARKER]: true, edges };
|
|
519
1689
|
}
|
|
520
1690
|
function edgeKeyFields(segmented) {
|
|
521
1691
|
return [
|
|
@@ -531,335 +1701,1244 @@ function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
|
531
1701
|
);
|
|
532
1702
|
if (!relation) {
|
|
533
1703
|
throw new Error(
|
|
534
|
-
`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.`
|
|
1704
|
+
`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.`
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
const boundFields = Object.keys(relation.keyBinding);
|
|
1708
|
+
for (const targetField of boundFields) {
|
|
1709
|
+
if (!edgeFields.has(targetField)) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`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.`
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
resolveKey(boundFields, entity);
|
|
1717
|
+
} catch (cause) {
|
|
1718
|
+
throw new Error(
|
|
1719
|
+
`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})`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
return relation;
|
|
1723
|
+
}
|
|
1724
|
+
function templateRecord(fields) {
|
|
1725
|
+
const out = {};
|
|
1726
|
+
for (const field of [...fields].sort()) {
|
|
1727
|
+
out[field] = `{${field}}`;
|
|
1728
|
+
}
|
|
1729
|
+
return out;
|
|
1730
|
+
}
|
|
1731
|
+
function keyConditionTemplate(segmented, namespace) {
|
|
1732
|
+
const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
|
|
1733
|
+
const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
|
|
1734
|
+
const out = { PK: pk };
|
|
1735
|
+
if (sk !== void 0) out.SK = sk;
|
|
1736
|
+
return out;
|
|
1737
|
+
}
|
|
1738
|
+
function putItemTemplate(edgeFields) {
|
|
1739
|
+
return templateRecord(edgeFields);
|
|
1740
|
+
}
|
|
1741
|
+
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
1742
|
+
const entity = MetadataRegistry.get(adjacencyClass);
|
|
1743
|
+
if (!entity.primaryKey) {
|
|
1744
|
+
throw new Error(
|
|
1745
|
+
`Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
const segmented = entity.primaryKey.segmented;
|
|
1749
|
+
const edgeFields = edgeKeyFields(segmented);
|
|
1750
|
+
assertRelationRoundTrips(entity, new Set(edgeFields), decl);
|
|
1751
|
+
const tableName = TableMapping.resolve(entity.tableName);
|
|
1752
|
+
const entityName = adjacencyClass.name;
|
|
1753
|
+
const put = {
|
|
1754
|
+
type: "Put",
|
|
1755
|
+
tableName,
|
|
1756
|
+
entity: entityName,
|
|
1757
|
+
item: putItemTemplate(edgeFields)
|
|
1758
|
+
};
|
|
1759
|
+
const deleteNew = {
|
|
1760
|
+
type: "Delete",
|
|
1761
|
+
tableName,
|
|
1762
|
+
entity: entityName,
|
|
1763
|
+
keyCondition: keyConditionTemplate(segmented)
|
|
1764
|
+
};
|
|
1765
|
+
const deleteOld = {
|
|
1766
|
+
type: "Delete",
|
|
1767
|
+
tableName,
|
|
1768
|
+
entity: entityName,
|
|
1769
|
+
keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
|
|
1770
|
+
};
|
|
1771
|
+
switch (lifecycle) {
|
|
1772
|
+
case "onCreate":
|
|
1773
|
+
return [put];
|
|
1774
|
+
case "onDelete":
|
|
1775
|
+
return [deleteNew];
|
|
1776
|
+
case "onUpdateKeyChange":
|
|
1777
|
+
return [deleteOld, put];
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
|
|
1781
|
+
const items = [];
|
|
1782
|
+
for (const decl of writes.edges) {
|
|
1783
|
+
items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
|
|
1784
|
+
}
|
|
1785
|
+
return items;
|
|
1786
|
+
}
|
|
1787
|
+
function getEdgeWrites(modelClass) {
|
|
1788
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
1789
|
+
let value;
|
|
1790
|
+
try {
|
|
1791
|
+
value = modelClass[name];
|
|
1792
|
+
} catch {
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
if (isEdgeWritesDefinition(value)) return value;
|
|
1796
|
+
}
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1799
|
+
function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
1800
|
+
const writes = getEdgeWrites(modelClass);
|
|
1801
|
+
if (!writes) {
|
|
1802
|
+
throw new Error(
|
|
1803
|
+
`Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/runtime/command-runtime.ts
|
|
1810
|
+
function resolveCommandMethod(contract, methodName) {
|
|
1811
|
+
const method = contract.methods[methodName];
|
|
1812
|
+
if (method === void 0) {
|
|
1813
|
+
throw new Error(
|
|
1814
|
+
`Contract runtime: command method '${methodName}' does not exist on this contract. Available methods: ${Object.keys(contract.methods).sort().join(", ") || "(none)"}.`
|
|
535
1815
|
);
|
|
536
1816
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
1817
|
+
return method;
|
|
1818
|
+
}
|
|
1819
|
+
function modelClassOf(op) {
|
|
1820
|
+
return op.entity.modelClass;
|
|
1821
|
+
}
|
|
1822
|
+
function modelStaticOf(op) {
|
|
1823
|
+
return op.entity.modelClass.asModel();
|
|
1824
|
+
}
|
|
1825
|
+
function renderLeaf(leaf, key, params, context) {
|
|
1826
|
+
if (isContractParamRef(leaf)) {
|
|
1827
|
+
if (!(leaf.field in params)) {
|
|
540
1828
|
throw new Error(
|
|
541
|
-
`
|
|
1829
|
+
`Contract runtime: ${context} references params field '${leaf.field}', which was not supplied. Pass it in the method's params argument.`
|
|
542
1830
|
);
|
|
543
1831
|
}
|
|
1832
|
+
return params[leaf.field];
|
|
544
1833
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
1834
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
1835
|
+
if (leaf.field in key) return key[leaf.field];
|
|
1836
|
+
if (leaf.field in params) return params[leaf.field];
|
|
548
1837
|
throw new Error(
|
|
549
|
-
`
|
|
1838
|
+
`Contract runtime: ${context} references key field '${leaf.field}', which was not supplied. Pass it in the method's key or params argument.`
|
|
550
1839
|
);
|
|
551
1840
|
}
|
|
552
|
-
return
|
|
1841
|
+
return leaf;
|
|
553
1842
|
}
|
|
554
|
-
function
|
|
1843
|
+
function renderRecord(structure, key, params, context) {
|
|
555
1844
|
const out = {};
|
|
556
|
-
|
|
557
|
-
|
|
1845
|
+
if (structure === void 0) return out;
|
|
1846
|
+
for (const [field, leaf] of Object.entries(structure)) {
|
|
1847
|
+
out[field] = renderLeaf(leaf, key, params, `${context} field '${field}'`);
|
|
558
1848
|
}
|
|
559
1849
|
return out;
|
|
560
1850
|
}
|
|
561
|
-
function
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1851
|
+
function renderCondition(condition, key, params, context) {
|
|
1852
|
+
if (condition === void 0) return void 0;
|
|
1853
|
+
if (isRawCondition(condition)) {
|
|
1854
|
+
const parts = condition.parts.map(
|
|
1855
|
+
(part) => isColumn(part) ? part : renderLeaf(part, key, params, `${context} condition`)
|
|
1856
|
+
);
|
|
1857
|
+
return { ...condition, parts };
|
|
1858
|
+
}
|
|
1859
|
+
if (typeof condition === "object" && condition !== null) {
|
|
1860
|
+
const obj = condition;
|
|
1861
|
+
if (obj.notExists === true) return { notExists: true };
|
|
1862
|
+
if (typeof obj.attributeExists === "string") {
|
|
1863
|
+
return { attributeExists: obj.attributeExists };
|
|
1864
|
+
}
|
|
1865
|
+
if (typeof obj.attributeNotExists === "string") {
|
|
1866
|
+
return { attributeNotExists: obj.attributeNotExists };
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return renderRecord(
|
|
1870
|
+
condition,
|
|
1871
|
+
key,
|
|
1872
|
+
params,
|
|
1873
|
+
`${context} condition`
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
function renderWrite(op, key, params, contextLabel) {
|
|
1877
|
+
if (!isContractKeyRef(op.keys) && op.operation !== "put") {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
`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.`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
const base = {
|
|
1883
|
+
operation: op.operation,
|
|
1884
|
+
modelClass: modelClassOf(op),
|
|
1885
|
+
modelStatic: modelStaticOf(op),
|
|
1886
|
+
key,
|
|
1887
|
+
condition: renderCondition(op.condition, key, params, contextLabel)
|
|
1888
|
+
};
|
|
1889
|
+
if (op.operation === "put") {
|
|
1890
|
+
return { ...base, item: renderRecord(op.item, key, params, `${contextLabel} item`) };
|
|
1891
|
+
}
|
|
1892
|
+
if (op.operation === "update") {
|
|
1893
|
+
return { ...base, changes: renderRecord(op.changes, key, params, `${contextLabel} changes`) };
|
|
1894
|
+
}
|
|
1895
|
+
return base;
|
|
1896
|
+
}
|
|
1897
|
+
async function executeSingleWrite(w) {
|
|
1898
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
1899
|
+
if (w.operation === "put") {
|
|
1900
|
+
await executePut(w.modelClass, w.item ?? {}, options);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
if (w.operation === "update") {
|
|
1904
|
+
await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
await executeDelete(w.modelClass, w.key, options);
|
|
1908
|
+
}
|
|
1909
|
+
async function readBackProjection(write, returnSelection) {
|
|
1910
|
+
const select = {};
|
|
1911
|
+
for (const field of Object.keys(returnSelection)) {
|
|
1912
|
+
if (returnSelection[field] === true) select[field] = true;
|
|
1913
|
+
}
|
|
1914
|
+
const key = readBackKey(write);
|
|
1915
|
+
return executeQuery(write.modelClass, key, select, { consistentRead: true });
|
|
1916
|
+
}
|
|
1917
|
+
function readBackKey(write) {
|
|
1918
|
+
if (write.operation !== "put") return write.key;
|
|
1919
|
+
const metadata = MetadataRegistry.get(write.modelClass);
|
|
1920
|
+
const item = write.item ?? {};
|
|
1921
|
+
const key = {};
|
|
1922
|
+
const fields = metadata.primaryKey ? metadata.primaryKey.inputFieldNames : Object.keys(item);
|
|
1923
|
+
for (const field of fields) {
|
|
1924
|
+
if (field in item) key[field] = item[field];
|
|
1925
|
+
}
|
|
1926
|
+
return key;
|
|
1927
|
+
}
|
|
1928
|
+
function renderConditionCheck(check, key, params, contextLabel) {
|
|
1929
|
+
const modelClass = check.entity.modelClass;
|
|
1930
|
+
const referencedKey = {};
|
|
1931
|
+
for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
|
|
1932
|
+
if (inputField in params) {
|
|
1933
|
+
referencedKey[targetField] = params[inputField];
|
|
1934
|
+
} else if (inputField in key) {
|
|
1935
|
+
referencedKey[targetField] = key[inputField];
|
|
1936
|
+
} else {
|
|
1937
|
+
throw new Error(
|
|
1938
|
+
`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.`
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
const { TableName, Key } = buildDeleteInput(
|
|
1943
|
+
modelClass,
|
|
1944
|
+
referencedKey
|
|
1945
|
+
);
|
|
1946
|
+
const cond = buildConditionExpression({ attributeExists: "PK" });
|
|
1947
|
+
const checkInput = {
|
|
1948
|
+
TableName,
|
|
1949
|
+
Key,
|
|
1950
|
+
ConditionExpression: cond.expression
|
|
1951
|
+
};
|
|
1952
|
+
if (Object.keys(cond.names).length > 0) checkInput.ExpressionAttributeNames = cond.names;
|
|
1953
|
+
if (Object.keys(cond.values).length > 0) checkInput.ExpressionAttributeValues = cond.values;
|
|
1954
|
+
return checkInput;
|
|
1955
|
+
}
|
|
1956
|
+
var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
|
|
1957
|
+
function renderTemplate(template, key, params, contextLabel) {
|
|
1958
|
+
const resolveToken = (token) => {
|
|
1959
|
+
if (token in params) return params[token];
|
|
1960
|
+
if (token in key) return key[token];
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
`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 }\`).` : `.`)
|
|
1963
|
+
);
|
|
1964
|
+
};
|
|
1965
|
+
const whole = /^\{([^{}]+)\}$/.exec(template);
|
|
1966
|
+
if (whole !== null) return resolveToken(whole[1]);
|
|
1967
|
+
return template.replace(RUNTIME_TOKEN_RE, (_m, token) => String(resolveToken(token)));
|
|
1968
|
+
}
|
|
1969
|
+
function renderTemplateRecord(record, key, params, contextLabel) {
|
|
1970
|
+
const out = {};
|
|
1971
|
+
for (const [field, template] of Object.entries(record ?? {})) {
|
|
1972
|
+
out[field] = renderTemplate(template, key, params, contextLabel);
|
|
1973
|
+
}
|
|
566
1974
|
return out;
|
|
567
1975
|
}
|
|
568
|
-
function
|
|
569
|
-
|
|
1976
|
+
function renderEdgeWriteItems(edge, key, params, contextLabel, modelBySignature) {
|
|
1977
|
+
const adjacency = edge.entity.modelClass;
|
|
1978
|
+
return edge.items.map((item) => {
|
|
1979
|
+
if (item.type === "Put") {
|
|
1980
|
+
const rendered = renderTemplateRecord(item.item, key, params, `${contextLabel} edge Put`);
|
|
1981
|
+
const out2 = { Put: buildPutInput(adjacency, rendered) };
|
|
1982
|
+
modelBySignature?.set(execItemKeySignature(out2), adjacency.name);
|
|
1983
|
+
return out2;
|
|
1984
|
+
}
|
|
1985
|
+
const renderedKey = renderTemplateRecord(
|
|
1986
|
+
item.keyCondition,
|
|
1987
|
+
key,
|
|
1988
|
+
params,
|
|
1989
|
+
`${contextLabel} edge Delete`
|
|
1990
|
+
);
|
|
1991
|
+
const del = {
|
|
1992
|
+
TableName: TableMapping.resolve(item.tableName),
|
|
1993
|
+
Key: renderedKey
|
|
1994
|
+
};
|
|
1995
|
+
const out = { Delete: del };
|
|
1996
|
+
modelBySignature?.set(execItemKeySignature(out), adjacency.name);
|
|
1997
|
+
return out;
|
|
1998
|
+
});
|
|
570
1999
|
}
|
|
571
|
-
function
|
|
572
|
-
const
|
|
573
|
-
if (!
|
|
2000
|
+
function renderDerivedUpdate(update, key, params, contextLabel, modelBySignature) {
|
|
2001
|
+
const metadata = MetadataRegistry.get(update.entity.modelClass);
|
|
2002
|
+
if (!metadata.primaryKey) {
|
|
574
2003
|
throw new Error(
|
|
575
|
-
`
|
|
2004
|
+
`Contract runtime: ${contextLabel} derives a counter update on '${update.entity.name}', which has no primary key.`
|
|
576
2005
|
);
|
|
577
2006
|
}
|
|
578
|
-
const
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
2007
|
+
const keyInput = {};
|
|
2008
|
+
for (const [targetField, inputField] of Object.entries(update.keyBinding)) {
|
|
2009
|
+
keyInput[targetField] = renderTemplate(
|
|
2010
|
+
`{${inputField}}`,
|
|
2011
|
+
key,
|
|
2012
|
+
params,
|
|
2013
|
+
`${contextLabel} derived-update key`
|
|
2014
|
+
);
|
|
2015
|
+
}
|
|
2016
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2017
|
+
update.entity.modelClass,
|
|
2018
|
+
keyInput
|
|
2019
|
+
);
|
|
2020
|
+
const updateInput = {
|
|
2021
|
+
TableName,
|
|
2022
|
+
Key,
|
|
2023
|
+
UpdateExpression: "ADD #a0 :a0",
|
|
2024
|
+
ExpressionAttributeNames: { "#a0": update.attribute },
|
|
2025
|
+
ExpressionAttributeValues: { ":a0": update.amount }
|
|
588
2026
|
};
|
|
589
|
-
const
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
2027
|
+
const out = { Update: updateInput };
|
|
2028
|
+
modelBySignature?.set(execItemKeySignature(out), update.entity.modelClass.name);
|
|
2029
|
+
return out;
|
|
2030
|
+
}
|
|
2031
|
+
function renderUniqueGuardItems(guard, key, params, contextLabel) {
|
|
2032
|
+
return guard.items.map((item) => {
|
|
2033
|
+
if (item.type === "Put") {
|
|
2034
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} guard Put`);
|
|
2035
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2036
|
+
const cond = buildConditionExpression({ attributeNotExists: "PK" });
|
|
2037
|
+
put.ConditionExpression = cond.expression;
|
|
2038
|
+
if (Object.keys(cond.names).length > 0) put.ExpressionAttributeNames = cond.names;
|
|
2039
|
+
if (Object.keys(cond.values).length > 0) put.ExpressionAttributeValues = cond.values;
|
|
2040
|
+
return { Put: put };
|
|
2041
|
+
}
|
|
2042
|
+
const Key = renderTemplateRecord(item.keyCondition, key, params, `${contextLabel} guard Delete`);
|
|
2043
|
+
const del = { TableName: TableMapping.resolve(item.tableName), Key };
|
|
2044
|
+
return { Delete: del };
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
function renderOutboxEventItems(event, key, params, contextLabel) {
|
|
2048
|
+
return event.items.map((item) => {
|
|
2049
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} outbox Put`);
|
|
2050
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2051
|
+
return { Put: put };
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
|
|
2055
|
+
return guard.items.map((item) => {
|
|
2056
|
+
const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
|
|
2057
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2058
|
+
const cond = buildConditionExpression({ attributeNotExists: "PK" });
|
|
2059
|
+
put.ConditionExpression = cond.expression;
|
|
2060
|
+
if (Object.keys(cond.names).length > 0) put.ExpressionAttributeNames = cond.names;
|
|
2061
|
+
if (Object.keys(cond.values).length > 0) put.ExpressionAttributeValues = cond.values;
|
|
2062
|
+
return { Put: put };
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map()) {
|
|
2066
|
+
const writeItems = writes.map((w) => {
|
|
2067
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2068
|
+
let item;
|
|
2069
|
+
if (w.operation === "put") {
|
|
2070
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
2071
|
+
} else if (w.operation === "update") {
|
|
2072
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
2073
|
+
} else {
|
|
2074
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
2075
|
+
}
|
|
2076
|
+
modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
|
|
2077
|
+
return item;
|
|
2078
|
+
});
|
|
2079
|
+
const checkItems = checks.map((c) => ({ ConditionCheck: c.check }));
|
|
2080
|
+
await commitTransaction(
|
|
2081
|
+
[
|
|
2082
|
+
...writeItems,
|
|
2083
|
+
...edgeItems,
|
|
2084
|
+
...updateItems,
|
|
2085
|
+
...guardItemsRaw,
|
|
2086
|
+
...outboxItems,
|
|
2087
|
+
...idempotencyItems,
|
|
2088
|
+
...checkItems
|
|
2089
|
+
],
|
|
2090
|
+
{
|
|
2091
|
+
modelBySignature,
|
|
2092
|
+
limitError: (count) => new Error(
|
|
2093
|
+
`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.`
|
|
2094
|
+
)
|
|
2095
|
+
}
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
async function executeTransactWrites(writes, methodName) {
|
|
2099
|
+
if (writes.length === 0) return;
|
|
2100
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2101
|
+
const transactItems = writes.map((w) => {
|
|
2102
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2103
|
+
let item;
|
|
2104
|
+
if (w.operation === "put") {
|
|
2105
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
2106
|
+
} else if (w.operation === "update") {
|
|
2107
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
2108
|
+
} else {
|
|
2109
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
2110
|
+
}
|
|
2111
|
+
modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
|
|
2112
|
+
return item;
|
|
2113
|
+
});
|
|
2114
|
+
await commitTransaction(transactItems, {
|
|
2115
|
+
modelBySignature,
|
|
2116
|
+
limitError: (count) => new Error(
|
|
2117
|
+
`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.`
|
|
2118
|
+
)
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
async function executeParallelWrites(writes) {
|
|
2122
|
+
if (writes.length === 0) return [];
|
|
2123
|
+
const coalescible = writes.every(
|
|
2124
|
+
(w) => w.condition === void 0 && (w.operation === "put" || w.operation === "delete")
|
|
2125
|
+
);
|
|
2126
|
+
if (coalescible) {
|
|
2127
|
+
const requests = writes.map(
|
|
2128
|
+
(w) => w.operation === "put" ? { type: "put", model: w.modelStatic, item: w.item ?? {} } : { type: "delete", model: w.modelStatic, key: w.key }
|
|
2129
|
+
);
|
|
2130
|
+
await executeBatchWrite(requests);
|
|
2131
|
+
return writes.map(() => ({ ok: true }));
|
|
2132
|
+
}
|
|
2133
|
+
return Promise.all(
|
|
2134
|
+
writes.map(async (w) => {
|
|
2135
|
+
try {
|
|
2136
|
+
await executeSingleWrite(w);
|
|
2137
|
+
return { ok: true };
|
|
2138
|
+
} catch (e) {
|
|
2139
|
+
return { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
2140
|
+
}
|
|
2141
|
+
})
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}) {
|
|
2145
|
+
const method = resolveCommandMethod(contract, methodName);
|
|
2146
|
+
if (Array.isArray(keyOrKeys)) {
|
|
2147
|
+
const keys = keyOrKeys;
|
|
2148
|
+
if (method.ops !== void 0 && method.ops.length > 1) {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
`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.`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
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;
|
|
2154
|
+
if (hasDerivedEffects2) {
|
|
2155
|
+
throw new Error(
|
|
2156
|
+
`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.`
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
const writes = keys.map(
|
|
2160
|
+
(key2, i) => renderWrite(method.op, key2, params, `command method '${methodName}' (key #${i})`)
|
|
2161
|
+
);
|
|
2162
|
+
if (method.mode === "parallel") {
|
|
2163
|
+
return { results: await executeParallelWrites(writes) };
|
|
2164
|
+
}
|
|
2165
|
+
await executeTransactWrites(writes, methodName);
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
const key = keyOrKeys;
|
|
2169
|
+
const conditionChecks = method.conditionChecks ?? [];
|
|
2170
|
+
const edgeWrites2 = method.edgeWrites ?? [];
|
|
2171
|
+
const derivedUpdates = method.derivedUpdates ?? [];
|
|
2172
|
+
const uniqueGuards = method.uniqueGuards ?? [];
|
|
2173
|
+
const outboxEvents = method.outboxEvents ?? [];
|
|
2174
|
+
const idempotencyGuard = method.idempotencyGuard;
|
|
2175
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
|
|
2176
|
+
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2177
|
+
if (isMultiFragment || hasDerivedEffects) {
|
|
2178
|
+
const ops = method.ops ?? [method.op];
|
|
2179
|
+
const writes = ops.map(
|
|
2180
|
+
(opn, i) => renderWrite(opn, key, params, `command method '${methodName}' (fragment #${i})`)
|
|
2181
|
+
);
|
|
2182
|
+
if (hasDerivedEffects) {
|
|
2183
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2184
|
+
const edgeItems = edgeWrites2.flatMap(
|
|
2185
|
+
(edge, i) => renderEdgeWriteItems(
|
|
2186
|
+
edge,
|
|
2187
|
+
key,
|
|
2188
|
+
params,
|
|
2189
|
+
`command method '${methodName}' (edge #${i})`,
|
|
2190
|
+
modelBySignature
|
|
2191
|
+
)
|
|
2192
|
+
);
|
|
2193
|
+
const updateItems = derivedUpdates.map(
|
|
2194
|
+
(update, i) => renderDerivedUpdate(
|
|
2195
|
+
update,
|
|
2196
|
+
key,
|
|
2197
|
+
params,
|
|
2198
|
+
`command method '${methodName}' (derive #${i})`,
|
|
2199
|
+
modelBySignature
|
|
2200
|
+
)
|
|
2201
|
+
);
|
|
2202
|
+
const guardItems = uniqueGuards.flatMap(
|
|
2203
|
+
(guard, i) => renderUniqueGuardItems(guard, key, params, `command method '${methodName}' (unique #${i})`)
|
|
2204
|
+
);
|
|
2205
|
+
const outboxItems = outboxEvents.flatMap(
|
|
2206
|
+
(event, i) => renderOutboxEventItems(event, key, params, `command method '${methodName}' (emit #${i})`)
|
|
2207
|
+
);
|
|
2208
|
+
const idempotencyItems = idempotencyGuard !== void 0 ? renderIdempotencyGuardItems(
|
|
2209
|
+
idempotencyGuard,
|
|
2210
|
+
key,
|
|
2211
|
+
params,
|
|
2212
|
+
`command method '${methodName}' (idempotency)`
|
|
2213
|
+
) : [];
|
|
2214
|
+
const checks = conditionChecks.map((check, i) => ({
|
|
2215
|
+
check: renderConditionCheck(
|
|
2216
|
+
check,
|
|
2217
|
+
key,
|
|
2218
|
+
params,
|
|
2219
|
+
`command method '${methodName}' (requires #${i})`
|
|
2220
|
+
)
|
|
2221
|
+
}));
|
|
2222
|
+
await executeReferentialTransaction(
|
|
2223
|
+
writes,
|
|
2224
|
+
edgeItems,
|
|
2225
|
+
updateItems,
|
|
2226
|
+
guardItems,
|
|
2227
|
+
outboxItems,
|
|
2228
|
+
idempotencyItems,
|
|
2229
|
+
checks,
|
|
2230
|
+
methodName,
|
|
2231
|
+
modelBySignature
|
|
2232
|
+
);
|
|
2233
|
+
} else {
|
|
2234
|
+
await executeTransactWrites(writes, methodName);
|
|
2235
|
+
}
|
|
2236
|
+
if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
|
|
2237
|
+
return readBackProjection(writes[0], method.returnSelection);
|
|
2238
|
+
}
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
const write = renderWrite(method.op, key, params, `command method '${methodName}'`);
|
|
2242
|
+
await executeSingleWrite(write);
|
|
2243
|
+
if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
|
|
2244
|
+
return readBackProjection(write, method.returnSelection);
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function renderCompiledOp(op, ctx) {
|
|
2248
|
+
const fragment = op.fragment;
|
|
2249
|
+
const baseOp = op.condition !== void 0 ? withCondition(fragment.op, op.condition) : fragment.op;
|
|
2250
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2251
|
+
const write = renderWrite(baseOp, op.params, op.params, ctx);
|
|
2252
|
+
const edgeItems = [];
|
|
2253
|
+
const updateItems = [];
|
|
2254
|
+
const guardItems = [];
|
|
2255
|
+
const outboxItems = [];
|
|
2256
|
+
const idempotencyItems = [];
|
|
2257
|
+
const checkItems = [];
|
|
2258
|
+
for (const edge of fragment.edgeWrites ?? []) {
|
|
2259
|
+
edgeItems.push(...renderEdgeWriteItems(edge, op.params, op.params, `${ctx} edge`, modelBySignature));
|
|
2260
|
+
}
|
|
2261
|
+
for (const u of fragment.derivedUpdates ?? []) {
|
|
2262
|
+
updateItems.push(renderDerivedUpdate(u, op.params, op.params, `${ctx} derive`, modelBySignature));
|
|
2263
|
+
}
|
|
2264
|
+
for (const g of fragment.uniqueGuards ?? []) {
|
|
2265
|
+
guardItems.push(...renderUniqueGuardItems(g, op.params, op.params, `${ctx} unique`));
|
|
2266
|
+
}
|
|
2267
|
+
for (const e of fragment.outboxEvents ?? []) {
|
|
2268
|
+
outboxItems.push(...renderOutboxEventItems(e, op.params, op.params, `${ctx} emit`));
|
|
2269
|
+
}
|
|
2270
|
+
if (fragment.idempotencyGuard !== void 0) {
|
|
2271
|
+
idempotencyItems.push(
|
|
2272
|
+
...renderIdempotencyGuardItems(fragment.idempotencyGuard, op.params, op.params, `${ctx} idempotency`)
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
for (const c of fragment.conditionChecks ?? []) {
|
|
2276
|
+
checkItems.push({ check: renderConditionCheck(c, op.params, op.params, `${ctx} requires`) });
|
|
2277
|
+
}
|
|
2278
|
+
const hasDerivedEffects = edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || checkItems.length > 0;
|
|
2279
|
+
return {
|
|
2280
|
+
write,
|
|
2281
|
+
edgeItems,
|
|
2282
|
+
updateItems,
|
|
2283
|
+
guardItems,
|
|
2284
|
+
outboxItems,
|
|
2285
|
+
idempotencyItems,
|
|
2286
|
+
checkItems,
|
|
2287
|
+
modelBySignature,
|
|
2288
|
+
hasDerivedEffects
|
|
594
2289
|
};
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
2290
|
+
}
|
|
2291
|
+
function readBackForCompiledOp(op, write) {
|
|
2292
|
+
const sel = op.result?.select;
|
|
2293
|
+
if (sel === void 0 || Object.keys(sel).length === 0) return void 0;
|
|
2294
|
+
const key = {};
|
|
2295
|
+
for (const f of op.fragment.keyFields) {
|
|
2296
|
+
if (f in op.params) key[f] = op.params[f];
|
|
2297
|
+
}
|
|
2298
|
+
return executeQuery(write.modelClass, key, sel, {
|
|
2299
|
+
consistentRead: op.result?.options?.consistentRead ?? true,
|
|
2300
|
+
maxDepth: op.result?.options?.maxDepth
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
async function executeCompiledWriteOp(ops, options) {
|
|
2304
|
+
const label = options.label;
|
|
2305
|
+
const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
|
|
2306
|
+
const rendered = renderedOps.map((r) => r.write);
|
|
2307
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2308
|
+
const edgeItems = [];
|
|
2309
|
+
const updateItems = [];
|
|
2310
|
+
const guardItems = [];
|
|
2311
|
+
const outboxItems = [];
|
|
2312
|
+
const idempotencyItems = [];
|
|
2313
|
+
const checkItems = [];
|
|
2314
|
+
for (const r of renderedOps) {
|
|
2315
|
+
for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
|
|
2316
|
+
edgeItems.push(...r.edgeItems);
|
|
2317
|
+
updateItems.push(...r.updateItems);
|
|
2318
|
+
guardItems.push(...r.guardItems);
|
|
2319
|
+
outboxItems.push(...r.outboxItems);
|
|
2320
|
+
idempotencyItems.push(...r.idempotencyItems);
|
|
2321
|
+
checkItems.push(...r.checkItems);
|
|
2322
|
+
}
|
|
2323
|
+
const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
|
|
2324
|
+
if (rendered.length === 1 && !hasDerivedEffects) {
|
|
2325
|
+
await executeSingleWrite(rendered[0]);
|
|
2326
|
+
} else {
|
|
2327
|
+
await executeReferentialTransaction(
|
|
2328
|
+
rendered,
|
|
2329
|
+
edgeItems,
|
|
2330
|
+
updateItems,
|
|
2331
|
+
guardItems,
|
|
2332
|
+
outboxItems,
|
|
2333
|
+
idempotencyItems,
|
|
2334
|
+
checkItems,
|
|
2335
|
+
label,
|
|
2336
|
+
modelBySignature
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
const readBacks = await Promise.all(
|
|
2340
|
+
ops.map((op, i) => readBackForCompiledOp(op, rendered[i]))
|
|
2341
|
+
);
|
|
2342
|
+
return { readBacks };
|
|
2343
|
+
}
|
|
2344
|
+
async function executeCompiledParallelWrites(ops, options) {
|
|
2345
|
+
const label = options.label;
|
|
2346
|
+
const results = new Array(ops.length);
|
|
2347
|
+
const renderedOps = ops.map((op, i) => renderCompiledOp(op, `${label} (op #${i})`));
|
|
2348
|
+
const coalescibleIdx = [];
|
|
2349
|
+
const individualIdx = [];
|
|
2350
|
+
renderedOps.forEach((r, i) => {
|
|
2351
|
+
const w = r.write;
|
|
2352
|
+
if (!r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
|
|
2353
|
+
coalescibleIdx.push(i);
|
|
2354
|
+
} else {
|
|
2355
|
+
individualIdx.push(i);
|
|
2356
|
+
}
|
|
2357
|
+
});
|
|
2358
|
+
await Promise.all([
|
|
2359
|
+
// The coalescible group → ONE BatchWriteItem (chunked, UnprocessedItems retry) via
|
|
2360
|
+
// the shared public-layer coalescing. On success every op reports `{ ok: true }`
|
|
2361
|
+
// plus its read-back; a BatchWriteItem request failure fails the whole group.
|
|
2362
|
+
(async () => {
|
|
2363
|
+
if (coalescibleIdx.length === 0) return;
|
|
2364
|
+
const writes = coalescibleIdx.map((i) => renderedOps[i].write);
|
|
2365
|
+
try {
|
|
2366
|
+
await executeParallelWrites(writes);
|
|
2367
|
+
await Promise.all(
|
|
2368
|
+
coalescibleIdx.map(async (i) => {
|
|
2369
|
+
const value = await readBackForCompiledOp(ops[i], renderedOps[i].write);
|
|
2370
|
+
results[i] = { ok: true, value };
|
|
2371
|
+
})
|
|
2372
|
+
);
|
|
2373
|
+
} catch (e) {
|
|
2374
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
2375
|
+
for (const i of coalescibleIdx) results[i] = { ok: false, error };
|
|
2376
|
+
}
|
|
2377
|
+
})(),
|
|
2378
|
+
// Each individual op → its own single-op atomic execution (condition gate / derived
|
|
2379
|
+
// effects / read-back preserved), concurrently and with per-op partial success.
|
|
2380
|
+
...individualIdx.map(async (i) => {
|
|
2381
|
+
try {
|
|
2382
|
+
const { readBacks } = await executeCompiledWriteOp([ops[i]], {
|
|
2383
|
+
label: `${label} (op #${i})`
|
|
2384
|
+
});
|
|
2385
|
+
results[i] = { ok: true, value: readBacks[0] };
|
|
2386
|
+
} catch (e) {
|
|
2387
|
+
results[i] = { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
|
|
2388
|
+
}
|
|
2389
|
+
})
|
|
2390
|
+
]);
|
|
2391
|
+
return results;
|
|
2392
|
+
}
|
|
2393
|
+
function withCondition(op, condition) {
|
|
2394
|
+
if (op.condition === void 0) return { ...op, condition };
|
|
2395
|
+
const base = op.condition;
|
|
2396
|
+
if (base.notExists === true && Object.keys(condition).length > 0) {
|
|
2397
|
+
throw new Error(
|
|
2398
|
+
`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.`
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
return { ...op, condition };
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
// src/runtime/envelope.ts
|
|
2405
|
+
async function executeReadEnvelope(envelope) {
|
|
2406
|
+
const aliases = Object.keys(envelope);
|
|
2407
|
+
const results = await Promise.all(
|
|
2408
|
+
aliases.map((alias) => executeReadRoute(alias, envelope[alias]))
|
|
2409
|
+
);
|
|
2410
|
+
const out = {};
|
|
2411
|
+
aliases.forEach((alias, i) => {
|
|
2412
|
+
out[alias] = results[i];
|
|
2413
|
+
});
|
|
2414
|
+
return out;
|
|
2415
|
+
}
|
|
2416
|
+
async function executeReadRoute(alias, route) {
|
|
2417
|
+
if (route === null || typeof route !== "object") {
|
|
2418
|
+
throw new Error(
|
|
2419
|
+
`DDBModel.query: the descriptor for alias '${alias}' is not a read descriptor (\`{ query | list: Model, key, select, options? }\`).`
|
|
2420
|
+
);
|
|
2421
|
+
}
|
|
2422
|
+
const isQuery = route.query !== void 0;
|
|
2423
|
+
const isList = route.list !== void 0;
|
|
2424
|
+
if (isQuery === isList) {
|
|
2425
|
+
throw new Error(
|
|
2426
|
+
`DDBModel.query: the descriptor for alias '${alias}' must carry exactly one of \`query\` or \`list\` (the target model). It declared ${isQuery && isList ? "both" : "neither"}.`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2429
|
+
if (route.key === void 0 || route.select === void 0) {
|
|
2430
|
+
throw new Error(
|
|
2431
|
+
`DDBModel.query: the '${isQuery ? "query" : "list"}' descriptor for alias '${alias}' must declare both \`key\` and \`select\`.`
|
|
2432
|
+
);
|
|
2433
|
+
}
|
|
2434
|
+
const modelClass = resolveModelClass(
|
|
2435
|
+
resolveModelStatic(isQuery ? route.query : route.list, alias)
|
|
2436
|
+
);
|
|
2437
|
+
const opt = route.options ?? {};
|
|
2438
|
+
if (isQuery) {
|
|
2439
|
+
return executeQuery(modelClass, route.key, route.select, {
|
|
2440
|
+
consistentRead: opt.consistentRead,
|
|
2441
|
+
maxDepth: opt.maxDepth
|
|
2442
|
+
});
|
|
2443
|
+
}
|
|
2444
|
+
return executeList(modelClass, route.key, route.select, {
|
|
2445
|
+
limit: opt.limit,
|
|
2446
|
+
after: opt.after,
|
|
2447
|
+
order: opt.order,
|
|
2448
|
+
filter: opt.filter
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
var INTENT_KEYS = ["create", "update", "remove"];
|
|
2452
|
+
async function executeWriteEnvelope(envelope, options = {}) {
|
|
2453
|
+
const mode = options.mode ?? "transaction";
|
|
2454
|
+
const aliases = Object.keys(envelope);
|
|
2455
|
+
if (aliases.length === 0) {
|
|
2456
|
+
throw new Error(
|
|
2457
|
+
"DDBModel.mutate: the write envelope is empty. Declare at least one `{ alias: { create | update | remove: Model, key, input? } }` operation."
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
const ops = aliases.flatMap((alias) => normalizeWriteOp(alias, envelope[alias]));
|
|
2461
|
+
if (mode === "transaction") {
|
|
2462
|
+
return executeTransactionMode(ops);
|
|
2463
|
+
}
|
|
2464
|
+
return executeParallelMode(ops);
|
|
2465
|
+
}
|
|
2466
|
+
function normalizeWriteOp(alias, d) {
|
|
2467
|
+
if (d === null || typeof d !== "object") {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
`DDBModel.mutate: the descriptor for alias '${alias}' is not a write descriptor (\`{ create | update | remove: Model, key, input?, condition?, result? }\`).`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
const present = INTENT_KEYS.filter((k) => d[k] !== void 0);
|
|
2473
|
+
if (present.length !== 1) {
|
|
2474
|
+
throw new Error(
|
|
2475
|
+
`DDBModel.mutate: the descriptor for alias '${alias}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
|
|
2476
|
+
);
|
|
2477
|
+
}
|
|
2478
|
+
const intent = present[0];
|
|
2479
|
+
if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
|
|
2480
|
+
throw new Error(
|
|
2481
|
+
`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.`
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
const model = resolveModelStatic(d[intent], alias);
|
|
2485
|
+
const input = d.input ?? {};
|
|
2486
|
+
const base = { alias, intent, model, input, condition: d.condition, result: d.result };
|
|
2487
|
+
if (Array.isArray(d.key)) {
|
|
2488
|
+
const keys = d.key;
|
|
2489
|
+
if (keys.length === 0) {
|
|
2490
|
+
throw new Error(
|
|
2491
|
+
`DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' declared an EMPTY key array. Pass at least one key, or omit the operation.`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
return keys.map((key, bulkIndex) => {
|
|
2495
|
+
if (key === null || typeof key !== "object") {
|
|
2496
|
+
throw new Error(
|
|
2497
|
+
`DDBModel.mutate: the '${intent}' descriptor for alias '${alias}' key #${bulkIndex} is not a key object.`
|
|
2498
|
+
);
|
|
2499
|
+
}
|
|
2500
|
+
return { ...base, key, bulkIndex };
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
return [{ ...base, key: d.key }];
|
|
2504
|
+
}
|
|
2505
|
+
function compileWriteOp(op) {
|
|
2506
|
+
const keyFieldNames2 = Object.keys(op.key);
|
|
2507
|
+
const inputFieldNames = Object.keys(op.input);
|
|
2508
|
+
const model = op.model;
|
|
2509
|
+
const body = ($) => {
|
|
2510
|
+
const descriptor = {
|
|
2511
|
+
[op.intent]: (() => model),
|
|
2512
|
+
key: mapToRefs($, keyFieldNames2),
|
|
2513
|
+
...inputFieldNames.length > 0 ? { input: mapToRefs($, inputFieldNames) } : {}
|
|
2514
|
+
};
|
|
2515
|
+
const map = { [op.alias]: descriptor };
|
|
2516
|
+
return map;
|
|
600
2517
|
};
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
case "onDelete":
|
|
605
|
-
return [deleteNew];
|
|
606
|
-
case "onUpdateKeyChange":
|
|
607
|
-
return [deleteOld, put];
|
|
2518
|
+
const plan2 = mutation(body);
|
|
2519
|
+
if (!isCommandPlan(plan2)) {
|
|
2520
|
+
throw new Error("DDBModel.mutate: internal error building the mutation plan.");
|
|
608
2521
|
}
|
|
2522
|
+
const compiled = compileMutationPlan(plan2);
|
|
2523
|
+
const fragment = compiled.fragments[0];
|
|
2524
|
+
const params = { ...op.input, ...op.key };
|
|
2525
|
+
return {
|
|
2526
|
+
fragment,
|
|
2527
|
+
params,
|
|
2528
|
+
condition: op.condition,
|
|
2529
|
+
result: op.result
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function mapToRefs($, fields) {
|
|
2533
|
+
const out = {};
|
|
2534
|
+
for (const f of fields) out[f] = $[f];
|
|
2535
|
+
return out;
|
|
609
2536
|
}
|
|
610
|
-
function
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
}
|
|
615
|
-
|
|
2537
|
+
async function executeTransactionMode(ops) {
|
|
2538
|
+
const compiled = ops.map(compileWriteOp);
|
|
2539
|
+
const { readBacks } = await executeCompiledWriteOp(compiled, {
|
|
2540
|
+
label: "DDBModel.mutate"
|
|
2541
|
+
});
|
|
2542
|
+
const out = {};
|
|
2543
|
+
ops.forEach((op, i) => {
|
|
2544
|
+
if (op.result === void 0) return;
|
|
2545
|
+
if (op.bulkIndex === void 0) {
|
|
2546
|
+
out[op.alias] = readBacks[i];
|
|
2547
|
+
} else {
|
|
2548
|
+
const arr = out[op.alias] ?? [];
|
|
2549
|
+
arr[op.bulkIndex] = readBacks[i];
|
|
2550
|
+
out[op.alias] = arr;
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
return out;
|
|
616
2554
|
}
|
|
617
|
-
function
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
2555
|
+
async function executeParallelMode(ops) {
|
|
2556
|
+
const compiled = ops.map(compileWriteOp);
|
|
2557
|
+
const settled = await executeCompiledParallelWrites(compiled, {
|
|
2558
|
+
label: "DDBModel.mutate"
|
|
2559
|
+
});
|
|
2560
|
+
const out = {};
|
|
2561
|
+
ops.forEach((op, i) => {
|
|
2562
|
+
const res = settled[i];
|
|
2563
|
+
const mapped = res.ok ? { ok: true, value: op.result !== void 0 ? res.value : void 0 } : { ok: false, error: res.error };
|
|
2564
|
+
if (op.bulkIndex === void 0) {
|
|
2565
|
+
out[op.alias] = mapped;
|
|
2566
|
+
} else {
|
|
2567
|
+
const arr = out[op.alias] ?? [];
|
|
2568
|
+
arr[op.bulkIndex] = mapped;
|
|
2569
|
+
out[op.alias] = arr;
|
|
624
2570
|
}
|
|
625
|
-
|
|
2571
|
+
});
|
|
2572
|
+
return out;
|
|
2573
|
+
}
|
|
2574
|
+
function resolveModelStatic(ref, alias) {
|
|
2575
|
+
if (ref === null || ref === void 0) {
|
|
2576
|
+
throw new Error(`DDBModel: the descriptor for alias '${alias}' names no target model.`);
|
|
626
2577
|
}
|
|
627
|
-
|
|
2578
|
+
if (typeof ref === "function") {
|
|
2579
|
+
const asModel = ref.asModel;
|
|
2580
|
+
if (typeof asModel === "function") {
|
|
2581
|
+
return asModel.call(ref);
|
|
2582
|
+
}
|
|
2583
|
+
return resolveModelStatic(ref(), alias);
|
|
2584
|
+
}
|
|
2585
|
+
return ref;
|
|
628
2586
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
2587
|
+
|
|
2588
|
+
// src/model/DDBModel.ts
|
|
2589
|
+
var DDBModel = class {
|
|
2590
|
+
static setClient(client) {
|
|
2591
|
+
ClientManager.setClient(client);
|
|
2592
|
+
}
|
|
2593
|
+
static setTableMapping(mapping) {
|
|
2594
|
+
TableMapping.set(mapping);
|
|
2595
|
+
}
|
|
2596
|
+
static async transaction(fn) {
|
|
2597
|
+
await executeTransaction(fn);
|
|
2598
|
+
}
|
|
2599
|
+
/**
|
|
2600
|
+
* In-process multi-route READ (issue #101 — the unified envelope). Each alias
|
|
2601
|
+
* route (`{ query | list: Model, key, select, options? }`) runs **independently
|
|
2602
|
+
* and in parallel**; the result is an object keyed by the input aliases. A
|
|
2603
|
+
* `query` route delegates to the single-item read path, a `list` route to the
|
|
2604
|
+
* partition read path. Cross-route consistency is NOT guaranteed.
|
|
2605
|
+
*/
|
|
2606
|
+
static async query(envelope) {
|
|
2607
|
+
return executeReadEnvelope(envelope);
|
|
2608
|
+
}
|
|
2609
|
+
static mutate(envelope, options) {
|
|
2610
|
+
return executeWriteEnvelope(envelope, options);
|
|
2611
|
+
}
|
|
2612
|
+
static async batchGet(requests) {
|
|
2613
|
+
return executeBatchGet(requests);
|
|
2614
|
+
}
|
|
2615
|
+
static async batchWrite(requests) {
|
|
2616
|
+
return executeBatchWrite(requests);
|
|
2617
|
+
}
|
|
2618
|
+
static asModel() {
|
|
2619
|
+
const modelClass = this;
|
|
2620
|
+
const modelStatic = {
|
|
2621
|
+
col: createColumnMap(),
|
|
2622
|
+
project(select) {
|
|
2623
|
+
return buildProject(select);
|
|
2624
|
+
},
|
|
2625
|
+
relation(select) {
|
|
2626
|
+
return buildRelation(select);
|
|
2627
|
+
},
|
|
2628
|
+
// The public `query` is overloaded (plain / updatable / hydrate, the last
|
|
2629
|
+
// introducing a fresh return-type parameter `R`, issue #53). An overloaded
|
|
2630
|
+
// method on an object literal cannot contextually infer its single
|
|
2631
|
+
// implementation signature, so the implementation is written with explicit
|
|
2632
|
+
// runtime-facing parameter types and assigned through the interface's
|
|
2633
|
+
// method type — the public overloads (the only caller-visible surface)
|
|
2634
|
+
// are untouched.
|
|
2635
|
+
query: ((key, select, options) => {
|
|
2636
|
+
return executeQuery(modelClass, key, select, options);
|
|
2637
|
+
}),
|
|
2638
|
+
list: ((key, select, options) => {
|
|
2639
|
+
return executeList(modelClass, key, select, options);
|
|
2640
|
+
}),
|
|
2641
|
+
explain(key, options) {
|
|
2642
|
+
return executeExplain(
|
|
2643
|
+
modelClass,
|
|
2644
|
+
key,
|
|
2645
|
+
options
|
|
2646
|
+
);
|
|
2647
|
+
},
|
|
2648
|
+
async putItem(item, options) {
|
|
2649
|
+
await executePut(
|
|
2650
|
+
modelClass,
|
|
2651
|
+
item,
|
|
2652
|
+
options
|
|
2653
|
+
);
|
|
2654
|
+
},
|
|
2655
|
+
async updateItem(entity, changes, options) {
|
|
2656
|
+
await executeUpdate(
|
|
2657
|
+
modelClass,
|
|
2658
|
+
entity,
|
|
2659
|
+
changes,
|
|
2660
|
+
options
|
|
2661
|
+
);
|
|
2662
|
+
},
|
|
2663
|
+
async deleteItem(key, options) {
|
|
2664
|
+
await executeDelete(modelClass, key, options);
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
return attachModelClass(
|
|
2668
|
+
modelStatic,
|
|
2669
|
+
modelClass
|
|
634
2670
|
);
|
|
635
2671
|
}
|
|
636
|
-
|
|
637
|
-
}
|
|
2672
|
+
};
|
|
638
2673
|
|
|
639
|
-
// src/define/
|
|
640
|
-
|
|
641
|
-
|
|
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}}` };
|
|
2674
|
+
// src/define/mutation.ts
|
|
2675
|
+
var INPUT_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationInputRef");
|
|
2676
|
+
function isMutationInputRef(value) {
|
|
2677
|
+
return typeof value === "object" && value !== null && value[INPUT_REF_BRAND] === true;
|
|
650
2678
|
}
|
|
651
|
-
|
|
652
|
-
|
|
2679
|
+
var ENTITY_REF_RE = /^\$\.entity\[(\d+)\]\.([A-Za-z_$][\w$]*)$/;
|
|
2680
|
+
var ENTITY_REF_PREFIX_RE = /^\s*\$\s*\.\s*entity\b/;
|
|
2681
|
+
var ENTITY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationEntityRef");
|
|
2682
|
+
function parseEntityRef(leaf) {
|
|
2683
|
+
if (typeof leaf !== "string") return void 0;
|
|
2684
|
+
const match = ENTITY_REF_RE.exec(leaf);
|
|
2685
|
+
if (match === null) {
|
|
2686
|
+
if (ENTITY_REF_PREFIX_RE.test(leaf)) {
|
|
2687
|
+
throw new Error(
|
|
2688
|
+
`mutation: the input value ${JSON.stringify(leaf)} looks like a cross-fragment reference but is malformed. The only supported form is \`$.entity[<index>].<field>\` (e.g. \`$.entity[0].postId\`) \u2014 a single dotted field on a 0-based, non-negative fragment index. Fix the path or, if you meant a literal string, it must not begin with \`$.entity\`.`
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
return void 0;
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
[ENTITY_REF_BRAND]: true,
|
|
2695
|
+
fragmentIndex: Number(match[1]),
|
|
2696
|
+
field: match[2],
|
|
2697
|
+
path: leaf
|
|
2698
|
+
};
|
|
653
2699
|
}
|
|
654
|
-
|
|
655
|
-
|
|
2700
|
+
var ALIAS_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationAliasFieldRef");
|
|
2701
|
+
function isAliasFieldRef(value) {
|
|
2702
|
+
return typeof value === "object" && value !== null && value[ALIAS_FIELD_REF_BRAND] === true;
|
|
656
2703
|
}
|
|
657
|
-
function
|
|
658
|
-
const token = `{
|
|
659
|
-
const origin = `
|
|
2704
|
+
function makeRootRef(root) {
|
|
2705
|
+
const token = `{${root}}`;
|
|
2706
|
+
const origin = `the placeholder '${root}' (\`$.${root}\`)`;
|
|
2707
|
+
const aliasFieldCache = /* @__PURE__ */ new Map();
|
|
660
2708
|
const target = /* @__PURE__ */ Object.create(null);
|
|
661
2709
|
const proxy = new Proxy(target, {
|
|
662
2710
|
get(_t, prop) {
|
|
663
|
-
if (prop ===
|
|
664
|
-
if (prop === "field") return
|
|
2711
|
+
if (prop === INPUT_REF_BRAND) return true;
|
|
2712
|
+
if (prop === "field") return root;
|
|
665
2713
|
if (prop === "token") return token;
|
|
666
|
-
if (prop ===
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
673
|
-
return () => {
|
|
674
|
-
onCoerce();
|
|
675
|
-
return marker;
|
|
676
|
-
};
|
|
2714
|
+
if (typeof prop === "symbol") return void 0;
|
|
2715
|
+
if (prop === "toString" || prop === "valueOf" || prop === "constructor") {
|
|
2716
|
+
throw new Error(
|
|
2717
|
+
`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.`
|
|
2718
|
+
);
|
|
677
2719
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
};
|
|
2720
|
+
let ref = aliasFieldCache.get(prop);
|
|
2721
|
+
if (!ref) {
|
|
2722
|
+
ref = { [ALIAS_FIELD_REF_BRAND]: true, alias: root, field: prop };
|
|
2723
|
+
aliasFieldCache.set(prop, ref);
|
|
683
2724
|
}
|
|
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
|
-
);
|
|
2725
|
+
return ref;
|
|
689
2726
|
},
|
|
690
2727
|
set() {
|
|
691
|
-
throw new Error(`
|
|
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}.`);
|
|
2728
|
+
throw new Error(`mutation: cannot assign on ${origin}.`);
|
|
700
2729
|
}
|
|
701
2730
|
});
|
|
702
2731
|
return proxy;
|
|
703
2732
|
}
|
|
704
|
-
function
|
|
2733
|
+
function makeInputProxy() {
|
|
705
2734
|
const cache = /* @__PURE__ */ new Map();
|
|
706
|
-
|
|
707
|
-
const proxy = new Proxy(target, {
|
|
2735
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
708
2736
|
get(_t, prop) {
|
|
709
|
-
if (prop ===
|
|
710
|
-
|
|
711
|
-
|
|
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") {
|
|
732
|
-
throw new Error(
|
|
733
|
-
"publicQueryModel/publicCommandModel: unsupported symbol access on `keys`."
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
onFieldAccess(prop);
|
|
737
|
-
let ref = cache.get(prop);
|
|
2737
|
+
if (typeof prop === "symbol") return void 0;
|
|
2738
|
+
const root = prop;
|
|
2739
|
+
let ref = cache.get(root);
|
|
738
2740
|
if (!ref) {
|
|
739
|
-
ref =
|
|
740
|
-
cache.set(
|
|
2741
|
+
ref = makeRootRef(root);
|
|
2742
|
+
cache.set(root, ref);
|
|
741
2743
|
}
|
|
742
2744
|
return ref;
|
|
743
2745
|
},
|
|
744
2746
|
set() {
|
|
745
|
-
throw new Error("
|
|
746
|
-
},
|
|
747
|
-
defineProperty() {
|
|
748
|
-
throw new Error(
|
|
749
|
-
"publicQueryModel/publicCommandModel: cannot define a property on `keys`."
|
|
750
|
-
);
|
|
751
|
-
},
|
|
752
|
-
deleteProperty() {
|
|
753
|
-
throw new Error(
|
|
754
|
-
"publicQueryModel/publicCommandModel: cannot delete a field of `keys`."
|
|
755
|
-
);
|
|
2747
|
+
throw new Error("mutation: cannot assign to the `$` placeholder proxy.");
|
|
756
2748
|
}
|
|
757
2749
|
});
|
|
758
|
-
return proxy;
|
|
759
2750
|
}
|
|
760
|
-
var
|
|
761
|
-
function
|
|
762
|
-
return typeof value === "object" && value !== null && value[
|
|
2751
|
+
var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
|
|
2752
|
+
function isMutationFragment(value) {
|
|
2753
|
+
return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
|
|
763
2754
|
}
|
|
764
|
-
|
|
765
|
-
|
|
2755
|
+
var INTENT_KEYS2 = ["create", "update", "remove"];
|
|
2756
|
+
function resolveDescriptorTarget(descriptor, alias) {
|
|
2757
|
+
const present = INTENT_KEYS2.filter((k) => descriptor[k] !== void 0);
|
|
2758
|
+
if (present.length === 0) {
|
|
2759
|
+
throw new Error(
|
|
2760
|
+
`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 }\`.`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
if (present.length > 1) {
|
|
2764
|
+
throw new Error(
|
|
2765
|
+
`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\`.`
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
const intent = present[0];
|
|
2769
|
+
const ref = descriptor[intent];
|
|
2770
|
+
const resolved = resolveModelRef(ref, intent, alias);
|
|
2771
|
+
const modelClass = resolveModelClass(resolved);
|
|
2772
|
+
return { intent, entity: { name: modelClass.name, modelClass } };
|
|
766
2773
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
2774
|
+
function resolveModelRef(ref, intent, alias) {
|
|
2775
|
+
if (ref === null || ref === void 0) {
|
|
2776
|
+
throw new Error(
|
|
2777
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' names no target model.`
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2780
|
+
if (typeof ref === "function") {
|
|
2781
|
+
const proto = ref.prototype;
|
|
2782
|
+
const isModelClass = proto instanceof DDBModel || typeof ref === "function" && ref.prototype instanceof DDBModel;
|
|
2783
|
+
if (isModelClass) {
|
|
2784
|
+
return ref;
|
|
2785
|
+
}
|
|
2786
|
+
return ref();
|
|
2787
|
+
}
|
|
2788
|
+
return ref;
|
|
775
2789
|
}
|
|
776
|
-
function
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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() {
|
|
2790
|
+
function assertFaithfulInput(input, intent, entityName) {
|
|
2791
|
+
if (input === null || typeof input !== "object") {
|
|
2792
|
+
throw new Error(
|
|
2793
|
+
`mutation: the '${intent}' fragment on '${entityName}' must declare an \`input\` object binding model fields to \`$.field\` references or literals.`
|
|
2794
|
+
);
|
|
2795
|
+
}
|
|
2796
|
+
for (const [field, leaf] of Object.entries(input)) {
|
|
2797
|
+
if (!isMutationInputRef(leaf) && !isAliasFieldRef(leaf) && !isConcreteLiteral(leaf)) {
|
|
815
2798
|
throw new Error(
|
|
816
|
-
`
|
|
2799
|
+
`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.`
|
|
817
2800
|
);
|
|
818
|
-
}
|
|
819
|
-
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
return input;
|
|
2804
|
+
}
|
|
2805
|
+
function isConcreteLiteral(value) {
|
|
2806
|
+
const t = typeof value;
|
|
2807
|
+
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
2808
|
+
}
|
|
2809
|
+
function makeFragment(intent, entity, input, use) {
|
|
2810
|
+
const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
|
|
2811
|
+
if (use !== void 0 && !isEntityWritesDefinition(use)) {
|
|
2812
|
+
throw new Error(
|
|
2813
|
+
`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.`
|
|
2814
|
+
);
|
|
2815
|
+
}
|
|
2816
|
+
return {
|
|
2817
|
+
[FRAGMENT_BRAND]: true,
|
|
2818
|
+
intent,
|
|
2819
|
+
entity,
|
|
2820
|
+
input: checked,
|
|
2821
|
+
...use !== void 0 ? { use } : {}
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
|
|
2825
|
+
function isCommandPlan(value) {
|
|
2826
|
+
return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
|
|
2827
|
+
}
|
|
2828
|
+
function mergeBindings(descriptor, intent, alias) {
|
|
2829
|
+
const merged = {};
|
|
2830
|
+
const key = descriptor.key;
|
|
2831
|
+
if (key === void 0 || key === null || typeof key !== "object") {
|
|
2832
|
+
throw new Error(
|
|
2833
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object binding the target's primary-key fields (\`{ field: $.field, \u2026 }\`).`
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2836
|
+
for (const [field, leaf] of Object.entries(key)) merged[field] = leaf;
|
|
2837
|
+
const input = descriptor.input;
|
|
2838
|
+
if (input !== void 0 && input !== null) {
|
|
2839
|
+
if (typeof input !== "object") {
|
|
820
2840
|
throw new Error(
|
|
821
|
-
`
|
|
2841
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' has a non-object \`input\`.`
|
|
822
2842
|
);
|
|
823
2843
|
}
|
|
824
|
-
|
|
825
|
-
|
|
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
|
-
}
|
|
2844
|
+
for (const [field, leaf] of Object.entries(input)) {
|
|
2845
|
+
if (field in merged) {
|
|
835
2846
|
throw new Error(
|
|
836
|
-
|
|
2847
|
+
`mutation: the '${intent}' descriptor for alias '${alias}' binds field '${field}' in both \`key\` and \`input\`. A primary-key field belongs in \`key\` only.`
|
|
837
2848
|
);
|
|
838
2849
|
}
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
2850
|
+
merged[field] = leaf;
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
return merged;
|
|
2854
|
+
}
|
|
2855
|
+
function resolveAliasRefs(input, aliasToIndex, alias) {
|
|
2856
|
+
const out = {};
|
|
2857
|
+
for (const [field, leaf] of Object.entries(input)) {
|
|
2858
|
+
if (isAliasFieldRef(leaf)) {
|
|
2859
|
+
const index = aliasToIndex.get(leaf.alias);
|
|
2860
|
+
if (index === void 0) {
|
|
2861
|
+
throw new Error(
|
|
2862
|
+
`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.`
|
|
2863
|
+
);
|
|
844
2864
|
}
|
|
845
|
-
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
2865
|
+
out[field] = `$.entity[${index}].${leaf.field}`;
|
|
2866
|
+
} else {
|
|
2867
|
+
out[field] = leaf;
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
return out;
|
|
2871
|
+
}
|
|
2872
|
+
function mutation(nameOrBody, maybeBody) {
|
|
2873
|
+
let name;
|
|
2874
|
+
let body;
|
|
2875
|
+
if (typeof nameOrBody === "string") {
|
|
2876
|
+
if (maybeBody === void 0) {
|
|
2877
|
+
throw new Error("mutation: a name was given but no body function.");
|
|
2878
|
+
}
|
|
2879
|
+
name = nameOrBody;
|
|
2880
|
+
body = maybeBody;
|
|
2881
|
+
} else {
|
|
2882
|
+
body = nameOrBody;
|
|
2883
|
+
name = "mutation";
|
|
2884
|
+
}
|
|
2885
|
+
if (typeof body !== "function") {
|
|
2886
|
+
throw new Error(
|
|
2887
|
+
"mutation: the body must be a function `$ => ({ alias: descriptor, \u2026 })` returning an alias \u2192 write-descriptor map."
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
const $ = makeInputProxy();
|
|
2891
|
+
const map = body($);
|
|
2892
|
+
if (map === null || typeof map !== "object" || Array.isArray(map)) {
|
|
2893
|
+
throw new Error(
|
|
2894
|
+
`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.`
|
|
2895
|
+
);
|
|
2896
|
+
}
|
|
2897
|
+
const aliases = Object.keys(map);
|
|
2898
|
+
if (aliases.length === 0) {
|
|
2899
|
+
throw new Error(
|
|
2900
|
+
`mutation '${name}': the body returned an empty descriptor map. A mutation must declare at least one write descriptor.`
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
const aliasToIndex = /* @__PURE__ */ new Map();
|
|
2904
|
+
aliases.forEach((alias, i) => aliasToIndex.set(alias, i));
|
|
2905
|
+
const fragments = aliases.map((alias) => {
|
|
2906
|
+
const descriptor = map[alias];
|
|
2907
|
+
if (descriptor === null || typeof descriptor !== "object") {
|
|
858
2908
|
throw new Error(
|
|
859
|
-
|
|
2909
|
+
`mutation '${name}': the value for alias '${alias}' is not a write descriptor (an object \`{ create | update | remove: Model, key, input? }\`).`
|
|
860
2910
|
);
|
|
861
2911
|
}
|
|
2912
|
+
const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
|
|
2913
|
+
const merged = mergeBindings(descriptor, intent, alias);
|
|
2914
|
+
const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
|
|
2915
|
+
return makeFragment(intent, entity, resolved, descriptor.use);
|
|
862
2916
|
});
|
|
2917
|
+
return { [COMMAND_PLAN_BRAND]: true, name, fragments };
|
|
2918
|
+
}
|
|
2919
|
+
var definePlan = mutation;
|
|
2920
|
+
|
|
2921
|
+
// src/define/contract.ts
|
|
2922
|
+
var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
|
|
2923
|
+
var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
|
|
2924
|
+
function isContractKeyRef(value) {
|
|
2925
|
+
return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
|
|
2926
|
+
}
|
|
2927
|
+
function mintContractKeyFieldRef(field) {
|
|
2928
|
+
return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
|
|
2929
|
+
}
|
|
2930
|
+
function wholeKeysSentinel() {
|
|
2931
|
+
return { [KEY_REF_BRAND]: true };
|
|
2932
|
+
}
|
|
2933
|
+
function isContractKeyFieldRef(value) {
|
|
2934
|
+
return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
|
|
2935
|
+
}
|
|
2936
|
+
var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
|
|
2937
|
+
function isContractParamRef(value) {
|
|
2938
|
+
return typeof value === "object" && value !== null && value[PARAM_REF_BRAND] === true;
|
|
2939
|
+
}
|
|
2940
|
+
function mintContractParamRef(field) {
|
|
2941
|
+
return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
|
|
863
2942
|
}
|
|
864
2943
|
var FROM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractFromRef");
|
|
865
2944
|
var COMPOSE_NODE_BRAND = /* @__PURE__ */ Symbol("graphddb:contractComposeNode");
|
|
@@ -925,69 +3004,10 @@ function entityRef(model) {
|
|
|
925
3004
|
const modelClass = resolveModelClass(model);
|
|
926
3005
|
return { name: modelClass.name, modelClass };
|
|
927
3006
|
}
|
|
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
3007
|
function isConcreteLiteral2(value) {
|
|
947
3008
|
const t = typeof value;
|
|
948
3009
|
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
949
3010
|
}
|
|
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
3011
|
function extractCompose(select, operation, entityName) {
|
|
992
3012
|
if (select === void 0) return { select: void 0, compose: [] };
|
|
993
3013
|
const projection = {};
|
|
@@ -1006,225 +3026,6 @@ function extractCompose(select, operation, entityName) {
|
|
|
1006
3026
|
}
|
|
1007
3027
|
return { select: projection, compose };
|
|
1008
3028
|
}
|
|
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
|
-
);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
return ops[0];
|
|
1227
|
-
}
|
|
1228
3029
|
function deriveReadFacts(op) {
|
|
1229
3030
|
if (op.operation === "list") {
|
|
1230
3031
|
return { resolution: "range", inputArity: "single" };
|
|
@@ -1247,103 +3048,32 @@ function deriveReadFacts(op) {
|
|
|
1247
3048
|
}
|
|
1248
3049
|
return { resolution: "point", inputArity: "either" };
|
|
1249
3050
|
}
|
|
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
3051
|
function isQueryModelContract(value) {
|
|
1282
3052
|
return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "query";
|
|
1283
3053
|
}
|
|
1284
3054
|
function isCommandModelContract(value) {
|
|
1285
3055
|
return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "command";
|
|
1286
3056
|
}
|
|
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 };
|
|
3057
|
+
function publicQueryModel(methods) {
|
|
3058
|
+
return buildQueryContract(methods);
|
|
1330
3059
|
}
|
|
1331
|
-
function buildQueryContract(methods
|
|
3060
|
+
function buildQueryContract(methods) {
|
|
1332
3061
|
const resolved = {};
|
|
1333
3062
|
for (const [name, body] of Object.entries(methods)) {
|
|
1334
|
-
|
|
3063
|
+
let op;
|
|
3064
|
+
if (isPublicReadDescriptor(body)) {
|
|
3065
|
+
op = readDescriptorToOp(name, body);
|
|
3066
|
+
} else {
|
|
1335
3067
|
throw new Error(
|
|
1336
|
-
`publicQueryModel: method '${name}' must be a
|
|
3068
|
+
`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
3069
|
);
|
|
1338
3070
|
}
|
|
1339
|
-
const op = withKeyFields(resolveMethodOp(name, body), keyFields);
|
|
1340
3071
|
if (op.operation !== "query" && op.operation !== "list") {
|
|
1341
3072
|
throw new Error(
|
|
1342
3073
|
`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
3074
|
);
|
|
1344
3075
|
}
|
|
1345
3076
|
const { resolution, inputArity } = deriveReadFacts(op);
|
|
1346
|
-
assertDeclaredArity(name, declaredArityOf(body), resolution, inputArity);
|
|
1347
3077
|
resolved[name] = {
|
|
1348
3078
|
__methodKind: "query",
|
|
1349
3079
|
op,
|
|
@@ -1356,115 +3086,61 @@ function buildQueryContract(methods, keyFields) {
|
|
|
1356
3086
|
for (const spec of Object.values(resolved)) methodSpecToContract.set(spec, contract);
|
|
1357
3087
|
return contract;
|
|
1358
3088
|
}
|
|
1359
|
-
function publicCommandModel(
|
|
1360
|
-
|
|
1361
|
-
return (methods) => buildCommandContract(methods, keyFields);
|
|
3089
|
+
function publicCommandModel(methods) {
|
|
3090
|
+
return buildCommandContract(methods);
|
|
1362
3091
|
}
|
|
1363
|
-
function buildCommandContract(methods
|
|
3092
|
+
function buildCommandContract(methods) {
|
|
1364
3093
|
const resolved = {};
|
|
1365
3094
|
for (const [name, body] of Object.entries(methods)) {
|
|
1366
|
-
if (
|
|
1367
|
-
|
|
3095
|
+
if (isPublicWriteDescriptor(body)) {
|
|
3096
|
+
const { plan: plan2, select, condition, mode } = writeDescriptorToPlan(name, body);
|
|
3097
|
+
resolved[name] = finalizeDescriptorCommand(name, plan2, select, condition, mode);
|
|
1368
3098
|
continue;
|
|
1369
3099
|
}
|
|
1370
|
-
if (
|
|
1371
|
-
|
|
1372
|
-
|
|
3100
|
+
if (isPublicComposeDescriptor(body)) {
|
|
3101
|
+
const select = body.result !== void 0 && body.result.select !== void 0 ? body.result.select : void 0;
|
|
3102
|
+
resolved[name] = finalizeDescriptorCommand(
|
|
3103
|
+
name,
|
|
3104
|
+
body.command,
|
|
3105
|
+
select,
|
|
3106
|
+
void 0,
|
|
3107
|
+
body.mode ?? "transaction"
|
|
1373
3108
|
);
|
|
3109
|
+
continue;
|
|
1374
3110
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
3111
|
+
if (isCommandPlan(body)) {
|
|
3112
|
+
resolved[name] = finalizeDescriptorCommand(
|
|
3113
|
+
name,
|
|
3114
|
+
body,
|
|
3115
|
+
void 0,
|
|
3116
|
+
void 0,
|
|
3117
|
+
"transaction"
|
|
1379
3118
|
);
|
|
3119
|
+
continue;
|
|
1380
3120
|
}
|
|
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
|
-
};
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`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).`
|
|
3123
|
+
);
|
|
1391
3124
|
}
|
|
1392
3125
|
return { __isContract: true, kind: "command", methods: resolved };
|
|
1393
3126
|
}
|
|
1394
|
-
function commandResultOf(op) {
|
|
1395
|
-
return op.operation === "update" ? "entity" : "void";
|
|
1396
|
-
}
|
|
1397
3127
|
var PLANNED_COMMAND_BRAND = /* @__PURE__ */ Symbol("graphddb:plannedCommandMethod");
|
|
1398
3128
|
function isPlannedCommandMethod(value) {
|
|
1399
|
-
return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
|
|
1400
|
-
}
|
|
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
|
-
}
|
|
3129
|
+
return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
|
|
3130
|
+
}
|
|
3131
|
+
function buildPlannedCommandMethod(name, planned) {
|
|
3132
|
+
const plan2 = compileMutationPlan(planned.plan);
|
|
3133
|
+
const primary = plan2.fragments[0];
|
|
1458
3134
|
const stamp = (fragment) => fragment.op.operation === "put" ? fragment.op : { ...fragment.op, keyFields: fragment.keyFields };
|
|
1459
3135
|
const op = stamp(primary);
|
|
1460
3136
|
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 =
|
|
3137
|
+
const ops = plan2.fragments.length > 1 ? plan2.fragments.map(stamp) : void 0;
|
|
3138
|
+
const conditionChecks = plan2.fragments.flatMap((f) => f.conditionChecks ?? []);
|
|
3139
|
+
const edgeWrites2 = plan2.fragments.flatMap((f) => f.edgeWrites ?? []);
|
|
3140
|
+
const derivedUpdates = plan2.fragments.flatMap((f) => f.derivedUpdates ?? []);
|
|
3141
|
+
const uniqueGuards = plan2.fragments.flatMap((f) => f.uniqueGuards ?? []);
|
|
3142
|
+
const outboxEvents = plan2.fragments.flatMap((f) => f.outboxEvents ?? []);
|
|
3143
|
+
const idempotencyGuards = plan2.fragments.map((f) => f.idempotencyGuard).filter((g) => g !== void 0);
|
|
1468
3144
|
if (idempotencyGuards.length > 1) {
|
|
1469
3145
|
throw new Error(
|
|
1470
3146
|
`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 +3164,21 @@ function buildPlannedCommandMethod(name, planned, keyFields) {
|
|
|
1488
3164
|
...returnSelection !== void 0 ? { returnSelection } : {}
|
|
1489
3165
|
};
|
|
1490
3166
|
}
|
|
3167
|
+
function finalizeDescriptorCommand(name, plan2, select, condition, mode) {
|
|
3168
|
+
const planned = {
|
|
3169
|
+
[PLANNED_COMMAND_BRAND]: true,
|
|
3170
|
+
plan: plan2,
|
|
3171
|
+
input: {},
|
|
3172
|
+
select
|
|
3173
|
+
};
|
|
3174
|
+
const built = buildPlannedCommandMethod(name, planned);
|
|
3175
|
+
const op = condition !== void 0 ? { ...built.op, condition } : built.op;
|
|
3176
|
+
return {
|
|
3177
|
+
...built,
|
|
3178
|
+
op,
|
|
3179
|
+
mode
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
1491
3182
|
function normalizeReturnSelection(select) {
|
|
1492
3183
|
if (select === void 0) return void 0;
|
|
1493
3184
|
const out = {};
|
|
@@ -1496,6 +3187,117 @@ function normalizeReturnSelection(select) {
|
|
|
1496
3187
|
}
|
|
1497
3188
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
1498
3189
|
}
|
|
3190
|
+
var READ_INTENT_KEYS = ["query", "list"];
|
|
3191
|
+
var WRITE_INTENT_KEYS = ["create", "update", "remove"];
|
|
3192
|
+
function isPublicComposeDescriptor(value) {
|
|
3193
|
+
return typeof value === "object" && value !== null && isCommandPlan(value.command);
|
|
3194
|
+
}
|
|
3195
|
+
function isPublicReadDescriptor(value) {
|
|
3196
|
+
return typeof value === "object" && value !== null && READ_INTENT_KEYS.some((k) => value[k] !== void 0);
|
|
3197
|
+
}
|
|
3198
|
+
function isPublicWriteDescriptor(value) {
|
|
3199
|
+
return typeof value === "object" && value !== null && WRITE_INTENT_KEYS.some((k) => value[k] !== void 0);
|
|
3200
|
+
}
|
|
3201
|
+
function resolveDescriptorModel(ref, name) {
|
|
3202
|
+
if (ref === null || ref === void 0) {
|
|
3203
|
+
throw new Error(`publicQueryModel/publicCommandModel: method '${name}' names no target model.`);
|
|
3204
|
+
}
|
|
3205
|
+
if (typeof ref === "function") {
|
|
3206
|
+
const asModel = ref.asModel;
|
|
3207
|
+
if (typeof asModel === "function") return asModel.call(ref);
|
|
3208
|
+
return resolveDescriptorModel(ref(), name);
|
|
3209
|
+
}
|
|
3210
|
+
return ref;
|
|
3211
|
+
}
|
|
3212
|
+
function descriptorKeyRecord(record, name, what) {
|
|
3213
|
+
const out = {};
|
|
3214
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
3215
|
+
if (isParam(leaf)) {
|
|
3216
|
+
out[field] = mintContractKeyFieldRef(field);
|
|
3217
|
+
} else if (isConcreteLiteral2(leaf)) {
|
|
3218
|
+
out[field] = leaf;
|
|
3219
|
+
} else {
|
|
3220
|
+
throw new Error(
|
|
3221
|
+
`publicQueryModel/publicCommandModel: method '${name}' ${what} field '${field}' must be a \`param.*\` placeholder or a concrete literal, not ${JSON.stringify(leaf)}.`
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return out;
|
|
3226
|
+
}
|
|
3227
|
+
function descriptorConditionRecord(record, name) {
|
|
3228
|
+
const out = {};
|
|
3229
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
3230
|
+
if (isParam(leaf)) {
|
|
3231
|
+
out[field] = mintContractParamRef(field);
|
|
3232
|
+
} else {
|
|
3233
|
+
out[field] = leaf;
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
return out;
|
|
3237
|
+
}
|
|
3238
|
+
function readDescriptorToOp(name, d) {
|
|
3239
|
+
const isQuery = d.query !== void 0;
|
|
3240
|
+
const isList = d.list !== void 0;
|
|
3241
|
+
if (isQuery === isList) {
|
|
3242
|
+
throw new Error(
|
|
3243
|
+
`publicQueryModel: method '${name}' must carry exactly one of \`query\` or \`list\` (the target model); it declared ${isQuery && isList ? "both" : "neither"}.`
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
if (d.key === void 0 || d.select === void 0) {
|
|
3247
|
+
throw new Error(
|
|
3248
|
+
`publicQueryModel: the '${isQuery ? "query" : "list"}' descriptor for method '${name}' must declare both \`key\` and \`select\`.`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
const model = resolveDescriptorModel(isQuery ? d.query : d.list, name);
|
|
3252
|
+
const entity = entityRef(model);
|
|
3253
|
+
const operation = isQuery ? "query" : "list";
|
|
3254
|
+
const keyFieldNames2 = Object.keys(d.key);
|
|
3255
|
+
const keyRecord = descriptorKeyRecord(d.key, name, `${operation} key`);
|
|
3256
|
+
const { select, compose } = extractCompose({ ...d.select }, operation, entity.name);
|
|
3257
|
+
const keys = isQuery ? wholeKeysSentinel() : keyRecord;
|
|
3258
|
+
return {
|
|
3259
|
+
__isContractMethodOp: true,
|
|
3260
|
+
entity,
|
|
3261
|
+
operation,
|
|
3262
|
+
keys,
|
|
3263
|
+
...isQuery ? { keyFields: keyFieldNames2 } : {},
|
|
3264
|
+
...select !== void 0 ? { select } : {},
|
|
3265
|
+
...compose.length > 0 ? { compose } : {}
|
|
3266
|
+
};
|
|
3267
|
+
}
|
|
3268
|
+
function writeDescriptorToPlan(name, d) {
|
|
3269
|
+
const present = WRITE_INTENT_KEYS.filter((k) => d[k] !== void 0);
|
|
3270
|
+
if (present.length !== 1) {
|
|
3271
|
+
throw new Error(
|
|
3272
|
+
`publicCommandModel: method '${name}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
|
|
3273
|
+
);
|
|
3274
|
+
}
|
|
3275
|
+
const intent = present[0];
|
|
3276
|
+
if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
|
|
3277
|
+
throw new Error(
|
|
3278
|
+
`publicCommandModel: the '${intent}' descriptor for method '${name}' must declare a \`key\` object binding the target's primary-key fields.`
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
const model = resolveDescriptorModel(d[intent], name);
|
|
3282
|
+
const keyFieldNames2 = Object.keys(d.key);
|
|
3283
|
+
const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
|
|
3284
|
+
const plan2 = mutation(name, ($) => {
|
|
3285
|
+
const refMap = (fields) => {
|
|
3286
|
+
const out = {};
|
|
3287
|
+
for (const f of fields) out[f] = $[f];
|
|
3288
|
+
return out;
|
|
3289
|
+
};
|
|
3290
|
+
const descriptor = {
|
|
3291
|
+
[intent]: (() => model),
|
|
3292
|
+
key: refMap(keyFieldNames2),
|
|
3293
|
+
...inputFieldNames.length > 0 ? { input: refMap(inputFieldNames) } : {}
|
|
3294
|
+
};
|
|
3295
|
+
return { [name]: descriptor };
|
|
3296
|
+
});
|
|
3297
|
+
const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
|
|
3298
|
+
const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
|
|
3299
|
+
return { plan: plan2, select, condition, mode: d.mode ?? "transaction" };
|
|
3300
|
+
}
|
|
1499
3301
|
|
|
1500
3302
|
// src/spec/mutation-command.ts
|
|
1501
3303
|
var INTENT_OPERATION = {
|
|
@@ -1928,18 +3730,18 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
|
1928
3730
|
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
|
|
1929
3731
|
};
|
|
1930
3732
|
}
|
|
1931
|
-
function compileSingleFragmentPlan(
|
|
1932
|
-
if (!isCommandPlan(
|
|
3733
|
+
function compileSingleFragmentPlan(plan2) {
|
|
3734
|
+
if (!isCommandPlan(plan2)) {
|
|
1933
3735
|
throw new Error(
|
|
1934
3736
|
"mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
|
|
1935
3737
|
);
|
|
1936
3738
|
}
|
|
1937
|
-
if (
|
|
3739
|
+
if (plan2.fragments.length !== 1) {
|
|
1938
3740
|
throw new Error(
|
|
1939
|
-
`mutation '${
|
|
3741
|
+
`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
3742
|
);
|
|
1941
3743
|
}
|
|
1942
|
-
return compileFragment(
|
|
3744
|
+
return compileFragment(plan2.fragments[0], 0, null);
|
|
1943
3745
|
}
|
|
1944
3746
|
var MAX_TRANSACT_COMPOSE_ITEMS = 25;
|
|
1945
3747
|
function keySignature(fragment) {
|
|
@@ -1963,26 +3765,26 @@ function keyLeafToken(leaf) {
|
|
|
1963
3765
|
if (isContractParamRef(leaf)) return `input:${leaf.field}`;
|
|
1964
3766
|
return `literal:${JSON.stringify(leaf)}`;
|
|
1965
3767
|
}
|
|
1966
|
-
function compileMutationPlan(
|
|
1967
|
-
if (!isCommandPlan(
|
|
3768
|
+
function compileMutationPlan(plan2) {
|
|
3769
|
+
if (!isCommandPlan(plan2)) {
|
|
1968
3770
|
throw new Error(
|
|
1969
3771
|
"mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
|
|
1970
3772
|
);
|
|
1971
3773
|
}
|
|
1972
|
-
const fragments =
|
|
3774
|
+
const fragments = plan2.fragments;
|
|
1973
3775
|
const compiled = [];
|
|
1974
3776
|
for (let i = 0; i < fragments.length; i++) {
|
|
1975
3777
|
const resolveEntityRef = (ref, consumerIndex, consumerField) => {
|
|
1976
3778
|
if (ref.fragmentIndex >= consumerIndex) {
|
|
1977
3779
|
throw new Error(
|
|
1978
|
-
`mutation '${
|
|
3780
|
+
`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
3781
|
);
|
|
1980
3782
|
}
|
|
1981
3783
|
const producer = compiled[ref.fragmentIndex];
|
|
1982
3784
|
const resolved = producedFieldLeaf(producer, ref.field);
|
|
1983
3785
|
if (resolved === void 0) {
|
|
1984
3786
|
throw new Error(
|
|
1985
|
-
`mutation '${
|
|
3787
|
+
`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
3788
|
);
|
|
1987
3789
|
}
|
|
1988
3790
|
return resolved;
|
|
@@ -1995,17 +3797,17 @@ function compileMutationPlan(plan) {
|
|
|
1995
3797
|
const prior = byKey.get(sig);
|
|
1996
3798
|
if (prior !== void 0) {
|
|
1997
3799
|
throw new Error(
|
|
1998
|
-
`mutation '${
|
|
3800
|
+
`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
3801
|
);
|
|
2000
3802
|
}
|
|
2001
3803
|
byKey.set(sig, i);
|
|
2002
3804
|
}
|
|
2003
3805
|
if (compiled.length > MAX_TRANSACT_COMPOSE_ITEMS) {
|
|
2004
3806
|
throw new Error(
|
|
2005
|
-
`mutation '${
|
|
3807
|
+
`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
3808
|
);
|
|
2007
3809
|
}
|
|
2008
|
-
return { name:
|
|
3810
|
+
return { name: plan2.name, fragments: compiled };
|
|
2009
3811
|
}
|
|
2010
3812
|
function producedFieldLeaf(producer, field) {
|
|
2011
3813
|
const op = producer.op;
|
|
@@ -2029,17 +3831,24 @@ function describeKey(fragment) {
|
|
|
2029
3831
|
}
|
|
2030
3832
|
|
|
2031
3833
|
// src/spec/guard.ts
|
|
2032
|
-
function conditionInputToSpec(condition, context,
|
|
3834
|
+
function conditionInputToSpec(condition, context, renderLeaf2, renderTreeLeaf, renderRawSlot) {
|
|
2033
3835
|
if (condition === void 0 || condition === null) return void 0;
|
|
3836
|
+
if (isRawCondition(condition)) {
|
|
3837
|
+
const { expression, names, values } = serializeRawCondition(
|
|
3838
|
+
condition,
|
|
3839
|
+
renderRawSlot
|
|
3840
|
+
);
|
|
3841
|
+
return { kind: "raw", expression, names, values };
|
|
3842
|
+
}
|
|
2034
3843
|
if (typeof condition !== "object") {
|
|
2035
3844
|
throw new Error(
|
|
2036
|
-
`${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or field
|
|
3845
|
+
`${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or a declarative field condition).`
|
|
2037
3846
|
);
|
|
2038
3847
|
}
|
|
2039
3848
|
const obj = condition;
|
|
2040
3849
|
if (obj.notExists === true) return { kind: "notExists" };
|
|
2041
|
-
if ("attributeExists" in obj) {
|
|
2042
|
-
if (
|
|
3850
|
+
if ("attributeExists" in obj && typeof obj.attributeExists === "string") {
|
|
3851
|
+
if (obj.attributeExists.length === 0) {
|
|
2043
3852
|
throw new Error(
|
|
2044
3853
|
`${context}: \`attributeExists\` must name a non-empty attribute field.`
|
|
2045
3854
|
);
|
|
@@ -2054,12 +3863,122 @@ function conditionInputToSpec(condition, context, renderLeaf) {
|
|
|
2054
3863
|
}
|
|
2055
3864
|
return { kind: "attributeNotExists", field: obj.attributeNotExists };
|
|
2056
3865
|
}
|
|
3866
|
+
if (usesOperatorTree(obj)) {
|
|
3867
|
+
const leaf = renderTreeLeaf ?? ((value, name) => defaultTreeLeaf(value, name, context));
|
|
3868
|
+
return { kind: "expr", declarative: buildTree(obj, context, leaf) };
|
|
3869
|
+
}
|
|
2057
3870
|
const fields = {};
|
|
2058
3871
|
for (const key of Object.keys(obj).sort()) {
|
|
2059
|
-
fields[key] =
|
|
3872
|
+
fields[key] = renderLeaf2(key, obj[key]);
|
|
2060
3873
|
}
|
|
2061
3874
|
return { kind: "equals", fields };
|
|
2062
3875
|
}
|
|
3876
|
+
function assertNotNestedRaw(sub, context) {
|
|
3877
|
+
if (isRawCondition(sub)) {
|
|
3878
|
+
throw new Error(
|
|
3879
|
+
`${context}: a raw \`cond\` write condition may be used as a whole condition, but not nested inside an \`and\` / \`or\` / \`not\` group of a serializable (public-contract / Python-bridge) command. Move the \`cond\` to the top level, or express the group declaratively.`
|
|
3880
|
+
);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
var LOGICAL_KEYS2 = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
3884
|
+
var OPERATOR_KEYS2 = /* @__PURE__ */ new Set([
|
|
3885
|
+
"eq",
|
|
3886
|
+
"ne",
|
|
3887
|
+
"gt",
|
|
3888
|
+
"ge",
|
|
3889
|
+
"lt",
|
|
3890
|
+
"le",
|
|
3891
|
+
"between",
|
|
3892
|
+
"in",
|
|
3893
|
+
"beginsWith",
|
|
3894
|
+
"contains",
|
|
3895
|
+
"notContains",
|
|
3896
|
+
"attributeExists",
|
|
3897
|
+
"attributeType",
|
|
3898
|
+
"size"
|
|
3899
|
+
]);
|
|
3900
|
+
function isOperatorObject2(value) {
|
|
3901
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
|
|
3902
|
+
return false;
|
|
3903
|
+
}
|
|
3904
|
+
const keys = Object.keys(value);
|
|
3905
|
+
if (keys.length === 0) return false;
|
|
3906
|
+
return keys.every((k) => OPERATOR_KEYS2.has(k));
|
|
3907
|
+
}
|
|
3908
|
+
function usesOperatorTree(obj) {
|
|
3909
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3910
|
+
if (LOGICAL_KEYS2.has(key)) return true;
|
|
3911
|
+
if (isOperatorObject2(value)) return true;
|
|
3912
|
+
}
|
|
3913
|
+
return false;
|
|
3914
|
+
}
|
|
3915
|
+
function defaultTreeLeaf(value, name, context) {
|
|
3916
|
+
void name;
|
|
3917
|
+
if (value instanceof Date) return value.toISOString();
|
|
3918
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
3919
|
+
return value;
|
|
3920
|
+
}
|
|
3921
|
+
throw new Error(
|
|
3922
|
+
`${context}: condition operand must be a concrete literal or a param; got ${typeof value}.`
|
|
3923
|
+
);
|
|
3924
|
+
}
|
|
3925
|
+
function conditionParamName(field, op, index) {
|
|
3926
|
+
if (op === "between") return `${field}_${index === 0 ? "lo" : "hi"}`;
|
|
3927
|
+
if (op === "in") return `${field}_${index ?? 0}`;
|
|
3928
|
+
if (op === "size") return `${field}_size`;
|
|
3929
|
+
return field;
|
|
3930
|
+
}
|
|
3931
|
+
function buildTree(obj, context, renderTreeLeaf) {
|
|
3932
|
+
const out = {};
|
|
3933
|
+
for (const key of Object.keys(obj).sort()) {
|
|
3934
|
+
const value = obj[key];
|
|
3935
|
+
if (value === void 0) continue;
|
|
3936
|
+
if (key === "and" || key === "or") {
|
|
3937
|
+
out[key] = value.map((sub) => {
|
|
3938
|
+
assertNotNestedRaw(sub, context);
|
|
3939
|
+
return buildTree(sub, context, renderTreeLeaf);
|
|
3940
|
+
});
|
|
3941
|
+
continue;
|
|
3942
|
+
}
|
|
3943
|
+
if (key === "not") {
|
|
3944
|
+
assertNotNestedRaw(value, context);
|
|
3945
|
+
out[key] = buildTree(
|
|
3946
|
+
value,
|
|
3947
|
+
context,
|
|
3948
|
+
renderTreeLeaf
|
|
3949
|
+
);
|
|
3950
|
+
continue;
|
|
3951
|
+
}
|
|
3952
|
+
if (!isOperatorObject2(value)) {
|
|
3953
|
+
out[key] = { eq: renderTreeLeaf(value, conditionParamName(key, "eq")) };
|
|
3954
|
+
continue;
|
|
3955
|
+
}
|
|
3956
|
+
const ops = value;
|
|
3957
|
+
const rendered = {};
|
|
3958
|
+
for (const op of Object.keys(ops).sort()) {
|
|
3959
|
+
const opVal = ops[op];
|
|
3960
|
+
if (op === "between") {
|
|
3961
|
+
const [lo, hi] = opVal;
|
|
3962
|
+
rendered[op] = [
|
|
3963
|
+
renderTreeLeaf(lo, conditionParamName(key, op, 0)),
|
|
3964
|
+
renderTreeLeaf(hi, conditionParamName(key, op, 1))
|
|
3965
|
+
];
|
|
3966
|
+
} else if (op === "in") {
|
|
3967
|
+
rendered[op] = opVal.map(
|
|
3968
|
+
(v, i) => renderTreeLeaf(v, conditionParamName(key, op, i))
|
|
3969
|
+
);
|
|
3970
|
+
} else if (op === "attributeExists") {
|
|
3971
|
+
rendered[op] = opVal;
|
|
3972
|
+
} else if (op === "attributeType") {
|
|
3973
|
+
rendered[op] = String(opVal);
|
|
3974
|
+
} else {
|
|
3975
|
+
rendered[op] = renderTreeLeaf(opVal, conditionParamName(key, op));
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
out[key] = rendered;
|
|
3979
|
+
}
|
|
3980
|
+
return out;
|
|
3981
|
+
}
|
|
2063
3982
|
function assertSupportedCondition(commandName, condition) {
|
|
2064
3983
|
if (condition.kind === "notExists") return;
|
|
2065
3984
|
if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
|
|
@@ -2086,8 +4005,25 @@ function assertSupportedCondition(commandName, condition) {
|
|
|
2086
4005
|
}
|
|
2087
4006
|
return;
|
|
2088
4007
|
}
|
|
4008
|
+
if (condition.kind === "raw") {
|
|
4009
|
+
if (typeof condition.expression !== "string" || condition.expression.length === 0) {
|
|
4010
|
+
throw new Error(
|
|
4011
|
+
`Command '${commandName}' has an empty raw \`cond\` write condition.`
|
|
4012
|
+
);
|
|
4013
|
+
}
|
|
4014
|
+
return;
|
|
4015
|
+
}
|
|
4016
|
+
if (condition.kind === "expr") {
|
|
4017
|
+
const tree = condition.declarative;
|
|
4018
|
+
if (!tree || Object.keys(tree).length === 0) {
|
|
4019
|
+
throw new Error(
|
|
4020
|
+
`Command '${commandName}' has an empty declarative write condition.`
|
|
4021
|
+
);
|
|
4022
|
+
}
|
|
4023
|
+
return;
|
|
4024
|
+
}
|
|
2089
4025
|
throw new Error(
|
|
2090
|
-
`Command '${commandName}' has an unsupported write condition
|
|
4026
|
+
`Command '${commandName}' has an unsupported write condition.`
|
|
2091
4027
|
);
|
|
2092
4028
|
}
|
|
2093
4029
|
function assertJsonSerializable(value, path = "$") {
|
|
@@ -2500,17 +4436,17 @@ function verifyWrite(instrs, surfaced) {
|
|
|
2500
4436
|
"defineTransaction: write operation diverged between evaluation passes; the callback must be a pure, declarative description of the writes."
|
|
2501
4437
|
);
|
|
2502
4438
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
4439
|
+
verifyRecord(instrs.map((x) => x.item), surfaced, `${op} item`);
|
|
4440
|
+
verifyRecord(instrs.map((x) => x.key), surfaced, `${op} key`);
|
|
4441
|
+
verifyRecord(instrs.map((x) => x.changes), surfaced, `${op} changes`);
|
|
4442
|
+
verifyRecord(
|
|
2507
4443
|
instrs.map((x) => x.condition),
|
|
2508
4444
|
surfaced,
|
|
2509
4445
|
`${op} condition`
|
|
2510
4446
|
);
|
|
2511
4447
|
verifyWhen(instrs.map((x) => x.when), surfaced, `${op} when`);
|
|
2512
4448
|
}
|
|
2513
|
-
function
|
|
4449
|
+
function verifyRecord(records, surfaced, context) {
|
|
2514
4450
|
const present = records.filter((r) => r !== void 0);
|
|
2515
4451
|
if (present.length === 0) return;
|
|
2516
4452
|
if (present.length !== records.length) {
|
|
@@ -2528,7 +4464,7 @@ function verifyRecord2(records, surfaced, context) {
|
|
|
2528
4464
|
}
|
|
2529
4465
|
}
|
|
2530
4466
|
for (const field of keys) {
|
|
2531
|
-
|
|
4467
|
+
verifyLeaf(
|
|
2532
4468
|
present.map((r) => r[field]),
|
|
2533
4469
|
surfaced,
|
|
2534
4470
|
`${context} field '${field}'`
|
|
@@ -2546,9 +4482,9 @@ function verifyWhen(whens, surfaced, context) {
|
|
|
2546
4482
|
for (const w of present) {
|
|
2547
4483
|
if (isTransactionRef(w.left)) surfaced.add(w.left.token);
|
|
2548
4484
|
}
|
|
2549
|
-
|
|
4485
|
+
verifyLeaf(present.map((w) => w.right), surfaced, `${context} right-hand value`);
|
|
2550
4486
|
}
|
|
2551
|
-
function
|
|
4487
|
+
function verifyLeaf(values, surfaced, context) {
|
|
2552
4488
|
const refTokens = values.map((v) => isTransactionRef(v) ? v.token : void 0);
|
|
2553
4489
|
const anyRef = refTokens.some((t) => t !== void 0);
|
|
2554
4490
|
if (anyRef) {
|
|
@@ -2566,7 +4502,7 @@ function verifyLeaf2(values, surfaced, context) {
|
|
|
2566
4502
|
surfaced.add(token);
|
|
2567
4503
|
return;
|
|
2568
4504
|
}
|
|
2569
|
-
const norm = values.map(
|
|
4505
|
+
const norm = values.map(normalizeLiteral);
|
|
2570
4506
|
for (let i = 1; i < norm.length; i++) {
|
|
2571
4507
|
if (norm[i] !== norm[0]) {
|
|
2572
4508
|
throw new Error(
|
|
@@ -2575,7 +4511,7 @@ function verifyLeaf2(values, surfaced, context) {
|
|
|
2575
4511
|
}
|
|
2576
4512
|
}
|
|
2577
4513
|
}
|
|
2578
|
-
function
|
|
4514
|
+
function normalizeLiteral(value) {
|
|
2579
4515
|
if (value === null) return "t:null";
|
|
2580
4516
|
if (value instanceof Date) return `t:date:${value.toISOString()}`;
|
|
2581
4517
|
const t = typeof value;
|
|
@@ -2636,6 +4572,17 @@ function leafTemplate(value, context) {
|
|
|
2636
4572
|
`defineTransaction: ${context} must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
|
|
2637
4573
|
);
|
|
2638
4574
|
}
|
|
4575
|
+
function conditionTreeLeaf(value, name, context) {
|
|
4576
|
+
void name;
|
|
4577
|
+
if (isTransactionRef(value)) return { $param: tokenName(value.token) };
|
|
4578
|
+
if (value instanceof Date) return value.toISOString();
|
|
4579
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
4580
|
+
return value;
|
|
4581
|
+
}
|
|
4582
|
+
throw new Error(
|
|
4583
|
+
`defineTransaction: ${context} condition operand must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
|
|
4584
|
+
);
|
|
4585
|
+
}
|
|
2639
4586
|
function templateRecord2(structure, context) {
|
|
2640
4587
|
const out = {};
|
|
2641
4588
|
for (const field of Object.keys(structure).sort()) {
|
|
@@ -2679,7 +4626,12 @@ function conditionSpec(condition, commandName) {
|
|
|
2679
4626
|
const spec = conditionInputToSpec(
|
|
2680
4627
|
condition,
|
|
2681
4628
|
commandName,
|
|
2682
|
-
(field, value) => leafTemplate(value, `condition field '${field}'`)
|
|
4629
|
+
(field, value) => leafTemplate(value, `condition field '${field}'`),
|
|
4630
|
+
(value, name) => conditionTreeLeaf(value, name, commandName),
|
|
4631
|
+
// A raw `cond` value slot in a transaction is a field reference
|
|
4632
|
+
// (`p.*` / forEach element) → a `{ $param }` marker carrying its token name;
|
|
4633
|
+
// a concrete literal falls through to the default renderer (issue #114-B).
|
|
4634
|
+
(part) => isTransactionRef(part) ? { $param: tokenName(part.token) } : void 0
|
|
2683
4635
|
);
|
|
2684
4636
|
if (spec) assertSupportedCondition(commandName, spec);
|
|
2685
4637
|
return spec;
|
|
@@ -2866,113 +4818,6 @@ ${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
|
|
|
2866
4818
|
throw new Error(first.message + suffix);
|
|
2867
4819
|
}
|
|
2868
4820
|
|
|
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
4821
|
// src/spec/contract-boundary-check.ts
|
|
2977
4822
|
function buildOwnershipIndex(contexts) {
|
|
2978
4823
|
const modelOwner = /* @__PURE__ */ new Map();
|
|
@@ -3202,12 +5047,12 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
|
|
|
3202
5047
|
const keyCondition2 = {
|
|
3203
5048
|
PK: pk2
|
|
3204
5049
|
};
|
|
3205
|
-
const
|
|
5050
|
+
const range2 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
|
|
3206
5051
|
return {
|
|
3207
5052
|
type: "Query",
|
|
3208
5053
|
tableName,
|
|
3209
5054
|
keyCondition: keyCondition2,
|
|
3210
|
-
...
|
|
5055
|
+
...range2 ? { rangeCondition: range2 } : {},
|
|
3211
5056
|
projection: projectionFields(select),
|
|
3212
5057
|
limit,
|
|
3213
5058
|
resultPath,
|
|
@@ -3223,13 +5068,13 @@ function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPat
|
|
|
3223
5068
|
resolved.partial
|
|
3224
5069
|
);
|
|
3225
5070
|
const keyCondition = { [`${gsi.indexName}PK`]: pk };
|
|
3226
|
-
const
|
|
5071
|
+
const range = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
|
|
3227
5072
|
return {
|
|
3228
5073
|
type: "Query",
|
|
3229
5074
|
tableName,
|
|
3230
5075
|
indexName: gsi.indexName,
|
|
3231
5076
|
keyCondition,
|
|
3232
|
-
...
|
|
5077
|
+
...range ? { rangeCondition: range } : {},
|
|
3233
5078
|
projection: projectionFields(select),
|
|
3234
5079
|
limit,
|
|
3235
5080
|
resultPath,
|
|
@@ -3337,9 +5182,32 @@ function extractCondition(def) {
|
|
|
3337
5182
|
return conditionInputToSpec(
|
|
3338
5183
|
def.condition,
|
|
3339
5184
|
`Command on '${def.entity.name}'`,
|
|
3340
|
-
(field, value) => templateLeaf(field, value)
|
|
5185
|
+
(field, value) => templateLeaf(field, value),
|
|
5186
|
+
(value, name) => conditionTreeLeaf2(value, name),
|
|
5187
|
+
// A raw `cond` value slot (issue #114-B): a contract param / key ref carries
|
|
5188
|
+
// its own name; a plain `param.*` falls through to the positional default.
|
|
5189
|
+
(part) => {
|
|
5190
|
+
if (isContractParamRef(part)) return { $param: tokenName2(part.token) };
|
|
5191
|
+
if (isContractKeyFieldRef(part)) return { $param: part.field };
|
|
5192
|
+
return void 0;
|
|
5193
|
+
}
|
|
3341
5194
|
);
|
|
3342
5195
|
}
|
|
5196
|
+
function conditionTreeLeaf2(value, name) {
|
|
5197
|
+
if (isContractParamRef(value)) return { $param: tokenName2(value.token) };
|
|
5198
|
+
if (isContractKeyFieldRef(value)) return { $param: value.field };
|
|
5199
|
+
if (isParam(value)) return { $param: name };
|
|
5200
|
+
if (value instanceof Date) return value.toISOString();
|
|
5201
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
5202
|
+
return value;
|
|
5203
|
+
}
|
|
5204
|
+
throw new Error(
|
|
5205
|
+
`A declarative write-condition operand must be a concrete literal or a param reference; got ${typeof value}.`
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
5208
|
+
function tokenName2(token) {
|
|
5209
|
+
return token.startsWith("{") && token.endsWith("}") ? token.slice(1, -1) : token;
|
|
5210
|
+
}
|
|
3343
5211
|
function buildQuerySpec(def) {
|
|
3344
5212
|
const operations = buildReadOperations(def);
|
|
3345
5213
|
const executionPlan = deriveExecutionPlan(operations.map((op) => op.resultPath));
|
|
@@ -3497,6 +5365,7 @@ function convertLeaf(leaf, bindField, place, context) {
|
|
|
3497
5365
|
);
|
|
3498
5366
|
}
|
|
3499
5367
|
function convertCondition(condition, place, context) {
|
|
5368
|
+
if (isRawCondition(condition)) return condition;
|
|
3500
5369
|
if (typeof condition === "object" && condition !== null) {
|
|
3501
5370
|
const obj = condition;
|
|
3502
5371
|
if (obj.notExists === true) return { notExists: true };
|
|
@@ -3659,8 +5528,32 @@ function toElementRecord(record, keyParams) {
|
|
|
3659
5528
|
return out;
|
|
3660
5529
|
}
|
|
3661
5530
|
function toElementCondition(condition, keyParams) {
|
|
3662
|
-
if (condition.kind
|
|
3663
|
-
|
|
5531
|
+
if (condition.kind === "equals") {
|
|
5532
|
+
return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
|
|
5533
|
+
}
|
|
5534
|
+
if (condition.kind === "expr") {
|
|
5535
|
+
return { kind: "expr", declarative: toElementTree(condition.declarative, keyParams) };
|
|
5536
|
+
}
|
|
5537
|
+
if (condition.kind === "raw") {
|
|
5538
|
+
return {
|
|
5539
|
+
kind: "raw",
|
|
5540
|
+
expression: condition.expression,
|
|
5541
|
+
names: condition.names,
|
|
5542
|
+
values: toElementTree(condition.values, keyParams)
|
|
5543
|
+
};
|
|
5544
|
+
}
|
|
5545
|
+
return condition;
|
|
5546
|
+
}
|
|
5547
|
+
function toElementTree(node, keyParams) {
|
|
5548
|
+
if (Array.isArray(node)) return node.map((n) => toElementTree(n, keyParams));
|
|
5549
|
+
if (node === null || typeof node !== "object") return node;
|
|
5550
|
+
const obj = node;
|
|
5551
|
+
if (typeof obj.$param === "string" && !obj.$param.startsWith("item.")) {
|
|
5552
|
+
return keyParams.has(obj.$param) ? { $param: `item.${obj.$param}` } : obj;
|
|
5553
|
+
}
|
|
5554
|
+
const out = {};
|
|
5555
|
+
for (const [k, v] of Object.entries(obj)) out[k] = toElementTree(v, keyParams);
|
|
5556
|
+
return out;
|
|
3664
5557
|
}
|
|
3665
5558
|
function synthesizeBatchTransaction(commandSpec, op) {
|
|
3666
5559
|
const keyParams = new Set(keyParamNames(op));
|
|
@@ -3933,15 +5826,9 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
|
|
|
3933
5826
|
}
|
|
3934
5827
|
return { params: sortedParams(params), items, maxItems: items.length };
|
|
3935
5828
|
}
|
|
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 };
|
|
5829
|
+
function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
|
|
5830
|
+
if (mode === "parallel") {
|
|
5831
|
+
return { mode: "parallel", operation: opName };
|
|
3945
5832
|
}
|
|
3946
5833
|
const txName = batchTxName(contractName, methodName);
|
|
3947
5834
|
transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
|
|
@@ -4076,15 +5963,15 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
|
|
|
4076
5963
|
returnSelection
|
|
4077
5964
|
);
|
|
4078
5965
|
}
|
|
4079
|
-
const batch =
|
|
4080
|
-
method.
|
|
5966
|
+
const batch = method.mode !== void 0 && !promotedToTransaction ? modeTargetFor(
|
|
5967
|
+
method.mode,
|
|
4081
5968
|
op,
|
|
4082
5969
|
commandSpec,
|
|
4083
5970
|
contractName,
|
|
4084
5971
|
methodName,
|
|
4085
5972
|
opName,
|
|
4086
5973
|
transactionSpecs
|
|
4087
|
-
);
|
|
5974
|
+
) : void 0;
|
|
4088
5975
|
const single = promotedToTransaction ? { mode: "transaction", transaction: opName } : { mode: "op", operation: opName };
|
|
4089
5976
|
methods[methodName] = {
|
|
4090
5977
|
inputArity: method.inputArity,
|
|
@@ -4197,13 +6084,28 @@ export {
|
|
|
4197
6084
|
buildManifest,
|
|
4198
6085
|
isSelectBuilder,
|
|
4199
6086
|
normalizeSelectSpec,
|
|
4200
|
-
normalizeTopLevelSelect,
|
|
4201
|
-
buildProject,
|
|
4202
|
-
buildRelation,
|
|
4203
6087
|
detectRelationFields,
|
|
4204
6088
|
getImplicitKeyFields,
|
|
4205
|
-
|
|
4206
|
-
|
|
6089
|
+
buildProjection,
|
|
6090
|
+
compileFilterExpression,
|
|
6091
|
+
evaluateFilter,
|
|
6092
|
+
plan,
|
|
6093
|
+
execute,
|
|
6094
|
+
hydrate,
|
|
6095
|
+
encodeCursor,
|
|
6096
|
+
decodeCursor,
|
|
6097
|
+
executeListInternal,
|
|
6098
|
+
executeList,
|
|
6099
|
+
RELATION_TRAVERSAL_CONCURRENCY,
|
|
6100
|
+
mapWithConcurrency,
|
|
6101
|
+
deriveCompositionPlan,
|
|
6102
|
+
validateDepth,
|
|
6103
|
+
resolveRelations,
|
|
6104
|
+
executeQuery,
|
|
6105
|
+
executeExplain,
|
|
6106
|
+
BatchGetResult,
|
|
6107
|
+
executeBatchGet,
|
|
6108
|
+
executeBatchWrite,
|
|
4207
6109
|
LIFECYCLE_CONTRACT_MARKER,
|
|
4208
6110
|
isLifecycleContract,
|
|
4209
6111
|
ENTITY_WRITES_MARKER,
|
|
@@ -4211,11 +6113,6 @@ export {
|
|
|
4211
6113
|
entityWrites,
|
|
4212
6114
|
getEntityWrites,
|
|
4213
6115
|
lifecyclePhaseForIntent,
|
|
4214
|
-
isMutationInputRef,
|
|
4215
|
-
isMutationFragment,
|
|
4216
|
-
isCommandPlan,
|
|
4217
|
-
mutation,
|
|
4218
|
-
definePlan,
|
|
4219
6116
|
OLD_VALUE_NAMESPACE,
|
|
4220
6117
|
EDGE_WRITES_MARKER,
|
|
4221
6118
|
isEdgeWritesDefinition,
|
|
@@ -4228,7 +6125,13 @@ export {
|
|
|
4228
6125
|
compileFragment,
|
|
4229
6126
|
compileSingleFragmentPlan,
|
|
4230
6127
|
compileMutationPlan,
|
|
4231
|
-
|
|
6128
|
+
executeCommandMethod,
|
|
6129
|
+
DDBModel,
|
|
6130
|
+
isMutationInputRef,
|
|
6131
|
+
isMutationFragment,
|
|
6132
|
+
isCommandPlan,
|
|
6133
|
+
mutation,
|
|
6134
|
+
definePlan,
|
|
4232
6135
|
isContractKeyRef,
|
|
4233
6136
|
mintContractKeyFieldRef,
|
|
4234
6137
|
wholeKeysSentinel,
|
|
@@ -4240,21 +6143,12 @@ export {
|
|
|
4240
6143
|
from,
|
|
4241
6144
|
query,
|
|
4242
6145
|
contractOfMethodSpec,
|
|
4243
|
-
recordContractOp,
|
|
4244
|
-
isRecordingContractMethod,
|
|
4245
|
-
point,
|
|
4246
|
-
range,
|
|
4247
6146
|
isQueryModelContract,
|
|
4248
6147
|
isCommandModelContract,
|
|
4249
6148
|
publicQueryModel,
|
|
4250
6149
|
publicCommandModel,
|
|
4251
6150
|
isPlannedCommandMethod,
|
|
4252
|
-
|
|
4253
|
-
RELATION_TRAVERSAL_CONCURRENCY,
|
|
4254
|
-
mapWithConcurrency,
|
|
4255
|
-
buildRelationExecutionPlan,
|
|
4256
|
-
deriveCompositionPlan,
|
|
4257
|
-
stageOfResultPath,
|
|
6151
|
+
conditionParamName,
|
|
4258
6152
|
assertSupportedCondition,
|
|
4259
6153
|
assertJsonSerializable,
|
|
4260
6154
|
assertBundleSerializable,
|