graphddb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +553 -0
- package/dist/chunk-347U24SB.js +1818 -0
- package/dist/chunk-6LEHSX45.js +4276 -0
- package/dist/chunk-UNRQ5YJT.js +461 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +890 -0
- package/dist/index.d.ts +5309 -0
- package/dist/index.js +2887 -0
- package/dist/testing/index.d.ts +182 -0
- package/dist/testing/index.js +898 -0
- package/dist/types-CDrWiPxp.d.ts +1203 -0
- package/package.json +76 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2887 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EDGE_WRITES_MARKER,
|
|
3
|
+
ENTITY_WRITES_MARKER,
|
|
4
|
+
LIFECYCLE_CONTRACT_MARKER,
|
|
5
|
+
MARKER_ROW_ENTITY,
|
|
6
|
+
OLD_VALUE_NAMESPACE,
|
|
7
|
+
RELATION_TRAVERSAL_CONCURRENCY,
|
|
8
|
+
SPEC_VERSION,
|
|
9
|
+
assertBundleSerializable,
|
|
10
|
+
assertContractBoundaries,
|
|
11
|
+
assertContractN1Safe,
|
|
12
|
+
assertJsonSerializable,
|
|
13
|
+
assertSupportedCondition,
|
|
14
|
+
buildBridgeBundle,
|
|
15
|
+
buildContexts,
|
|
16
|
+
buildContracts,
|
|
17
|
+
buildManifest,
|
|
18
|
+
buildOperations,
|
|
19
|
+
buildProject,
|
|
20
|
+
buildQuerySpec,
|
|
21
|
+
buildRelation,
|
|
22
|
+
buildRelationExecutionPlan,
|
|
23
|
+
buildTransactionSpec,
|
|
24
|
+
buildTransactions,
|
|
25
|
+
collectContractBoundaryViolations,
|
|
26
|
+
collectContractN1Violations,
|
|
27
|
+
command,
|
|
28
|
+
compileFragment,
|
|
29
|
+
compileMutationPlan,
|
|
30
|
+
compileSingleFragmentPlan,
|
|
31
|
+
contractOfMethodSpec,
|
|
32
|
+
definePlan,
|
|
33
|
+
defineTransaction,
|
|
34
|
+
defineTransactions,
|
|
35
|
+
deriveCompositionPlan,
|
|
36
|
+
deriveEdgeWriteItems,
|
|
37
|
+
deriveEdgeWriteItemsFor,
|
|
38
|
+
deriveModelEdgeWriteItems,
|
|
39
|
+
detectRelationFields,
|
|
40
|
+
edgeWrites,
|
|
41
|
+
entityWrites,
|
|
42
|
+
from,
|
|
43
|
+
getEdgeWrites,
|
|
44
|
+
getEntityWrites,
|
|
45
|
+
getImplicitKeyFields,
|
|
46
|
+
isCommandBatchMode,
|
|
47
|
+
isCommandModelContract,
|
|
48
|
+
isCommandPlan,
|
|
49
|
+
isContractComposeNode,
|
|
50
|
+
isContractFromRef,
|
|
51
|
+
isContractKeyFieldRef,
|
|
52
|
+
isContractKeyRef,
|
|
53
|
+
isContractParamRef,
|
|
54
|
+
isEdgeWritesDefinition,
|
|
55
|
+
isEntityWritesDefinition,
|
|
56
|
+
isLifecycleContract,
|
|
57
|
+
isMutationFragment,
|
|
58
|
+
isMutationInputRef,
|
|
59
|
+
isParam,
|
|
60
|
+
isPlannedCommandMethod,
|
|
61
|
+
isQueryModelContract,
|
|
62
|
+
isRecordingContractMethod,
|
|
63
|
+
isSelectBuilder,
|
|
64
|
+
isTransactionRef,
|
|
65
|
+
lifecyclePhaseForIntent,
|
|
66
|
+
mapWithConcurrency,
|
|
67
|
+
mintContractKeyFieldRef,
|
|
68
|
+
mintContractParamRef,
|
|
69
|
+
mutation,
|
|
70
|
+
normalizeSelectSpec,
|
|
71
|
+
normalizeTopLevelSelect,
|
|
72
|
+
param,
|
|
73
|
+
point,
|
|
74
|
+
publicCommandModel,
|
|
75
|
+
publicQueryModel,
|
|
76
|
+
query,
|
|
77
|
+
range,
|
|
78
|
+
recordContractOp,
|
|
79
|
+
resolveLifecycle,
|
|
80
|
+
stageOfResultPath,
|
|
81
|
+
when,
|
|
82
|
+
wholeKeysSentinel
|
|
83
|
+
} from "./chunk-6LEHSX45.js";
|
|
84
|
+
import {
|
|
85
|
+
CdcEmulator,
|
|
86
|
+
createCdcEmulator
|
|
87
|
+
} from "./chunk-UNRQ5YJT.js";
|
|
88
|
+
import {
|
|
89
|
+
BATCH_GET_MAX_KEYS,
|
|
90
|
+
BATCH_WRITE_MAX_ITEMS,
|
|
91
|
+
ChangeCaptureRegistry,
|
|
92
|
+
ClientManager,
|
|
93
|
+
DynamoExecutor,
|
|
94
|
+
Linter,
|
|
95
|
+
MAX_TRANSACT_ITEMS,
|
|
96
|
+
MetadataRegistry,
|
|
97
|
+
TableMapping,
|
|
98
|
+
TransactionContext,
|
|
99
|
+
attachHiddenKey,
|
|
100
|
+
attachModelClass,
|
|
101
|
+
buildConditionExpression,
|
|
102
|
+
buildDeleteInput,
|
|
103
|
+
buildPutInput,
|
|
104
|
+
buildUpdateExpression,
|
|
105
|
+
buildUpdateInput,
|
|
106
|
+
captureWrite,
|
|
107
|
+
collapseSameKeyItems,
|
|
108
|
+
commitTransaction,
|
|
109
|
+
createColumnMap,
|
|
110
|
+
createDefaultLinter,
|
|
111
|
+
execItemKeySignature,
|
|
112
|
+
executeDelete,
|
|
113
|
+
executePut,
|
|
114
|
+
executeTransaction,
|
|
115
|
+
executeUpdate,
|
|
116
|
+
gsi,
|
|
117
|
+
gsiAmbiguityRule,
|
|
118
|
+
isColumn,
|
|
119
|
+
isKeySegment,
|
|
120
|
+
k,
|
|
121
|
+
key,
|
|
122
|
+
missingGsiRule,
|
|
123
|
+
noScanRule,
|
|
124
|
+
relationDepthRule,
|
|
125
|
+
requireLimitRule,
|
|
126
|
+
resolveKey,
|
|
127
|
+
resolveModelClass,
|
|
128
|
+
resolveSegmentedKey,
|
|
129
|
+
serializeFieldValue
|
|
130
|
+
} from "./chunk-347U24SB.js";
|
|
131
|
+
|
|
132
|
+
// src/metadata/prefix.ts
|
|
133
|
+
function derivePrefix(customPrefix, className) {
|
|
134
|
+
if (customPrefix != null) {
|
|
135
|
+
return `${customPrefix}#`;
|
|
136
|
+
}
|
|
137
|
+
if (!className) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'Cannot derive PK prefix: class name is undefined. Provide an explicit prefix via @model({ prefix: "..." }).'
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const base = className.endsWith("Model") ? className.slice(0, -"Model".length) : className;
|
|
143
|
+
return `${base}#`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/metadata/validation.ts
|
|
147
|
+
function isSubsetOf(a, b) {
|
|
148
|
+
const setB = new Set(b);
|
|
149
|
+
return a.length > 0 && a.every((f) => setB.has(f));
|
|
150
|
+
}
|
|
151
|
+
function validateGsiAmbiguity(primaryKey, gsiDefinitions) {
|
|
152
|
+
const patterns = [];
|
|
153
|
+
if (primaryKey) {
|
|
154
|
+
patterns.push({
|
|
155
|
+
label: "primaryKey",
|
|
156
|
+
fieldNames: primaryKey.inputFieldNames
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
for (const gsi2 of gsiDefinitions) {
|
|
160
|
+
patterns.push({ label: gsi2.indexName, fieldNames: gsi2.inputFieldNames });
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < patterns.length; i++) {
|
|
163
|
+
for (let j = i + 1; j < patterns.length; j++) {
|
|
164
|
+
const a = patterns[i];
|
|
165
|
+
const b = patterns[j];
|
|
166
|
+
if (isSubsetOf(a.fieldNames, b.fieldNames) || isSubsetOf(b.fieldNames, a.fieldNames)) {
|
|
167
|
+
const smaller = a.fieldNames.length <= b.fieldNames.length ? a : b;
|
|
168
|
+
throw new Error(
|
|
169
|
+
`Ambiguous GSI resolution: field set [${smaller.fieldNames.join(", ")}] matches both '${a.label}' and '${b.label}'. Each access pattern must have a unique field combination.`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/decorators/collector.ts
|
|
177
|
+
var pendingFields = [];
|
|
178
|
+
var pendingEmbedded = [];
|
|
179
|
+
var pendingRelations = [];
|
|
180
|
+
function collectField(field2) {
|
|
181
|
+
pendingFields.push(field2);
|
|
182
|
+
}
|
|
183
|
+
function collectEmbedded(embedded2) {
|
|
184
|
+
pendingEmbedded.push(embedded2);
|
|
185
|
+
}
|
|
186
|
+
function collectRelation(relation) {
|
|
187
|
+
pendingRelations.push(relation);
|
|
188
|
+
}
|
|
189
|
+
function drainFields() {
|
|
190
|
+
const result = pendingFields;
|
|
191
|
+
pendingFields = [];
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
function drainEmbedded() {
|
|
195
|
+
const result = pendingEmbedded;
|
|
196
|
+
pendingEmbedded = [];
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
function drainRelations() {
|
|
200
|
+
const result = pendingRelations;
|
|
201
|
+
pendingRelations = [];
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/decorators/model.ts
|
|
206
|
+
function model(options) {
|
|
207
|
+
return function(target, context) {
|
|
208
|
+
const fields = drainFields();
|
|
209
|
+
const embeddedFields = drainEmbedded();
|
|
210
|
+
const relations = drainRelations();
|
|
211
|
+
const prefix = derivePrefix(options.prefix, context.name);
|
|
212
|
+
MetadataRegistry.register(target, {
|
|
213
|
+
tableName: options.table,
|
|
214
|
+
prefix,
|
|
215
|
+
fields,
|
|
216
|
+
primaryKey: null,
|
|
217
|
+
gsiDefinitions: [],
|
|
218
|
+
relations,
|
|
219
|
+
embeddedFields
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// src/decorators/field.ts
|
|
225
|
+
function field(dynamoType, options) {
|
|
226
|
+
return function(_value, context) {
|
|
227
|
+
collectField({
|
|
228
|
+
propertyName: String(context.name),
|
|
229
|
+
dynamoType,
|
|
230
|
+
options
|
|
231
|
+
});
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
function createFieldDecorator(dynamoType, defaultOptions) {
|
|
235
|
+
function fieldDecorator(optionsOrContext, context) {
|
|
236
|
+
if (context) {
|
|
237
|
+
collectField({
|
|
238
|
+
propertyName: String(context.name),
|
|
239
|
+
dynamoType,
|
|
240
|
+
options: defaultOptions
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const options = { ...defaultOptions, ...optionsOrContext };
|
|
245
|
+
return (_value, ctx) => {
|
|
246
|
+
collectField({
|
|
247
|
+
propertyName: String(ctx.name),
|
|
248
|
+
dynamoType,
|
|
249
|
+
options: Object.keys(options).length > 0 ? options : void 0
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return fieldDecorator;
|
|
254
|
+
}
|
|
255
|
+
function literal(...values) {
|
|
256
|
+
if (values.length === 0) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
"@literal(...) requires at least one allowed value (e.g. @literal('active', 'disabled'))."
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return (_value, context) => {
|
|
262
|
+
collectField({
|
|
263
|
+
propertyName: String(context.name),
|
|
264
|
+
dynamoType: "S",
|
|
265
|
+
literals: [...values]
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
var string = createFieldDecorator("S");
|
|
270
|
+
var number = createFieldDecorator("N");
|
|
271
|
+
var boolean = createFieldDecorator("BOOL");
|
|
272
|
+
var datetime = createFieldDecorator("S", { format: "datetime" });
|
|
273
|
+
var binary = createFieldDecorator("B");
|
|
274
|
+
var stringSet = createFieldDecorator("SS");
|
|
275
|
+
var numberSet = createFieldDecorator("NS");
|
|
276
|
+
var list = createFieldDecorator("L");
|
|
277
|
+
var map = createFieldDecorator("M");
|
|
278
|
+
|
|
279
|
+
// src/decorators/embedded.ts
|
|
280
|
+
function embedded(modelFactory) {
|
|
281
|
+
return function(_value, context) {
|
|
282
|
+
collectEmbedded({
|
|
283
|
+
propertyName: String(context.name),
|
|
284
|
+
modelFactory
|
|
285
|
+
});
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/decorators/relation.ts
|
|
290
|
+
var RESERVED_RELATION_PROPERTY_NAMES = /* @__PURE__ */ new Set([
|
|
291
|
+
"items"
|
|
292
|
+
]);
|
|
293
|
+
function assertRelationPropertyNameAllowed(propertyName) {
|
|
294
|
+
if (RESERVED_RELATION_PROPERTY_NAMES.has(propertyName)) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Relation property name '${propertyName}' is reserved: it collides with the relation result-path grammar token '${propertyName}', which the planner and runtimes use to mark a hasMany connection (a relation writes to '$.<prop>.items'). A relation named '${propertyName}' would be mis-staged at runtime. Rename the relation property (reserved names: ${[...RESERVED_RELATION_PROPERTY_NAMES].map((n) => `'${n}'`).join(", ")}).`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function hasMany(targetFactory, keyBinding, options) {
|
|
301
|
+
return function(_value, context) {
|
|
302
|
+
const propertyName = String(context.name);
|
|
303
|
+
assertRelationPropertyNameAllowed(propertyName);
|
|
304
|
+
collectRelation({
|
|
305
|
+
type: "hasMany",
|
|
306
|
+
propertyName,
|
|
307
|
+
targetFactory,
|
|
308
|
+
keyBinding,
|
|
309
|
+
options
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
function belongsTo(targetFactory, keyBinding) {
|
|
314
|
+
return function(_value, context) {
|
|
315
|
+
const propertyName = String(context.name);
|
|
316
|
+
assertRelationPropertyNameAllowed(propertyName);
|
|
317
|
+
collectRelation({
|
|
318
|
+
type: "belongsTo",
|
|
319
|
+
propertyName,
|
|
320
|
+
targetFactory,
|
|
321
|
+
keyBinding
|
|
322
|
+
});
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function hasOne(targetFactory, keyBinding) {
|
|
326
|
+
return function(_value, context) {
|
|
327
|
+
const propertyName = String(context.name);
|
|
328
|
+
assertRelationPropertyNameAllowed(propertyName);
|
|
329
|
+
collectRelation({
|
|
330
|
+
type: "hasOne",
|
|
331
|
+
propertyName,
|
|
332
|
+
targetFactory,
|
|
333
|
+
keyBinding
|
|
334
|
+
});
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/planner/projection.ts
|
|
339
|
+
function buildProjection(select, additionalFields) {
|
|
340
|
+
const paths = [];
|
|
341
|
+
const names = {};
|
|
342
|
+
let nameCounter = 0;
|
|
343
|
+
function addPath(segments) {
|
|
344
|
+
const aliased = segments.map((seg) => {
|
|
345
|
+
const alias = `#p${nameCounter++}`;
|
|
346
|
+
names[alias] = seg;
|
|
347
|
+
return alias;
|
|
348
|
+
});
|
|
349
|
+
paths.push(aliased.join("."));
|
|
350
|
+
}
|
|
351
|
+
function traverse(selectObj, parentSegments) {
|
|
352
|
+
for (const [fieldName, value] of Object.entries(selectObj)) {
|
|
353
|
+
if (value === true) {
|
|
354
|
+
addPath([...parentSegments, fieldName]);
|
|
355
|
+
} else if (typeof value === "object" && value !== null) {
|
|
356
|
+
if ("select" in value || isSelectBuilder(value)) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
traverse(value, [...parentSegments, fieldName]);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
traverse(select, []);
|
|
364
|
+
if (additionalFields) {
|
|
365
|
+
for (const fieldName of additionalFields) {
|
|
366
|
+
addPath([fieldName]);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (paths.length === 0) {
|
|
370
|
+
return void 0;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
projectionExpression: paths.join(", "),
|
|
374
|
+
expressionAttributeNames: names
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/select/cond.ts
|
|
379
|
+
var RAW_COND = /* @__PURE__ */ Symbol.for("graphddb.rawCond");
|
|
380
|
+
function isRawCondition(value) {
|
|
381
|
+
return typeof value === "object" && value !== null && value[RAW_COND] === true;
|
|
382
|
+
}
|
|
383
|
+
function cond(template, ...parts) {
|
|
384
|
+
return {
|
|
385
|
+
[RAW_COND]: true,
|
|
386
|
+
template,
|
|
387
|
+
parts
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
function compileRawCondition(raw, allocName, allocValue) {
|
|
391
|
+
let out = raw.template[0];
|
|
392
|
+
for (let i = 0; i < raw.parts.length; i++) {
|
|
393
|
+
const part = raw.parts[i];
|
|
394
|
+
if (isColumn(part)) {
|
|
395
|
+
out += allocName(part.name);
|
|
396
|
+
} else {
|
|
397
|
+
out += allocValue(part);
|
|
398
|
+
}
|
|
399
|
+
out += raw.template[i + 1];
|
|
400
|
+
}
|
|
401
|
+
return out;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/expression/filter-expression.ts
|
|
405
|
+
var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
|
|
406
|
+
var OPERATOR_KEYS = /* @__PURE__ */ new Set([
|
|
407
|
+
"eq",
|
|
408
|
+
"ne",
|
|
409
|
+
"gt",
|
|
410
|
+
"ge",
|
|
411
|
+
"lt",
|
|
412
|
+
"le",
|
|
413
|
+
"between",
|
|
414
|
+
"in",
|
|
415
|
+
"beginsWith",
|
|
416
|
+
"contains",
|
|
417
|
+
"notContains",
|
|
418
|
+
"attributeExists",
|
|
419
|
+
"attributeType",
|
|
420
|
+
"size"
|
|
421
|
+
]);
|
|
422
|
+
function nameAlias(ctx, column) {
|
|
423
|
+
for (const [alias2, col] of Object.entries(ctx.names)) {
|
|
424
|
+
if (col === column) return alias2;
|
|
425
|
+
}
|
|
426
|
+
const alias = `#f${ctx.nameCounter.n++}`;
|
|
427
|
+
ctx.names[alias] = column;
|
|
428
|
+
return alias;
|
|
429
|
+
}
|
|
430
|
+
function valueAlias(ctx, field2, raw) {
|
|
431
|
+
const alias = `:vf${ctx.valueCounter.n++}`;
|
|
432
|
+
const fieldMeta = ctx.fieldMap.get(field2);
|
|
433
|
+
ctx.values[alias] = fieldMeta ? serializeFieldValue(raw, fieldMeta) : raw;
|
|
434
|
+
return alias;
|
|
435
|
+
}
|
|
436
|
+
function compileField(ctx, field2, condition) {
|
|
437
|
+
const nAlias = nameAlias(ctx, field2);
|
|
438
|
+
if (!isOperatorObject(condition)) {
|
|
439
|
+
const vAlias = valueAlias(ctx, field2, condition);
|
|
440
|
+
return `${nAlias} = ${vAlias}`;
|
|
441
|
+
}
|
|
442
|
+
const ops = condition;
|
|
443
|
+
const clauses = [];
|
|
444
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
445
|
+
switch (op) {
|
|
446
|
+
case "eq":
|
|
447
|
+
clauses.push(`${nAlias} = ${valueAlias(ctx, field2, value)}`);
|
|
448
|
+
break;
|
|
449
|
+
case "ne":
|
|
450
|
+
clauses.push(`${nAlias} <> ${valueAlias(ctx, field2, value)}`);
|
|
451
|
+
break;
|
|
452
|
+
case "gt":
|
|
453
|
+
clauses.push(`${nAlias} > ${valueAlias(ctx, field2, value)}`);
|
|
454
|
+
break;
|
|
455
|
+
case "ge":
|
|
456
|
+
clauses.push(`${nAlias} >= ${valueAlias(ctx, field2, value)}`);
|
|
457
|
+
break;
|
|
458
|
+
case "lt":
|
|
459
|
+
clauses.push(`${nAlias} < ${valueAlias(ctx, field2, value)}`);
|
|
460
|
+
break;
|
|
461
|
+
case "le":
|
|
462
|
+
clauses.push(`${nAlias} <= ${valueAlias(ctx, field2, value)}`);
|
|
463
|
+
break;
|
|
464
|
+
case "between": {
|
|
465
|
+
const [lo, hi] = value;
|
|
466
|
+
clauses.push(
|
|
467
|
+
`${nAlias} BETWEEN ${valueAlias(ctx, field2, lo)} AND ${valueAlias(ctx, field2, hi)}`
|
|
468
|
+
);
|
|
469
|
+
break;
|
|
470
|
+
}
|
|
471
|
+
case "in": {
|
|
472
|
+
const arr = value;
|
|
473
|
+
const aliases = arr.map((v) => valueAlias(ctx, field2, v));
|
|
474
|
+
clauses.push(`${nAlias} IN (${aliases.join(", ")})`);
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case "beginsWith":
|
|
478
|
+
clauses.push(`begins_with(${nAlias}, ${valueAlias(ctx, field2, value)})`);
|
|
479
|
+
break;
|
|
480
|
+
case "contains":
|
|
481
|
+
clauses.push(`contains(${nAlias}, ${valueAlias(ctx, field2, value)})`);
|
|
482
|
+
break;
|
|
483
|
+
case "notContains":
|
|
484
|
+
clauses.push(
|
|
485
|
+
`NOT contains(${nAlias}, ${valueAlias(ctx, field2, value)})`
|
|
486
|
+
);
|
|
487
|
+
break;
|
|
488
|
+
case "attributeExists":
|
|
489
|
+
clauses.push(
|
|
490
|
+
value === false ? `attribute_not_exists(${nAlias})` : `attribute_exists(${nAlias})`
|
|
491
|
+
);
|
|
492
|
+
break;
|
|
493
|
+
case "attributeType":
|
|
494
|
+
clauses.push(
|
|
495
|
+
`attribute_type(${nAlias}, ${valueAlias(ctx, field2, value)})`
|
|
496
|
+
);
|
|
497
|
+
break;
|
|
498
|
+
case "size":
|
|
499
|
+
clauses.push(`size(${nAlias}) = ${valueAlias(ctx, field2, value)}`);
|
|
500
|
+
break;
|
|
501
|
+
default:
|
|
502
|
+
throw new Error(`Unknown filter operator '${op}' on field '${field2}'`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return joinAnd(clauses);
|
|
506
|
+
}
|
|
507
|
+
function isOperatorObject(value) {
|
|
508
|
+
if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array || value instanceof Set) {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
const keys = Object.keys(value);
|
|
512
|
+
if (keys.length === 0) return false;
|
|
513
|
+
return keys.every((k2) => OPERATOR_KEYS.has(k2));
|
|
514
|
+
}
|
|
515
|
+
function joinAnd(clauses) {
|
|
516
|
+
if (clauses.length === 1) return clauses[0];
|
|
517
|
+
return clauses.map((c) => wrap(c)).join(" AND ");
|
|
518
|
+
}
|
|
519
|
+
function wrap(expr) {
|
|
520
|
+
if (isAlreadyWrapped(expr)) return expr;
|
|
521
|
+
if (expr.includes(" AND ") || expr.includes(" OR ")) {
|
|
522
|
+
return `(${expr})`;
|
|
523
|
+
}
|
|
524
|
+
return expr;
|
|
525
|
+
}
|
|
526
|
+
function isAlreadyWrapped(expr) {
|
|
527
|
+
if (!expr.startsWith("(") || !expr.endsWith(")")) return false;
|
|
528
|
+
let depth = 0;
|
|
529
|
+
for (let i = 0; i < expr.length; i++) {
|
|
530
|
+
if (expr[i] === "(") depth++;
|
|
531
|
+
else if (expr[i] === ")") {
|
|
532
|
+
depth--;
|
|
533
|
+
if (depth === 0 && i < expr.length - 1) return false;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return depth === 0;
|
|
537
|
+
}
|
|
538
|
+
function compileRaw(ctx, raw) {
|
|
539
|
+
return compileRawCondition(
|
|
540
|
+
raw,
|
|
541
|
+
(column) => nameAlias(ctx, column),
|
|
542
|
+
(value) => {
|
|
543
|
+
const alias = `:vf${ctx.valueCounter.n++}`;
|
|
544
|
+
ctx.values[alias] = value;
|
|
545
|
+
return alias;
|
|
546
|
+
}
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
function compileNode(ctx, node) {
|
|
550
|
+
if (isRawCondition(node)) {
|
|
551
|
+
return compileRaw(ctx, node);
|
|
552
|
+
}
|
|
553
|
+
const clauses = [];
|
|
554
|
+
for (const [key2, value] of Object.entries(node)) {
|
|
555
|
+
if (value === void 0) continue;
|
|
556
|
+
if (LOGICAL_KEYS.has(key2)) {
|
|
557
|
+
if (key2 === "and" || key2 === "or") {
|
|
558
|
+
const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
|
|
559
|
+
if (parts.length === 0) continue;
|
|
560
|
+
if (parts.length === 1) {
|
|
561
|
+
clauses.push(parts[0]);
|
|
562
|
+
} else {
|
|
563
|
+
const sep = key2 === "and" ? " AND " : " OR ";
|
|
564
|
+
const joined = parts.map((p) => wrap(p)).join(sep);
|
|
565
|
+
clauses.push(`(${joined})`);
|
|
566
|
+
}
|
|
567
|
+
} else {
|
|
568
|
+
const inner = compileNode(ctx, value);
|
|
569
|
+
if (inner.length > 0) clauses.push(`NOT ${wrap(inner)}`);
|
|
570
|
+
}
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const clause = compileField(ctx, key2, value);
|
|
574
|
+
if (clause.length > 0) clauses.push(clause);
|
|
575
|
+
}
|
|
576
|
+
return joinAnd(clauses);
|
|
577
|
+
}
|
|
578
|
+
function compileFilterExpression(filter, metadata) {
|
|
579
|
+
if (!filter || Object.keys(filter).length === 0) {
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
const ctx = {
|
|
583
|
+
names: {},
|
|
584
|
+
values: {},
|
|
585
|
+
fieldMap: new Map(metadata.fields.map((f) => [f.propertyName, f])),
|
|
586
|
+
nameCounter: { n: 0 },
|
|
587
|
+
valueCounter: { n: 0 }
|
|
588
|
+
};
|
|
589
|
+
const expression = compileNode(ctx, filter);
|
|
590
|
+
if (expression.length === 0) {
|
|
591
|
+
return void 0;
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
filterExpression: expression,
|
|
595
|
+
expressionAttributeNames: ctx.names,
|
|
596
|
+
expressionAttributeValues: ctx.values
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function evaluateFilter(item, filter) {
|
|
600
|
+
for (const [key2, value] of Object.entries(filter)) {
|
|
601
|
+
if (value === void 0) continue;
|
|
602
|
+
if (key2 === "and") {
|
|
603
|
+
if (!value.every((s) => evaluateFilter(item, s)))
|
|
604
|
+
return false;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
if (key2 === "or") {
|
|
608
|
+
if (!value.some((s) => evaluateFilter(item, s)))
|
|
609
|
+
return false;
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (key2 === "not") {
|
|
613
|
+
if (evaluateFilter(item, value)) return false;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (!evaluateFieldCondition(item[key2], item, key2, value)) {
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return true;
|
|
621
|
+
}
|
|
622
|
+
function cmp(a, b) {
|
|
623
|
+
if (a instanceof Date && b instanceof Date) {
|
|
624
|
+
return a.getTime() - b.getTime();
|
|
625
|
+
}
|
|
626
|
+
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
627
|
+
if (typeof a === "string" && typeof b === "string")
|
|
628
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
629
|
+
if (a === b) return 0;
|
|
630
|
+
return a < b ? -1 : 1;
|
|
631
|
+
}
|
|
632
|
+
function eq(a, b) {
|
|
633
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
634
|
+
return a === b;
|
|
635
|
+
}
|
|
636
|
+
function evaluateFieldCondition(actual, item, field2, condition) {
|
|
637
|
+
if (!isOperatorObject(condition)) {
|
|
638
|
+
return eq(actual, condition);
|
|
639
|
+
}
|
|
640
|
+
const ops = condition;
|
|
641
|
+
for (const [op, value] of Object.entries(ops)) {
|
|
642
|
+
switch (op) {
|
|
643
|
+
case "eq":
|
|
644
|
+
if (!eq(actual, value)) return false;
|
|
645
|
+
break;
|
|
646
|
+
case "ne":
|
|
647
|
+
if (eq(actual, value)) return false;
|
|
648
|
+
break;
|
|
649
|
+
case "gt":
|
|
650
|
+
if (!(cmp(actual, value) > 0)) return false;
|
|
651
|
+
break;
|
|
652
|
+
case "ge":
|
|
653
|
+
if (!(cmp(actual, value) >= 0)) return false;
|
|
654
|
+
break;
|
|
655
|
+
case "lt":
|
|
656
|
+
if (!(cmp(actual, value) < 0)) return false;
|
|
657
|
+
break;
|
|
658
|
+
case "le":
|
|
659
|
+
if (!(cmp(actual, value) <= 0)) return false;
|
|
660
|
+
break;
|
|
661
|
+
case "between": {
|
|
662
|
+
const [lo, hi] = value;
|
|
663
|
+
if (!(cmp(actual, lo) >= 0 && cmp(actual, hi) <= 0)) return false;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "in":
|
|
667
|
+
if (!value.some((v) => eq(actual, v))) return false;
|
|
668
|
+
break;
|
|
669
|
+
case "beginsWith":
|
|
670
|
+
if (typeof actual !== "string" || !actual.startsWith(value))
|
|
671
|
+
return false;
|
|
672
|
+
break;
|
|
673
|
+
case "contains":
|
|
674
|
+
if (typeof actual !== "string" || !actual.includes(value))
|
|
675
|
+
return false;
|
|
676
|
+
break;
|
|
677
|
+
case "notContains":
|
|
678
|
+
if (typeof actual === "string" && actual.includes(value))
|
|
679
|
+
return false;
|
|
680
|
+
break;
|
|
681
|
+
case "attributeExists": {
|
|
682
|
+
const exists = field2 in item && actual !== void 0 && actual !== null;
|
|
683
|
+
if (value === false ? exists : !exists) return false;
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case "attributeType":
|
|
687
|
+
break;
|
|
688
|
+
case "size": {
|
|
689
|
+
const len = typeof actual === "string" || Array.isArray(actual) ? actual.length : void 0;
|
|
690
|
+
if (len !== value) return false;
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
default:
|
|
694
|
+
throw new Error(`Unknown filter operator '${op}' on field '${field2}'`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/planner/planner.ts
|
|
701
|
+
function plan(metadata, input) {
|
|
702
|
+
const queryFields = Object.keys(input.key);
|
|
703
|
+
const resolved = resolveKey(queryFields, metadata);
|
|
704
|
+
if (input.consistentRead && resolved.type === "gsi") {
|
|
705
|
+
throw new Error(
|
|
706
|
+
"consistentRead is not supported for GSI queries. Use a primary key query instead."
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
710
|
+
const built = buildKeyCondition(metadata, resolved, input.key);
|
|
711
|
+
const keyCondition = built.keyCondition;
|
|
712
|
+
const additionalProjectionFields = input.updatable ? Array.from(
|
|
713
|
+
/* @__PURE__ */ new Set([...input.additionalProjectionFields ?? [], "PK", "SK"])
|
|
714
|
+
) : input.additionalProjectionFields;
|
|
715
|
+
const projection = buildProjection(input.select, additionalProjectionFields);
|
|
716
|
+
const compiledFilter = compileFilterExpression(input.filter, metadata);
|
|
717
|
+
const isGetItem = resolved.type === "pk" && !resolved.partial && built.skValue !== void 0 && // GetItem does not support FilterExpression — fall back to Query when a
|
|
718
|
+
// server-side filter is requested so the condition can be applied.
|
|
719
|
+
!compiledFilter;
|
|
720
|
+
const projectionFields = projection ? {
|
|
721
|
+
projectionExpression: projection.projectionExpression,
|
|
722
|
+
expressionAttributeNames: projection.expressionAttributeNames
|
|
723
|
+
} : {};
|
|
724
|
+
const filterFields = compiledFilter ? {
|
|
725
|
+
filterExpression: compiledFilter.filterExpression,
|
|
726
|
+
filterExpressionAttributeNames: compiledFilter.expressionAttributeNames,
|
|
727
|
+
filterExpressionAttributeValues: compiledFilter.expressionAttributeValues
|
|
728
|
+
} : {};
|
|
729
|
+
const consistentRead = input.consistentRead && resolved.type === "pk" ? { consistentRead: true } : {};
|
|
730
|
+
let op;
|
|
731
|
+
if (isGetItem) {
|
|
732
|
+
op = {
|
|
733
|
+
type: "GetItem",
|
|
734
|
+
tableName,
|
|
735
|
+
keyCondition,
|
|
736
|
+
...projectionFields,
|
|
737
|
+
...consistentRead
|
|
738
|
+
};
|
|
739
|
+
} else {
|
|
740
|
+
const queryOp = {
|
|
741
|
+
type: "Query",
|
|
742
|
+
tableName,
|
|
743
|
+
keyCondition,
|
|
744
|
+
scanIndexForward: (input.order ?? "ASC") === "ASC",
|
|
745
|
+
...projectionFields,
|
|
746
|
+
...filterFields,
|
|
747
|
+
...consistentRead
|
|
748
|
+
};
|
|
749
|
+
if (resolved.type === "gsi") {
|
|
750
|
+
queryOp.indexName = resolved.indexName;
|
|
751
|
+
}
|
|
752
|
+
if (built.rangeCondition) {
|
|
753
|
+
queryOp.rangeCondition = built.rangeCondition;
|
|
754
|
+
}
|
|
755
|
+
if (input.limit != null) {
|
|
756
|
+
queryOp.limit = input.limit;
|
|
757
|
+
}
|
|
758
|
+
if (input.after) {
|
|
759
|
+
queryOp.exclusiveStartKey = input.after;
|
|
760
|
+
}
|
|
761
|
+
op = queryOp;
|
|
762
|
+
}
|
|
763
|
+
return { operations: [op] };
|
|
764
|
+
}
|
|
765
|
+
function buildKeyCondition(metadata, resolved, queryKey) {
|
|
766
|
+
if (resolved.type === "pk") {
|
|
767
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
768
|
+
return buildFromSegments(
|
|
769
|
+
metadata.primaryKey.segmented,
|
|
770
|
+
metadata.primaryKey.inputFieldNames,
|
|
771
|
+
queryKey,
|
|
772
|
+
"PK",
|
|
773
|
+
"SK"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const gsiDef = metadata.gsiDefinitions.find(
|
|
777
|
+
(g) => g.indexName === resolved.indexName
|
|
778
|
+
);
|
|
779
|
+
if (!gsiDef) throw new Error(`GSI '${resolved.indexName}' not found`);
|
|
780
|
+
return buildFromSegments(
|
|
781
|
+
gsiDef.segmented,
|
|
782
|
+
gsiDef.inputFieldNames,
|
|
783
|
+
queryKey,
|
|
784
|
+
`${gsiDef.indexName}PK`,
|
|
785
|
+
`${gsiDef.indexName}SK`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
function buildFromSegments(segmented, inputFieldNames, queryKey, pkAttr, skAttr) {
|
|
789
|
+
const keyInput = {};
|
|
790
|
+
for (const fieldName of inputFieldNames) {
|
|
791
|
+
keyInput[fieldName] = queryKey[fieldName];
|
|
792
|
+
}
|
|
793
|
+
const { pk, sk, skTruncated } = resolveSegmentedKey(segmented, keyInput);
|
|
794
|
+
const keyCondition = { [pkAttr]: pk };
|
|
795
|
+
if (sk === void 0) {
|
|
796
|
+
return { keyCondition };
|
|
797
|
+
}
|
|
798
|
+
if (skTruncated) {
|
|
799
|
+
return {
|
|
800
|
+
keyCondition,
|
|
801
|
+
rangeCondition: { operator: "begins_with", key: skAttr, value: sk }
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
keyCondition[skAttr] = sk;
|
|
805
|
+
return { keyCondition, skValue: sk };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/executor/executor.ts
|
|
809
|
+
async function execute(operation) {
|
|
810
|
+
return ClientManager.getExecutor().execute(operation);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/hydrator/hydrator.ts
|
|
814
|
+
function hydrate(rawItems, select, metadata, updatable = false) {
|
|
815
|
+
return rawItems.map((item) => hydrateItem(item, select, metadata, updatable));
|
|
816
|
+
}
|
|
817
|
+
function hydrateItem(raw, select, metadata, updatable = false) {
|
|
818
|
+
const fieldMap = new Map(
|
|
819
|
+
metadata.fields.map((f) => [f.propertyName, f])
|
|
820
|
+
);
|
|
821
|
+
const embeddedMap = new Map(
|
|
822
|
+
metadata.embeddedFields.map((e) => [e.propertyName, e])
|
|
823
|
+
);
|
|
824
|
+
const result = {};
|
|
825
|
+
for (const [fieldName, selectValue] of Object.entries(select)) {
|
|
826
|
+
if (selectValue === true) {
|
|
827
|
+
const value = raw[fieldName];
|
|
828
|
+
if (value !== void 0) {
|
|
829
|
+
const fieldMeta = fieldMap.get(fieldName);
|
|
830
|
+
result[fieldName] = deserializeValue(value, fieldMeta);
|
|
831
|
+
}
|
|
832
|
+
} else if (typeof selectValue === "object" && selectValue !== null) {
|
|
833
|
+
if ("select" in selectValue || isSelectBuilder(selectValue)) {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
const rawEmb = raw[fieldName];
|
|
837
|
+
if (rawEmb && typeof rawEmb === "object") {
|
|
838
|
+
result[fieldName] = hydrateEmbedded(
|
|
839
|
+
rawEmb,
|
|
840
|
+
selectValue,
|
|
841
|
+
metadata,
|
|
842
|
+
fieldName
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
if (updatable) {
|
|
848
|
+
attachHiddenKey(result, raw);
|
|
849
|
+
}
|
|
850
|
+
return result;
|
|
851
|
+
}
|
|
852
|
+
function hydrateEmbedded(raw, select, _metadata, _fieldName) {
|
|
853
|
+
const result = {};
|
|
854
|
+
for (const [key2, selectValue] of Object.entries(select)) {
|
|
855
|
+
if (selectValue === true) {
|
|
856
|
+
if (raw[key2] !== void 0) {
|
|
857
|
+
result[key2] = raw[key2];
|
|
858
|
+
}
|
|
859
|
+
} else if (typeof selectValue === "object" && selectValue !== null && !("select" in selectValue)) {
|
|
860
|
+
const nested = raw[key2];
|
|
861
|
+
if (nested && typeof nested === "object") {
|
|
862
|
+
result[key2] = hydrateEmbedded(
|
|
863
|
+
nested,
|
|
864
|
+
selectValue,
|
|
865
|
+
_metadata,
|
|
866
|
+
key2
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
function deserializeValue(value, fieldMeta) {
|
|
874
|
+
if (!fieldMeta) return value;
|
|
875
|
+
if (fieldMeta.options?.deserialize) {
|
|
876
|
+
return fieldMeta.options.deserialize(value);
|
|
877
|
+
}
|
|
878
|
+
if (fieldMeta.options?.format === "datetime" && typeof value === "string") {
|
|
879
|
+
return new Date(value);
|
|
880
|
+
}
|
|
881
|
+
if (fieldMeta.options?.format === "date" && typeof value === "string") {
|
|
882
|
+
return /* @__PURE__ */ new Date(value + "T00:00:00.000Z");
|
|
883
|
+
}
|
|
884
|
+
return value;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// src/pagination/cursor.ts
|
|
888
|
+
function encodeCursor(lastEvaluatedKey) {
|
|
889
|
+
const json = JSON.stringify(lastEvaluatedKey);
|
|
890
|
+
const bytes = new TextEncoder().encode(json);
|
|
891
|
+
let binary2 = "";
|
|
892
|
+
for (const b of bytes) {
|
|
893
|
+
binary2 += String.fromCharCode(b);
|
|
894
|
+
}
|
|
895
|
+
return btoa(binary2).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
896
|
+
}
|
|
897
|
+
function decodeCursor(cursor) {
|
|
898
|
+
let base64 = cursor.replace(/-/g, "+").replace(/_/g, "/");
|
|
899
|
+
while (base64.length % 4 !== 0) {
|
|
900
|
+
base64 += "=";
|
|
901
|
+
}
|
|
902
|
+
const binary2 = atob(base64);
|
|
903
|
+
const bytes = new Uint8Array(binary2.length);
|
|
904
|
+
for (let i = 0; i < binary2.length; i++) {
|
|
905
|
+
bytes[i] = binary2.charCodeAt(i);
|
|
906
|
+
}
|
|
907
|
+
const json = new TextDecoder().decode(bytes);
|
|
908
|
+
return JSON.parse(json);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// src/operations/list.ts
|
|
912
|
+
async function executeListInternal(modelClass, key2, options) {
|
|
913
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
914
|
+
const normalized = normalizeTopLevelSelect(options.select);
|
|
915
|
+
const select = normalized.select;
|
|
916
|
+
const filter = options.filter ?? normalized.filter;
|
|
917
|
+
const after = options.after ?? normalized.after;
|
|
918
|
+
const limit = options.limit ?? normalized.limit;
|
|
919
|
+
const order = options.order ?? normalized.order;
|
|
920
|
+
const exclusiveStartKey = after ? decodeCursor(after) : void 0;
|
|
921
|
+
const implicitKeys = getImplicitKeyFields(select, metadata);
|
|
922
|
+
const executionPlan = plan(metadata, {
|
|
923
|
+
key: key2,
|
|
924
|
+
select,
|
|
925
|
+
limit,
|
|
926
|
+
after: exclusiveStartKey,
|
|
927
|
+
order,
|
|
928
|
+
filter,
|
|
929
|
+
additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
|
|
930
|
+
updatable: options.updatable ?? false
|
|
931
|
+
});
|
|
932
|
+
const operation = executionPlan.operations[0];
|
|
933
|
+
const result = await execute(operation);
|
|
934
|
+
const rawItems = result.items;
|
|
935
|
+
const hydrated = hydrate(rawItems, select, metadata, options.updatable ?? false);
|
|
936
|
+
const cursor = result.lastEvaluatedKey ? encodeCursor(result.lastEvaluatedKey) : null;
|
|
937
|
+
return { items: hydrated, rawItems, cursor };
|
|
938
|
+
}
|
|
939
|
+
async function executeList(modelClass, key2, options) {
|
|
940
|
+
const { items, cursor } = await executeListInternal(modelClass, key2, options);
|
|
941
|
+
return { items, cursor };
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/planner/relation-planner.ts
|
|
945
|
+
function buildRelationQueryKey(rel, rawParentItem) {
|
|
946
|
+
const queryKey = {};
|
|
947
|
+
for (const [targetField, sourceField] of Object.entries(rel.keyBinding)) {
|
|
948
|
+
queryKey[targetField] = rawParentItem[sourceField];
|
|
949
|
+
}
|
|
950
|
+
return queryKey;
|
|
951
|
+
}
|
|
952
|
+
function serializeQueryKey(queryKey) {
|
|
953
|
+
const entries = Object.entries(queryKey).sort(([a], [b]) => a.localeCompare(b));
|
|
954
|
+
return JSON.stringify(entries);
|
|
955
|
+
}
|
|
956
|
+
function planGetItemForQueryKey(metadata, queryKey, select, additionalProjectionFields) {
|
|
957
|
+
const executionPlan = plan(metadata, {
|
|
958
|
+
key: queryKey,
|
|
959
|
+
select,
|
|
960
|
+
additionalProjectionFields
|
|
961
|
+
});
|
|
962
|
+
const operation = executionPlan.operations[0];
|
|
963
|
+
if (operation.type !== "GetItem") {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`Relation target requires GetItem access pattern, got ${operation.type}`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
return operation;
|
|
969
|
+
}
|
|
970
|
+
function planBatchGetForQueryKeys(metadata, queryKeys, select, additionalProjectionFields) {
|
|
971
|
+
if (queryKeys.length === 0) {
|
|
972
|
+
return [];
|
|
973
|
+
}
|
|
974
|
+
const keyFields = [
|
|
975
|
+
...new Set(queryKeys.flatMap((queryKey) => Object.keys(queryKey)))
|
|
976
|
+
].filter((field2) => !(field2 in select && select[field2] === true));
|
|
977
|
+
const projectionFields = [
|
|
978
|
+
.../* @__PURE__ */ new Set([
|
|
979
|
+
...(additionalProjectionFields ?? []).filter(
|
|
980
|
+
(field2) => !(field2 in select && select[field2] === true)
|
|
981
|
+
),
|
|
982
|
+
...keyFields
|
|
983
|
+
])
|
|
984
|
+
];
|
|
985
|
+
const samplePlan = planGetItemForQueryKey(
|
|
986
|
+
metadata,
|
|
987
|
+
queryKeys[0],
|
|
988
|
+
select,
|
|
989
|
+
projectionFields.length > 0 ? projectionFields : void 0
|
|
990
|
+
);
|
|
991
|
+
const uniqueKeys = dedupeDynamoKeys(
|
|
992
|
+
queryKeys.map(
|
|
993
|
+
(queryKey) => planGetItemForQueryKey(
|
|
994
|
+
metadata,
|
|
995
|
+
queryKey,
|
|
996
|
+
select,
|
|
997
|
+
projectionFields.length > 0 ? projectionFields : void 0
|
|
998
|
+
).keyCondition
|
|
999
|
+
)
|
|
1000
|
+
);
|
|
1001
|
+
const operations = [];
|
|
1002
|
+
for (let i = 0; i < uniqueKeys.length; i += BATCH_GET_MAX_KEYS) {
|
|
1003
|
+
operations.push({
|
|
1004
|
+
type: "BatchGetItem",
|
|
1005
|
+
tableName: samplePlan.tableName,
|
|
1006
|
+
keys: uniqueKeys.slice(i, i + BATCH_GET_MAX_KEYS),
|
|
1007
|
+
...samplePlan.projectionExpression ? { projectionExpression: samplePlan.projectionExpression } : {},
|
|
1008
|
+
...samplePlan.expressionAttributeNames ? { expressionAttributeNames: samplePlan.expressionAttributeNames } : {},
|
|
1009
|
+
...samplePlan.consistentRead ? { consistentRead: true } : {}
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
return operations;
|
|
1013
|
+
}
|
|
1014
|
+
function dedupeDynamoKeys(keys) {
|
|
1015
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1016
|
+
const result = [];
|
|
1017
|
+
for (const key2 of keys) {
|
|
1018
|
+
const serialized = JSON.stringify(
|
|
1019
|
+
Object.entries(key2).sort(([a], [b]) => a.localeCompare(b))
|
|
1020
|
+
);
|
|
1021
|
+
if (seen.has(serialized)) continue;
|
|
1022
|
+
seen.add(serialized);
|
|
1023
|
+
result.push(key2);
|
|
1024
|
+
}
|
|
1025
|
+
return result;
|
|
1026
|
+
}
|
|
1027
|
+
function planRelationOperations(metadata, select, context = { estimatedParentCount: 1 }) {
|
|
1028
|
+
const operations = [];
|
|
1029
|
+
const relations = detectRelationFields(select, metadata);
|
|
1030
|
+
for (const rel of relations) {
|
|
1031
|
+
const selectSpec = normalizeSelectSpec(select[rel.propertyName]);
|
|
1032
|
+
const targetClass = rel.targetFactory();
|
|
1033
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1034
|
+
if (rel.type === "hasMany") {
|
|
1035
|
+
const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
|
|
1036
|
+
const tableName = TableMapping.resolve(targetMeta.tableName);
|
|
1037
|
+
const listPlan = plan(targetMeta, {
|
|
1038
|
+
key: buildPlaceholderHasManyKey(rel, targetMeta),
|
|
1039
|
+
select: selectSpec.select,
|
|
1040
|
+
limit
|
|
1041
|
+
});
|
|
1042
|
+
operations.push(...listPlan.operations);
|
|
1043
|
+
operations.push(
|
|
1044
|
+
...planRelationOperations(targetMeta, selectSpec.select, {
|
|
1045
|
+
estimatedParentCount: limit
|
|
1046
|
+
})
|
|
1047
|
+
);
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
const estimatedCount = context.estimatedParentCount ?? 1;
|
|
1051
|
+
operations.push(
|
|
1052
|
+
...planBatchGetForExplain(targetMeta, selectSpec.select, estimatedCount)
|
|
1053
|
+
);
|
|
1054
|
+
operations.push(
|
|
1055
|
+
...planRelationOperations(targetMeta, selectSpec.select, {
|
|
1056
|
+
estimatedParentCount: 1
|
|
1057
|
+
})
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
return operations;
|
|
1061
|
+
}
|
|
1062
|
+
function buildPlaceholderHasManyKey(rel, targetMeta) {
|
|
1063
|
+
const queryKey = {};
|
|
1064
|
+
for (const targetField of Object.keys(rel.keyBinding)) {
|
|
1065
|
+
queryKey[targetField] = `__${targetField}__`;
|
|
1066
|
+
}
|
|
1067
|
+
if (targetMeta.primaryKey) {
|
|
1068
|
+
for (const fieldName of targetMeta.primaryKey.inputFieldNames) {
|
|
1069
|
+
if (!(fieldName in queryKey)) {
|
|
1070
|
+
queryKey[fieldName] = `__${fieldName}__`;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return queryKey;
|
|
1075
|
+
}
|
|
1076
|
+
function planBatchGetForExplain(metadata, select, estimatedKeyCount) {
|
|
1077
|
+
const placeholderKeys = Array.from(
|
|
1078
|
+
{ length: estimatedKeyCount },
|
|
1079
|
+
(_, index) => buildExplainPlaceholderKey(metadata, index)
|
|
1080
|
+
);
|
|
1081
|
+
return planBatchGetForQueryKeys(metadata, placeholderKeys, select);
|
|
1082
|
+
}
|
|
1083
|
+
function buildExplainPlaceholderKey(metadata, index) {
|
|
1084
|
+
const queryKey = {};
|
|
1085
|
+
if (metadata.primaryKey) {
|
|
1086
|
+
for (const fieldName of metadata.primaryKey.inputFieldNames) {
|
|
1087
|
+
queryKey[fieldName] = `__explain_${fieldName}_${index}__`;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return queryKey;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/relation/traversal.ts
|
|
1094
|
+
function evaluateFilterClientSide(item, filter, _targetMeta) {
|
|
1095
|
+
return evaluateFilter(item, filter);
|
|
1096
|
+
}
|
|
1097
|
+
function relationSpec(value) {
|
|
1098
|
+
return normalizeSelectSpec(value);
|
|
1099
|
+
}
|
|
1100
|
+
function validateDepth(select, metadata, maxDepth, currentDepth = 1) {
|
|
1101
|
+
for (const [fieldName, value] of Object.entries(select)) {
|
|
1102
|
+
if (typeof value === "object" && value !== null && ("select" in value || isSelectBuilder(value))) {
|
|
1103
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
1104
|
+
if (!rel) continue;
|
|
1105
|
+
if (currentDepth > maxDepth) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`Relation traversal depth exceeded: '${fieldName}' at depth ${currentDepth} exceeds maxDepth ${maxDepth}. Set maxDepth: ${currentDepth} to allow this.`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
const targetClass = rel.targetFactory();
|
|
1111
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1112
|
+
const innerSelect = relationSpec(value).select;
|
|
1113
|
+
validateDepth(innerSelect, targetMeta, maxDepth, currentDepth + 1);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
function relationResultPath(parentPath, rel) {
|
|
1118
|
+
const base = `${parentPath}.${rel.propertyName}`;
|
|
1119
|
+
return rel.type === "hasMany" ? `${base}.items` : base;
|
|
1120
|
+
}
|
|
1121
|
+
function stageRelations(relations, parentPath, plan2) {
|
|
1122
|
+
if (plan2 === void 0 || relations.length === 0) {
|
|
1123
|
+
return relations.length > 0 ? { stages: [[...relations]], concurrency: RELATION_TRAVERSAL_CONCURRENCY } : { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
1124
|
+
}
|
|
1125
|
+
const byStage = /* @__PURE__ */ new Map();
|
|
1126
|
+
const unstaged = [];
|
|
1127
|
+
for (const rel of relations) {
|
|
1128
|
+
const stage = stageOfResultPath(plan2, relationResultPath(parentPath, rel));
|
|
1129
|
+
if (stage === void 0) {
|
|
1130
|
+
unstaged.push(rel);
|
|
1131
|
+
continue;
|
|
1132
|
+
}
|
|
1133
|
+
const bucket = byStage.get(stage);
|
|
1134
|
+
if (bucket) bucket.push(rel);
|
|
1135
|
+
else byStage.set(stage, [rel]);
|
|
1136
|
+
}
|
|
1137
|
+
const stages = [...byStage.keys()].sort((a, b) => a - b).map((s) => byStage.get(s));
|
|
1138
|
+
if (unstaged.length > 0) stages.push(unstaged);
|
|
1139
|
+
return { stages, concurrency: plan2.plan.concurrency };
|
|
1140
|
+
}
|
|
1141
|
+
async function runStaged(relations, parentPath, plan2, worker) {
|
|
1142
|
+
const { stages, concurrency } = stageRelations(relations, parentPath, plan2);
|
|
1143
|
+
for (const stage of stages) {
|
|
1144
|
+
await mapWithConcurrency(stage, concurrency, (rel) => worker(rel));
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function hasCompleteQueryKey(queryKey) {
|
|
1148
|
+
return Object.values(queryKey).every((value) => value != null);
|
|
1149
|
+
}
|
|
1150
|
+
function matchesQueryKey(item, queryKey) {
|
|
1151
|
+
return Object.entries(queryKey).every(
|
|
1152
|
+
([field2, value]) => item[field2] === value
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
async function fetchBelongsToHasOneBatch(rel, rawParentItems, selectSpec, targetMeta, options, currentDepth, parentPath) {
|
|
1156
|
+
const results = rawParentItems.map(
|
|
1157
|
+
() => null
|
|
1158
|
+
);
|
|
1159
|
+
const queryKeys = [];
|
|
1160
|
+
const parentIndexes = [];
|
|
1161
|
+
for (let i = 0; i < rawParentItems.length; i++) {
|
|
1162
|
+
const queryKey = buildRelationQueryKey(rel, rawParentItems[i]);
|
|
1163
|
+
if (!hasCompleteQueryKey(queryKey)) {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
queryKeys.push(queryKey);
|
|
1167
|
+
parentIndexes.push(i);
|
|
1168
|
+
}
|
|
1169
|
+
if (queryKeys.length === 0) {
|
|
1170
|
+
return results;
|
|
1171
|
+
}
|
|
1172
|
+
const implicitKeys = getImplicitKeyFields(selectSpec.select, targetMeta);
|
|
1173
|
+
const projectionExtras = options.updatable ? [...implicitKeys, "PK", "SK"] : implicitKeys;
|
|
1174
|
+
const batchOps = planBatchGetForQueryKeys(
|
|
1175
|
+
targetMeta,
|
|
1176
|
+
queryKeys,
|
|
1177
|
+
selectSpec.select,
|
|
1178
|
+
projectionExtras.length > 0 ? projectionExtras : void 0
|
|
1179
|
+
);
|
|
1180
|
+
const executor = ClientManager.getExecutor();
|
|
1181
|
+
const queryKeyToRaw = /* @__PURE__ */ new Map();
|
|
1182
|
+
for (const operation of batchOps) {
|
|
1183
|
+
const batchResult = await executor.batchGet(operation);
|
|
1184
|
+
for (const item of batchResult.items) {
|
|
1185
|
+
for (const queryKey of queryKeys) {
|
|
1186
|
+
if (!matchesQueryKey(item, queryKey)) {
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
queryKeyToRaw.set(serializeQueryKey(queryKey), item);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
const nestedRelations = detectRelationFields(selectSpec.select, targetMeta);
|
|
1194
|
+
const ownPath = relationResultPath(parentPath, rel);
|
|
1195
|
+
await mapWithConcurrency(
|
|
1196
|
+
queryKeys,
|
|
1197
|
+
options.plan?.plan.concurrency ?? RELATION_TRAVERSAL_CONCURRENCY,
|
|
1198
|
+
async (queryKey, j) => {
|
|
1199
|
+
const parentIndex = parentIndexes[j];
|
|
1200
|
+
const raw = queryKeyToRaw.get(serializeQueryKey(queryKey));
|
|
1201
|
+
if (!raw) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const updatable = options.updatable ?? false;
|
|
1205
|
+
let resolvedItem = hydrate([raw], selectSpec.select, targetMeta, updatable)[0] ?? null;
|
|
1206
|
+
if (resolvedItem && nestedRelations.length > 0) {
|
|
1207
|
+
resolvedItem = await resolveRelations(
|
|
1208
|
+
resolvedItem,
|
|
1209
|
+
raw,
|
|
1210
|
+
selectSpec.select,
|
|
1211
|
+
targetMeta,
|
|
1212
|
+
options,
|
|
1213
|
+
currentDepth + 1,
|
|
1214
|
+
ownPath
|
|
1215
|
+
);
|
|
1216
|
+
if (resolvedItem && updatable) {
|
|
1217
|
+
attachHiddenKey(resolvedItem, raw);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (resolvedItem && !passesPostLoad(resolvedItem, selectSpec, targetMeta)) {
|
|
1221
|
+
results[parentIndex] = null;
|
|
1222
|
+
} else {
|
|
1223
|
+
results[parentIndex] = resolvedItem;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
return results;
|
|
1228
|
+
}
|
|
1229
|
+
function passesPostLoad(item, spec, targetMeta) {
|
|
1230
|
+
if (spec.filter && !evaluateFilterClientSide(item, spec.filter, targetMeta)) {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
async function resolveSingleValueRelationsBatch(parentItems, rawParentItems, select, metadata, options, currentDepth, parentPath) {
|
|
1236
|
+
const results = parentItems.map((item, i) => {
|
|
1237
|
+
const copy = { ...item };
|
|
1238
|
+
if (options.updatable) {
|
|
1239
|
+
attachHiddenKey(copy, rawParentItems[i]);
|
|
1240
|
+
}
|
|
1241
|
+
return copy;
|
|
1242
|
+
});
|
|
1243
|
+
const relations = detectRelationFields(select, metadata).filter(
|
|
1244
|
+
(rel) => rel.type === "belongsTo" || rel.type === "hasOne"
|
|
1245
|
+
);
|
|
1246
|
+
await runStaged(relations, parentPath, options.plan, async (rel) => {
|
|
1247
|
+
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1248
|
+
const targetClass = rel.targetFactory();
|
|
1249
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1250
|
+
const batchResults = await fetchBelongsToHasOneBatch(
|
|
1251
|
+
rel,
|
|
1252
|
+
rawParentItems,
|
|
1253
|
+
selectSpec,
|
|
1254
|
+
targetMeta,
|
|
1255
|
+
options,
|
|
1256
|
+
currentDepth,
|
|
1257
|
+
parentPath
|
|
1258
|
+
);
|
|
1259
|
+
for (let i = 0; i < results.length; i++) {
|
|
1260
|
+
results[i][rel.propertyName] = batchResults[i];
|
|
1261
|
+
}
|
|
1262
|
+
});
|
|
1263
|
+
return results;
|
|
1264
|
+
}
|
|
1265
|
+
async function resolveRelations(parentItem, rawParentItem, select, metadata, options, currentDepth = 1, currentPath = "$") {
|
|
1266
|
+
const result = { ...parentItem };
|
|
1267
|
+
const relations = detectRelationFields(select, metadata);
|
|
1268
|
+
const plan2 = options.plan ?? (currentPath === "$" ? buildRelationExecutionPlan(select, metadata) : void 0);
|
|
1269
|
+
const opts = plan2 === options.plan ? options : { ...options, plan: plan2 };
|
|
1270
|
+
await runStaged(relations, currentPath, opts.plan, async (rel) => {
|
|
1271
|
+
const selectSpec = relationSpec(select[rel.propertyName]);
|
|
1272
|
+
const ownPath = relationResultPath(currentPath, rel);
|
|
1273
|
+
if (rel.type === "hasMany") {
|
|
1274
|
+
const targetClass2 = rel.targetFactory();
|
|
1275
|
+
const queryKey = buildRelationQueryKey(rel, rawParentItem);
|
|
1276
|
+
const limit = selectSpec.limit ?? rel.options?.limit?.default ?? 20;
|
|
1277
|
+
const order = selectSpec.order ?? rel.options?.order ?? "ASC";
|
|
1278
|
+
const listResult = await executeListInternal(targetClass2, queryKey, {
|
|
1279
|
+
select: selectSpec.select,
|
|
1280
|
+
limit,
|
|
1281
|
+
after: selectSpec.after,
|
|
1282
|
+
order,
|
|
1283
|
+
filter: selectSpec.filter,
|
|
1284
|
+
updatable: opts.updatable
|
|
1285
|
+
});
|
|
1286
|
+
let items = listResult.items;
|
|
1287
|
+
const rawItems = listResult.rawItems;
|
|
1288
|
+
const targetMeta2 = MetadataRegistry.get(targetClass2);
|
|
1289
|
+
const nestedRelations = detectRelationFields(selectSpec.select, targetMeta2);
|
|
1290
|
+
if (nestedRelations.length > 0) {
|
|
1291
|
+
items = await resolveSingleValueRelationsBatch(
|
|
1292
|
+
items,
|
|
1293
|
+
rawItems,
|
|
1294
|
+
selectSpec.select,
|
|
1295
|
+
targetMeta2,
|
|
1296
|
+
opts,
|
|
1297
|
+
currentDepth + 1,
|
|
1298
|
+
ownPath
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
result[rel.propertyName] = {
|
|
1302
|
+
items,
|
|
1303
|
+
cursor: listResult.cursor
|
|
1304
|
+
};
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
const targetClass = rel.targetFactory();
|
|
1308
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
1309
|
+
const [resolvedItem] = await fetchBelongsToHasOneBatch(
|
|
1310
|
+
rel,
|
|
1311
|
+
[rawParentItem],
|
|
1312
|
+
selectSpec,
|
|
1313
|
+
targetMeta,
|
|
1314
|
+
opts,
|
|
1315
|
+
currentDepth,
|
|
1316
|
+
currentPath
|
|
1317
|
+
);
|
|
1318
|
+
result[rel.propertyName] = resolvedItem;
|
|
1319
|
+
});
|
|
1320
|
+
return result;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// src/operations/query.ts
|
|
1324
|
+
async function executeQuery(modelClass, key2, selectSpec, options) {
|
|
1325
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1326
|
+
const queryFields = Object.keys(key2);
|
|
1327
|
+
const resolved = resolveKey(queryFields, metadata);
|
|
1328
|
+
if (resolved.type === "gsi" && !resolved.unique) {
|
|
1329
|
+
throw new Error(
|
|
1330
|
+
`Cannot use query() with non-unique GSI '${resolved.indexName}'. Use list() instead.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
const normalized = normalizeTopLevelSelect(selectSpec);
|
|
1334
|
+
const select = normalized.select;
|
|
1335
|
+
const filter = normalized.filter;
|
|
1336
|
+
const maxDepth = options?.maxDepth ?? 1;
|
|
1337
|
+
const relations = detectRelationFields(select, metadata);
|
|
1338
|
+
if (relations.length > 0) {
|
|
1339
|
+
validateDepth(select, metadata, maxDepth);
|
|
1340
|
+
}
|
|
1341
|
+
const implicitKeys = getImplicitKeyFields(select, metadata);
|
|
1342
|
+
const updatable = options?.updatable ?? false;
|
|
1343
|
+
const executionPlan = plan(metadata, {
|
|
1344
|
+
key: key2,
|
|
1345
|
+
select,
|
|
1346
|
+
filter,
|
|
1347
|
+
consistentRead: options?.consistentRead,
|
|
1348
|
+
additionalProjectionFields: implicitKeys.length > 0 ? implicitKeys : void 0,
|
|
1349
|
+
updatable
|
|
1350
|
+
});
|
|
1351
|
+
const operation = executionPlan.operations[0];
|
|
1352
|
+
const canParallelizeRelations = relations.length > 0 && relations.every(
|
|
1353
|
+
(rel) => Object.values(rel.keyBinding).every(
|
|
1354
|
+
(sourceField) => key2[sourceField] != null
|
|
1355
|
+
)
|
|
1356
|
+
);
|
|
1357
|
+
const parentPromise = execute(operation);
|
|
1358
|
+
const relationPromise = canParallelizeRelations ? resolveRelations({}, key2, select, metadata, { maxDepth, updatable }, 1) : null;
|
|
1359
|
+
let result;
|
|
1360
|
+
try {
|
|
1361
|
+
result = await parentPromise;
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
if (relationPromise) await relationPromise.catch(() => void 0);
|
|
1364
|
+
throw err;
|
|
1365
|
+
}
|
|
1366
|
+
if (result.items.length === 0) {
|
|
1367
|
+
if (relationPromise) await relationPromise.catch(() => void 0);
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
const rawItem = result.items[0];
|
|
1371
|
+
const hydrated = hydrate(result.items, select, metadata, updatable);
|
|
1372
|
+
let hydratedItem = hydrated[0] ?? null;
|
|
1373
|
+
if (hydratedItem && relations.length > 0) {
|
|
1374
|
+
if (relationPromise) {
|
|
1375
|
+
const resolvedRelations = await relationPromise;
|
|
1376
|
+
for (const rel of relations) {
|
|
1377
|
+
hydratedItem[rel.propertyName] = resolvedRelations[rel.propertyName];
|
|
1378
|
+
}
|
|
1379
|
+
} else {
|
|
1380
|
+
hydratedItem = await resolveRelations(
|
|
1381
|
+
hydratedItem,
|
|
1382
|
+
rawItem,
|
|
1383
|
+
select,
|
|
1384
|
+
metadata,
|
|
1385
|
+
{ maxDepth, updatable },
|
|
1386
|
+
1
|
|
1387
|
+
);
|
|
1388
|
+
if (hydratedItem && updatable) {
|
|
1389
|
+
attachHiddenKey(hydratedItem, rawItem);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
if (options?.hydrate) {
|
|
1394
|
+
return options.hydrate(hydratedItem);
|
|
1395
|
+
}
|
|
1396
|
+
return hydratedItem;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/operations/explain.ts
|
|
1400
|
+
function executeExplain(modelClass, key2, options) {
|
|
1401
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1402
|
+
const exclusiveStartKey = options?.after ? decodeCursor(options.after) : void 0;
|
|
1403
|
+
const normalized = normalizeTopLevelSelect(options?.select);
|
|
1404
|
+
const select = normalized.select ?? {};
|
|
1405
|
+
const filter = options?.filter ?? normalized.filter;
|
|
1406
|
+
const mainPlan = plan(metadata, {
|
|
1407
|
+
key: key2,
|
|
1408
|
+
select,
|
|
1409
|
+
limit: options?.limit,
|
|
1410
|
+
after: exclusiveStartKey,
|
|
1411
|
+
order: options?.order,
|
|
1412
|
+
consistentRead: options?.consistentRead,
|
|
1413
|
+
filter
|
|
1414
|
+
});
|
|
1415
|
+
const relationOps = planRelationOperations(metadata, select, {
|
|
1416
|
+
estimatedParentCount: 1
|
|
1417
|
+
});
|
|
1418
|
+
return {
|
|
1419
|
+
operations: [...mainPlan.operations, ...relationOps]
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/operations/batch.ts
|
|
1424
|
+
var BatchGetResult = class {
|
|
1425
|
+
groups;
|
|
1426
|
+
constructor(groups) {
|
|
1427
|
+
this.groups = groups;
|
|
1428
|
+
}
|
|
1429
|
+
get(model2) {
|
|
1430
|
+
const modelClass = resolveModelClass(model2);
|
|
1431
|
+
return this.groups.get(modelClass) ?? [];
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
function buildScalarSelect(metadata) {
|
|
1435
|
+
const select = {};
|
|
1436
|
+
for (const field2 of metadata.fields) {
|
|
1437
|
+
select[field2.propertyName] = true;
|
|
1438
|
+
}
|
|
1439
|
+
return select;
|
|
1440
|
+
}
|
|
1441
|
+
function buildDynamoKey(modelClass, keyObj) {
|
|
1442
|
+
const deleteInput = buildDeleteInput(modelClass, keyObj);
|
|
1443
|
+
return deleteInput.Key;
|
|
1444
|
+
}
|
|
1445
|
+
function hydrateBatchItems(rawItems, metadata) {
|
|
1446
|
+
if (rawItems.length === 0) {
|
|
1447
|
+
return [];
|
|
1448
|
+
}
|
|
1449
|
+
const select = buildScalarSelect(metadata);
|
|
1450
|
+
const hydrated = hydrate(rawItems, select, metadata);
|
|
1451
|
+
if (metadata.embeddedFields.length === 0) {
|
|
1452
|
+
return hydrated;
|
|
1453
|
+
}
|
|
1454
|
+
return hydrated.map((item, index) => {
|
|
1455
|
+
const raw = rawItems[index];
|
|
1456
|
+
const merged = { ...item };
|
|
1457
|
+
for (const emb of metadata.embeddedFields) {
|
|
1458
|
+
const rawEmb = raw?.[emb.propertyName];
|
|
1459
|
+
if (rawEmb !== void 0 && rawEmb !== null) {
|
|
1460
|
+
merged[emb.propertyName] = rawEmb;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
return merged;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
async function executeBatchGetForTable(tableName, entries) {
|
|
1467
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1468
|
+
if (entries.length === 0) {
|
|
1469
|
+
return groups;
|
|
1470
|
+
}
|
|
1471
|
+
const keys = entries.map((entry) => entry.dynamoKey);
|
|
1472
|
+
const { items: rawItems } = await ClientManager.getExecutor().batchGet({
|
|
1473
|
+
tableName,
|
|
1474
|
+
keys
|
|
1475
|
+
});
|
|
1476
|
+
const entriesByModel = /* @__PURE__ */ new Map();
|
|
1477
|
+
for (const entry of entries) {
|
|
1478
|
+
const existing = entriesByModel.get(entry.modelClass) ?? [];
|
|
1479
|
+
existing.push(entry);
|
|
1480
|
+
entriesByModel.set(entry.modelClass, existing);
|
|
1481
|
+
}
|
|
1482
|
+
for (const [modelClass, modelEntries] of entriesByModel) {
|
|
1483
|
+
const metadata = modelEntries[0].metadata;
|
|
1484
|
+
const expectedKeys = new Set(
|
|
1485
|
+
modelEntries.map(
|
|
1486
|
+
(entry) => `${String(entry.dynamoKey.PK)}::${String(entry.dynamoKey.SK)}`
|
|
1487
|
+
)
|
|
1488
|
+
);
|
|
1489
|
+
const modelRawItems = rawItems.filter(
|
|
1490
|
+
(item) => expectedKeys.has(`${String(item.PK)}::${String(item.SK)}`)
|
|
1491
|
+
);
|
|
1492
|
+
groups.set(modelClass, hydrateBatchItems(modelRawItems, metadata));
|
|
1493
|
+
}
|
|
1494
|
+
return groups;
|
|
1495
|
+
}
|
|
1496
|
+
async function executeBatchGet(requests) {
|
|
1497
|
+
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1498
|
+
for (const request of requests) {
|
|
1499
|
+
const modelClass = resolveModelClass(request.model);
|
|
1500
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1501
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1502
|
+
for (const keyObj of request.keys) {
|
|
1503
|
+
const entry = {
|
|
1504
|
+
modelClass,
|
|
1505
|
+
metadata,
|
|
1506
|
+
dynamoKey: buildDynamoKey(modelClass, keyObj)
|
|
1507
|
+
};
|
|
1508
|
+
const existing = entriesByTable.get(tableName) ?? [];
|
|
1509
|
+
existing.push(entry);
|
|
1510
|
+
entriesByTable.set(tableName, existing);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const mergedGroups = /* @__PURE__ */ new Map();
|
|
1514
|
+
for (const [, entries] of entriesByTable) {
|
|
1515
|
+
const tableName = TableMapping.resolve(entries[0].metadata.tableName);
|
|
1516
|
+
const tableGroups = await executeBatchGetForTable(tableName, entries);
|
|
1517
|
+
for (const [modelClass, items] of tableGroups) {
|
|
1518
|
+
const existing = mergedGroups.get(modelClass) ?? [];
|
|
1519
|
+
mergedGroups.set(modelClass, existing.concat(items));
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return new BatchGetResult(mergedGroups);
|
|
1523
|
+
}
|
|
1524
|
+
function toExecItem(entry) {
|
|
1525
|
+
if (entry.request.type === "put") {
|
|
1526
|
+
return { type: "put", item: entry.request.putInput.Item };
|
|
1527
|
+
}
|
|
1528
|
+
return { type: "delete", key: entry.request.deleteInput.Key };
|
|
1529
|
+
}
|
|
1530
|
+
async function executeBatchWrite(requests) {
|
|
1531
|
+
const entriesByTable = /* @__PURE__ */ new Map();
|
|
1532
|
+
for (const request of requests) {
|
|
1533
|
+
const modelClass = resolveModelClass(request.model);
|
|
1534
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
1535
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
1536
|
+
if (request.type === "put") {
|
|
1537
|
+
const putInput = buildPutInput(modelClass, request.item, request.options);
|
|
1538
|
+
const entry2 = {
|
|
1539
|
+
tableName,
|
|
1540
|
+
modelName: modelClass.name,
|
|
1541
|
+
request: { type: "put", putInput }
|
|
1542
|
+
};
|
|
1543
|
+
const existing2 = entriesByTable.get(tableName) ?? [];
|
|
1544
|
+
existing2.push(entry2);
|
|
1545
|
+
entriesByTable.set(tableName, existing2);
|
|
1546
|
+
continue;
|
|
1547
|
+
}
|
|
1548
|
+
const deleteInput = buildDeleteInput(modelClass, request.key, request.options);
|
|
1549
|
+
const entry = {
|
|
1550
|
+
tableName,
|
|
1551
|
+
modelName: modelClass.name,
|
|
1552
|
+
request: { type: "delete", deleteInput }
|
|
1553
|
+
};
|
|
1554
|
+
const existing = entriesByTable.get(tableName) ?? [];
|
|
1555
|
+
existing.push(entry);
|
|
1556
|
+
entriesByTable.set(tableName, existing);
|
|
1557
|
+
}
|
|
1558
|
+
const executor = ClientManager.getExecutor();
|
|
1559
|
+
for (const [tableName, entries] of entriesByTable) {
|
|
1560
|
+
await executor.batchWrite(tableName, entries.map(toExecItem));
|
|
1561
|
+
}
|
|
1562
|
+
if (ChangeCaptureRegistry.hasSubscribers()) {
|
|
1563
|
+
for (const [, entries] of entriesByTable) {
|
|
1564
|
+
for (const entry of entries) {
|
|
1565
|
+
if (entry.request.type === "put") {
|
|
1566
|
+
const item = entry.request.putInput.Item;
|
|
1567
|
+
captureWrite({
|
|
1568
|
+
table: entry.tableName,
|
|
1569
|
+
...entry.modelName ? { model: entry.modelName } : {},
|
|
1570
|
+
op: "put",
|
|
1571
|
+
keys: { pk: String(item.PK), sk: String(item.SK ?? "") },
|
|
1572
|
+
newItem: item
|
|
1573
|
+
});
|
|
1574
|
+
} else {
|
|
1575
|
+
const dkey = entry.request.deleteInput.Key;
|
|
1576
|
+
captureWrite({
|
|
1577
|
+
table: entry.tableName,
|
|
1578
|
+
...entry.modelName ? { model: entry.modelName } : {},
|
|
1579
|
+
op: "delete",
|
|
1580
|
+
keys: { pk: String(dkey.PK), sk: String(dkey.SK ?? "") }
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// src/model/DDBModel.ts
|
|
1589
|
+
var DDBModel = class {
|
|
1590
|
+
static setClient(client) {
|
|
1591
|
+
ClientManager.setClient(client);
|
|
1592
|
+
}
|
|
1593
|
+
static setTableMapping(mapping) {
|
|
1594
|
+
TableMapping.set(mapping);
|
|
1595
|
+
}
|
|
1596
|
+
static async transaction(fn) {
|
|
1597
|
+
await executeTransaction(fn);
|
|
1598
|
+
}
|
|
1599
|
+
static async batchGet(requests) {
|
|
1600
|
+
return executeBatchGet(requests);
|
|
1601
|
+
}
|
|
1602
|
+
static async batchWrite(requests) {
|
|
1603
|
+
return executeBatchWrite(requests);
|
|
1604
|
+
}
|
|
1605
|
+
static asModel() {
|
|
1606
|
+
const modelClass = this;
|
|
1607
|
+
const modelStatic = {
|
|
1608
|
+
col: createColumnMap(),
|
|
1609
|
+
project(select) {
|
|
1610
|
+
return buildProject(select);
|
|
1611
|
+
},
|
|
1612
|
+
relation(select) {
|
|
1613
|
+
return buildRelation(select);
|
|
1614
|
+
},
|
|
1615
|
+
// The public `query` is overloaded (plain / updatable / hydrate, the last
|
|
1616
|
+
// introducing a fresh return-type parameter `R`, issue #53). An overloaded
|
|
1617
|
+
// method on an object literal cannot contextually infer its single
|
|
1618
|
+
// implementation signature, so the implementation is written with explicit
|
|
1619
|
+
// runtime-facing parameter types and assigned through the interface's
|
|
1620
|
+
// method type — the public overloads (the only caller-visible surface)
|
|
1621
|
+
// are untouched.
|
|
1622
|
+
query: ((key2, select, options) => {
|
|
1623
|
+
if (isRecordingContractMethod()) {
|
|
1624
|
+
recordContractOp("query", modelStatic, {
|
|
1625
|
+
keys: key2,
|
|
1626
|
+
select
|
|
1627
|
+
});
|
|
1628
|
+
return Promise.resolve(null);
|
|
1629
|
+
}
|
|
1630
|
+
return executeQuery(modelClass, key2, select, options);
|
|
1631
|
+
}),
|
|
1632
|
+
async list(key2, options) {
|
|
1633
|
+
if (isRecordingContractMethod()) {
|
|
1634
|
+
recordContractOp("list", modelStatic, {
|
|
1635
|
+
keys: key2,
|
|
1636
|
+
select: options?.select
|
|
1637
|
+
});
|
|
1638
|
+
return { items: [], cursor: null };
|
|
1639
|
+
}
|
|
1640
|
+
const result = await executeList(
|
|
1641
|
+
modelClass,
|
|
1642
|
+
key2,
|
|
1643
|
+
options
|
|
1644
|
+
);
|
|
1645
|
+
return result;
|
|
1646
|
+
},
|
|
1647
|
+
explain(key2, options) {
|
|
1648
|
+
return executeExplain(
|
|
1649
|
+
modelClass,
|
|
1650
|
+
key2,
|
|
1651
|
+
options
|
|
1652
|
+
);
|
|
1653
|
+
},
|
|
1654
|
+
async put(item, options) {
|
|
1655
|
+
if (isRecordingContractMethod()) {
|
|
1656
|
+
recordContractOp("put", modelStatic, {
|
|
1657
|
+
item,
|
|
1658
|
+
condition: options?.condition,
|
|
1659
|
+
batch: options?.batch
|
|
1660
|
+
});
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
await executePut(
|
|
1664
|
+
modelClass,
|
|
1665
|
+
item,
|
|
1666
|
+
options
|
|
1667
|
+
);
|
|
1668
|
+
},
|
|
1669
|
+
async update(entity, changes, options) {
|
|
1670
|
+
if (isRecordingContractMethod()) {
|
|
1671
|
+
recordContractOp("update", modelStatic, {
|
|
1672
|
+
keys: entity,
|
|
1673
|
+
changes,
|
|
1674
|
+
condition: options?.condition,
|
|
1675
|
+
batch: options?.batch
|
|
1676
|
+
});
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
await executeUpdate(
|
|
1680
|
+
modelClass,
|
|
1681
|
+
entity,
|
|
1682
|
+
changes,
|
|
1683
|
+
options
|
|
1684
|
+
);
|
|
1685
|
+
},
|
|
1686
|
+
async delete(key2, options) {
|
|
1687
|
+
if (isRecordingContractMethod()) {
|
|
1688
|
+
recordContractOp("delete", modelStatic, {
|
|
1689
|
+
keys: key2,
|
|
1690
|
+
condition: options?.condition,
|
|
1691
|
+
batch: options?.batch
|
|
1692
|
+
});
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
await executeDelete(modelClass, key2, options);
|
|
1696
|
+
}
|
|
1697
|
+
};
|
|
1698
|
+
return attachModelClass(
|
|
1699
|
+
modelStatic,
|
|
1700
|
+
modelClass
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
|
|
1705
|
+
// src/operations/declarative-transaction.ts
|
|
1706
|
+
var PLACEHOLDER_RE = /\{[^{}]+\}/g;
|
|
1707
|
+
function resolveTemplate(template, params, element) {
|
|
1708
|
+
return template.replace(PLACEHOLDER_RE, (match) => {
|
|
1709
|
+
const name = match.slice(1, -1);
|
|
1710
|
+
if (name.startsWith("item.")) {
|
|
1711
|
+
const field2 = name.slice("item.".length);
|
|
1712
|
+
if (!element || !(field2 in element)) {
|
|
1713
|
+
throw new Error(
|
|
1714
|
+
`declarative transaction: element field '${field2}' is not bound; template '${template}' is only valid inside a forEach over the array param that supplies '${field2}'.`
|
|
1715
|
+
);
|
|
1716
|
+
}
|
|
1717
|
+
return String(element[field2]);
|
|
1718
|
+
}
|
|
1719
|
+
if (!(name in params) || params[name] === void 0 || params[name] === null) {
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`declarative transaction: template '${template}' references unbound parameter '${name}'.`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return String(params[name]);
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
function isWholePlaceholder(template) {
|
|
1728
|
+
return /^\{[^{}]+\}$/.test(template);
|
|
1729
|
+
}
|
|
1730
|
+
function resolveValue(template, params, element) {
|
|
1731
|
+
if (isWholePlaceholder(template)) {
|
|
1732
|
+
const name = template.slice(1, -1);
|
|
1733
|
+
if (name.startsWith("item.")) {
|
|
1734
|
+
const field2 = name.slice("item.".length);
|
|
1735
|
+
if (element && field2 in element) return element[field2];
|
|
1736
|
+
} else if (name in params) {
|
|
1737
|
+
return params[name];
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
return resolveTemplate(template, params, element);
|
|
1741
|
+
}
|
|
1742
|
+
function resolveRecord(record, params, element) {
|
|
1743
|
+
const out = {};
|
|
1744
|
+
for (const [field2, tmpl] of Object.entries(record ?? {})) {
|
|
1745
|
+
out[field2] = resolveValue(tmpl, params, element);
|
|
1746
|
+
}
|
|
1747
|
+
return out;
|
|
1748
|
+
}
|
|
1749
|
+
function fillKeyTemplate(template, item) {
|
|
1750
|
+
return template.replace(PLACEHOLDER_RE, (match) => {
|
|
1751
|
+
const name = match.slice(1, -1);
|
|
1752
|
+
return item[name] === void 0 ? "" : String(item[name]);
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
function keyTemplateFillable(template, item) {
|
|
1756
|
+
const matches = template.match(PLACEHOLDER_RE) ?? [];
|
|
1757
|
+
return matches.every((m) => m.slice(1, -1) in item);
|
|
1758
|
+
}
|
|
1759
|
+
function addKeyAttributes(entity, item) {
|
|
1760
|
+
if (entity.key) {
|
|
1761
|
+
item.PK = fillKeyTemplate(entity.key.pkTemplate, item);
|
|
1762
|
+
if (entity.key.skTemplate !== null) {
|
|
1763
|
+
item.SK = fillKeyTemplate(entity.key.skTemplate, item);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
for (const gsi2 of entity.gsis) {
|
|
1767
|
+
if (keyTemplateFillable(gsi2.pkTemplate, item)) {
|
|
1768
|
+
item[`${gsi2.indexName}PK`] = fillKeyTemplate(gsi2.pkTemplate, item);
|
|
1769
|
+
}
|
|
1770
|
+
if (gsi2.skTemplate !== null && keyTemplateFillable(gsi2.skTemplate, item)) {
|
|
1771
|
+
item[`${gsi2.indexName}SK`] = fillKeyTemplate(gsi2.skTemplate, item);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function whenHolds(when2, params, element) {
|
|
1776
|
+
const left = resolveTemplate(when2.left, params, element);
|
|
1777
|
+
const right = resolveTemplate(when2.right, params, element);
|
|
1778
|
+
return when2.op === "eq" ? left === right : left !== right;
|
|
1779
|
+
}
|
|
1780
|
+
function applyCondition(condition, target, params, element) {
|
|
1781
|
+
if (!condition) return;
|
|
1782
|
+
const names = target.ExpressionAttributeNames ??= {};
|
|
1783
|
+
if (condition.kind === "notExists") {
|
|
1784
|
+
names["#pk"] = "PK";
|
|
1785
|
+
target.ConditionExpression = "attribute_not_exists(#pk)";
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
|
|
1789
|
+
const fn = condition.kind === "attributeExists" ? "attribute_exists" : "attribute_not_exists";
|
|
1790
|
+
names["#ce"] = condition.field;
|
|
1791
|
+
target.ConditionExpression = `${fn}(#ce)`;
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
const values = target.ExpressionAttributeValues ??= {};
|
|
1795
|
+
const clauses = [];
|
|
1796
|
+
let i = 0;
|
|
1797
|
+
for (const [field2, tmpl] of Object.entries(condition.fields)) {
|
|
1798
|
+
const n = `#e${i}`;
|
|
1799
|
+
const v = `:e${i}`;
|
|
1800
|
+
names[n] = field2;
|
|
1801
|
+
values[v] = resolveValue(tmpl, params, element);
|
|
1802
|
+
clauses.push(`${n} = ${v}`);
|
|
1803
|
+
i++;
|
|
1804
|
+
}
|
|
1805
|
+
target.ConditionExpression = clauses.join(" AND ");
|
|
1806
|
+
}
|
|
1807
|
+
function entityByName(manifest, name) {
|
|
1808
|
+
const entity = manifest.entities[name];
|
|
1809
|
+
if (!entity) {
|
|
1810
|
+
throw new Error(`declarative transaction: unknown entity '${name}' in manifest.`);
|
|
1811
|
+
}
|
|
1812
|
+
return entity;
|
|
1813
|
+
}
|
|
1814
|
+
function physicalTableName(manifest, logical) {
|
|
1815
|
+
return manifest.tables[logical]?.physicalName ?? logical;
|
|
1816
|
+
}
|
|
1817
|
+
function buildTransactItem(spec, manifest, params, element) {
|
|
1818
|
+
const literal2 = spec.literalKey === true;
|
|
1819
|
+
const entity = literal2 ? void 0 : entityByName(manifest, spec.entity);
|
|
1820
|
+
const table = literal2 ? physicalTableName(manifest, spec.tableName) : entity.physicalName;
|
|
1821
|
+
if (spec.type === "Put") {
|
|
1822
|
+
const item = resolveRecord(spec.item, params, element);
|
|
1823
|
+
if (entity) addKeyAttributes(entity, item);
|
|
1824
|
+
const put = { TableName: table, Item: item };
|
|
1825
|
+
applyCondition(spec.condition, put, params, element);
|
|
1826
|
+
return { Put: put };
|
|
1827
|
+
}
|
|
1828
|
+
const key2 = resolveRecord(spec.keyCondition, params, element);
|
|
1829
|
+
if (spec.type === "Delete") {
|
|
1830
|
+
const del = { TableName: table, Key: key2 };
|
|
1831
|
+
applyCondition(spec.condition, del, params, element);
|
|
1832
|
+
return { Delete: del };
|
|
1833
|
+
}
|
|
1834
|
+
if (spec.type === "ConditionCheck") {
|
|
1835
|
+
if (!spec.condition) {
|
|
1836
|
+
throw new Error(
|
|
1837
|
+
`declarative transaction: ConditionCheck on '${spec.entity}' has no condition; a ConditionCheck must carry the assertion it makes.`
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
const check = { TableName: table, Key: key2 };
|
|
1841
|
+
applyCondition(spec.condition, check, params, element);
|
|
1842
|
+
return { ConditionCheck: check };
|
|
1843
|
+
}
|
|
1844
|
+
const changes = resolveRecord(spec.changes, params, element);
|
|
1845
|
+
const adds = resolveRecord(spec.add, params, element);
|
|
1846
|
+
const names = {};
|
|
1847
|
+
const values = {};
|
|
1848
|
+
const sets = [];
|
|
1849
|
+
const addClauses = [];
|
|
1850
|
+
let i = 0;
|
|
1851
|
+
for (const [field2, value] of Object.entries(changes)) {
|
|
1852
|
+
const n = `#c${i}`;
|
|
1853
|
+
const v = `:c${i}`;
|
|
1854
|
+
names[n] = field2;
|
|
1855
|
+
values[v] = value;
|
|
1856
|
+
sets.push(`${n} = ${v}`);
|
|
1857
|
+
i++;
|
|
1858
|
+
}
|
|
1859
|
+
let j = 0;
|
|
1860
|
+
for (const [field2, value] of Object.entries(adds)) {
|
|
1861
|
+
const n = `#a${j}`;
|
|
1862
|
+
const v = `:a${j}`;
|
|
1863
|
+
names[n] = field2;
|
|
1864
|
+
values[v] = typeof value === "string" ? Number(value) : value;
|
|
1865
|
+
addClauses.push(`${n} ${v}`);
|
|
1866
|
+
j++;
|
|
1867
|
+
}
|
|
1868
|
+
const update = { TableName: table, Key: key2 };
|
|
1869
|
+
const exprParts = [];
|
|
1870
|
+
if (sets.length > 0) exprParts.push("SET " + sets.join(", "));
|
|
1871
|
+
if (addClauses.length > 0) exprParts.push("ADD " + addClauses.join(", "));
|
|
1872
|
+
if (exprParts.length > 0) {
|
|
1873
|
+
update.UpdateExpression = exprParts.join(" ");
|
|
1874
|
+
update.ExpressionAttributeNames = names;
|
|
1875
|
+
update.ExpressionAttributeValues = values;
|
|
1876
|
+
}
|
|
1877
|
+
applyCondition(spec.condition, update, params, element);
|
|
1878
|
+
return { Update: update };
|
|
1879
|
+
}
|
|
1880
|
+
function expandTransactionItems(spec, manifest, params) {
|
|
1881
|
+
const expanded = [];
|
|
1882
|
+
const emit = (item, element) => {
|
|
1883
|
+
const model2 = item.literalKey === true || item.entity === MARKER_ROW_ENTITY ? void 0 : item.entity;
|
|
1884
|
+
expanded.push({ item: buildTransactItem(item, manifest, params, element), model: model2 });
|
|
1885
|
+
};
|
|
1886
|
+
for (const item of spec.items) {
|
|
1887
|
+
if (item.forEach) {
|
|
1888
|
+
const source = params[item.forEach.source];
|
|
1889
|
+
if (!Array.isArray(source)) {
|
|
1890
|
+
throw new Error(
|
|
1891
|
+
`declarative transaction: forEach source '${item.forEach.source}' must be an array param; got ${typeof source}.`
|
|
1892
|
+
);
|
|
1893
|
+
}
|
|
1894
|
+
for (const element of source) {
|
|
1895
|
+
if (item.when && !whenHolds(item.when, params, element)) continue;
|
|
1896
|
+
emit(item, element);
|
|
1897
|
+
}
|
|
1898
|
+
} else {
|
|
1899
|
+
if (item.when && !whenHolds(item.when, params, void 0)) continue;
|
|
1900
|
+
emit(item, void 0);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return expanded;
|
|
1904
|
+
}
|
|
1905
|
+
function expandTransaction(spec, manifest, params) {
|
|
1906
|
+
const expanded = expandTransactionItems(spec, manifest, params).map((e) => e.item);
|
|
1907
|
+
const out = collapseSameKeyItems(
|
|
1908
|
+
expanded
|
|
1909
|
+
);
|
|
1910
|
+
if (out.length > MAX_TRANSACT_ITEMS) {
|
|
1911
|
+
throw new Error(
|
|
1912
|
+
`declarative transaction: expanded to ${out.length} items, exceeds the DynamoDB TransactWriteItems limit of ${MAX_TRANSACT_ITEMS}.`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
return out;
|
|
1916
|
+
}
|
|
1917
|
+
async function executeDeclarativeTransaction(spec, manifest, params) {
|
|
1918
|
+
const expanded = expandTransactionItems(spec, manifest, params);
|
|
1919
|
+
if (expanded.length === 0) return;
|
|
1920
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
1921
|
+
for (const { item, model: model2 } of expanded) {
|
|
1922
|
+
if (model2 !== void 0) {
|
|
1923
|
+
modelBySignature.set(
|
|
1924
|
+
execItemKeySignature(item),
|
|
1925
|
+
model2
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
await commitTransaction(
|
|
1930
|
+
expanded.map((e) => e.item),
|
|
1931
|
+
{
|
|
1932
|
+
modelBySignature,
|
|
1933
|
+
limitError: (count) => new Error(
|
|
1934
|
+
`declarative transaction: expanded to ${count} items, exceeds the DynamoDB TransactWriteItems limit of ${MAX_TRANSACT_ITEMS}.`
|
|
1935
|
+
)
|
|
1936
|
+
}
|
|
1937
|
+
);
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/runtime/per-key-cursor.ts
|
|
1941
|
+
function serializeContractKey(key2) {
|
|
1942
|
+
const sortedEntries = Object.keys(key2).sort().map((field2) => [field2, key2[field2]]);
|
|
1943
|
+
return JSON.stringify(Object.fromEntries(sortedEntries));
|
|
1944
|
+
}
|
|
1945
|
+
function encodePerKeyCursor(key2, inner) {
|
|
1946
|
+
if (inner === null) return null;
|
|
1947
|
+
const envelope = {
|
|
1948
|
+
key: serializeContractKey(key2),
|
|
1949
|
+
inner
|
|
1950
|
+
};
|
|
1951
|
+
return encodeCursor(envelope);
|
|
1952
|
+
}
|
|
1953
|
+
function decodePerKeyCursor(cursor, expectedKey) {
|
|
1954
|
+
let envelope;
|
|
1955
|
+
try {
|
|
1956
|
+
envelope = decodeCursor(cursor);
|
|
1957
|
+
} catch {
|
|
1958
|
+
throw new Error(
|
|
1959
|
+
`Invalid per-key cursor: the value passed as \`after\` is not a cursor minted by this runtime (it failed to decode).`
|
|
1960
|
+
);
|
|
1961
|
+
}
|
|
1962
|
+
if (typeof envelope !== "object" || envelope === null || typeof envelope.key !== "string" || typeof envelope.inner !== "string") {
|
|
1963
|
+
throw new Error(
|
|
1964
|
+
`Invalid per-key cursor: the decoded envelope is missing its key / inner fields. A range cursor must be a per-key envelope minted by this runtime.`
|
|
1965
|
+
);
|
|
1966
|
+
}
|
|
1967
|
+
const expected = serializeContractKey(expectedKey);
|
|
1968
|
+
if (envelope.key !== expected) {
|
|
1969
|
+
throw new Error(
|
|
1970
|
+
`Per-key cursor mismatch: the supplied \`after\` cursor belongs to key ${envelope.key}, but the method is being called for key ${expected}. A range cursor may only resume pagination of the same key it was issued for.`
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
return envelope.inner;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// src/runtime/contract-runtime.ts
|
|
1977
|
+
var CONTRACT_RANGE_FANOUT_CONCURRENCY = RELATION_TRAVERSAL_CONCURRENCY;
|
|
1978
|
+
function resolveQueryMethod(contract, methodName) {
|
|
1979
|
+
const method = contract.methods[methodName];
|
|
1980
|
+
if (method === void 0) {
|
|
1981
|
+
throw new Error(
|
|
1982
|
+
`Contract runtime: query method '${methodName}' does not exist on this contract. Available methods: ${Object.keys(contract.methods).sort().join(", ") || "(none)"}.`
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
return method;
|
|
1986
|
+
}
|
|
1987
|
+
function modelClassOf(op) {
|
|
1988
|
+
return op.entity.modelClass;
|
|
1989
|
+
}
|
|
1990
|
+
function selectOf(op) {
|
|
1991
|
+
return op.select ?? {};
|
|
1992
|
+
}
|
|
1993
|
+
async function executePointSingle(op, key2, params) {
|
|
1994
|
+
return executeQuery(modelClassOf(op), key2, selectOf(op), {
|
|
1995
|
+
consistentRead: params.consistentRead
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
async function executeRangeSingle(op, key2, params) {
|
|
1999
|
+
const innerAfter = params.after !== void 0 ? decodePerKeyCursor(params.after, key2) : void 0;
|
|
2000
|
+
const result = await executeListInternal(modelClassOf(op), key2, {
|
|
2001
|
+
select: selectOf(op),
|
|
2002
|
+
limit: params.limit,
|
|
2003
|
+
after: innerAfter,
|
|
2004
|
+
order: params.order
|
|
2005
|
+
});
|
|
2006
|
+
return {
|
|
2007
|
+
items: result.items,
|
|
2008
|
+
cursor: encodePerKeyCursor(key2, result.cursor)
|
|
2009
|
+
};
|
|
2010
|
+
}
|
|
2011
|
+
async function executePointBatch(op, keys, _params) {
|
|
2012
|
+
return executeKeyedBatchGet(modelClassOf(op), keys, selectOf(op));
|
|
2013
|
+
}
|
|
2014
|
+
async function executeKeyedBatchGet(modelClass, keys, select) {
|
|
2015
|
+
const metadata = MetadataRegistry.get(modelClass);
|
|
2016
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
2017
|
+
const resolved = keys.map((key2) => {
|
|
2018
|
+
const dynamoKey = buildDeleteInput(modelClass, key2).Key;
|
|
2019
|
+
return { id: serializeContractKey(key2), dynamoKey, marker: dynamoKeyMarker(dynamoKey) };
|
|
2020
|
+
});
|
|
2021
|
+
const uniqueByMarker = /* @__PURE__ */ new Map();
|
|
2022
|
+
for (const r of resolved) {
|
|
2023
|
+
if (!uniqueByMarker.has(r.marker)) uniqueByMarker.set(r.marker, r.dynamoKey);
|
|
2024
|
+
}
|
|
2025
|
+
const rawByMarker = /* @__PURE__ */ new Map();
|
|
2026
|
+
const uniqueKeys = [...uniqueByMarker.values()];
|
|
2027
|
+
if (uniqueKeys.length > 0) {
|
|
2028
|
+
const { items } = await ClientManager.getExecutor().batchGet({
|
|
2029
|
+
tableName,
|
|
2030
|
+
keys: uniqueKeys
|
|
2031
|
+
});
|
|
2032
|
+
for (const raw of items) {
|
|
2033
|
+
rawByMarker.set(dynamoKeyMarker(raw), raw);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
const out = /* @__PURE__ */ new Map();
|
|
2037
|
+
for (const r of resolved) {
|
|
2038
|
+
if (out.has(r.id)) continue;
|
|
2039
|
+
const raw = rawByMarker.get(r.marker);
|
|
2040
|
+
out.set(r.id, raw !== void 0 ? hydrateOne(raw, select, metadata) : null);
|
|
2041
|
+
}
|
|
2042
|
+
return out;
|
|
2043
|
+
}
|
|
2044
|
+
function dynamoKeyMarker(keyOrItem) {
|
|
2045
|
+
return `${String(keyOrItem.PK)}::${String(keyOrItem.SK ?? "")}`;
|
|
2046
|
+
}
|
|
2047
|
+
function hydrateOne(raw, select, metadata) {
|
|
2048
|
+
return hydrate([raw], select, metadata)[0];
|
|
2049
|
+
}
|
|
2050
|
+
async function executeQueryMethod(contract, methodName, keyOrKeys, params = {}) {
|
|
2051
|
+
const method = resolveQueryMethod(contract, methodName);
|
|
2052
|
+
if (Array.isArray(keyOrKeys)) {
|
|
2053
|
+
if (method.inputArity === "single") {
|
|
2054
|
+
const kind = method.resolution === "range" ? "range" : "unique-GSI point";
|
|
2055
|
+
throw new Error(
|
|
2056
|
+
`Contract runtime: ${kind} method '${methodName}' was called with an array of keys, but its inputArity is 'single'. A ${kind} resolution is one ${method.resolution === "range" ? "partition" : "GSI"} Query per key, so an array would be an N+1 fan-out \u2014 forbidden by the contract's N+1 rule. Loop in application code if you genuinely need N independent reads.`
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
const keyed = await executePointBatch(method.op, keyOrKeys, params);
|
|
2060
|
+
await resolveCompositions(method.op, [...keyed.values()].filter(isRecord));
|
|
2061
|
+
return keyed;
|
|
2062
|
+
}
|
|
2063
|
+
const key2 = keyOrKeys;
|
|
2064
|
+
if (method.resolution === "point") {
|
|
2065
|
+
const item = await executePointSingle(method.op, key2, params);
|
|
2066
|
+
if (item !== null) await resolveCompositions(method.op, [item]);
|
|
2067
|
+
return item;
|
|
2068
|
+
}
|
|
2069
|
+
const connection = await executeRangeSingle(method.op, key2, params);
|
|
2070
|
+
await resolveCompositions(method.op, connection.items.filter(isRecord));
|
|
2071
|
+
return connection;
|
|
2072
|
+
}
|
|
2073
|
+
function isRecord(value) {
|
|
2074
|
+
return typeof value === "object" && value !== null;
|
|
2075
|
+
}
|
|
2076
|
+
async function resolveCompositions(op, parents, plan2) {
|
|
2077
|
+
const compose = op.compose;
|
|
2078
|
+
if (compose === void 0 || compose.length === 0 || parents.length === 0) return;
|
|
2079
|
+
const resolved = plan2 ?? deriveCompositionPlan(compose.length);
|
|
2080
|
+
for (const stage of resolved.stages) {
|
|
2081
|
+
await mapWithConcurrency(
|
|
2082
|
+
stage.map((i) => compose[i]),
|
|
2083
|
+
resolved.concurrency,
|
|
2084
|
+
(edge) => resolveComposition(edge, parents)
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
async function resolveComposition(edge, parents) {
|
|
2089
|
+
const childMethod = edge.method;
|
|
2090
|
+
if (childMethod.resolution !== "point") {
|
|
2091
|
+
throw new Error(
|
|
2092
|
+
`Contract runtime: composed child '${edge.as}' resolves to a 'range' method, but an External Query child must be 'point' so it coalesces to one BatchGetItem across all parent records. A range child is an N+1 fan-out (it is rejected at build time by the N+1 checker).`
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
const childOp = childMethod.op;
|
|
2096
|
+
const childSelect = selectOf(childOp);
|
|
2097
|
+
const bindFields = Object.keys(edge.bind);
|
|
2098
|
+
const bound = [];
|
|
2099
|
+
const childKeys = [];
|
|
2100
|
+
for (const parent of parents) {
|
|
2101
|
+
const childKey = {};
|
|
2102
|
+
let complete = true;
|
|
2103
|
+
for (const field2 of bindFields) {
|
|
2104
|
+
const value = readFromPath(parent, edge.bind[field2].path);
|
|
2105
|
+
if (value === void 0 || value === null) {
|
|
2106
|
+
complete = false;
|
|
2107
|
+
break;
|
|
2108
|
+
}
|
|
2109
|
+
childKey[field2] = value;
|
|
2110
|
+
}
|
|
2111
|
+
if (!complete) {
|
|
2112
|
+
parent[edge.as] = null;
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
bound.push({ parent, childKey, keyId: serializeContractKey(childKey) });
|
|
2116
|
+
childKeys.push(childKey);
|
|
2117
|
+
}
|
|
2118
|
+
if (childKeys.length === 0) return;
|
|
2119
|
+
const childResults = await executeKeyedBatchGet(
|
|
2120
|
+
modelClassOf(childOp),
|
|
2121
|
+
childKeys,
|
|
2122
|
+
childSelect
|
|
2123
|
+
);
|
|
2124
|
+
for (const { parent, keyId } of bound) {
|
|
2125
|
+
const child = childResults.get(keyId);
|
|
2126
|
+
parent[edge.as] = child ?? null;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
function readFromPath(record, path) {
|
|
2130
|
+
if (path === "$") return record;
|
|
2131
|
+
const segments = path.slice(2).split(".");
|
|
2132
|
+
let current = record;
|
|
2133
|
+
for (const segment of segments) {
|
|
2134
|
+
if (typeof current !== "object" || current === null) return void 0;
|
|
2135
|
+
current = current[segment];
|
|
2136
|
+
}
|
|
2137
|
+
return current;
|
|
2138
|
+
}
|
|
2139
|
+
async function executeRangeFanout(op, keys, params = {}) {
|
|
2140
|
+
const connections = await mapWithConcurrency(
|
|
2141
|
+
keys,
|
|
2142
|
+
CONTRACT_RANGE_FANOUT_CONCURRENCY,
|
|
2143
|
+
(key2) => executeRangeSingle(op, key2, params)
|
|
2144
|
+
);
|
|
2145
|
+
const out = /* @__PURE__ */ new Map();
|
|
2146
|
+
keys.forEach((key2, i) => {
|
|
2147
|
+
out.set(serializeContractKey(key2), connections[i]);
|
|
2148
|
+
});
|
|
2149
|
+
return out;
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// src/runtime/command-runtime.ts
|
|
2153
|
+
function resolveCommandMethod(contract, methodName) {
|
|
2154
|
+
const method = contract.methods[methodName];
|
|
2155
|
+
if (method === void 0) {
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
`Contract runtime: command method '${methodName}' does not exist on this contract. Available methods: ${Object.keys(contract.methods).sort().join(", ") || "(none)"}.`
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
return method;
|
|
2161
|
+
}
|
|
2162
|
+
function modelClassOf2(op) {
|
|
2163
|
+
return op.entity.modelClass;
|
|
2164
|
+
}
|
|
2165
|
+
function modelStaticOf(op) {
|
|
2166
|
+
return op.entity.modelClass.asModel();
|
|
2167
|
+
}
|
|
2168
|
+
function renderLeaf(leaf, key2, params, context) {
|
|
2169
|
+
if (isContractParamRef(leaf)) {
|
|
2170
|
+
if (!(leaf.field in params)) {
|
|
2171
|
+
throw new Error(
|
|
2172
|
+
`Contract runtime: ${context} references params field '${leaf.field}', which was not supplied. Pass it in the method's params argument.`
|
|
2173
|
+
);
|
|
2174
|
+
}
|
|
2175
|
+
return params[leaf.field];
|
|
2176
|
+
}
|
|
2177
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
2178
|
+
if (leaf.field in key2) return key2[leaf.field];
|
|
2179
|
+
if (leaf.field in params) return params[leaf.field];
|
|
2180
|
+
throw new Error(
|
|
2181
|
+
`Contract runtime: ${context} references key field '${leaf.field}', which was not supplied. Pass it in the method's key or params argument.`
|
|
2182
|
+
);
|
|
2183
|
+
}
|
|
2184
|
+
return leaf;
|
|
2185
|
+
}
|
|
2186
|
+
function renderRecord(structure, key2, params, context) {
|
|
2187
|
+
const out = {};
|
|
2188
|
+
if (structure === void 0) return out;
|
|
2189
|
+
for (const [field2, leaf] of Object.entries(structure)) {
|
|
2190
|
+
out[field2] = renderLeaf(leaf, key2, params, `${context} field '${field2}'`);
|
|
2191
|
+
}
|
|
2192
|
+
return out;
|
|
2193
|
+
}
|
|
2194
|
+
function renderCondition(condition, key2, params, context) {
|
|
2195
|
+
if (condition === void 0) return void 0;
|
|
2196
|
+
if (typeof condition === "object" && condition !== null) {
|
|
2197
|
+
const obj = condition;
|
|
2198
|
+
if (obj.notExists === true) return { notExists: true };
|
|
2199
|
+
if (typeof obj.attributeExists === "string") {
|
|
2200
|
+
return { attributeExists: obj.attributeExists };
|
|
2201
|
+
}
|
|
2202
|
+
if (typeof obj.attributeNotExists === "string") {
|
|
2203
|
+
return { attributeNotExists: obj.attributeNotExists };
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return renderRecord(
|
|
2207
|
+
condition,
|
|
2208
|
+
key2,
|
|
2209
|
+
params,
|
|
2210
|
+
`${context} condition`
|
|
2211
|
+
);
|
|
2212
|
+
}
|
|
2213
|
+
function renderWrite(op, key2, params, contextLabel) {
|
|
2214
|
+
if (!isContractKeyRef(op.keys) && op.operation !== "put") {
|
|
2215
|
+
throw new Error(
|
|
2216
|
+
`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.`
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
const base = {
|
|
2220
|
+
operation: op.operation,
|
|
2221
|
+
modelClass: modelClassOf2(op),
|
|
2222
|
+
modelStatic: modelStaticOf(op),
|
|
2223
|
+
key: key2,
|
|
2224
|
+
condition: renderCondition(op.condition, key2, params, contextLabel)
|
|
2225
|
+
};
|
|
2226
|
+
if (op.operation === "put") {
|
|
2227
|
+
return { ...base, item: renderRecord(op.item, key2, params, `${contextLabel} item`) };
|
|
2228
|
+
}
|
|
2229
|
+
if (op.operation === "update") {
|
|
2230
|
+
return { ...base, changes: renderRecord(op.changes, key2, params, `${contextLabel} changes`) };
|
|
2231
|
+
}
|
|
2232
|
+
return base;
|
|
2233
|
+
}
|
|
2234
|
+
async function executeSingleWrite(w) {
|
|
2235
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2236
|
+
if (w.operation === "put") {
|
|
2237
|
+
await executePut(w.modelClass, w.item ?? {}, options);
|
|
2238
|
+
return;
|
|
2239
|
+
}
|
|
2240
|
+
if (w.operation === "update") {
|
|
2241
|
+
await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
|
|
2242
|
+
return;
|
|
2243
|
+
}
|
|
2244
|
+
await executeDelete(w.modelClass, w.key, options);
|
|
2245
|
+
}
|
|
2246
|
+
async function readBackProjection(write, returnSelection) {
|
|
2247
|
+
const select = {};
|
|
2248
|
+
for (const field2 of Object.keys(returnSelection)) {
|
|
2249
|
+
if (returnSelection[field2] === true) select[field2] = true;
|
|
2250
|
+
}
|
|
2251
|
+
const key2 = readBackKey(write);
|
|
2252
|
+
return executeQuery(write.modelClass, key2, select, { consistentRead: true });
|
|
2253
|
+
}
|
|
2254
|
+
function readBackKey(write) {
|
|
2255
|
+
if (write.operation !== "put") return write.key;
|
|
2256
|
+
const metadata = MetadataRegistry.get(write.modelClass);
|
|
2257
|
+
const item = write.item ?? {};
|
|
2258
|
+
const key2 = {};
|
|
2259
|
+
const fields = metadata.primaryKey ? metadata.primaryKey.inputFieldNames : Object.keys(item);
|
|
2260
|
+
for (const field2 of fields) {
|
|
2261
|
+
if (field2 in item) key2[field2] = item[field2];
|
|
2262
|
+
}
|
|
2263
|
+
return key2;
|
|
2264
|
+
}
|
|
2265
|
+
function renderConditionCheck(check, key2, params, contextLabel) {
|
|
2266
|
+
const modelClass = check.entity.modelClass;
|
|
2267
|
+
const referencedKey = {};
|
|
2268
|
+
for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
|
|
2269
|
+
if (inputField in params) {
|
|
2270
|
+
referencedKey[targetField] = params[inputField];
|
|
2271
|
+
} else if (inputField in key2) {
|
|
2272
|
+
referencedKey[targetField] = key2[inputField];
|
|
2273
|
+
} else {
|
|
2274
|
+
throw new Error(
|
|
2275
|
+
`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.`
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2280
|
+
modelClass,
|
|
2281
|
+
referencedKey
|
|
2282
|
+
);
|
|
2283
|
+
const cond2 = buildConditionExpression({ attributeExists: "PK" });
|
|
2284
|
+
const checkInput = {
|
|
2285
|
+
TableName,
|
|
2286
|
+
Key,
|
|
2287
|
+
ConditionExpression: cond2.expression
|
|
2288
|
+
};
|
|
2289
|
+
if (Object.keys(cond2.names).length > 0) checkInput.ExpressionAttributeNames = cond2.names;
|
|
2290
|
+
if (Object.keys(cond2.values).length > 0) checkInput.ExpressionAttributeValues = cond2.values;
|
|
2291
|
+
return checkInput;
|
|
2292
|
+
}
|
|
2293
|
+
var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
|
|
2294
|
+
function renderTemplate(template, key2, params, contextLabel) {
|
|
2295
|
+
const resolveToken = (token) => {
|
|
2296
|
+
if (token in params) return params[token];
|
|
2297
|
+
if (token in key2) return key2[token];
|
|
2298
|
+
throw new Error(
|
|
2299
|
+
`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 }\`).` : `.`)
|
|
2300
|
+
);
|
|
2301
|
+
};
|
|
2302
|
+
const whole = /^\{([^{}]+)\}$/.exec(template);
|
|
2303
|
+
if (whole !== null) return resolveToken(whole[1]);
|
|
2304
|
+
return template.replace(RUNTIME_TOKEN_RE, (_m, token) => String(resolveToken(token)));
|
|
2305
|
+
}
|
|
2306
|
+
function renderTemplateRecord(record, key2, params, contextLabel) {
|
|
2307
|
+
const out = {};
|
|
2308
|
+
for (const [field2, template] of Object.entries(record ?? {})) {
|
|
2309
|
+
out[field2] = renderTemplate(template, key2, params, contextLabel);
|
|
2310
|
+
}
|
|
2311
|
+
return out;
|
|
2312
|
+
}
|
|
2313
|
+
function renderEdgeWriteItems(edge, key2, params, contextLabel, modelBySignature) {
|
|
2314
|
+
const adjacency = edge.entity.modelClass;
|
|
2315
|
+
return edge.items.map((item) => {
|
|
2316
|
+
if (item.type === "Put") {
|
|
2317
|
+
const rendered = renderTemplateRecord(item.item, key2, params, `${contextLabel} edge Put`);
|
|
2318
|
+
const out2 = { Put: buildPutInput(adjacency, rendered) };
|
|
2319
|
+
modelBySignature?.set(execItemKeySignature(out2), adjacency.name);
|
|
2320
|
+
return out2;
|
|
2321
|
+
}
|
|
2322
|
+
const renderedKey = renderTemplateRecord(
|
|
2323
|
+
item.keyCondition,
|
|
2324
|
+
key2,
|
|
2325
|
+
params,
|
|
2326
|
+
`${contextLabel} edge Delete`
|
|
2327
|
+
);
|
|
2328
|
+
const del = {
|
|
2329
|
+
TableName: TableMapping.resolve(item.tableName),
|
|
2330
|
+
Key: renderedKey
|
|
2331
|
+
};
|
|
2332
|
+
const out = { Delete: del };
|
|
2333
|
+
modelBySignature?.set(execItemKeySignature(out), adjacency.name);
|
|
2334
|
+
return out;
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
function renderDerivedUpdate(update, key2, params, contextLabel, modelBySignature) {
|
|
2338
|
+
const metadata = MetadataRegistry.get(update.entity.modelClass);
|
|
2339
|
+
if (!metadata.primaryKey) {
|
|
2340
|
+
throw new Error(
|
|
2341
|
+
`Contract runtime: ${contextLabel} derives a counter update on '${update.entity.name}', which has no primary key.`
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
const keyInput = {};
|
|
2345
|
+
for (const [targetField, inputField] of Object.entries(update.keyBinding)) {
|
|
2346
|
+
keyInput[targetField] = renderTemplate(
|
|
2347
|
+
`{${inputField}}`,
|
|
2348
|
+
key2,
|
|
2349
|
+
params,
|
|
2350
|
+
`${contextLabel} derived-update key`
|
|
2351
|
+
);
|
|
2352
|
+
}
|
|
2353
|
+
const { TableName, Key } = buildDeleteInput(
|
|
2354
|
+
update.entity.modelClass,
|
|
2355
|
+
keyInput
|
|
2356
|
+
);
|
|
2357
|
+
const updateInput = {
|
|
2358
|
+
TableName,
|
|
2359
|
+
Key,
|
|
2360
|
+
UpdateExpression: "ADD #a0 :a0",
|
|
2361
|
+
ExpressionAttributeNames: { "#a0": update.attribute },
|
|
2362
|
+
ExpressionAttributeValues: { ":a0": update.amount }
|
|
2363
|
+
};
|
|
2364
|
+
const out = { Update: updateInput };
|
|
2365
|
+
modelBySignature?.set(execItemKeySignature(out), update.entity.modelClass.name);
|
|
2366
|
+
return out;
|
|
2367
|
+
}
|
|
2368
|
+
function renderUniqueGuardItems(guard, key2, params, contextLabel) {
|
|
2369
|
+
return guard.items.map((item) => {
|
|
2370
|
+
if (item.type === "Put") {
|
|
2371
|
+
const Item = renderTemplateRecord(item.item, key2, params, `${contextLabel} guard Put`);
|
|
2372
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2373
|
+
const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
|
|
2374
|
+
put.ConditionExpression = cond2.expression;
|
|
2375
|
+
if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
|
|
2376
|
+
if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
|
|
2377
|
+
return { Put: put };
|
|
2378
|
+
}
|
|
2379
|
+
const Key = renderTemplateRecord(item.keyCondition, key2, params, `${contextLabel} guard Delete`);
|
|
2380
|
+
const del = { TableName: TableMapping.resolve(item.tableName), Key };
|
|
2381
|
+
return { Delete: del };
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
function renderOutboxEventItems(event, key2, params, contextLabel) {
|
|
2385
|
+
return event.items.map((item) => {
|
|
2386
|
+
const Item = renderTemplateRecord(item.item, key2, params, `${contextLabel} outbox Put`);
|
|
2387
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2388
|
+
return { Put: put };
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
function renderIdempotencyGuardItems(guard, key2, params, contextLabel) {
|
|
2392
|
+
return guard.items.map((item) => {
|
|
2393
|
+
const Item = renderTemplateRecord(item.item, key2, params, `${contextLabel} idempotency Put`);
|
|
2394
|
+
const put = { TableName: TableMapping.resolve(item.tableName), Item };
|
|
2395
|
+
const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
|
|
2396
|
+
put.ConditionExpression = cond2.expression;
|
|
2397
|
+
if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
|
|
2398
|
+
if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
|
|
2399
|
+
return { Put: put };
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
async function executeReferentialTransaction(writes, edgeItems, updateItems, guardItemsRaw, outboxItems, idempotencyItems, checks, methodName, modelBySignature = /* @__PURE__ */ new Map()) {
|
|
2403
|
+
const writeItems = writes.map((w) => {
|
|
2404
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2405
|
+
let item;
|
|
2406
|
+
if (w.operation === "put") {
|
|
2407
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
2408
|
+
} else if (w.operation === "update") {
|
|
2409
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
2410
|
+
} else {
|
|
2411
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
2412
|
+
}
|
|
2413
|
+
modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
|
|
2414
|
+
return item;
|
|
2415
|
+
});
|
|
2416
|
+
const checkItems = checks.map((c) => ({ ConditionCheck: c.check }));
|
|
2417
|
+
await commitTransaction(
|
|
2418
|
+
[
|
|
2419
|
+
...writeItems,
|
|
2420
|
+
...edgeItems,
|
|
2421
|
+
...updateItems,
|
|
2422
|
+
...guardItemsRaw,
|
|
2423
|
+
...outboxItems,
|
|
2424
|
+
...idempotencyItems,
|
|
2425
|
+
...checkItems
|
|
2426
|
+
],
|
|
2427
|
+
{
|
|
2428
|
+
modelBySignature,
|
|
2429
|
+
limitError: (count) => new Error(
|
|
2430
|
+
`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.`
|
|
2431
|
+
)
|
|
2432
|
+
}
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
async function executeTransactWrites(writes, methodName) {
|
|
2436
|
+
if (writes.length === 0) return;
|
|
2437
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2438
|
+
const transactItems = writes.map((w) => {
|
|
2439
|
+
const options = w.condition !== void 0 ? { condition: w.condition } : void 0;
|
|
2440
|
+
let item;
|
|
2441
|
+
if (w.operation === "put") {
|
|
2442
|
+
item = { Put: buildPutInput(w.modelClass, w.item ?? {}, options) };
|
|
2443
|
+
} else if (w.operation === "update") {
|
|
2444
|
+
item = { Update: buildUpdateInput(w.modelClass, w.key, w.changes ?? {}, options) };
|
|
2445
|
+
} else {
|
|
2446
|
+
item = { Delete: buildDeleteInput(w.modelClass, w.key, options) };
|
|
2447
|
+
}
|
|
2448
|
+
modelBySignature.set(execItemKeySignature(item), w.modelClass.name);
|
|
2449
|
+
return item;
|
|
2450
|
+
});
|
|
2451
|
+
await commitTransaction(transactItems, {
|
|
2452
|
+
modelBySignature,
|
|
2453
|
+
limitError: (count) => new Error(
|
|
2454
|
+
`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.`
|
|
2455
|
+
)
|
|
2456
|
+
});
|
|
2457
|
+
}
|
|
2458
|
+
async function executeBatchWrites(writes, methodName) {
|
|
2459
|
+
if (writes.length === 0) return;
|
|
2460
|
+
const requests = writes.map((w) => {
|
|
2461
|
+
if (w.operation === "put") {
|
|
2462
|
+
return { type: "put", model: w.modelStatic, item: w.item ?? {} };
|
|
2463
|
+
}
|
|
2464
|
+
if (w.operation === "delete") {
|
|
2465
|
+
return { type: "delete", model: w.modelStatic, key: w.key };
|
|
2466
|
+
}
|
|
2467
|
+
throw new Error(
|
|
2468
|
+
`Contract runtime: command method '${methodName}' resolves to an 'update', but \`batch: 'batchWrite'\` only supports put / delete (DynamoDB's BatchWriteItem has no Update request). Declare \`batch: 'transact'\` for a batched update.`
|
|
2469
|
+
);
|
|
2470
|
+
});
|
|
2471
|
+
await executeBatchWrite(requests);
|
|
2472
|
+
}
|
|
2473
|
+
async function executeCommandMethod(contract, methodName, keyOrKeys, params = {}) {
|
|
2474
|
+
const method = resolveCommandMethod(contract, methodName);
|
|
2475
|
+
if (Array.isArray(keyOrKeys)) {
|
|
2476
|
+
const keys = keyOrKeys;
|
|
2477
|
+
if (method.ops !== void 0 && method.ops.length > 1) {
|
|
2478
|
+
throw new Error(
|
|
2479
|
+
`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.`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
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;
|
|
2483
|
+
if (hasDerivedEffects2) {
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`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.`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
if (method.batch === void 0) {
|
|
2489
|
+
throw new Error(
|
|
2490
|
+
`Contract runtime: command method '${methodName}' was called with an array of keys, but it declares no batched-write form. Declare \`batch: 'transact'\` (atomic TransactWriteItems, \u226425) or \`batch: 'batchWrite'\` (non-atomic BatchWriteItem) on the method body, or call it with a single key.`
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
const writes = keys.map(
|
|
2494
|
+
(key3, i) => renderWrite(method.op, key3, params, `command method '${methodName}' (key #${i})`)
|
|
2495
|
+
);
|
|
2496
|
+
if (method.batch === "transact") {
|
|
2497
|
+
await executeTransactWrites(writes, methodName);
|
|
2498
|
+
} else {
|
|
2499
|
+
await executeBatchWrites(writes, methodName);
|
|
2500
|
+
}
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
const key2 = keyOrKeys;
|
|
2504
|
+
const conditionChecks = method.conditionChecks ?? [];
|
|
2505
|
+
const edgeWrites2 = method.edgeWrites ?? [];
|
|
2506
|
+
const derivedUpdates = method.derivedUpdates ?? [];
|
|
2507
|
+
const uniqueGuards = method.uniqueGuards ?? [];
|
|
2508
|
+
const outboxEvents = method.outboxEvents ?? [];
|
|
2509
|
+
const idempotencyGuard = method.idempotencyGuard;
|
|
2510
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
|
|
2511
|
+
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
2512
|
+
if (isMultiFragment || hasDerivedEffects) {
|
|
2513
|
+
const ops = method.ops ?? [method.op];
|
|
2514
|
+
const writes = ops.map(
|
|
2515
|
+
(opn, i) => renderWrite(opn, key2, params, `command method '${methodName}' (fragment #${i})`)
|
|
2516
|
+
);
|
|
2517
|
+
if (hasDerivedEffects) {
|
|
2518
|
+
const modelBySignature = /* @__PURE__ */ new Map();
|
|
2519
|
+
const edgeItems = edgeWrites2.flatMap(
|
|
2520
|
+
(edge, i) => renderEdgeWriteItems(
|
|
2521
|
+
edge,
|
|
2522
|
+
key2,
|
|
2523
|
+
params,
|
|
2524
|
+
`command method '${methodName}' (edge #${i})`,
|
|
2525
|
+
modelBySignature
|
|
2526
|
+
)
|
|
2527
|
+
);
|
|
2528
|
+
const updateItems = derivedUpdates.map(
|
|
2529
|
+
(update, i) => renderDerivedUpdate(
|
|
2530
|
+
update,
|
|
2531
|
+
key2,
|
|
2532
|
+
params,
|
|
2533
|
+
`command method '${methodName}' (derive #${i})`,
|
|
2534
|
+
modelBySignature
|
|
2535
|
+
)
|
|
2536
|
+
);
|
|
2537
|
+
const guardItems = uniqueGuards.flatMap(
|
|
2538
|
+
(guard, i) => renderUniqueGuardItems(guard, key2, params, `command method '${methodName}' (unique #${i})`)
|
|
2539
|
+
);
|
|
2540
|
+
const outboxItems = outboxEvents.flatMap(
|
|
2541
|
+
(event, i) => renderOutboxEventItems(event, key2, params, `command method '${methodName}' (emit #${i})`)
|
|
2542
|
+
);
|
|
2543
|
+
const idempotencyItems = idempotencyGuard !== void 0 ? renderIdempotencyGuardItems(
|
|
2544
|
+
idempotencyGuard,
|
|
2545
|
+
key2,
|
|
2546
|
+
params,
|
|
2547
|
+
`command method '${methodName}' (idempotency)`
|
|
2548
|
+
) : [];
|
|
2549
|
+
const checks = conditionChecks.map((check, i) => ({
|
|
2550
|
+
check: renderConditionCheck(
|
|
2551
|
+
check,
|
|
2552
|
+
key2,
|
|
2553
|
+
params,
|
|
2554
|
+
`command method '${methodName}' (requires #${i})`
|
|
2555
|
+
)
|
|
2556
|
+
}));
|
|
2557
|
+
await executeReferentialTransaction(
|
|
2558
|
+
writes,
|
|
2559
|
+
edgeItems,
|
|
2560
|
+
updateItems,
|
|
2561
|
+
guardItems,
|
|
2562
|
+
outboxItems,
|
|
2563
|
+
idempotencyItems,
|
|
2564
|
+
checks,
|
|
2565
|
+
methodName,
|
|
2566
|
+
modelBySignature
|
|
2567
|
+
);
|
|
2568
|
+
} else {
|
|
2569
|
+
await executeTransactWrites(writes, methodName);
|
|
2570
|
+
}
|
|
2571
|
+
if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
|
|
2572
|
+
return readBackProjection(writes[0], method.returnSelection);
|
|
2573
|
+
}
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
const write = renderWrite(method.op, key2, params, `command method '${methodName}'`);
|
|
2577
|
+
await executeSingleWrite(write);
|
|
2578
|
+
if (method.returnSelection !== void 0 && Object.keys(method.returnSelection).length > 0) {
|
|
2579
|
+
return readBackProjection(write, method.returnSelection);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/linter/rules/query-boundary.ts
|
|
2584
|
+
var queryBoundaryRule = {
|
|
2585
|
+
id: "query-boundary",
|
|
2586
|
+
severity: "error",
|
|
2587
|
+
check(metadata) {
|
|
2588
|
+
const results = [];
|
|
2589
|
+
for (const gsi2 of metadata.gsiDefinitions) {
|
|
2590
|
+
if (!gsi2.unique) {
|
|
2591
|
+
results.push({
|
|
2592
|
+
ruleId: "query-boundary",
|
|
2593
|
+
severity: "error",
|
|
2594
|
+
message: `GSI '${gsi2.indexName}' on entity '${metadata.prefix}' is not marked as unique. Using query() on a non-unique GSI may return multiple items. Use list() instead, or mark the GSI as unique if it guarantees single results.`,
|
|
2595
|
+
entity: metadata.prefix,
|
|
2596
|
+
field: gsi2.indexName
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
return results;
|
|
2601
|
+
}
|
|
2602
|
+
};
|
|
2603
|
+
|
|
2604
|
+
// src/define/define.ts
|
|
2605
|
+
function collectParams(structure, out = {}) {
|
|
2606
|
+
if (structure === null || typeof structure !== "object") return out;
|
|
2607
|
+
for (const [key2, value] of Object.entries(structure)) {
|
|
2608
|
+
if (isParam(value)) {
|
|
2609
|
+
out[key2] = descriptorFromParam(key2, value, out[key2]);
|
|
2610
|
+
} else if (value !== null && typeof value === "object") {
|
|
2611
|
+
collectParams(value, out);
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
return out;
|
|
2615
|
+
}
|
|
2616
|
+
function descriptorFromParam(name, value, existing) {
|
|
2617
|
+
const descriptor = value.literals === void 0 ? { kind: value.kind, required: true } : { kind: value.kind, literals: value.literals, required: true };
|
|
2618
|
+
if (existing !== void 0 && existing.kind !== descriptor.kind) {
|
|
2619
|
+
throw new Error(
|
|
2620
|
+
`Parameter '${name}' is declared with conflicting types ('${existing.kind}' and '${descriptor.kind}'). A parameter name must have a single type across a definition.`
|
|
2621
|
+
);
|
|
2622
|
+
}
|
|
2623
|
+
return descriptor;
|
|
2624
|
+
}
|
|
2625
|
+
function entityRef(model2) {
|
|
2626
|
+
const modelClass = resolveModelClass(model2);
|
|
2627
|
+
return { name: modelClass.name, modelClass };
|
|
2628
|
+
}
|
|
2629
|
+
function collectConditionParams(condition, out) {
|
|
2630
|
+
if (condition === void 0) return;
|
|
2631
|
+
const obj = condition;
|
|
2632
|
+
if (obj.notExists === true) return;
|
|
2633
|
+
if (typeof obj.attributeExists === "string") return;
|
|
2634
|
+
if (typeof obj.attributeNotExists === "string") return;
|
|
2635
|
+
collectParams(condition, out);
|
|
2636
|
+
}
|
|
2637
|
+
function makeDefinition(model2, operation, key2, select, changes, condition) {
|
|
2638
|
+
const params = collectParams(key2);
|
|
2639
|
+
if (changes !== void 0) collectParams(changes, params);
|
|
2640
|
+
collectConditionParams(condition, params);
|
|
2641
|
+
return {
|
|
2642
|
+
__isOperationDefinition: true,
|
|
2643
|
+
entity: entityRef(model2),
|
|
2644
|
+
operation,
|
|
2645
|
+
key: key2,
|
|
2646
|
+
select,
|
|
2647
|
+
changes,
|
|
2648
|
+
...condition !== void 0 ? { condition } : {},
|
|
2649
|
+
params
|
|
2650
|
+
};
|
|
2651
|
+
}
|
|
2652
|
+
function defineQuery(model2, key2, select) {
|
|
2653
|
+
return makeDefinition(
|
|
2654
|
+
model2,
|
|
2655
|
+
"query",
|
|
2656
|
+
key2,
|
|
2657
|
+
select,
|
|
2658
|
+
void 0
|
|
2659
|
+
);
|
|
2660
|
+
}
|
|
2661
|
+
function defineList(model2, key2, select) {
|
|
2662
|
+
return makeDefinition(
|
|
2663
|
+
model2,
|
|
2664
|
+
"list",
|
|
2665
|
+
key2,
|
|
2666
|
+
select,
|
|
2667
|
+
void 0
|
|
2668
|
+
);
|
|
2669
|
+
}
|
|
2670
|
+
function definePut(model2, item, options) {
|
|
2671
|
+
return makeDefinition(
|
|
2672
|
+
model2,
|
|
2673
|
+
"put",
|
|
2674
|
+
item,
|
|
2675
|
+
void 0,
|
|
2676
|
+
void 0,
|
|
2677
|
+
options?.condition
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
function defineUpdate(model2, key2, changes, options) {
|
|
2681
|
+
return makeDefinition(
|
|
2682
|
+
model2,
|
|
2683
|
+
"update",
|
|
2684
|
+
key2,
|
|
2685
|
+
void 0,
|
|
2686
|
+
changes,
|
|
2687
|
+
options?.condition
|
|
2688
|
+
);
|
|
2689
|
+
}
|
|
2690
|
+
function defineDelete(model2, key2, options) {
|
|
2691
|
+
return makeDefinition(
|
|
2692
|
+
model2,
|
|
2693
|
+
"delete",
|
|
2694
|
+
key2,
|
|
2695
|
+
void 0,
|
|
2696
|
+
void 0,
|
|
2697
|
+
options?.condition
|
|
2698
|
+
);
|
|
2699
|
+
}
|
|
2700
|
+
function defineQueries(definitions) {
|
|
2701
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
2702
|
+
assertDefinition(name, def);
|
|
2703
|
+
if (def.operation !== "query" && def.operation !== "list") {
|
|
2704
|
+
throw new Error(
|
|
2705
|
+
`defineQueries: '${name}' is a '${def.operation}' operation; only query/list definitions are allowed. Use defineCommands for writes.`
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return definitions;
|
|
2710
|
+
}
|
|
2711
|
+
function defineCommands(definitions) {
|
|
2712
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
2713
|
+
assertDefinition(name, def);
|
|
2714
|
+
if (def.operation !== "put" && def.operation !== "update" && def.operation !== "delete") {
|
|
2715
|
+
throw new Error(
|
|
2716
|
+
`defineCommands: '${name}' is a '${def.operation}' operation; only put/update/delete definitions are allowed. Use defineQueries for reads.`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return definitions;
|
|
2721
|
+
}
|
|
2722
|
+
function assertDefinition(name, def) {
|
|
2723
|
+
if (def === null || typeof def !== "object" || def.__isOperationDefinition !== true) {
|
|
2724
|
+
throw new Error(
|
|
2725
|
+
`'${name}' is not a definition. Use defineQuery / defineList / definePut / defineUpdate / defineDelete to build entries.`
|
|
2726
|
+
);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
export {
|
|
2730
|
+
BATCH_GET_MAX_KEYS,
|
|
2731
|
+
BATCH_WRITE_MAX_ITEMS,
|
|
2732
|
+
BatchGetResult,
|
|
2733
|
+
CONTRACT_RANGE_FANOUT_CONCURRENCY,
|
|
2734
|
+
CdcEmulator,
|
|
2735
|
+
ClientManager,
|
|
2736
|
+
DDBModel,
|
|
2737
|
+
DynamoExecutor,
|
|
2738
|
+
EDGE_WRITES_MARKER,
|
|
2739
|
+
ENTITY_WRITES_MARKER,
|
|
2740
|
+
LIFECYCLE_CONTRACT_MARKER,
|
|
2741
|
+
Linter,
|
|
2742
|
+
MAX_TRANSACT_ITEMS,
|
|
2743
|
+
MetadataRegistry,
|
|
2744
|
+
OLD_VALUE_NAMESPACE,
|
|
2745
|
+
SPEC_VERSION,
|
|
2746
|
+
TableMapping,
|
|
2747
|
+
TransactionContext,
|
|
2748
|
+
assertBundleSerializable,
|
|
2749
|
+
assertContractBoundaries,
|
|
2750
|
+
assertContractN1Safe,
|
|
2751
|
+
assertJsonSerializable,
|
|
2752
|
+
assertSupportedCondition,
|
|
2753
|
+
attachModelClass,
|
|
2754
|
+
belongsTo,
|
|
2755
|
+
binary,
|
|
2756
|
+
boolean,
|
|
2757
|
+
buildBridgeBundle,
|
|
2758
|
+
buildConditionExpression,
|
|
2759
|
+
buildContexts,
|
|
2760
|
+
buildContracts,
|
|
2761
|
+
buildDeleteInput,
|
|
2762
|
+
buildManifest,
|
|
2763
|
+
buildOperations,
|
|
2764
|
+
buildProjection,
|
|
2765
|
+
buildPutInput,
|
|
2766
|
+
buildQuerySpec,
|
|
2767
|
+
buildTransactionSpec,
|
|
2768
|
+
buildTransactions,
|
|
2769
|
+
buildUpdateExpression,
|
|
2770
|
+
buildUpdateInput,
|
|
2771
|
+
collectContractBoundaryViolations,
|
|
2772
|
+
collectContractN1Violations,
|
|
2773
|
+
command,
|
|
2774
|
+
compileFilterExpression,
|
|
2775
|
+
compileFragment,
|
|
2776
|
+
compileMutationPlan,
|
|
2777
|
+
compileSingleFragmentPlan,
|
|
2778
|
+
cond,
|
|
2779
|
+
contractOfMethodSpec,
|
|
2780
|
+
createCdcEmulator,
|
|
2781
|
+
createDefaultLinter,
|
|
2782
|
+
datetime,
|
|
2783
|
+
decodeCursor,
|
|
2784
|
+
decodePerKeyCursor,
|
|
2785
|
+
defineCommands,
|
|
2786
|
+
defineDelete,
|
|
2787
|
+
defineList,
|
|
2788
|
+
definePlan,
|
|
2789
|
+
definePut,
|
|
2790
|
+
defineQueries,
|
|
2791
|
+
defineQuery,
|
|
2792
|
+
defineTransaction,
|
|
2793
|
+
defineTransactions,
|
|
2794
|
+
defineUpdate,
|
|
2795
|
+
deriveEdgeWriteItems,
|
|
2796
|
+
deriveEdgeWriteItemsFor,
|
|
2797
|
+
deriveModelEdgeWriteItems,
|
|
2798
|
+
derivePrefix,
|
|
2799
|
+
detectRelationFields,
|
|
2800
|
+
edgeWrites,
|
|
2801
|
+
embedded,
|
|
2802
|
+
encodeCursor,
|
|
2803
|
+
encodePerKeyCursor,
|
|
2804
|
+
entityWrites,
|
|
2805
|
+
evaluateFilter,
|
|
2806
|
+
execute,
|
|
2807
|
+
executeBatchGet,
|
|
2808
|
+
executeBatchWrite,
|
|
2809
|
+
executeCommandMethod,
|
|
2810
|
+
executeDeclarativeTransaction,
|
|
2811
|
+
executeDelete,
|
|
2812
|
+
executeExplain,
|
|
2813
|
+
executeKeyedBatchGet,
|
|
2814
|
+
executeList,
|
|
2815
|
+
executePut,
|
|
2816
|
+
executeQuery,
|
|
2817
|
+
executeQueryMethod,
|
|
2818
|
+
executeRangeFanout,
|
|
2819
|
+
executeTransaction,
|
|
2820
|
+
executeUpdate,
|
|
2821
|
+
expandTransaction,
|
|
2822
|
+
field,
|
|
2823
|
+
from,
|
|
2824
|
+
getEdgeWrites,
|
|
2825
|
+
getEntityWrites,
|
|
2826
|
+
getImplicitKeyFields,
|
|
2827
|
+
gsi,
|
|
2828
|
+
gsiAmbiguityRule,
|
|
2829
|
+
hasMany,
|
|
2830
|
+
hasOne,
|
|
2831
|
+
hydrate,
|
|
2832
|
+
isColumn,
|
|
2833
|
+
isCommandBatchMode,
|
|
2834
|
+
isCommandModelContract,
|
|
2835
|
+
isCommandPlan,
|
|
2836
|
+
isContractComposeNode,
|
|
2837
|
+
isContractFromRef,
|
|
2838
|
+
isContractKeyFieldRef,
|
|
2839
|
+
isContractKeyRef,
|
|
2840
|
+
isContractParamRef,
|
|
2841
|
+
isEdgeWritesDefinition,
|
|
2842
|
+
isEntityWritesDefinition,
|
|
2843
|
+
isKeySegment,
|
|
2844
|
+
isLifecycleContract,
|
|
2845
|
+
isMutationFragment,
|
|
2846
|
+
isMutationInputRef,
|
|
2847
|
+
isParam,
|
|
2848
|
+
isPlannedCommandMethod,
|
|
2849
|
+
isQueryModelContract,
|
|
2850
|
+
isSelectBuilder,
|
|
2851
|
+
isTransactionRef,
|
|
2852
|
+
k,
|
|
2853
|
+
key,
|
|
2854
|
+
lifecyclePhaseForIntent,
|
|
2855
|
+
list,
|
|
2856
|
+
literal,
|
|
2857
|
+
map,
|
|
2858
|
+
mintContractKeyFieldRef,
|
|
2859
|
+
mintContractParamRef,
|
|
2860
|
+
missingGsiRule,
|
|
2861
|
+
model,
|
|
2862
|
+
mutation,
|
|
2863
|
+
noScanRule,
|
|
2864
|
+
number,
|
|
2865
|
+
numberSet,
|
|
2866
|
+
param,
|
|
2867
|
+
plan,
|
|
2868
|
+
point,
|
|
2869
|
+
publicCommandModel,
|
|
2870
|
+
publicQueryModel,
|
|
2871
|
+
query,
|
|
2872
|
+
queryBoundaryRule,
|
|
2873
|
+
range,
|
|
2874
|
+
relationDepthRule,
|
|
2875
|
+
requireLimitRule,
|
|
2876
|
+
resolveKey,
|
|
2877
|
+
resolveLifecycle,
|
|
2878
|
+
resolveRelations,
|
|
2879
|
+
serializeContractKey,
|
|
2880
|
+
serializeFieldValue,
|
|
2881
|
+
string,
|
|
2882
|
+
stringSet,
|
|
2883
|
+
validateDepth,
|
|
2884
|
+
validateGsiAmbiguity,
|
|
2885
|
+
when,
|
|
2886
|
+
wholeKeysSentinel
|
|
2887
|
+
};
|