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
|
@@ -0,0 +1,4276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BATCH_GET_MAX_KEYS,
|
|
3
|
+
MAX_TRANSACT_ITEMS,
|
|
4
|
+
MetadataRegistry,
|
|
5
|
+
TableMapping,
|
|
6
|
+
pkTemplate,
|
|
7
|
+
resolveKey,
|
|
8
|
+
resolveModelClass,
|
|
9
|
+
segmentFieldNames,
|
|
10
|
+
skTemplate
|
|
11
|
+
} from "./chunk-347U24SB.js";
|
|
12
|
+
|
|
13
|
+
// src/spec/types.ts
|
|
14
|
+
var SPEC_VERSION = "1.0";
|
|
15
|
+
var MARKER_ROW_ENTITY = "__marker__";
|
|
16
|
+
|
|
17
|
+
// src/spec/symbolic.ts
|
|
18
|
+
function evaluateKey(segmented, scope, presentFields, nameOf = (f) => f, partial = false) {
|
|
19
|
+
void partial;
|
|
20
|
+
const pk = pkTemplate(segmented, scope, nameOf);
|
|
21
|
+
const sk = skTemplate(segmented, presentFields, scope, nameOf);
|
|
22
|
+
return { pk, sk: sk?.template };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// src/spec/manifest.ts
|
|
26
|
+
var DYNAMO_TYPE_TO_FIELD_TYPE = {
|
|
27
|
+
S: "string",
|
|
28
|
+
N: "number",
|
|
29
|
+
BOOL: "boolean",
|
|
30
|
+
B: "binary",
|
|
31
|
+
SS: "stringSet",
|
|
32
|
+
NS: "numberSet",
|
|
33
|
+
L: "list",
|
|
34
|
+
M: "map"
|
|
35
|
+
};
|
|
36
|
+
function sortedRecord(record) {
|
|
37
|
+
const out = {};
|
|
38
|
+
for (const key of Object.keys(record).sort()) {
|
|
39
|
+
out[key] = record[key];
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
function buildKey(key) {
|
|
44
|
+
if (!key) return null;
|
|
45
|
+
const { pk, sk } = evaluateKey(key.segmented, "param");
|
|
46
|
+
return {
|
|
47
|
+
inputFields: [...key.inputFieldNames],
|
|
48
|
+
pkTemplate: pk,
|
|
49
|
+
skTemplate: sk ?? null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function buildGsis(metadata) {
|
|
53
|
+
return [...metadata.gsiDefinitions].sort((a, b) => a.indexName.localeCompare(b.indexName)).map((gsi) => {
|
|
54
|
+
const { pk, sk } = evaluateKey(gsi.segmented, "param");
|
|
55
|
+
return {
|
|
56
|
+
indexName: gsi.indexName,
|
|
57
|
+
unique: gsi.unique,
|
|
58
|
+
inputFields: [...gsi.inputFieldNames],
|
|
59
|
+
pkTemplate: pk,
|
|
60
|
+
skTemplate: sk ?? null
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function buildRelations(metadata) {
|
|
65
|
+
const relations = {};
|
|
66
|
+
for (const rel of metadata.relations) {
|
|
67
|
+
const targetClass = rel.targetFactory();
|
|
68
|
+
relations[rel.propertyName] = {
|
|
69
|
+
type: rel.type,
|
|
70
|
+
target: targetClass.name,
|
|
71
|
+
keyBinding: { ...rel.keyBinding }
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return sortedRecord(relations);
|
|
75
|
+
}
|
|
76
|
+
function buildFields(metadata) {
|
|
77
|
+
const fields = {};
|
|
78
|
+
for (const field of metadata.fields) {
|
|
79
|
+
const format = field.options?.format;
|
|
80
|
+
fields[field.propertyName] = format ? { type: DYNAMO_TYPE_TO_FIELD_TYPE[field.dynamoType], format } : { type: DYNAMO_TYPE_TO_FIELD_TYPE[field.dynamoType] };
|
|
81
|
+
}
|
|
82
|
+
return sortedRecord(fields);
|
|
83
|
+
}
|
|
84
|
+
function buildEntity(metadata) {
|
|
85
|
+
return {
|
|
86
|
+
table: metadata.tableName,
|
|
87
|
+
physicalName: TableMapping.resolve(metadata.tableName),
|
|
88
|
+
prefix: metadata.prefix,
|
|
89
|
+
fields: buildFields(metadata),
|
|
90
|
+
key: buildKey(metadata.primaryKey),
|
|
91
|
+
gsis: buildGsis(metadata),
|
|
92
|
+
relations: buildRelations(metadata)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function buildManifest(registry = MetadataRegistry) {
|
|
96
|
+
const all = registry.getAll();
|
|
97
|
+
const entitiesByName = /* @__PURE__ */ new Map();
|
|
98
|
+
for (const [cls, metadata] of all) {
|
|
99
|
+
entitiesByName.set(cls.name, metadata);
|
|
100
|
+
}
|
|
101
|
+
const entities = {};
|
|
102
|
+
const tables = {};
|
|
103
|
+
for (const name of [...entitiesByName.keys()].sort()) {
|
|
104
|
+
const metadata = entitiesByName.get(name);
|
|
105
|
+
entities[name] = buildEntity(metadata);
|
|
106
|
+
tables[metadata.tableName] = {
|
|
107
|
+
physicalName: TableMapping.resolve(metadata.tableName)
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
version: SPEC_VERSION,
|
|
112
|
+
tables: sortedRecord(tables),
|
|
113
|
+
entities
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/types/select-builder.ts
|
|
118
|
+
var SELECT_BUILDER = /* @__PURE__ */ Symbol.for("graphddb.selectBuilder");
|
|
119
|
+
var SELECT_SPEC = /* @__PURE__ */ Symbol.for("graphddb.selectSpec");
|
|
120
|
+
|
|
121
|
+
// src/select/builder.ts
|
|
122
|
+
var BuilderImpl = class _BuilderImpl {
|
|
123
|
+
[SELECT_BUILDER] = true;
|
|
124
|
+
/** The resolved spec, exposed (non-method) so normalizers can read it. */
|
|
125
|
+
[SELECT_SPEC];
|
|
126
|
+
constructor(spec) {
|
|
127
|
+
this[SELECT_SPEC] = spec;
|
|
128
|
+
}
|
|
129
|
+
clone(patch) {
|
|
130
|
+
return new _BuilderImpl({ ...this[SELECT_SPEC], ...patch });
|
|
131
|
+
}
|
|
132
|
+
filter(filter) {
|
|
133
|
+
return this.clone({ filter });
|
|
134
|
+
}
|
|
135
|
+
limit(limit) {
|
|
136
|
+
return this.clone({ limit });
|
|
137
|
+
}
|
|
138
|
+
after(cursor) {
|
|
139
|
+
return this.clone({ after: cursor });
|
|
140
|
+
}
|
|
141
|
+
order(order) {
|
|
142
|
+
return this.clone({ order });
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
function isSelectBuilder(value) {
|
|
146
|
+
return typeof value === "object" && value !== null && value[SELECT_BUILDER] === true;
|
|
147
|
+
}
|
|
148
|
+
function fromBuilder(value) {
|
|
149
|
+
const spec = value[SELECT_SPEC];
|
|
150
|
+
return {
|
|
151
|
+
select: spec.select,
|
|
152
|
+
filter: spec.filter,
|
|
153
|
+
limit: spec.limit,
|
|
154
|
+
after: spec.after,
|
|
155
|
+
order: spec.order
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function normalizeSelectSpec(value) {
|
|
159
|
+
if (isSelectBuilder(value)) {
|
|
160
|
+
return fromBuilder(value);
|
|
161
|
+
}
|
|
162
|
+
const obj = value;
|
|
163
|
+
return {
|
|
164
|
+
select: obj.select,
|
|
165
|
+
filter: obj.filter,
|
|
166
|
+
limit: obj.limit,
|
|
167
|
+
after: obj.after,
|
|
168
|
+
order: obj.order
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function normalizeTopLevelSelect(value) {
|
|
172
|
+
if (isSelectBuilder(value)) {
|
|
173
|
+
return fromBuilder(value);
|
|
174
|
+
}
|
|
175
|
+
return { select: value ?? {} };
|
|
176
|
+
}
|
|
177
|
+
function buildProject(select) {
|
|
178
|
+
return new BuilderImpl({
|
|
179
|
+
[SELECT_BUILDER]: true,
|
|
180
|
+
select
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
function buildRelation(select) {
|
|
184
|
+
return new BuilderImpl({
|
|
185
|
+
[SELECT_BUILDER]: true,
|
|
186
|
+
select
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/relation/detect.ts
|
|
191
|
+
function isRelationSpec(value) {
|
|
192
|
+
if (typeof value !== "object" || value === null) return false;
|
|
193
|
+
return "select" in value || isSelectBuilder(value);
|
|
194
|
+
}
|
|
195
|
+
function detectRelationFields(select, metadata) {
|
|
196
|
+
const result = [];
|
|
197
|
+
for (const [fieldName, value] of Object.entries(select)) {
|
|
198
|
+
if (isRelationSpec(value)) {
|
|
199
|
+
const rel = metadata.relations.find((r) => r.propertyName === fieldName);
|
|
200
|
+
if (rel) {
|
|
201
|
+
result.push(rel);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
function getImplicitKeyFields(select, metadata) {
|
|
208
|
+
const relations = detectRelationFields(select, metadata);
|
|
209
|
+
const needed = /* @__PURE__ */ new Set();
|
|
210
|
+
for (const rel of relations) {
|
|
211
|
+
for (const sourceField of Object.values(rel.keyBinding)) {
|
|
212
|
+
if (!(sourceField in select) || select[sourceField] !== true) {
|
|
213
|
+
needed.add(sourceField);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return [...needed];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/define/param.ts
|
|
221
|
+
function makeParam(kind, literals) {
|
|
222
|
+
return literals === void 0 ? { kind } : { kind, literals };
|
|
223
|
+
}
|
|
224
|
+
var param = {
|
|
225
|
+
/** A placeholder for a `string` value. */
|
|
226
|
+
string() {
|
|
227
|
+
return makeParam("string");
|
|
228
|
+
},
|
|
229
|
+
/** A placeholder for a `number` value. */
|
|
230
|
+
number() {
|
|
231
|
+
return makeParam("number");
|
|
232
|
+
},
|
|
233
|
+
/**
|
|
234
|
+
* A placeholder constrained to one of the given literal values. The value
|
|
235
|
+
* type of the resulting {@link Param} is the **union of the literals**, so it
|
|
236
|
+
* is preserved precisely in the IR and the inferred definition types.
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```ts
|
|
240
|
+
* param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
literal(...values) {
|
|
244
|
+
return makeParam("literal", values);
|
|
245
|
+
},
|
|
246
|
+
/**
|
|
247
|
+
* A placeholder for an **array** parameter whose elements have the given field
|
|
248
|
+
* shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
|
|
249
|
+
* each element's fields to `{item.<field>}` templates.
|
|
250
|
+
*
|
|
251
|
+
* @example
|
|
252
|
+
* ```ts
|
|
253
|
+
* param.array({ userId: param.string(), role: param.string() });
|
|
254
|
+
* // Param<{ userId: string; role: string }[]>
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
array(element) {
|
|
258
|
+
return { kind: "array", element };
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
function isParam(value) {
|
|
262
|
+
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/define/entity-writes.ts
|
|
266
|
+
var LIFECYCLE_CONTRACT_MARKER = /* @__PURE__ */ Symbol(
|
|
267
|
+
"graphddb:lifecycleContract"
|
|
268
|
+
);
|
|
269
|
+
function isLifecycleContract(value) {
|
|
270
|
+
return typeof value === "object" && value !== null && value[LIFECYCLE_CONTRACT_MARKER] === true;
|
|
271
|
+
}
|
|
272
|
+
var ENTITY_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:entityWrites");
|
|
273
|
+
function isEntityWritesDefinition(value) {
|
|
274
|
+
return typeof value === "object" && value !== null && value[ENTITY_WRITES_MARKER] === true;
|
|
275
|
+
}
|
|
276
|
+
function freezeEffects(effects) {
|
|
277
|
+
const out = {};
|
|
278
|
+
if (effects.requires !== void 0) out.requires = effects.requires;
|
|
279
|
+
if (effects.unique !== void 0) out.unique = effects.unique;
|
|
280
|
+
if (effects.edges !== void 0) out.edges = effects.edges;
|
|
281
|
+
if (effects.derive !== void 0) out.derive = effects.derive;
|
|
282
|
+
if (effects.emits !== void 0) out.emits = effects.emits;
|
|
283
|
+
if (effects.idempotency !== void 0) out.idempotency = effects.idempotency;
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
var recorder = {
|
|
287
|
+
lifecycle(effects = {}) {
|
|
288
|
+
return {
|
|
289
|
+
[LIFECYCLE_CONTRACT_MARKER]: true,
|
|
290
|
+
effects: freezeEffects(effects)
|
|
291
|
+
};
|
|
292
|
+
},
|
|
293
|
+
exists(targetFactory, keys) {
|
|
294
|
+
return { kind: "requires", targetFactory, keys };
|
|
295
|
+
},
|
|
296
|
+
unique(spec) {
|
|
297
|
+
return { kind: "unique", name: spec.name, scope: spec.scope, fields: spec.fields };
|
|
298
|
+
},
|
|
299
|
+
putEdge(targetFactory, relationProperty) {
|
|
300
|
+
return { kind: "putEdge", targetFactory, relationProperty };
|
|
301
|
+
},
|
|
302
|
+
deleteEdge(targetFactory, relationProperty) {
|
|
303
|
+
return { kind: "deleteEdge", targetFactory, relationProperty };
|
|
304
|
+
},
|
|
305
|
+
increment(targetFactory, keys, attribute, amount) {
|
|
306
|
+
return { kind: "derive", targetFactory, keys, attribute, amount };
|
|
307
|
+
},
|
|
308
|
+
event(name, payload) {
|
|
309
|
+
return { kind: "event", name, payload };
|
|
310
|
+
},
|
|
311
|
+
idempotentBy(token) {
|
|
312
|
+
return { kind: "idempotency", token };
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
function entityWrites(builder) {
|
|
316
|
+
const shape = builder(recorder);
|
|
317
|
+
for (const phase of ["create", "update", "remove"]) {
|
|
318
|
+
const contract = shape[phase];
|
|
319
|
+
if (contract !== void 0 && !isLifecycleContract(contract)) {
|
|
320
|
+
throw new Error(
|
|
321
|
+
`entityWrites: the '${phase}' lifecycle must be built with \`w.lifecycle({...})\` (it carries the \xA72 effect arrays), but received ${JSON.stringify(contract)}.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (shape.create === void 0 && shape.update === void 0 && shape.remove === void 0) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
"entityWrites(...) must declare at least one lifecycle (`create` / `update` / `remove`); an empty save contract declares nothing."
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
[ENTITY_WRITES_MARKER]: true,
|
|
332
|
+
...shape.create !== void 0 ? { create: shape.create } : {},
|
|
333
|
+
...shape.update !== void 0 ? { update: shape.update } : {},
|
|
334
|
+
...shape.remove !== void 0 ? { remove: shape.remove } : {}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
function getEntityWrites(modelClass) {
|
|
338
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
339
|
+
let value;
|
|
340
|
+
try {
|
|
341
|
+
value = modelClass[name];
|
|
342
|
+
} catch {
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (isEntityWritesDefinition(value)) return value;
|
|
346
|
+
}
|
|
347
|
+
return void 0;
|
|
348
|
+
}
|
|
349
|
+
function lifecyclePhaseForIntent(intent) {
|
|
350
|
+
return intent;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/define/mutation.ts
|
|
354
|
+
var INPUT_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationInputRef");
|
|
355
|
+
function isMutationInputRef(value) {
|
|
356
|
+
return typeof value === "object" && value !== null && value[INPUT_REF_BRAND] === true;
|
|
357
|
+
}
|
|
358
|
+
var ENTITY_REF_RE = /^\$\.entity\[(\d+)\]\.([A-Za-z_$][\w$]*)$/;
|
|
359
|
+
var ENTITY_REF_PREFIX_RE = /^\s*\$\s*\.\s*entity\b/;
|
|
360
|
+
var ENTITY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationEntityRef");
|
|
361
|
+
function parseEntityRef(leaf) {
|
|
362
|
+
if (typeof leaf !== "string") return void 0;
|
|
363
|
+
const match = ENTITY_REF_RE.exec(leaf);
|
|
364
|
+
if (match === null) {
|
|
365
|
+
if (ENTITY_REF_PREFIX_RE.test(leaf)) {
|
|
366
|
+
throw new Error(
|
|
367
|
+
`mutation: the input value ${JSON.stringify(leaf)} looks like a cross-fragment reference but is malformed. The only supported form is \`$.entity[<index>].<field>\` (e.g. \`$.entity[0].postId\`) \u2014 a single dotted field on a 0-based, non-negative fragment index. Fix the path or, if you meant a literal string, it must not begin with \`$.entity\`.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return void 0;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
[ENTITY_REF_BRAND]: true,
|
|
374
|
+
fragmentIndex: Number(match[1]),
|
|
375
|
+
field: match[2],
|
|
376
|
+
path: leaf
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function makeInputFieldRef(field) {
|
|
380
|
+
const token = `{${field}}`;
|
|
381
|
+
const origin = `the input field '${field}' (\`$.${field}\`)`;
|
|
382
|
+
const target = /* @__PURE__ */ Object.create(null);
|
|
383
|
+
const proxy = new Proxy(target, {
|
|
384
|
+
get(_t, prop) {
|
|
385
|
+
if (prop === INPUT_REF_BRAND) return true;
|
|
386
|
+
if (prop === "field") return field;
|
|
387
|
+
if (prop === "token") return token;
|
|
388
|
+
if (typeof prop === "symbol") return void 0;
|
|
389
|
+
throw new Error(
|
|
390
|
+
`mutation: unsupported operation '${String(prop)}' on ${origin}. A fragment \`input\` binding only supports a *direct* \`$.field\` reference or a concrete literal \u2014 any transform, indexing, coercion, or concatenation on an input field is not representable as a declarative mutation.`
|
|
391
|
+
);
|
|
392
|
+
},
|
|
393
|
+
set() {
|
|
394
|
+
throw new Error(`mutation: cannot assign on ${origin}.`);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
return proxy;
|
|
398
|
+
}
|
|
399
|
+
function makeInputProxy() {
|
|
400
|
+
const cache = /* @__PURE__ */ new Map();
|
|
401
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
402
|
+
get(_t, prop) {
|
|
403
|
+
if (typeof prop === "symbol") return void 0;
|
|
404
|
+
const field = prop;
|
|
405
|
+
let ref = cache.get(field);
|
|
406
|
+
if (!ref) {
|
|
407
|
+
ref = makeInputFieldRef(field);
|
|
408
|
+
cache.set(field, ref);
|
|
409
|
+
}
|
|
410
|
+
return ref;
|
|
411
|
+
},
|
|
412
|
+
set() {
|
|
413
|
+
throw new Error("mutation: cannot assign to the `$` input proxy.");
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
var FRAGMENT_BRAND = /* @__PURE__ */ Symbol("graphddb:mutationFragment");
|
|
418
|
+
function isMutationFragment(value) {
|
|
419
|
+
return typeof value === "object" && value !== null && value[FRAGMENT_BRAND] === true;
|
|
420
|
+
}
|
|
421
|
+
function targetEntityRef(target) {
|
|
422
|
+
const resolved = target();
|
|
423
|
+
const modelClass = resolveModelClass(resolved);
|
|
424
|
+
return { name: modelClass.name, modelClass };
|
|
425
|
+
}
|
|
426
|
+
function assertFaithfulInput(input, intent, entityName) {
|
|
427
|
+
if (input === null || typeof input !== "object") {
|
|
428
|
+
throw new Error(
|
|
429
|
+
`mutation: the '${intent}' fragment on '${entityName}' must declare an \`input\` object binding model fields to \`$.field\` references or literals.`
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
for (const [field, leaf] of Object.entries(input)) {
|
|
433
|
+
if (!isMutationInputRef(leaf) && !isConcreteLiteral(leaf)) {
|
|
434
|
+
throw new Error(
|
|
435
|
+
`mutation: the '${intent}' fragment on '${entityName}' binds model field '${field}' to a value that is neither a \`$.field\` input reference nor a concrete literal. A fragment input is declarative \u2014 only \`$.<field>\` references or literals are allowed, never a computed value.`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return input;
|
|
440
|
+
}
|
|
441
|
+
function isConcreteLiteral(value) {
|
|
442
|
+
const t = typeof value;
|
|
443
|
+
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
444
|
+
}
|
|
445
|
+
function makeFragment(intent, target, options) {
|
|
446
|
+
const entity = targetEntityRef(target);
|
|
447
|
+
const input = assertFaithfulInput(options.input ?? {}, intent, entity.name);
|
|
448
|
+
if (options.use !== void 0 && !isEntityWritesDefinition(options.use)) {
|
|
449
|
+
throw new Error(
|
|
450
|
+
`mutation: the '${intent}' fragment on '${entity.name}' was given a \`use:\` that is not an \`entityWrites(...)\` save contract. \`use:\` takes the WHOLE \`writes\` set (an \`entityWrites(...)\` value), never a single lifecycle (e.g. \`Model.writes.create\`) \u2014 the fragment's intent already selects the lifecycle.`
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
[FRAGMENT_BRAND]: true,
|
|
455
|
+
intent,
|
|
456
|
+
entity,
|
|
457
|
+
input,
|
|
458
|
+
...options.use !== void 0 ? { use: options.use } : {}
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
var mutationRecorder = {
|
|
462
|
+
create: (target, options) => makeFragment("create", target, options),
|
|
463
|
+
update: (target, options) => makeFragment("update", target, options),
|
|
464
|
+
remove: (target, options) => makeFragment("remove", target, options)
|
|
465
|
+
};
|
|
466
|
+
var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
|
|
467
|
+
function isCommandPlan(value) {
|
|
468
|
+
return typeof value === "object" && value !== null && value[COMMAND_PLAN_BRAND] === true;
|
|
469
|
+
}
|
|
470
|
+
function mutation(name, body) {
|
|
471
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
472
|
+
throw new Error("mutation: the first argument must be a non-empty mutation name.");
|
|
473
|
+
}
|
|
474
|
+
const $ = makeInputProxy();
|
|
475
|
+
const result = body(mutationRecorder, $);
|
|
476
|
+
if (!Array.isArray(result)) {
|
|
477
|
+
throw new Error(
|
|
478
|
+
`mutation '${name}': the body must RETURN an array of write fragments (e.g. \`(m, $) => [ m.create(() => Model, { input }) ]\`). The fragments are declarations composed at compile time, not executed calls.`
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
if (result.length === 0) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`mutation '${name}': the body returned an empty fragment list. A mutation must declare at least one write fragment.`
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
result.forEach((fragment, i) => {
|
|
487
|
+
if (!isMutationFragment(fragment)) {
|
|
488
|
+
throw new Error(
|
|
489
|
+
`mutation '${name}': fragment #${i} is not a write fragment. Each list element must be an \`m.create\` / \`m.update\` / \`m.remove\` declaration.`
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
return { [COMMAND_PLAN_BRAND]: true, name, fragments: result };
|
|
494
|
+
}
|
|
495
|
+
var definePlan = mutation;
|
|
496
|
+
|
|
497
|
+
// src/relation/edge-write.ts
|
|
498
|
+
var OLD_VALUE_NAMESPACE = "old";
|
|
499
|
+
var EDGE_WRITES_MARKER = /* @__PURE__ */ Symbol("graphddb:edgeWrites");
|
|
500
|
+
function isEdgeWritesDefinition(value) {
|
|
501
|
+
return typeof value === "object" && value !== null && value[EDGE_WRITES_MARKER] === true;
|
|
502
|
+
}
|
|
503
|
+
function declaration(target, relationProperty) {
|
|
504
|
+
return { targetFactory: target, relationProperty };
|
|
505
|
+
}
|
|
506
|
+
var recorder2 = {
|
|
507
|
+
putEdge: declaration,
|
|
508
|
+
deleteEdge: declaration,
|
|
509
|
+
updateKeyChangeEdge: declaration
|
|
510
|
+
};
|
|
511
|
+
function edgeWrites(builder) {
|
|
512
|
+
const edges = builder(recorder2);
|
|
513
|
+
if (edges.length === 0) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
"edgeWrites(...) must declare at least one edge (w.putEdge / w.deleteEdge / w.updateKeyChangeEdge); an empty declaration writes nothing."
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return { [EDGE_WRITES_MARKER]: true, edges };
|
|
519
|
+
}
|
|
520
|
+
function edgeKeyFields(segmented) {
|
|
521
|
+
return [
|
|
522
|
+
...segmentFieldNames(segmented.pkSegments),
|
|
523
|
+
...segmentFieldNames(segmented.skSegments)
|
|
524
|
+
];
|
|
525
|
+
}
|
|
526
|
+
function assertRelationRoundTrips(entity, edgeFields, decl) {
|
|
527
|
+
const targetClass = decl.targetFactory();
|
|
528
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
529
|
+
const relation = targetMeta.relations.find(
|
|
530
|
+
(r) => r.propertyName === decl.relationProperty
|
|
531
|
+
);
|
|
532
|
+
if (!relation) {
|
|
533
|
+
throw new Error(
|
|
534
|
+
`Edge write declaration references relation '${decl.relationProperty}' on '${targetClass.name}', which has no such relation. Declare the edge with the read-side relation property it materializes.`
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
const boundFields = Object.keys(relation.keyBinding);
|
|
538
|
+
for (const targetField of boundFields) {
|
|
539
|
+
if (!edgeFields.has(targetField)) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds on field '${targetField}', which is not a key field of the adjacency entity '${entity.tableName}' edge (key fields: ${[...edgeFields].map((f) => `'${f}'`).join(", ")}). The written row would not be reachable by the relation's read \u2014 declare the edge on the entity whose key carries the relation's bound field.`
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
resolveKey(boundFields, entity);
|
|
547
|
+
} catch (cause) {
|
|
548
|
+
throw new Error(
|
|
549
|
+
`Edge write declaration for relation '${decl.relationProperty}' on '${targetClass.name}' binds [${boundFields.map((f) => `'${f}'`).join(", ")}], which does not cover any access-pattern partition of the adjacency entity '${entity.tableName}' edge (base key [${edgeKeyFields(entity.primaryKey.segmented).map((f) => `'${f}'`).join(", ")}]). The relation's read could not resolve a partition for the written row, so the edge would be unreadable \u2014 bind every partition-key field of the index the relation reads through. (${cause.message})`
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return relation;
|
|
553
|
+
}
|
|
554
|
+
function templateRecord(fields) {
|
|
555
|
+
const out = {};
|
|
556
|
+
for (const field of [...fields].sort()) {
|
|
557
|
+
out[field] = `{${field}}`;
|
|
558
|
+
}
|
|
559
|
+
return out;
|
|
560
|
+
}
|
|
561
|
+
function keyConditionTemplate(segmented, namespace) {
|
|
562
|
+
const nameOf = namespace ? (f) => `${namespace}.${f}` : (f) => f;
|
|
563
|
+
const { pk, sk } = evaluateKey(segmented, "param", void 0, nameOf);
|
|
564
|
+
const out = { PK: pk };
|
|
565
|
+
if (sk !== void 0) out.SK = sk;
|
|
566
|
+
return out;
|
|
567
|
+
}
|
|
568
|
+
function putItemTemplate(edgeFields) {
|
|
569
|
+
return templateRecord(edgeFields);
|
|
570
|
+
}
|
|
571
|
+
function deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle) {
|
|
572
|
+
const entity = MetadataRegistry.get(adjacencyClass);
|
|
573
|
+
if (!entity.primaryKey) {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`Cannot derive edge write items for an entity with no primary key (table '${entity.tableName}'): an edge row needs a key to write / delete.`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const segmented = entity.primaryKey.segmented;
|
|
579
|
+
const edgeFields = edgeKeyFields(segmented);
|
|
580
|
+
assertRelationRoundTrips(entity, new Set(edgeFields), decl);
|
|
581
|
+
const tableName = TableMapping.resolve(entity.tableName);
|
|
582
|
+
const entityName = adjacencyClass.name;
|
|
583
|
+
const put = {
|
|
584
|
+
type: "Put",
|
|
585
|
+
tableName,
|
|
586
|
+
entity: entityName,
|
|
587
|
+
item: putItemTemplate(edgeFields)
|
|
588
|
+
};
|
|
589
|
+
const deleteNew = {
|
|
590
|
+
type: "Delete",
|
|
591
|
+
tableName,
|
|
592
|
+
entity: entityName,
|
|
593
|
+
keyCondition: keyConditionTemplate(segmented)
|
|
594
|
+
};
|
|
595
|
+
const deleteOld = {
|
|
596
|
+
type: "Delete",
|
|
597
|
+
tableName,
|
|
598
|
+
entity: entityName,
|
|
599
|
+
keyCondition: keyConditionTemplate(segmented, OLD_VALUE_NAMESPACE)
|
|
600
|
+
};
|
|
601
|
+
switch (lifecycle) {
|
|
602
|
+
case "onCreate":
|
|
603
|
+
return [put];
|
|
604
|
+
case "onDelete":
|
|
605
|
+
return [deleteNew];
|
|
606
|
+
case "onUpdateKeyChange":
|
|
607
|
+
return [deleteOld, put];
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function deriveEdgeWriteItems(adjacencyClass, writes, lifecycle) {
|
|
611
|
+
const items = [];
|
|
612
|
+
for (const decl of writes.edges) {
|
|
613
|
+
items.push(...deriveEdgeWriteItemsFor(adjacencyClass, decl, lifecycle));
|
|
614
|
+
}
|
|
615
|
+
return items;
|
|
616
|
+
}
|
|
617
|
+
function getEdgeWrites(modelClass) {
|
|
618
|
+
for (const name of Object.getOwnPropertyNames(modelClass)) {
|
|
619
|
+
let value;
|
|
620
|
+
try {
|
|
621
|
+
value = modelClass[name];
|
|
622
|
+
} catch {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (isEdgeWritesDefinition(value)) return value;
|
|
626
|
+
}
|
|
627
|
+
return void 0;
|
|
628
|
+
}
|
|
629
|
+
function deriveModelEdgeWriteItems(modelClass, lifecycle) {
|
|
630
|
+
const writes = getEdgeWrites(modelClass);
|
|
631
|
+
if (!writes) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Model '${modelClass.name}' declares no edge write side (no static \`writes = edgeWrites(...)\`). Declare it to derive adjacency items.`
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return deriveEdgeWriteItems(modelClass, writes, lifecycle);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/define/contract.ts
|
|
640
|
+
function isCommandBatchMode(value) {
|
|
641
|
+
return value === "transact" || value === "batchWrite";
|
|
642
|
+
}
|
|
643
|
+
var KEY_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyRef");
|
|
644
|
+
var KEY_FIELD_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractKeyFieldRef");
|
|
645
|
+
function isContractKeyRef(value) {
|
|
646
|
+
return typeof value === "object" && value !== null && value[KEY_REF_BRAND] === true;
|
|
647
|
+
}
|
|
648
|
+
function mintContractKeyFieldRef(field) {
|
|
649
|
+
return { [KEY_FIELD_REF_BRAND]: true, field, token: `{key.${field}}` };
|
|
650
|
+
}
|
|
651
|
+
function wholeKeysSentinel() {
|
|
652
|
+
return { [KEY_REF_BRAND]: true };
|
|
653
|
+
}
|
|
654
|
+
function isContractKeyFieldRef(value) {
|
|
655
|
+
return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
|
|
656
|
+
}
|
|
657
|
+
function makeKeyFieldRef(field, marker, onCoerce) {
|
|
658
|
+
const token = `{key.${field}}`;
|
|
659
|
+
const origin = `keys field '${field}'`;
|
|
660
|
+
const target = /* @__PURE__ */ Object.create(null);
|
|
661
|
+
const proxy = new Proxy(target, {
|
|
662
|
+
get(_t, prop) {
|
|
663
|
+
if (prop === KEY_FIELD_REF_BRAND) return true;
|
|
664
|
+
if (prop === "field") return field;
|
|
665
|
+
if (prop === "token") return token;
|
|
666
|
+
if (prop === Symbol.toPrimitive) {
|
|
667
|
+
return (hint) => {
|
|
668
|
+
onCoerce();
|
|
669
|
+
return hint === "number" ? NaN : marker;
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
673
|
+
return () => {
|
|
674
|
+
onCoerce();
|
|
675
|
+
return marker;
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
if (prop === "valueOf") {
|
|
679
|
+
return () => {
|
|
680
|
+
onCoerce();
|
|
681
|
+
return NaN;
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
if (typeof prop === "symbol") return void 0;
|
|
685
|
+
const name = String(prop);
|
|
686
|
+
throw new Error(
|
|
687
|
+
`publicQueryModel/publicCommandModel: unsupported operation '${name}' on ${origin}. A contract method body only supports a *direct* key-field reference (\`keys.field\`) or the whole \`keys\` passed into a model op's key position; any transform, indexing, or coercion on a key field is not representable as a declarative contract method.`
|
|
688
|
+
);
|
|
689
|
+
},
|
|
690
|
+
set() {
|
|
691
|
+
throw new Error(`publicQueryModel/publicCommandModel: cannot assign on ${origin}.`);
|
|
692
|
+
},
|
|
693
|
+
defineProperty() {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
|
|
696
|
+
);
|
|
697
|
+
},
|
|
698
|
+
deleteProperty() {
|
|
699
|
+
throw new Error(`publicQueryModel/publicCommandModel: cannot delete on ${origin}.`);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
return proxy;
|
|
703
|
+
}
|
|
704
|
+
function makeKeySentinel(pass, onFieldAccess, onCoerce) {
|
|
705
|
+
const cache = /* @__PURE__ */ new Map();
|
|
706
|
+
const target = /* @__PURE__ */ Object.create(null);
|
|
707
|
+
const proxy = new Proxy(target, {
|
|
708
|
+
get(_t, prop) {
|
|
709
|
+
if (prop === KEY_REF_BRAND) return true;
|
|
710
|
+
if (prop === Symbol.toPrimitive) {
|
|
711
|
+
return (hint) => {
|
|
712
|
+
onCoerce();
|
|
713
|
+
return hint === "number" ? NaN : "[contract key]";
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
717
|
+
return () => {
|
|
718
|
+
onCoerce();
|
|
719
|
+
return "[contract key]";
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
if (prop === "valueOf") {
|
|
723
|
+
return () => {
|
|
724
|
+
onCoerce();
|
|
725
|
+
return NaN;
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
if (prop === Symbol.iterator || prop === Symbol.asyncIterator || prop === "then" || prop === "constructor" || prop === "prototype" || typeof prop === "symbol" && typeof Symbol.for === "function" && (prop === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || prop.description === "nodejs.util.inspect.custom")) {
|
|
729
|
+
return void 0;
|
|
730
|
+
}
|
|
731
|
+
if (typeof prop === "symbol") {
|
|
732
|
+
throw new Error(
|
|
733
|
+
"publicQueryModel/publicCommandModel: unsupported symbol access on `keys`."
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
onFieldAccess(prop);
|
|
737
|
+
let ref = cache.get(prop);
|
|
738
|
+
if (!ref) {
|
|
739
|
+
ref = makeKeyFieldRef(prop, paramMarker(pass, `key.${prop}`), onCoerce);
|
|
740
|
+
cache.set(prop, ref);
|
|
741
|
+
}
|
|
742
|
+
return ref;
|
|
743
|
+
},
|
|
744
|
+
set() {
|
|
745
|
+
throw new Error("publicQueryModel/publicCommandModel: cannot assign to `keys`.");
|
|
746
|
+
},
|
|
747
|
+
defineProperty() {
|
|
748
|
+
throw new Error(
|
|
749
|
+
"publicQueryModel/publicCommandModel: cannot define a property on `keys`."
|
|
750
|
+
);
|
|
751
|
+
},
|
|
752
|
+
deleteProperty() {
|
|
753
|
+
throw new Error(
|
|
754
|
+
"publicQueryModel/publicCommandModel: cannot delete a field of `keys`."
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
return proxy;
|
|
759
|
+
}
|
|
760
|
+
var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
|
|
761
|
+
function isContractParamRef(value) {
|
|
762
|
+
return typeof value === "object" && value !== null && value[PARAM_REF_BRAND] === true;
|
|
763
|
+
}
|
|
764
|
+
function mintContractParamRef(field) {
|
|
765
|
+
return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
|
|
766
|
+
}
|
|
767
|
+
var PARAM_MARKER_TAGS = [
|
|
768
|
+
"Bxqv",
|
|
769
|
+
"Ymnoprstuwy0123456789WIDEMARKERBODYFILLERY"
|
|
770
|
+
];
|
|
771
|
+
var PARAM_PASS_COUNT = PARAM_MARKER_TAGS.length;
|
|
772
|
+
function paramMarker(pass, field) {
|
|
773
|
+
const tag = PARAM_MARKER_TAGS[pass];
|
|
774
|
+
return `${tag}::${field}::${tag}`;
|
|
775
|
+
}
|
|
776
|
+
function makeParamFieldRef(field, marker, onCoerce) {
|
|
777
|
+
const token = `{${field}}`;
|
|
778
|
+
const origin = `params field '${field}'`;
|
|
779
|
+
const target = /* @__PURE__ */ Object.create(null);
|
|
780
|
+
const proxy = new Proxy(target, {
|
|
781
|
+
get(_t, prop) {
|
|
782
|
+
if (prop === PARAM_REF_BRAND) return true;
|
|
783
|
+
if (prop === "token") return token;
|
|
784
|
+
if (prop === "field") return field;
|
|
785
|
+
if (prop === Symbol.toPrimitive) {
|
|
786
|
+
return (hint) => {
|
|
787
|
+
onCoerce();
|
|
788
|
+
return hint === "number" ? NaN : marker;
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
if (prop === "toString" || prop === Symbol.toStringTag) {
|
|
792
|
+
return () => {
|
|
793
|
+
onCoerce();
|
|
794
|
+
return marker;
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (prop === "valueOf") {
|
|
798
|
+
return () => {
|
|
799
|
+
onCoerce();
|
|
800
|
+
return NaN;
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (typeof prop === "symbol") return void 0;
|
|
804
|
+
const name = String(prop);
|
|
805
|
+
throw new Error(
|
|
806
|
+
`publicQueryModel/publicCommandModel: unsupported operation '${name}' on ${origin}. A contract method body only supports a *direct* field reference (\`params.field\`) or a concrete literal at a value position; any string transform, property access (e.g. \`.length\`, \`[i]\`, \`.toUpperCase()\`), arithmetic, or value branching on a parameter is not representable as a declarative contract method.`
|
|
807
|
+
);
|
|
808
|
+
},
|
|
809
|
+
set() {
|
|
810
|
+
throw new Error(
|
|
811
|
+
`publicQueryModel/publicCommandModel: cannot assign on ${origin}.`
|
|
812
|
+
);
|
|
813
|
+
},
|
|
814
|
+
defineProperty() {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`publicQueryModel/publicCommandModel: cannot define a property on ${origin}.`
|
|
817
|
+
);
|
|
818
|
+
},
|
|
819
|
+
deleteProperty() {
|
|
820
|
+
throw new Error(
|
|
821
|
+
`publicQueryModel/publicCommandModel: cannot delete on ${origin}.`
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
return proxy;
|
|
826
|
+
}
|
|
827
|
+
function makeParamsProxy(pass, onAccess, onCoerce) {
|
|
828
|
+
const cache = /* @__PURE__ */ new Map();
|
|
829
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
830
|
+
get(_t, prop) {
|
|
831
|
+
if (typeof prop === "symbol") {
|
|
832
|
+
if (prop === Symbol.iterator || prop === Symbol.asyncIterator || prop === Symbol.toStringTag || typeof Symbol.for === "function" && (prop === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || prop.description === "nodejs.util.inspect.custom")) {
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
835
|
+
throw new Error(
|
|
836
|
+
"publicQueryModel/publicCommandModel: unsupported symbol access on `params`. Reference only declared param fields directly; any JS transform / branching / coercion on a parameter is not representable as a declarative contract method."
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
onAccess(prop);
|
|
840
|
+
let ref = cache.get(prop);
|
|
841
|
+
if (!ref) {
|
|
842
|
+
ref = makeParamFieldRef(prop, paramMarker(pass, prop), () => onCoerce(prop));
|
|
843
|
+
cache.set(prop, ref);
|
|
844
|
+
}
|
|
845
|
+
return ref;
|
|
846
|
+
},
|
|
847
|
+
set() {
|
|
848
|
+
throw new Error(
|
|
849
|
+
"publicQueryModel/publicCommandModel: cannot assign to a field of `params`."
|
|
850
|
+
);
|
|
851
|
+
},
|
|
852
|
+
defineProperty() {
|
|
853
|
+
throw new Error(
|
|
854
|
+
"publicQueryModel/publicCommandModel: cannot define a property on `params`."
|
|
855
|
+
);
|
|
856
|
+
},
|
|
857
|
+
deleteProperty() {
|
|
858
|
+
throw new Error(
|
|
859
|
+
"publicQueryModel/publicCommandModel: cannot delete a field of `params`."
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
var FROM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractFromRef");
|
|
865
|
+
var COMPOSE_NODE_BRAND = /* @__PURE__ */ Symbol("graphddb:contractComposeNode");
|
|
866
|
+
function isContractFromRef(value) {
|
|
867
|
+
return typeof value === "object" && value !== null && value[FROM_REF_BRAND] === true;
|
|
868
|
+
}
|
|
869
|
+
function isContractComposeNode(value) {
|
|
870
|
+
return typeof value === "object" && value !== null && value[COMPOSE_NODE_BRAND] === true;
|
|
871
|
+
}
|
|
872
|
+
var FROM_PATH_RE = /^\$(?:\.[^.\s$[\]*]+)*$/;
|
|
873
|
+
var FROM_PATH_BLOCKED_SEGMENTS = /* @__PURE__ */ new Set([
|
|
874
|
+
"__proto__",
|
|
875
|
+
"constructor",
|
|
876
|
+
"prototype"
|
|
877
|
+
]);
|
|
878
|
+
function from(path) {
|
|
879
|
+
if (typeof path !== "string" || !FROM_PATH_RE.test(path)) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`query/from: a composition binding source must be a \`$\`-rooted dotted field path string (e.g. \`from("$.billingAccountId")\` or \`from("$.a.b")\`), but received ${JSON.stringify(path)}. The binding is declarative \u2014 only a literal parent-result field path is allowed, never a computed value.`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
const blocked = path.split(".").find((segment) => FROM_PATH_BLOCKED_SEGMENTS.has(segment));
|
|
885
|
+
if (blocked !== void 0) {
|
|
886
|
+
throw new Error(
|
|
887
|
+
`query/from: a composition binding source path may not address the segment \`${blocked}\` (received ${JSON.stringify(path)}). The segments \`__proto__\`, \`constructor\`, and \`prototype\` are forbidden in a \`$\`-rooted field path \u2014 they are never valid parent-result fields.`
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
return { [FROM_REF_BRAND]: true, path };
|
|
891
|
+
}
|
|
892
|
+
function query(method, bind) {
|
|
893
|
+
if (typeof method !== "object" || method === null || method.__methodKind !== "query") {
|
|
894
|
+
throw new Error(
|
|
895
|
+
`query: the first argument must be a query contract method (e.g. \`OtherContract.get\`, the value of a publicQueryModel method). A command method or a non-method value is not composable as an External Query.`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
if (typeof bind !== "object" || bind === null) {
|
|
899
|
+
throw new Error(
|
|
900
|
+
`query: the second argument must be a child-key binding object (e.g. \`{ accountId: from("$.billingAccountId") }\`).`
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
const entries = Object.entries(bind);
|
|
904
|
+
if (entries.length === 0) {
|
|
905
|
+
throw new Error(
|
|
906
|
+
`query: a composition must bind at least one child key field via \`from(...)\` (e.g. \`{ accountId: from("$.billingAccountId") }\`); an empty binding cannot resolve the child's key.`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const resolvedBind = {};
|
|
910
|
+
for (const [field, ref] of entries) {
|
|
911
|
+
if (!isContractFromRef(ref)) {
|
|
912
|
+
throw new Error(
|
|
913
|
+
`query: composition binding for child key field '${field}' must be a \`from("$.parentField")\` reference, not ${JSON.stringify(ref)}. The parent\u2192child key binding is declarative \u2014 only \`from(...)\` paths are allowed.`
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
resolvedBind[field] = ref;
|
|
917
|
+
}
|
|
918
|
+
return { [COMPOSE_NODE_BRAND]: true, method, bind: resolvedBind };
|
|
919
|
+
}
|
|
920
|
+
var methodSpecToContract = /* @__PURE__ */ new WeakMap();
|
|
921
|
+
function contractOfMethodSpec(method) {
|
|
922
|
+
return methodSpecToContract.get(method);
|
|
923
|
+
}
|
|
924
|
+
function entityRef(model) {
|
|
925
|
+
const modelClass = resolveModelClass(model);
|
|
926
|
+
return { name: modelClass.name, modelClass };
|
|
927
|
+
}
|
|
928
|
+
var PUT_HAS_NO_KEY = { [KEY_REF_BRAND]: true };
|
|
929
|
+
function assertFaithfulKeys(keys, op) {
|
|
930
|
+
if (isContractKeyRef(keys)) return keys;
|
|
931
|
+
if (keys !== null && typeof keys === "object" && !isContractKeyFieldRef(keys)) {
|
|
932
|
+
const record = keys;
|
|
933
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
934
|
+
if (!isContractKeyFieldRef(leaf) && !isConcreteLiteral2(leaf)) {
|
|
935
|
+
throw new Error(
|
|
936
|
+
`publicQueryModel/publicCommandModel: the \`${op}\` operation's key field '${field}' is neither a direct \`keys.${field}\` reference nor a concrete literal. A rebuilt partition key may only place \`keys.<field>\` references or literals at its leaves; a transformed value is not representable as a declarative contract method.`
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
return record;
|
|
941
|
+
}
|
|
942
|
+
throw new Error(
|
|
943
|
+
`publicQueryModel/publicCommandModel: the \`${op}\` operation must receive the method's \`keys\` argument *directly* in its key position, or a partition key rebuilt from \`keys.<field>\` references. The value passed is not a valid contract key \u2014 it was transformed, coerced, or replaced, which is not representable as a declarative contract method.`
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
function isConcreteLiteral2(value) {
|
|
947
|
+
const t = typeof value;
|
|
948
|
+
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
949
|
+
}
|
|
950
|
+
var activeSink = null;
|
|
951
|
+
function recordContractOp(operation, model, args) {
|
|
952
|
+
const entity = entityRef(model);
|
|
953
|
+
let keys;
|
|
954
|
+
if (operation === "put") {
|
|
955
|
+
keys = PUT_HAS_NO_KEY;
|
|
956
|
+
} else {
|
|
957
|
+
keys = assertFaithfulKeys(args.keys, operation);
|
|
958
|
+
}
|
|
959
|
+
const { select, compose } = extractCompose(args.select, operation, entity.name);
|
|
960
|
+
const batch = args.batch === void 0 || args.batch === null ? void 0 : args.batch;
|
|
961
|
+
const op = {
|
|
962
|
+
__isContractMethodOp: true,
|
|
963
|
+
entity,
|
|
964
|
+
operation,
|
|
965
|
+
keys,
|
|
966
|
+
...select !== void 0 ? { select } : {},
|
|
967
|
+
...args.changes !== void 0 ? { changes: args.changes } : {},
|
|
968
|
+
...args.item !== void 0 ? { item: args.item } : {},
|
|
969
|
+
...args.condition !== void 0 ? { condition: args.condition } : {},
|
|
970
|
+
// Captured raw (cast through the field type); validated in the factory.
|
|
971
|
+
...batch !== void 0 ? { batch } : {},
|
|
972
|
+
...compose.length > 0 ? { compose } : {}
|
|
973
|
+
};
|
|
974
|
+
activeSink.push(op);
|
|
975
|
+
return op;
|
|
976
|
+
}
|
|
977
|
+
function assertBatchModeDeclaration(name, op) {
|
|
978
|
+
const batch = op.batch;
|
|
979
|
+
if (batch === void 0) return;
|
|
980
|
+
if (!isCommandBatchMode(batch)) {
|
|
981
|
+
throw new Error(
|
|
982
|
+
`publicCommandModel: method '${name}' declared an invalid \`batch\` mode \u2014 \`batch\` must be 'transact' (atomic TransactWriteItems, \u226425, condition-capable) or 'batchWrite' (non-atomic BatchWriteItem, no conditions), but received ${JSON.stringify(batch)}.`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
if (batch === "batchWrite" && op.condition !== void 0) {
|
|
986
|
+
throw new Error(
|
|
987
|
+
`publicCommandModel: method '${name}' declares a 'batchWrite' batch mode but also a write condition \u2014 DynamoDB's BatchWriteItem has no ConditionExpression and cannot carry one. Declare \`batch: 'transact'\` (an atomic TransactWriteItems, which supports the \`{ notExists }\` / equality condition subset) for a conditional batched write.`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
function extractCompose(select, operation, entityName) {
|
|
992
|
+
if (select === void 0) return { select: void 0, compose: [] };
|
|
993
|
+
const projection = {};
|
|
994
|
+
const compose = [];
|
|
995
|
+
for (const [field, value] of Object.entries(select)) {
|
|
996
|
+
if (isContractComposeNode(value)) {
|
|
997
|
+
if (operation !== "query" && operation !== "list") {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`publicQueryModel: an External Query composition (\`query(...)\`) at select property '${field}' is only valid on a read; the recorded op on '${entityName}' is a '${operation}'.`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
compose.push({ as: field, method: value.method, bind: value.bind });
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
projection[field] = value;
|
|
1006
|
+
}
|
|
1007
|
+
return { select: projection, compose };
|
|
1008
|
+
}
|
|
1009
|
+
function isRecordingContractMethod() {
|
|
1010
|
+
return activeSink !== null;
|
|
1011
|
+
}
|
|
1012
|
+
function runMethodPass(body, pass, accessed, coerced, keyCoerced) {
|
|
1013
|
+
const keys = makeKeySentinel(
|
|
1014
|
+
pass,
|
|
1015
|
+
() => {
|
|
1016
|
+
},
|
|
1017
|
+
() => {
|
|
1018
|
+
keyCoerced.value = true;
|
|
1019
|
+
}
|
|
1020
|
+
);
|
|
1021
|
+
const params = makeParamsProxy(
|
|
1022
|
+
pass,
|
|
1023
|
+
(field) => accessed.add(field),
|
|
1024
|
+
(field) => coerced.add(field)
|
|
1025
|
+
);
|
|
1026
|
+
const sink = [];
|
|
1027
|
+
const previous = activeSink;
|
|
1028
|
+
activeSink = sink;
|
|
1029
|
+
try {
|
|
1030
|
+
body(keys, params);
|
|
1031
|
+
} finally {
|
|
1032
|
+
activeSink = previous;
|
|
1033
|
+
}
|
|
1034
|
+
if (sink.length === 0) {
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
"publicQueryModel/publicCommandModel: a method body must resolve to exactly one model operation (e.g. `return Model.query(keys, select, params)`). No model operation was recorded \u2014 the body did not call `Model.query` / `Model.list` / `Model.put` / `Model.update` / `Model.delete`."
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
if (sink.length > 1) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`publicQueryModel/publicCommandModel: a method body must resolve to exactly one model operation, but it recorded ${sink.length}. A contract method is a single declarative operation; multiple writes belong in a transaction (defineTransaction), and branching across operations is not representable as a declarative contract method.`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
return sink[0];
|
|
1045
|
+
}
|
|
1046
|
+
function verifyOps(a, b, surfaced) {
|
|
1047
|
+
if (a.operation !== b.operation) {
|
|
1048
|
+
throw new Error(
|
|
1049
|
+
"publicQueryModel/publicCommandModel: the method body is not deterministic (it resolved to different operations across evaluation passes). A contract method must be a pure, declarative description of a single operation."
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
if (a.entity.name !== b.entity.name) {
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
"publicQueryModel/publicCommandModel: the method body targeted different models across evaluation passes; it must be a pure, declarative description."
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
verifyKeySlot(a.keys, b.keys, `${a.operation} key`);
|
|
1058
|
+
verifyRecord(a.select, b.select, surfaced, `${a.operation} select`);
|
|
1059
|
+
verifyRecord(a.changes, b.changes, surfaced, `${a.operation} changes`);
|
|
1060
|
+
verifyRecord(a.item, b.item, surfaced, `${a.operation} item`);
|
|
1061
|
+
verifyRecord(
|
|
1062
|
+
a.condition,
|
|
1063
|
+
b.condition,
|
|
1064
|
+
surfaced,
|
|
1065
|
+
`${a.operation} condition`
|
|
1066
|
+
);
|
|
1067
|
+
verifyCompose(a.compose, b.compose, `${a.operation} compose`);
|
|
1068
|
+
if (a.batch !== b.batch) {
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`publicCommandModel: the method body declared a different \`batch\` mode across evaluation passes ('${String(a.batch)}' vs '${String(b.batch)}'); the batched-write mode must be a fixed literal declaration.`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
function verifyCompose(a, b, context) {
|
|
1075
|
+
const ca = a ?? [];
|
|
1076
|
+
const cb = b ?? [];
|
|
1077
|
+
const byAsA = new Map(ca.map((c) => [c.as, c]));
|
|
1078
|
+
const byAsB = new Map(cb.map((c) => [c.as, c]));
|
|
1079
|
+
if (byAsA.size !== ca.length || byAsB.size !== cb.length) {
|
|
1080
|
+
throw new Error(
|
|
1081
|
+
`publicQueryModel: ${context} declares two compositions at the same select property; each \`query(...)\` must attach to a distinct property.`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
const keysA = [...byAsA.keys()].sort();
|
|
1085
|
+
const keysB = [...byAsB.keys()].sort();
|
|
1086
|
+
if (keysA.length !== keysB.length || keysA.some((x, i) => x !== keysB[i])) {
|
|
1087
|
+
throw new Error(
|
|
1088
|
+
`publicQueryModel: ${context} property set diverged between evaluation passes; the method body must declare a fixed set of compositions.`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
for (const as of keysA) {
|
|
1092
|
+
const na = byAsA.get(as);
|
|
1093
|
+
const nb = byAsB.get(as);
|
|
1094
|
+
if (na.method !== nb.method) {
|
|
1095
|
+
throw new Error(
|
|
1096
|
+
`publicQueryModel: ${context} '${as}' references a different contract method across evaluation passes; a composition must point at a single, stable contract method.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const fieldsA = Object.keys(na.bind).sort();
|
|
1100
|
+
const fieldsB = Object.keys(nb.bind).sort();
|
|
1101
|
+
if (fieldsA.length !== fieldsB.length || fieldsA.some((x, i) => x !== fieldsB[i])) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`publicQueryModel: ${context} '${as}' binds a different set of child key fields across evaluation passes; the binding must be a fixed declarative map.`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
for (const field of fieldsA) {
|
|
1107
|
+
if (na.bind[field].path !== nb.bind[field].path) {
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`publicQueryModel: ${context} '${as}' binding '${field}' resolves to a different \`from\` path across evaluation passes; the binding must be a literal declarative path.`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
function verifyKeySlot(a, b, context) {
|
|
1116
|
+
const wholeA = isContractKeyRef(a);
|
|
1117
|
+
const wholeB = isContractKeyRef(b);
|
|
1118
|
+
if (wholeA && wholeB) return;
|
|
1119
|
+
if (wholeA !== wholeB) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`publicQueryModel/publicCommandModel: ${context} is the whole \`keys\` in one evaluation pass but a rebuilt key in the other; the method body must be a pure, declarative description.`
|
|
1122
|
+
);
|
|
1123
|
+
}
|
|
1124
|
+
verifyRecord(
|
|
1125
|
+
a,
|
|
1126
|
+
b,
|
|
1127
|
+
/* @__PURE__ */ new Set(),
|
|
1128
|
+
context
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
function verifyRecord(a, b, surfaced, context) {
|
|
1132
|
+
if (a === void 0 && b === void 0) return;
|
|
1133
|
+
if (a === void 0 || b === void 0) {
|
|
1134
|
+
throw new Error(
|
|
1135
|
+
`publicQueryModel/publicCommandModel: ${context} present in one evaluation pass but not the other; the method body must be a pure, declarative description.`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
const ka = Object.keys(a).sort();
|
|
1139
|
+
const kb = Object.keys(b).sort();
|
|
1140
|
+
if (ka.length !== kb.length || ka.some((x, i) => x !== kb[i])) {
|
|
1141
|
+
throw new Error(
|
|
1142
|
+
`publicQueryModel/publicCommandModel: ${context} field set diverged between evaluation passes; the method body must be a pure, declarative description.`
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
for (const field of ka) {
|
|
1146
|
+
verifyLeaf(a[field], b[field], surfaced, `${context} field '${field}'`);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
function refTokenOf(value) {
|
|
1150
|
+
if (isContractParamRef(value)) return value.token;
|
|
1151
|
+
if (isContractKeyFieldRef(value)) return value.token;
|
|
1152
|
+
return void 0;
|
|
1153
|
+
}
|
|
1154
|
+
function verifyLeaf(va, vb, surfaced, context) {
|
|
1155
|
+
const ra = refTokenOf(va);
|
|
1156
|
+
const rb = refTokenOf(vb);
|
|
1157
|
+
if (ra !== void 0 || rb !== void 0) {
|
|
1158
|
+
if (ra === void 0 || rb === void 0) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
`publicQueryModel/publicCommandModel: ${context} is a field reference in one evaluation pass but a transformed value in the other \u2014 a coercion / transform escape was applied. Use a direct \`params.field\` / \`keys.field\` reference or a concrete literal.`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
if (ra !== rb) {
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
`publicQueryModel/publicCommandModel: ${context} resolves to different field references across evaluation passes \u2014 the value is not a stable, declarative reference.`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
if (isContractParamRef(va)) surfaced.add(ra);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
const na = normalizeLiteral(va);
|
|
1172
|
+
const nb = normalizeLiteral(vb);
|
|
1173
|
+
if (na !== nb) {
|
|
1174
|
+
throw new Error(
|
|
1175
|
+
`publicQueryModel/publicCommandModel: ${context} is not a stable concrete literal \u2014 evaluating the method body twice produced different values ('${na}' vs '${nb}'). A parameter was coerced or transformed into the value (e.g. \`String(params.x).toUpperCase()\`, \`\${params.x}-suffix\`, or arithmetic). Only a direct field reference or a concrete literal is allowed at a value position.`
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
function normalizeLiteral(value) {
|
|
1180
|
+
if (value === null) return "t:null";
|
|
1181
|
+
if (value === void 0) return "t:undef";
|
|
1182
|
+
if (value instanceof Date) return `t:date:${value.toISOString()}`;
|
|
1183
|
+
const t = typeof value;
|
|
1184
|
+
if (t === "string") return `t:s:${value}`;
|
|
1185
|
+
if (t === "boolean") return `t:b:${String(value)}`;
|
|
1186
|
+
if (t === "number") {
|
|
1187
|
+
if (Number.isNaN(value)) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
"publicQueryModel/publicCommandModel: a value leaf coerced to NaN, which means arithmetic was performed on a parameter. Only direct field references or concrete literals are allowed at value positions."
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
return `t:n:${String(value)}`;
|
|
1193
|
+
}
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`publicQueryModel/publicCommandModel: a value leaf has unsupported type '${t}'. Only a direct \`params.field\` reference or a concrete literal (string / number / boolean / Date) is allowed at a value position.`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
function resolveMethodOp(name, body) {
|
|
1199
|
+
const accessed = /* @__PURE__ */ new Set();
|
|
1200
|
+
const coerced = /* @__PURE__ */ new Set();
|
|
1201
|
+
const keyCoerced = { value: false };
|
|
1202
|
+
const ops = [];
|
|
1203
|
+
for (let pass = 0; pass < PARAM_PASS_COUNT; pass++) {
|
|
1204
|
+
ops.push(runMethodPass(body, pass, accessed, coerced, keyCoerced));
|
|
1205
|
+
}
|
|
1206
|
+
if (keyCoerced.value) {
|
|
1207
|
+
throw new Error(
|
|
1208
|
+
`publicQueryModel/publicCommandModel: method '${name}' coerces its \`keys\` argument to a primitive (e.g. via \`\${keys}\`, \`String(keys)\`, string concatenation, or a comparison). \`keys\` may only be passed *directly* into a model operation's key position; coercing or transforming it is not representable as a declarative contract method.`
|
|
1209
|
+
);
|
|
1210
|
+
}
|
|
1211
|
+
if (coerced.size > 0) {
|
|
1212
|
+
const field = [...coerced][0];
|
|
1213
|
+
throw new Error(
|
|
1214
|
+
`publicQueryModel/publicCommandModel: method '${name}' coerces params field '${field}' to a primitive (e.g. via \`String(params.${field})\`, a template literal \`\${\u2026}\`, string concatenation, or a comparison) and then consumes it in a transform or branch \u2014 for example \`String(params.${field}).slice(0, n)\` or \`\${params.${field}}-suffix\`. A contract method only supports a *direct* field reference or a concrete literal at a value position; a coerced / transformed parameter is not representable as a declarative contract method.`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
const surfaced = /* @__PURE__ */ new Set();
|
|
1218
|
+
verifyOps(ops[0], ops[1], surfaced);
|
|
1219
|
+
for (const field of accessed) {
|
|
1220
|
+
if (!surfaced.has(`{${field}}`)) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`publicQueryModel/publicCommandModel: method '${name}' accesses params field '${field}' but it never appears as a field reference in the recorded operation. This happens when a parameter is consumed by a value branch (e.g. \`params.${field} === 'x' ? \u2026 : \u2026\`) or a string transform instead of being referenced directly. A contract method only supports direct \`params.field\` references or concrete literals at value positions.`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
return ops[0];
|
|
1227
|
+
}
|
|
1228
|
+
function deriveReadFacts(op) {
|
|
1229
|
+
if (op.operation === "list") {
|
|
1230
|
+
return { resolution: "range", inputArity: "single" };
|
|
1231
|
+
}
|
|
1232
|
+
if (isContractKeyRef(op.keys) && op.keyFields !== void 0 && op.keyFields.length > 0) {
|
|
1233
|
+
const metadata = MetadataRegistry.get(
|
|
1234
|
+
op.entity.modelClass
|
|
1235
|
+
);
|
|
1236
|
+
const resolved = resolveKey([...op.keyFields], metadata);
|
|
1237
|
+
if (resolved.type === "gsi") {
|
|
1238
|
+
if (resolved.unique && !resolved.partial) {
|
|
1239
|
+
return { resolution: "point", inputArity: "single" };
|
|
1240
|
+
}
|
|
1241
|
+
return { resolution: "range", inputArity: "single" };
|
|
1242
|
+
}
|
|
1243
|
+
if (resolved.partial) {
|
|
1244
|
+
return { resolution: "range", inputArity: "single" };
|
|
1245
|
+
}
|
|
1246
|
+
return { resolution: "point", inputArity: "either" };
|
|
1247
|
+
}
|
|
1248
|
+
return { resolution: "point", inputArity: "either" };
|
|
1249
|
+
}
|
|
1250
|
+
function assertDeclaredArity(name, declared, resolution, derived) {
|
|
1251
|
+
if (declared === void 0) return;
|
|
1252
|
+
if (declared === derived) return;
|
|
1253
|
+
const hint = resolution === "range" ? `it resolved to a 'range' read (a partition \`Query\`), whose arity is always 'single' \u2014 use \`range(body)\`, not \`point(...)\`.` : derived === "single" ? `it resolved to a non-coalescible unique-GSI 'point' read (a per-key GSI \`Query\`; \`BatchGetItem\` cannot read a GSI), whose arity is 'single' \u2014 use \`point(body, { coalesce: false })\`.` : `it resolved to a coalescible base-table 'point' read (a key array \u2192 one \`BatchGetItem\`), whose arity is 'either' \u2014 use \`point(body)\` (\`coalesce\` defaults to \`true\`).`;
|
|
1254
|
+
throw new Error(
|
|
1255
|
+
`publicQueryModel: method '${name}' declared input arity '${declared}', but ${hint} The declared arity is threaded into the type-level call surface, so it must match the resolved access pattern.`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
var ARITY_TAG = "__contractArity";
|
|
1259
|
+
function tagArity(arity, body) {
|
|
1260
|
+
if (typeof body !== "function") {
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`publicQueryModel: point()/range() must wrap a method body function (\`(keys, params) => Model.query(...) | Model.list(...)\`), received ${body === null ? "null" : typeof body}.`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
Object.defineProperty(body, ARITY_TAG, { value: arity, enumerable: false });
|
|
1266
|
+
return body;
|
|
1267
|
+
}
|
|
1268
|
+
function declaredArityOf(body) {
|
|
1269
|
+
if (typeof body !== "function" && typeof body !== "object") return void 0;
|
|
1270
|
+
if (body === null) return void 0;
|
|
1271
|
+
const tag = body[ARITY_TAG];
|
|
1272
|
+
return tag === "single" || tag === "either" || tag === "array" ? tag : void 0;
|
|
1273
|
+
}
|
|
1274
|
+
function point(body, options) {
|
|
1275
|
+
const arity = options?.coalesce === false ? "single" : "either";
|
|
1276
|
+
return tagArity(arity, body);
|
|
1277
|
+
}
|
|
1278
|
+
function range(body) {
|
|
1279
|
+
return tagArity("single", body);
|
|
1280
|
+
}
|
|
1281
|
+
function isQueryModelContract(value) {
|
|
1282
|
+
return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "query";
|
|
1283
|
+
}
|
|
1284
|
+
function isCommandModelContract(value) {
|
|
1285
|
+
return typeof value === "object" && value !== null && value.__isContract === true && value.kind === "command";
|
|
1286
|
+
}
|
|
1287
|
+
function publicQueryModel(modelOrKeyFields, maybeKeyFields) {
|
|
1288
|
+
const { keyFields } = normalizeFactoryArgs("publicQueryModel", modelOrKeyFields, maybeKeyFields);
|
|
1289
|
+
return (methods) => buildQueryContract(methods, keyFields);
|
|
1290
|
+
}
|
|
1291
|
+
function normalizeFactoryArgs(factory, modelOrKeyFields, maybeKeyFields) {
|
|
1292
|
+
let model;
|
|
1293
|
+
let keyFields;
|
|
1294
|
+
if (Array.isArray(modelOrKeyFields)) {
|
|
1295
|
+
keyFields = modelOrKeyFields;
|
|
1296
|
+
} else if (modelOrKeyFields !== void 0) {
|
|
1297
|
+
model = modelOrKeyFields;
|
|
1298
|
+
keyFields = maybeKeyFields;
|
|
1299
|
+
}
|
|
1300
|
+
if (model !== void 0) resolveModelClass(model);
|
|
1301
|
+
if (keyFields !== void 0) assertKeyFieldList(factory, keyFields);
|
|
1302
|
+
return { keyFields };
|
|
1303
|
+
}
|
|
1304
|
+
function assertKeyFieldList(factory, keyFields) {
|
|
1305
|
+
if (keyFields.length === 0) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
`${factory}: the explicit Key-field list is empty. Supply at least one Key field (e.g. \`['email']\`), or omit the list to default to the model's primary key.`
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1311
|
+
for (const field of keyFields) {
|
|
1312
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
1313
|
+
throw new Error(
|
|
1314
|
+
`${factory}: an explicit Key field must be a non-empty string, but the list ${JSON.stringify(keyFields)} contains ${JSON.stringify(field)}.`
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
if (seen.has(field)) {
|
|
1318
|
+
throw new Error(
|
|
1319
|
+
`${factory}: the explicit Key-field list ${JSON.stringify(keyFields)} repeats the field '${field}'; each Key field must appear once.`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
seen.add(field);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
function withKeyFields(op, keyFields) {
|
|
1326
|
+
if (keyFields === void 0) return op;
|
|
1327
|
+
if (op.operation === "put") return op;
|
|
1328
|
+
if (!isContractKeyRef(op.keys)) return op;
|
|
1329
|
+
return { ...op, keyFields };
|
|
1330
|
+
}
|
|
1331
|
+
function buildQueryContract(methods, keyFields) {
|
|
1332
|
+
const resolved = {};
|
|
1333
|
+
for (const [name, body] of Object.entries(methods)) {
|
|
1334
|
+
if (typeof body !== "function") {
|
|
1335
|
+
throw new Error(
|
|
1336
|
+
`publicQueryModel: method '${name}' must be a function (keys, params) => Model.query(...) | Model.list(...).`
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
const op = withKeyFields(resolveMethodOp(name, body), keyFields);
|
|
1340
|
+
if (op.operation !== "query" && op.operation !== "list") {
|
|
1341
|
+
throw new Error(
|
|
1342
|
+
`publicQueryModel: method '${name}' resolves to a '${op.operation}' (write) operation; a query model may only resolve to a read (\`Model.query\` / \`Model.list\`). Use publicCommandModel for writes.`
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
const { resolution, inputArity } = deriveReadFacts(op);
|
|
1346
|
+
assertDeclaredArity(name, declaredArityOf(body), resolution, inputArity);
|
|
1347
|
+
resolved[name] = {
|
|
1348
|
+
__methodKind: "query",
|
|
1349
|
+
op,
|
|
1350
|
+
resolution,
|
|
1351
|
+
inputArity,
|
|
1352
|
+
__methodName: name
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
const contract = { __isContract: true, kind: "query", methods: resolved };
|
|
1356
|
+
for (const spec of Object.values(resolved)) methodSpecToContract.set(spec, contract);
|
|
1357
|
+
return contract;
|
|
1358
|
+
}
|
|
1359
|
+
function publicCommandModel(modelOrKeyFields, maybeKeyFields) {
|
|
1360
|
+
const { keyFields } = normalizeFactoryArgs("publicCommandModel", modelOrKeyFields, maybeKeyFields);
|
|
1361
|
+
return (methods) => buildCommandContract(methods, keyFields);
|
|
1362
|
+
}
|
|
1363
|
+
function buildCommandContract(methods, keyFields) {
|
|
1364
|
+
const resolved = {};
|
|
1365
|
+
for (const [name, body] of Object.entries(methods)) {
|
|
1366
|
+
if (isPlannedCommandMethod(body)) {
|
|
1367
|
+
resolved[name] = buildPlannedCommandMethod(name, body, keyFields);
|
|
1368
|
+
continue;
|
|
1369
|
+
}
|
|
1370
|
+
if (typeof body !== "function") {
|
|
1371
|
+
throw new Error(
|
|
1372
|
+
`publicCommandModel: method '${name}' must be a function (keys, params) => Model.put(...) | Model.update(...) | Model.delete(...), or a \`command({ input, select }).plan(mutation)\` planned method (#83).`
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
const op = withKeyFields(resolveMethodOp(name, body), keyFields);
|
|
1376
|
+
if (op.operation !== "put" && op.operation !== "update" && op.operation !== "delete") {
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
`publicCommandModel: method '${name}' resolves to a '${op.operation}' (read) operation; a command model may only resolve to a write (\`Model.put\` / \`Model.update\` / \`Model.delete\`). Use publicQueryModel for reads.`
|
|
1379
|
+
);
|
|
1380
|
+
}
|
|
1381
|
+
assertBatchModeDeclaration(name, op);
|
|
1382
|
+
resolved[name] = {
|
|
1383
|
+
__methodKind: "command",
|
|
1384
|
+
op,
|
|
1385
|
+
// A single key → one write op; a key array → a batched write. Both forms
|
|
1386
|
+
// are point writes against known keys, so the accepted arity is 'either'.
|
|
1387
|
+
inputArity: "either",
|
|
1388
|
+
result: commandResultOf(op),
|
|
1389
|
+
...op.batch !== void 0 ? { batch: op.batch } : {}
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
return { __isContract: true, kind: "command", methods: resolved };
|
|
1393
|
+
}
|
|
1394
|
+
function commandResultOf(op) {
|
|
1395
|
+
return op.operation === "update" ? "entity" : "void";
|
|
1396
|
+
}
|
|
1397
|
+
var PLANNED_COMMAND_BRAND = /* @__PURE__ */ Symbol("graphddb:plannedCommandMethod");
|
|
1398
|
+
function isPlannedCommandMethod(value) {
|
|
1399
|
+
return typeof value === "object" && value !== null && value[PLANNED_COMMAND_BRAND] === true;
|
|
1400
|
+
}
|
|
1401
|
+
function command(spec) {
|
|
1402
|
+
const input = spec.input;
|
|
1403
|
+
if (typeof input !== "object" || input === null) {
|
|
1404
|
+
throw new Error(
|
|
1405
|
+
"command({ input, select }): `input` must be a record of `param.*` placeholders (e.g. `{ userId: param.string(), title: param.string() }`)."
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
for (const [field, p] of Object.entries(input)) {
|
|
1409
|
+
if (!isParam(p)) {
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`command({ input }): input field '${field}' must be a \`param.*\` placeholder (e.g. \`param.string()\`), not ${JSON.stringify(p)}.`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
const select = spec.select;
|
|
1416
|
+
if (select !== void 0) {
|
|
1417
|
+
if (typeof select !== "object" || select === null) {
|
|
1418
|
+
throw new Error(
|
|
1419
|
+
"command({ select }): `select` must be a boolean field map (e.g. `{ postId: true, title: true }`), or omitted for no return projection."
|
|
1420
|
+
);
|
|
1421
|
+
}
|
|
1422
|
+
for (const [field, flag] of Object.entries(select)) {
|
|
1423
|
+
if (typeof flag !== "boolean") {
|
|
1424
|
+
throw new Error(
|
|
1425
|
+
`command({ select }): return projection field '${field}' must be a boolean flag (\`true\` / \`false\`), not ${JSON.stringify(flag)}.`
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return {
|
|
1431
|
+
plan(plan) {
|
|
1432
|
+
if (!isCommandPlan(plan)) {
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
"command(...).plan(...): expected a `mutation(...)` / `definePlan(...)` CommandPlan (the internal write-plan composition), but received a non-plan value."
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
return {
|
|
1438
|
+
[PLANNED_COMMAND_BRAND]: true,
|
|
1439
|
+
plan,
|
|
1440
|
+
input,
|
|
1441
|
+
select
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function buildPlannedCommandMethod(name, planned, keyFields) {
|
|
1447
|
+
const plan = compileMutationPlan(planned.plan);
|
|
1448
|
+
const primary = plan.fragments[0];
|
|
1449
|
+
if (keyFields !== void 0) {
|
|
1450
|
+
const want = [...keyFields].sort().join(",");
|
|
1451
|
+
const got = [...primary.keyFields].sort().join(",");
|
|
1452
|
+
if (want !== got) {
|
|
1453
|
+
throw new Error(
|
|
1454
|
+
`publicCommandModel: planned method '${name}' writes '${primary.op.entity.name}' keyed by its primary key [${primary.keyFields.join(", ")}], but the factory was given an explicit Key-field list [${keyFields.join(", ")}]. A mutation-derived command is keyed by the written entity's primary key; omit the explicit list or match it.`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
const stamp = (fragment) => fragment.op.operation === "put" ? fragment.op : { ...fragment.op, keyFields: fragment.keyFields };
|
|
1459
|
+
const op = stamp(primary);
|
|
1460
|
+
const returnSelection = normalizeReturnSelection(planned.select);
|
|
1461
|
+
const ops = plan.fragments.length > 1 ? plan.fragments.map(stamp) : void 0;
|
|
1462
|
+
const conditionChecks = plan.fragments.flatMap((f) => f.conditionChecks ?? []);
|
|
1463
|
+
const edgeWrites2 = plan.fragments.flatMap((f) => f.edgeWrites ?? []);
|
|
1464
|
+
const derivedUpdates = plan.fragments.flatMap((f) => f.derivedUpdates ?? []);
|
|
1465
|
+
const uniqueGuards = plan.fragments.flatMap((f) => f.uniqueGuards ?? []);
|
|
1466
|
+
const outboxEvents = plan.fragments.flatMap((f) => f.outboxEvents ?? []);
|
|
1467
|
+
const idempotencyGuards = plan.fragments.map((f) => f.idempotencyGuard).filter((g) => g !== void 0);
|
|
1468
|
+
if (idempotencyGuards.length > 1) {
|
|
1469
|
+
throw new Error(
|
|
1470
|
+
`publicCommandModel: planned method '${name}' declares an idempotency guard on more than one fragment. Idempotency is a single command-level client token (it keys the whole atomic write); declare \`w.idempotentBy(...)\` on at most one fragment's lifecycle.`
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
const idempotencyGuard = idempotencyGuards[0];
|
|
1474
|
+
return {
|
|
1475
|
+
__methodKind: "command",
|
|
1476
|
+
op,
|
|
1477
|
+
...ops !== void 0 ? { ops } : {},
|
|
1478
|
+
...conditionChecks.length > 0 ? { conditionChecks } : {},
|
|
1479
|
+
...edgeWrites2.length > 0 ? { edgeWrites: edgeWrites2 } : {},
|
|
1480
|
+
...derivedUpdates.length > 0 ? { derivedUpdates } : {},
|
|
1481
|
+
...uniqueGuards.length > 0 ? { uniqueGuards } : {},
|
|
1482
|
+
...outboxEvents.length > 0 ? { outboxEvents } : {},
|
|
1483
|
+
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {},
|
|
1484
|
+
inputArity: "either",
|
|
1485
|
+
// A return projection means the method returns the read-back entity; otherwise
|
|
1486
|
+
// it is a fire-and-forget write (`void`).
|
|
1487
|
+
result: returnSelection !== void 0 ? "entity" : "void",
|
|
1488
|
+
...returnSelection !== void 0 ? { returnSelection } : {}
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
function normalizeReturnSelection(select) {
|
|
1492
|
+
if (select === void 0) return void 0;
|
|
1493
|
+
const out = {};
|
|
1494
|
+
for (const [field, flag] of Object.entries(select)) {
|
|
1495
|
+
if (flag === true) out[field] = true;
|
|
1496
|
+
}
|
|
1497
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/spec/mutation-command.ts
|
|
1501
|
+
var INTENT_OPERATION = {
|
|
1502
|
+
create: "put",
|
|
1503
|
+
update: "update",
|
|
1504
|
+
remove: "delete"
|
|
1505
|
+
};
|
|
1506
|
+
var INPUT_PATH_RE = /^\$\.input\.([A-Za-z_$][\w$]*)$/;
|
|
1507
|
+
var ENTITY_FIELD_PATH_RE = /^\$\.entity\.([A-Za-z_$][\w$]*)$/;
|
|
1508
|
+
var OLD_PATH_RE = /^\$\.old\.([A-Za-z_$][\w$]*)$/;
|
|
1509
|
+
var INTENT_EDGE_LIFECYCLE = {
|
|
1510
|
+
create: "onCreate",
|
|
1511
|
+
update: "onUpdateKeyChange",
|
|
1512
|
+
remove: "onDelete"
|
|
1513
|
+
};
|
|
1514
|
+
function deriveConditionChecks(fragment, requires) {
|
|
1515
|
+
return requires.map((req) => deriveOneConditionCheck(fragment, req));
|
|
1516
|
+
}
|
|
1517
|
+
function deriveOneConditionCheck(fragment, req) {
|
|
1518
|
+
const targetClass = req.targetFactory();
|
|
1519
|
+
const targetName = targetClass.name;
|
|
1520
|
+
const metadata = MetadataRegistry.get(
|
|
1521
|
+
targetClass
|
|
1522
|
+
);
|
|
1523
|
+
if (!metadata.primaryKey) {
|
|
1524
|
+
throw new Error(
|
|
1525
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares a \`requires\` referential-integrity constraint on '${targetName}', but that model has no primary key \u2014 a ConditionCheck (\`attribute_exists\`) asserts a keyed row, so the referenced entity must declare a primary key.`
|
|
1526
|
+
);
|
|
1527
|
+
}
|
|
1528
|
+
const targetKeyFields = metadata.primaryKey.inputFieldNames;
|
|
1529
|
+
const keyBinding = {};
|
|
1530
|
+
for (const [targetField, source] of Object.entries(req.keys)) {
|
|
1531
|
+
if (!targetKeyFields.includes(targetField)) {
|
|
1532
|
+
throw new Error(
|
|
1533
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' binds '${targetName}.${targetField}' in its \`requires\` constraint, but '${targetField}' is not a primary-key field of '${targetName}' (its key is [${targetKeyFields.join(", ")}]). A referential-integrity assertion must key the referenced entity by its primary key.`
|
|
1534
|
+
);
|
|
1535
|
+
}
|
|
1536
|
+
const match = INPUT_PATH_RE.exec(source);
|
|
1537
|
+
if (match === null) {
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' binds '${targetName}.${targetField}' in its \`requires\` constraint to ${JSON.stringify(source)}, which is not a \`$.input.<field>\` reference. The referenced entity's key is resolved from the mutation input, so each key value must be a \`$.input.<field>\` path (e.g. \`'$.input.userId'\`).`
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
keyBinding[targetField] = match[1];
|
|
1543
|
+
}
|
|
1544
|
+
for (const field of targetKeyFields) {
|
|
1545
|
+
if (!(field in keyBinding)) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares a \`requires\` constraint on '${targetName}' that does not bind its primary-key field '${field}'. A ConditionCheck asserts a single keyed row, so the constraint must bind EVERY primary-key field of '${targetName}' ([${targetKeyFields.join(", ")}]) from \`$.input.*\`.`
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
return { entity: { name: targetName, modelClass: targetClass }, keyBinding };
|
|
1552
|
+
}
|
|
1553
|
+
function deriveEdgeWrites(fragment, edges, lifecycle) {
|
|
1554
|
+
return edges.map((edge) => deriveOneEdgeWrite(fragment, edge, lifecycle));
|
|
1555
|
+
}
|
|
1556
|
+
function deriveOneEdgeWrite(fragment, edge, lifecycle) {
|
|
1557
|
+
const targetClass = edge.targetFactory();
|
|
1558
|
+
const targetMeta = MetadataRegistry.get(
|
|
1559
|
+
targetClass
|
|
1560
|
+
);
|
|
1561
|
+
const relation = targetMeta.relations.find(
|
|
1562
|
+
(r) => r.propertyName === edge.relationProperty
|
|
1563
|
+
);
|
|
1564
|
+
if (!relation) {
|
|
1565
|
+
throw new Error(
|
|
1566
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares an edge effect on relation '${edge.relationProperty}' of '${targetClass.name}', which has no such relation. Declare the edge with the read-side relation property it materializes.`
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
const adjacencyClass = relation.targetFactory();
|
|
1570
|
+
const items = deriveEdgeWriteItemsFor(
|
|
1571
|
+
adjacencyClass,
|
|
1572
|
+
{ targetFactory: edge.targetFactory, relationProperty: edge.relationProperty },
|
|
1573
|
+
lifecycle
|
|
1574
|
+
);
|
|
1575
|
+
return {
|
|
1576
|
+
entity: { name: adjacencyClass.name, modelClass: adjacencyClass },
|
|
1577
|
+
relation: { target: targetClass.name, property: edge.relationProperty },
|
|
1578
|
+
items
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
function deriveUpdates(fragment, derive) {
|
|
1582
|
+
return derive.map((d) => deriveOneUpdate(fragment, d));
|
|
1583
|
+
}
|
|
1584
|
+
function deriveOneUpdate(fragment, effect) {
|
|
1585
|
+
const targetClass = effect.targetFactory();
|
|
1586
|
+
const targetName = targetClass.name;
|
|
1587
|
+
const metadata = MetadataRegistry.get(
|
|
1588
|
+
targetClass
|
|
1589
|
+
);
|
|
1590
|
+
if (!metadata.primaryKey) {
|
|
1591
|
+
throw new Error(
|
|
1592
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares a derived update on '${targetName}', but that model has no primary key \u2014 an \`UpdateItem\` keys a single row, so the target must declare a primary key.`
|
|
1593
|
+
);
|
|
1594
|
+
}
|
|
1595
|
+
const targetKeyFields = metadata.primaryKey.inputFieldNames;
|
|
1596
|
+
const keyBinding = {};
|
|
1597
|
+
for (const [targetField, source] of Object.entries(effect.keys)) {
|
|
1598
|
+
if (!targetKeyFields.includes(targetField)) {
|
|
1599
|
+
throw new Error(
|
|
1600
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' binds '${targetName}.${targetField}' in a derived update, but '${targetField}' is not a primary-key field of '${targetName}' (its key is [${targetKeyFields.join(", ")}]). A derived update keys the target by its primary key.`
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
const inputMatch = INPUT_PATH_RE.exec(source);
|
|
1604
|
+
const oldMatch = OLD_PATH_RE.exec(source);
|
|
1605
|
+
if (inputMatch !== null) {
|
|
1606
|
+
keyBinding[targetField] = inputMatch[1];
|
|
1607
|
+
} else if (oldMatch !== null) {
|
|
1608
|
+
keyBinding[targetField] = `${OLD_VALUE_NAMESPACE}.${oldMatch[1]}`;
|
|
1609
|
+
} else {
|
|
1610
|
+
throw new Error(
|
|
1611
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' binds '${targetName}.${targetField}' in a derived update to ${JSON.stringify(source)}, which is not a \`$.input.<field>\` (current value) or \`$.old.<field>\` (pre-mutation value) reference. The target row's key is resolved from the mutation input, so each key value must be a \`$.input.*\` / \`$.old.*\` path.`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
for (const field of targetKeyFields) {
|
|
1616
|
+
if (!(field in keyBinding)) {
|
|
1617
|
+
throw new Error(
|
|
1618
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares a derived update on '${targetName}' that does not bind its primary-key field '${field}'. An \`UpdateItem\` keys a single row, so the update must bind EVERY primary-key field of '${targetName}' ([${targetKeyFields.join(", ")}]).`
|
|
1619
|
+
);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
const isField = metadata.fields.some((f) => f.propertyName === effect.attribute);
|
|
1623
|
+
if (!isField) {
|
|
1624
|
+
throw new Error(
|
|
1625
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares a derived update of '${targetName}.${effect.attribute}', which is not a field of '${targetName}'. A derived counter must increment a declared numeric field.`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
const selfModel = targetClass === fragment.entity.modelClass;
|
|
1629
|
+
const allCurrentInput = Object.values(keyBinding).every(
|
|
1630
|
+
(inputField) => !inputField.startsWith(`${OLD_VALUE_NAMESPACE}.`)
|
|
1631
|
+
);
|
|
1632
|
+
const reproducesOwnKey = targetKeyFields.every((field) => keyBinding[field] === field);
|
|
1633
|
+
if (selfModel && allCurrentInput && reproducesOwnKey) {
|
|
1634
|
+
throw new Error(
|
|
1635
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares a derived counter (\`increment\`) on '${targetName}.${effect.attribute}' that targets the SAME row as the entity write (its key binds the fragment's own primary key from \`$.input.*\`). A Put+Update on one item is unsupported in a transaction (DynamoDB rejects touching one key twice), and silently dropping the \`ADD\` would lose the increment. A \`w.increment(...)\` must target a DIFFERENT row (e.g. a parent aggregate's counter); to maintain a counter ON the written entity, write the field as part of the entity itself, not via a derived counter onto its own row.`
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
entity: { name: targetName, modelClass: targetClass },
|
|
1640
|
+
keyBinding,
|
|
1641
|
+
attribute: effect.attribute,
|
|
1642
|
+
amount: effect.amount
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
var UNIQUE_GUARD_PK_PREFIX = "UNIQUE#";
|
|
1646
|
+
var UNIQUE_GUARD_SK_PREFIX = "VALUE#";
|
|
1647
|
+
function deriveUniqueGuards(fragment, unique) {
|
|
1648
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1649
|
+
return unique.map((u) => {
|
|
1650
|
+
if (seen.has(u.name)) {
|
|
1651
|
+
throw new Error(
|
|
1652
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares two uniqueness constraints named '${u.name}'. A uniqueness guard's marker-row key is built from its name, so the name must be unique within a lifecycle.`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
seen.add(u.name);
|
|
1656
|
+
return deriveOneUniqueGuard(fragment, u);
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
function uniquePathToInputField(fragment, effect, role, path) {
|
|
1660
|
+
const match = INPUT_PATH_RE.exec(path);
|
|
1661
|
+
if (match === null) {
|
|
1662
|
+
throw new Error(
|
|
1663
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' binds a \`${role}\` value of its '${effect.name}' uniqueness constraint to ${JSON.stringify(path)}, which is not a \`$.input.<field>\` reference. A uniqueness guard's marker-row key is resolved from the mutation input, so each \`scope\` / \`fields\` value must be a \`$.input.<field>\` path (e.g. \`'$.input.userId'\`).`
|
|
1664
|
+
);
|
|
1665
|
+
}
|
|
1666
|
+
return match[1];
|
|
1667
|
+
}
|
|
1668
|
+
function uniqueGuardKey(name, scopeFields, fieldFields, namespace) {
|
|
1669
|
+
const token = (field) => namespace === "old" ? `{${OLD_VALUE_NAMESPACE}.${field}}` : `{${field}}`;
|
|
1670
|
+
const pk = UNIQUE_GUARD_PK_PREFIX + name + scopeFields.map((f) => `#${token(f)}`).join("");
|
|
1671
|
+
const sk = UNIQUE_GUARD_SK_PREFIX + fieldFields.map((f) => token(f)).join("#");
|
|
1672
|
+
return { PK: pk, SK: sk };
|
|
1673
|
+
}
|
|
1674
|
+
function deriveOneUniqueGuard(fragment, effect) {
|
|
1675
|
+
if (effect.fields.length === 0) {
|
|
1676
|
+
throw new Error(
|
|
1677
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares the '${effect.name}' uniqueness constraint with no \`fields\`. A uniqueness guard must name at least one field whose value is unique within the scope.`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
const scopeFields = effect.scope.map(
|
|
1681
|
+
(p) => uniquePathToInputField(fragment, effect, "scope", p)
|
|
1682
|
+
);
|
|
1683
|
+
const fieldFields = effect.fields.map(
|
|
1684
|
+
(p) => uniquePathToInputField(fragment, effect, "fields", p)
|
|
1685
|
+
);
|
|
1686
|
+
const newKey = uniqueGuardKey(effect.name, scopeFields, fieldFields, "input");
|
|
1687
|
+
const putGuard = {
|
|
1688
|
+
type: "Put",
|
|
1689
|
+
tableName: tableNameOf(fragment),
|
|
1690
|
+
entity: MARKER_ROW_ENTITY,
|
|
1691
|
+
item: { PK: newKey.PK, SK: newKey.SK },
|
|
1692
|
+
condition: { kind: "attributeNotExists", field: "PK" },
|
|
1693
|
+
literalKey: true
|
|
1694
|
+
};
|
|
1695
|
+
if (fragment.intent === "create") {
|
|
1696
|
+
return { name: effect.name, items: [putGuard] };
|
|
1697
|
+
}
|
|
1698
|
+
if (fragment.intent === "remove") {
|
|
1699
|
+
return {
|
|
1700
|
+
name: effect.name,
|
|
1701
|
+
items: [
|
|
1702
|
+
{
|
|
1703
|
+
type: "Delete",
|
|
1704
|
+
tableName: tableNameOf(fragment),
|
|
1705
|
+
entity: MARKER_ROW_ENTITY,
|
|
1706
|
+
keyCondition: { PK: newKey.PK, SK: newKey.SK },
|
|
1707
|
+
literalKey: true
|
|
1708
|
+
}
|
|
1709
|
+
]
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
const oldKey = uniqueGuardKey(effect.name, scopeFields, fieldFields, "old");
|
|
1713
|
+
return {
|
|
1714
|
+
name: effect.name,
|
|
1715
|
+
items: [
|
|
1716
|
+
{
|
|
1717
|
+
type: "Delete",
|
|
1718
|
+
tableName: tableNameOf(fragment),
|
|
1719
|
+
entity: MARKER_ROW_ENTITY,
|
|
1720
|
+
keyCondition: { PK: oldKey.PK, SK: oldKey.SK },
|
|
1721
|
+
literalKey: true
|
|
1722
|
+
},
|
|
1723
|
+
putGuard
|
|
1724
|
+
]
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
function tableNameOf(fragment) {
|
|
1728
|
+
const metadata = MetadataRegistry.get(
|
|
1729
|
+
fragment.entity.modelClass
|
|
1730
|
+
);
|
|
1731
|
+
return metadata.tableName;
|
|
1732
|
+
}
|
|
1733
|
+
var OUTBOX_PK_PREFIX = "OUTBOX#";
|
|
1734
|
+
var OUTBOX_SK_PREFIX = "EVENT#";
|
|
1735
|
+
var OUTBOX_EVENT_NAME_ATTR = "eventName";
|
|
1736
|
+
var IDEMPOTENCY_PK_PREFIX = "IDEMP#";
|
|
1737
|
+
var IDEMPOTENCY_SK = "TOKEN";
|
|
1738
|
+
function emitPathToInputField(fragment, eventName, field, path) {
|
|
1739
|
+
const inputMatch = INPUT_PATH_RE.exec(path);
|
|
1740
|
+
if (inputMatch !== null) return inputMatch[1];
|
|
1741
|
+
const entityMatch = ENTITY_FIELD_PATH_RE.exec(path);
|
|
1742
|
+
if (entityMatch !== null) return entityMatch[1];
|
|
1743
|
+
throw new Error(
|
|
1744
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' binds the '${field}' payload field of its '${eventName}' event to ${JSON.stringify(path)}, which is not a \`$.input.<field>\` (mutation input) or \`$.entity.<field>\` (the written entity) reference. An outbox event payload must bind declaratively from the input / written entity (no arbitrary procedure), so each value must be a \`$.input.<field>\` / \`$.entity.<field>\` path (e.g. \`'$.input.userId'\`).`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
function deriveOutboxEvents(fragment, emits) {
|
|
1748
|
+
if (fragment.intent !== "create") {
|
|
1749
|
+
throw new Error(
|
|
1750
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares an outbox event, but \`emits\` is supported only on a 'create' lifecycle. The outbox row is keyed deterministically by (event name + entity primary key) with no per-emission discriminator, so a 'create' (guarded one-per-row by the entity's \`attribute_not_exists\`) gets exactly one event row; an 'update' would overwrite a prior, possibly-undrained, outbox row (losing the earlier event) and a 'remove' would race the entity's tombstone. An update / delete event stream is a separate concern (a dedicated append-keyed write), out of scope for the derived outbox.`
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
const keyFields = primaryKeyFields(fragment);
|
|
1754
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1755
|
+
return emits.map((emit) => {
|
|
1756
|
+
if (emit.name.includes("{") || emit.name.includes("}")) {
|
|
1757
|
+
throw new Error(
|
|
1758
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares an outbox event named ${JSON.stringify(emit.name)}, which contains a '{' / '}'. The event name is spliced into the outbox row's key template (\`OUTBOX#<name>#\u2026\`), so a brace would forge a spurious \`{token}\` placeholder. Use a brace-free event name.`
|
|
1759
|
+
);
|
|
1760
|
+
}
|
|
1761
|
+
if (seen.has(emit.name)) {
|
|
1762
|
+
throw new Error(
|
|
1763
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares two events named '${emit.name}'. An outbox row's marker key is built from the event name, so the name must be unique within a lifecycle.`
|
|
1764
|
+
);
|
|
1765
|
+
}
|
|
1766
|
+
seen.add(emit.name);
|
|
1767
|
+
return deriveOneOutboxEvent(fragment, emit, keyFields);
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
function deriveOneOutboxEvent(fragment, emit, keyFields) {
|
|
1771
|
+
const keySegments = keyFields.map((f) => `#{${f}}`).join("");
|
|
1772
|
+
const pk = OUTBOX_PK_PREFIX + emit.name + keySegments;
|
|
1773
|
+
const sk = OUTBOX_SK_PREFIX + keyFields.map((f) => `{${f}}`).join("#");
|
|
1774
|
+
const item = {
|
|
1775
|
+
PK: pk,
|
|
1776
|
+
SK: sk,
|
|
1777
|
+
// The event name is stored verbatim so the `newImage` carries it for routing.
|
|
1778
|
+
[OUTBOX_EVENT_NAME_ATTR]: emit.name
|
|
1779
|
+
};
|
|
1780
|
+
for (const [field, source] of Object.entries(emit.payload)) {
|
|
1781
|
+
const inputField = emitPathToInputField(fragment, emit.name, field, source);
|
|
1782
|
+
item[field] = `{${inputField}}`;
|
|
1783
|
+
}
|
|
1784
|
+
return {
|
|
1785
|
+
name: emit.name,
|
|
1786
|
+
items: [
|
|
1787
|
+
{
|
|
1788
|
+
type: "Put",
|
|
1789
|
+
tableName: tableNameOf(fragment),
|
|
1790
|
+
entity: MARKER_ROW_ENTITY,
|
|
1791
|
+
item,
|
|
1792
|
+
// No `attribute_not_exists`: the outbox row is keyed by the entity key, which
|
|
1793
|
+
// the entity `Put`'s own create guard already makes unique — the event is
|
|
1794
|
+
// appended atomically with the row it announces.
|
|
1795
|
+
literalKey: true
|
|
1796
|
+
}
|
|
1797
|
+
]
|
|
1798
|
+
};
|
|
1799
|
+
}
|
|
1800
|
+
function deriveIdempotencyGuard(fragment, effect) {
|
|
1801
|
+
const match = INPUT_PATH_RE.exec(effect.token);
|
|
1802
|
+
if (match === null) {
|
|
1803
|
+
throw new Error(
|
|
1804
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' declares an idempotency guard keyed on ${JSON.stringify(effect.token)}, which is not a \`$.input.<field>\` reference. The client token is a mutation input field, so \`w.idempotentBy(...)\` must name a \`$.input.<field>\` path (e.g. \`'$.input.requestId'\`).`
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
const tokenField = match[1];
|
|
1808
|
+
return {
|
|
1809
|
+
tokenField,
|
|
1810
|
+
items: [
|
|
1811
|
+
{
|
|
1812
|
+
type: "Put",
|
|
1813
|
+
tableName: tableNameOf(fragment),
|
|
1814
|
+
entity: MARKER_ROW_ENTITY,
|
|
1815
|
+
item: { PK: `${IDEMPOTENCY_PK_PREFIX}{${tokenField}}`, SK: IDEMPOTENCY_SK },
|
|
1816
|
+
// The claim guard: the token row must not already exist. A same-token replay
|
|
1817
|
+
// targets the SAME row and fails, rolling the whole tx back (no double effect).
|
|
1818
|
+
condition: { kind: "attributeNotExists", field: "PK" },
|
|
1819
|
+
literalKey: true
|
|
1820
|
+
}
|
|
1821
|
+
]
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
function resolveLifecycle(fragment) {
|
|
1825
|
+
const writes = fragment.use ?? getEntityWrites(fragment.entity.modelClass);
|
|
1826
|
+
if (writes === void 0) return void 0;
|
|
1827
|
+
const phase = lifecyclePhaseForIntent(fragment.intent);
|
|
1828
|
+
const contract = writes[phase];
|
|
1829
|
+
if (contract === void 0) return void 0;
|
|
1830
|
+
if (!isLifecycleContract(contract)) {
|
|
1831
|
+
throw new Error(
|
|
1832
|
+
`mutation: the '${fragment.intent}' lifecycle of the save contract adopted by the fragment on '${fragment.entity.name}' is not a \`w.lifecycle({...})\`.`
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
return contract;
|
|
1836
|
+
}
|
|
1837
|
+
function primaryKeyFields(fragment) {
|
|
1838
|
+
const metadata = MetadataRegistry.get(
|
|
1839
|
+
fragment.entity.modelClass
|
|
1840
|
+
);
|
|
1841
|
+
if (!metadata.primaryKey) {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
`mutation: the '${fragment.intent}' fragment targets '${fragment.entity.name}', which has no primary key. A mutation fragment writes / identifies a row by its primary key, so the target model must declare one.`
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
return metadata.primaryKey.inputFieldNames;
|
|
1847
|
+
}
|
|
1848
|
+
function renderInputLeaf(leaf, isKeyFieldInPut, consumerIndex, consumerField, resolveEntityRef) {
|
|
1849
|
+
if (isMutationInputRef(leaf)) {
|
|
1850
|
+
return isKeyFieldInPut ? mintContractKeyFieldRef(leaf.field) : mintContractParamRef(leaf.field);
|
|
1851
|
+
}
|
|
1852
|
+
const entityRef3 = parseEntityRef(leaf);
|
|
1853
|
+
if (entityRef3 !== void 0) {
|
|
1854
|
+
if (resolveEntityRef === null) {
|
|
1855
|
+
throw new Error(
|
|
1856
|
+
`mutation: the cross-fragment reference '${entityRef3.path}' is only valid in a MULTI-fragment mutation \u2014 it names a value produced by an earlier fragment, but this mutation has a single fragment (there is no fragment #${entityRef3.fragmentIndex} to read from). Bind '${consumerField}' to a \`$.field\` input reference or a literal instead.`
|
|
1857
|
+
);
|
|
1858
|
+
}
|
|
1859
|
+
return resolveEntityRef(entityRef3, consumerIndex, consumerField);
|
|
1860
|
+
}
|
|
1861
|
+
return leaf;
|
|
1862
|
+
}
|
|
1863
|
+
function compileFragment(fragment, index = 0, resolveEntityRef = null) {
|
|
1864
|
+
const keyFields = primaryKeyFields(fragment);
|
|
1865
|
+
const keyFieldSet = new Set(keyFields);
|
|
1866
|
+
const operation = INTENT_OPERATION[fragment.intent];
|
|
1867
|
+
const lifecycle = resolveLifecycle(fragment);
|
|
1868
|
+
for (const field of keyFields) {
|
|
1869
|
+
if (!(field in fragment.input)) {
|
|
1870
|
+
throw new Error(
|
|
1871
|
+
`mutation: the '${fragment.intent}' fragment on '${fragment.entity.name}' must bind the primary-key field '${field}' in its \`input\` (it identifies the row), e.g. \`{ ${field}: $.${field}, \u2026 }\`.`
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
let op;
|
|
1876
|
+
if (fragment.intent === "create") {
|
|
1877
|
+
const item = {};
|
|
1878
|
+
for (const [field, leaf] of Object.entries(fragment.input)) {
|
|
1879
|
+
item[field] = renderInputLeaf(
|
|
1880
|
+
leaf,
|
|
1881
|
+
keyFieldSet.has(field),
|
|
1882
|
+
index,
|
|
1883
|
+
field,
|
|
1884
|
+
resolveEntityRef
|
|
1885
|
+
);
|
|
1886
|
+
}
|
|
1887
|
+
op = {
|
|
1888
|
+
__isContractMethodOp: true,
|
|
1889
|
+
entity: fragment.entity,
|
|
1890
|
+
operation,
|
|
1891
|
+
keys: wholeKeysSentinel(),
|
|
1892
|
+
// a `put` derives identity from the item.
|
|
1893
|
+
item,
|
|
1894
|
+
// The base `create` op guards against clobbering an existing row, exactly as
|
|
1895
|
+
// the proposal specifies (create → Put `attribute_not_exists(PK)`).
|
|
1896
|
+
condition: { notExists: true }
|
|
1897
|
+
};
|
|
1898
|
+
} else {
|
|
1899
|
+
const changes = {};
|
|
1900
|
+
for (const [field, leaf] of Object.entries(fragment.input)) {
|
|
1901
|
+
if (keyFieldSet.has(field)) continue;
|
|
1902
|
+
changes[field] = renderInputLeaf(leaf, false, index, field, resolveEntityRef);
|
|
1903
|
+
}
|
|
1904
|
+
op = {
|
|
1905
|
+
__isContractMethodOp: true,
|
|
1906
|
+
entity: fragment.entity,
|
|
1907
|
+
operation,
|
|
1908
|
+
keys: wholeKeysSentinel(),
|
|
1909
|
+
keyFields,
|
|
1910
|
+
...fragment.intent === "update" ? { changes } : {}
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
const conditionChecks = lifecycle?.effects.requires !== void 0 && lifecycle.effects.requires.length > 0 ? deriveConditionChecks(fragment, lifecycle.effects.requires) : void 0;
|
|
1914
|
+
const edgeWrites2 = lifecycle?.effects.edges !== void 0 && lifecycle.effects.edges.length > 0 ? deriveEdgeWrites(fragment, lifecycle.effects.edges, INTENT_EDGE_LIFECYCLE[fragment.intent]) : void 0;
|
|
1915
|
+
const derivedUpdates = lifecycle?.effects.derive !== void 0 && lifecycle.effects.derive.length > 0 ? deriveUpdates(fragment, lifecycle.effects.derive) : void 0;
|
|
1916
|
+
const uniqueGuards = lifecycle?.effects.unique !== void 0 && lifecycle.effects.unique.length > 0 ? deriveUniqueGuards(fragment, lifecycle.effects.unique) : void 0;
|
|
1917
|
+
const outboxEvents = lifecycle?.effects.emits !== void 0 && lifecycle.effects.emits.length > 0 ? deriveOutboxEvents(fragment, lifecycle.effects.emits) : void 0;
|
|
1918
|
+
const idempotencyGuard = lifecycle?.effects.idempotency !== void 0 ? deriveIdempotencyGuard(fragment, lifecycle.effects.idempotency) : void 0;
|
|
1919
|
+
return {
|
|
1920
|
+
op,
|
|
1921
|
+
keyFields,
|
|
1922
|
+
...lifecycle !== void 0 ? { effects: lifecycle.effects } : {},
|
|
1923
|
+
...conditionChecks !== void 0 ? { conditionChecks } : {},
|
|
1924
|
+
...edgeWrites2 !== void 0 ? { edgeWrites: edgeWrites2 } : {},
|
|
1925
|
+
...derivedUpdates !== void 0 ? { derivedUpdates } : {},
|
|
1926
|
+
...uniqueGuards !== void 0 ? { uniqueGuards } : {},
|
|
1927
|
+
...outboxEvents !== void 0 ? { outboxEvents } : {},
|
|
1928
|
+
...idempotencyGuard !== void 0 ? { idempotencyGuard } : {}
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
function compileSingleFragmentPlan(plan) {
|
|
1932
|
+
if (!isCommandPlan(plan)) {
|
|
1933
|
+
throw new Error(
|
|
1934
|
+
"mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
if (plan.fragments.length !== 1) {
|
|
1938
|
+
throw new Error(
|
|
1939
|
+
`mutation '${plan.name}': compileSingleFragmentPlan expects exactly one fragment, but the plan declares ${plan.fragments.length}. Use compileMutationPlan for the N-fragment atomic merge (issue #90).`
|
|
1940
|
+
);
|
|
1941
|
+
}
|
|
1942
|
+
return compileFragment(plan.fragments[0], 0, null);
|
|
1943
|
+
}
|
|
1944
|
+
var MAX_TRANSACT_COMPOSE_ITEMS = 25;
|
|
1945
|
+
function keySignature(fragment) {
|
|
1946
|
+
const op = fragment.op;
|
|
1947
|
+
const parts = [];
|
|
1948
|
+
if (op.operation === "put") {
|
|
1949
|
+
const item = op.item ?? {};
|
|
1950
|
+
for (const field of [...fragment.keyFields].sort()) {
|
|
1951
|
+
const leaf = item[field];
|
|
1952
|
+
parts.push(`${field}=${keyLeafToken(leaf)}`);
|
|
1953
|
+
}
|
|
1954
|
+
} else {
|
|
1955
|
+
for (const field of [...fragment.keyFields].sort()) {
|
|
1956
|
+
parts.push(`${field}=input:${field}`);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
return `${op.entity.name}#${parts.join("&")}`;
|
|
1960
|
+
}
|
|
1961
|
+
function keyLeafToken(leaf) {
|
|
1962
|
+
if (isContractKeyFieldRef(leaf)) return `input:${leaf.field}`;
|
|
1963
|
+
if (isContractParamRef(leaf)) return `input:${leaf.field}`;
|
|
1964
|
+
return `literal:${JSON.stringify(leaf)}`;
|
|
1965
|
+
}
|
|
1966
|
+
function compileMutationPlan(plan) {
|
|
1967
|
+
if (!isCommandPlan(plan)) {
|
|
1968
|
+
throw new Error(
|
|
1969
|
+
"mutation compiler: expected a `mutation(...)` CommandPlan, but received a non-plan value."
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const fragments = plan.fragments;
|
|
1973
|
+
const compiled = [];
|
|
1974
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
1975
|
+
const resolveEntityRef = (ref, consumerIndex, consumerField) => {
|
|
1976
|
+
if (ref.fragmentIndex >= consumerIndex) {
|
|
1977
|
+
throw new Error(
|
|
1978
|
+
`mutation '${plan.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', a CIRCULAR cross-fragment dependency \u2014 a fragment may only reference a value produced by an EARLIER fragment (a lower index), never itself or a later one. A \`TransactWriteItems\` is atomic and cannot read one item's value while writing another, so the reference graph must be acyclic.`
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
const producer = compiled[ref.fragmentIndex];
|
|
1982
|
+
const resolved = producedFieldLeaf(producer, ref.field);
|
|
1983
|
+
if (resolved === void 0) {
|
|
1984
|
+
throw new Error(
|
|
1985
|
+
`mutation '${plan.name}': fragment #${consumerIndex} binds '${consumerField}' to '${ref.path}', but fragment #${ref.fragmentIndex} ('${producer.op.entity.name}') does not write a field '${ref.field}'. A cross-fragment reference must name a field the producing fragment binds in its \`input\`.`
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
return resolved;
|
|
1989
|
+
};
|
|
1990
|
+
compiled.push(compileFragment(fragments[i], i, resolveEntityRef));
|
|
1991
|
+
}
|
|
1992
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
1993
|
+
for (let i = 0; i < compiled.length; i++) {
|
|
1994
|
+
const sig = keySignature(compiled[i]);
|
|
1995
|
+
const prior = byKey.get(sig);
|
|
1996
|
+
if (prior !== void 0) {
|
|
1997
|
+
throw new Error(
|
|
1998
|
+
`mutation '${plan.name}': fragments #${prior} and #${i} both write the same item ('${compiled[i].op.entity.name}', key ${describeKey(compiled[i])}) in one \`TransactWriteItems\`. DynamoDB rejects a transaction that operates on the same primary key more than once \u2014 split the conflicting writes into separate mutations, or merge them into a single fragment.`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
byKey.set(sig, i);
|
|
2002
|
+
}
|
|
2003
|
+
if (compiled.length > MAX_TRANSACT_COMPOSE_ITEMS) {
|
|
2004
|
+
throw new Error(
|
|
2005
|
+
`mutation '${plan.name}': composes ${compiled.length} write fragments into one \`TransactWriteItems\`, but DynamoDB caps a transaction at ${MAX_TRANSACT_COMPOSE_ITEMS} items and a transaction is atomic \u2014 it cannot be split across requests without breaking atomicity. Reduce the fragment count to \u2264${MAX_TRANSACT_COMPOSE_ITEMS}.`
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
return { name: plan.name, fragments: compiled };
|
|
2009
|
+
}
|
|
2010
|
+
function producedFieldLeaf(producer, field) {
|
|
2011
|
+
const op = producer.op;
|
|
2012
|
+
if (op.operation === "put") {
|
|
2013
|
+
const item = op.item ?? {};
|
|
2014
|
+
if (field in item) {
|
|
2015
|
+
const leaf = item[field];
|
|
2016
|
+
return isContractKeyFieldRef(leaf) ? mintContractParamRef(leaf.field) : leaf;
|
|
2017
|
+
}
|
|
2018
|
+
return void 0;
|
|
2019
|
+
}
|
|
2020
|
+
if (op.operation === "update") {
|
|
2021
|
+
const changes = op.changes ?? {};
|
|
2022
|
+
if (field in changes) return changes[field];
|
|
2023
|
+
}
|
|
2024
|
+
if (producer.keyFields.includes(field)) return mintContractParamRef(field);
|
|
2025
|
+
return void 0;
|
|
2026
|
+
}
|
|
2027
|
+
function describeKey(fragment) {
|
|
2028
|
+
return `[${[...fragment.keyFields].sort().join(", ")}]`;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/spec/guard.ts
|
|
2032
|
+
function conditionInputToSpec(condition, context, renderLeaf) {
|
|
2033
|
+
if (condition === void 0 || condition === null) return void 0;
|
|
2034
|
+
if (typeof condition !== "object") {
|
|
2035
|
+
throw new Error(
|
|
2036
|
+
`${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or field equality).`
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
const obj = condition;
|
|
2040
|
+
if (obj.notExists === true) return { kind: "notExists" };
|
|
2041
|
+
if ("attributeExists" in obj) {
|
|
2042
|
+
if (typeof obj.attributeExists !== "string" || obj.attributeExists.length === 0) {
|
|
2043
|
+
throw new Error(
|
|
2044
|
+
`${context}: \`attributeExists\` must name a non-empty attribute field.`
|
|
2045
|
+
);
|
|
2046
|
+
}
|
|
2047
|
+
return { kind: "attributeExists", field: obj.attributeExists };
|
|
2048
|
+
}
|
|
2049
|
+
if ("attributeNotExists" in obj) {
|
|
2050
|
+
if (typeof obj.attributeNotExists !== "string" || obj.attributeNotExists.length === 0) {
|
|
2051
|
+
throw new Error(
|
|
2052
|
+
`${context}: \`attributeNotExists\` must name a non-empty attribute field.`
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
return { kind: "attributeNotExists", field: obj.attributeNotExists };
|
|
2056
|
+
}
|
|
2057
|
+
const fields = {};
|
|
2058
|
+
for (const key of Object.keys(obj).sort()) {
|
|
2059
|
+
fields[key] = renderLeaf(key, obj[key]);
|
|
2060
|
+
}
|
|
2061
|
+
return { kind: "equals", fields };
|
|
2062
|
+
}
|
|
2063
|
+
function assertSupportedCondition(commandName, condition) {
|
|
2064
|
+
if (condition.kind === "notExists") return;
|
|
2065
|
+
if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
|
|
2066
|
+
if (typeof condition.field !== "string" || condition.field.length === 0) {
|
|
2067
|
+
throw new Error(
|
|
2068
|
+
`Command '${commandName}' has an ${condition.kind} write condition with no attribute field. The Python bridge requires a non-empty field name.`
|
|
2069
|
+
);
|
|
2070
|
+
}
|
|
2071
|
+
return;
|
|
2072
|
+
}
|
|
2073
|
+
if (condition.kind === "equals") {
|
|
2074
|
+
const entries = Object.entries(condition.fields);
|
|
2075
|
+
if (entries.length === 0) {
|
|
2076
|
+
throw new Error(
|
|
2077
|
+
`Command '${commandName}' has an empty equality write condition. The Python bridge supports '{ notExists }' or non-empty field equality conditions only.`
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
for (const [field, value] of entries) {
|
|
2081
|
+
if (typeof value !== "string") {
|
|
2082
|
+
throw new Error(
|
|
2083
|
+
`Command '${commandName}' write condition on '${field}' must be a template string value; the Python bridge supports equality on param/literal values only.`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
return;
|
|
2088
|
+
}
|
|
2089
|
+
throw new Error(
|
|
2090
|
+
`Command '${commandName}' has an unsupported write condition. The Python bridge supports '{ notExists }' or field equality only.`
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
function assertJsonSerializable(value, path = "$") {
|
|
2094
|
+
if (value === null) return;
|
|
2095
|
+
switch (typeof value) {
|
|
2096
|
+
case "string":
|
|
2097
|
+
case "boolean":
|
|
2098
|
+
return;
|
|
2099
|
+
case "number":
|
|
2100
|
+
if (!Number.isFinite(value)) {
|
|
2101
|
+
throw new Error(
|
|
2102
|
+
`Bridge spec is not JSON-serializable: non-finite number at ${path}.`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
return;
|
|
2106
|
+
case "object":
|
|
2107
|
+
break;
|
|
2108
|
+
default:
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`Bridge spec is not JSON-serializable: '${typeof value}' at ${path}.`
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
if (Array.isArray(value)) {
|
|
2114
|
+
value.forEach((v, i) => assertJsonSerializable(v, `${path}[${i}]`));
|
|
2115
|
+
return;
|
|
2116
|
+
}
|
|
2117
|
+
const proto = Object.getPrototypeOf(value);
|
|
2118
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
2119
|
+
throw new Error(
|
|
2120
|
+
`Bridge spec is not JSON-serializable: non-plain object (${value.constructor?.name ?? "unknown"}) at ${path}.`
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
for (const [k, v] of Object.entries(value)) {
|
|
2124
|
+
if (v === void 0) {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
`Bridge spec is not JSON-serializable: 'undefined' value at ${path}.${k}.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
assertJsonSerializable(v, `${path}.${k}`);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
function assertBundleSerializable(bundle) {
|
|
2133
|
+
assertJsonSerializable(bundle.manifest, "$.manifest");
|
|
2134
|
+
assertJsonSerializable(bundle.operations, "$.operations");
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/define/transaction.ts
|
|
2138
|
+
var TX_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:txref");
|
|
2139
|
+
function isTransactionRef(value) {
|
|
2140
|
+
return typeof value === "object" && value !== null && value[TX_REF_BRAND] === true;
|
|
2141
|
+
}
|
|
2142
|
+
var MARKER_TAGS = [
|
|
2143
|
+
// pass 0: short, begins with 'A'.
|
|
2144
|
+
"Axqv",
|
|
2145
|
+
// pass 1: long, begins with 'Z'. The two tags share NO common prefix and NO
|
|
2146
|
+
// common substring of length >= 2 (their alphabets are disjoint), and differ
|
|
2147
|
+
// in length, so any fragment-extracting / length-dependent transform
|
|
2148
|
+
// (`slice(0,n)`, `[i]`, `charAt`, `padStart`, `padEnd`) yields *different*
|
|
2149
|
+
// fragments across the two passes -- the differential check then rejects it.
|
|
2150
|
+
// (Convergence-to-constant escapes that even disjoint markers cannot separate
|
|
2151
|
+
// -- `slice(0,0)` -> '' in both passes, `includes('x')` -> a constant boolean
|
|
2152
|
+
// -- are caught instead by the per-access coercion ledger; see
|
|
2153
|
+
// {@link makeFieldRef} and {@link defineTransaction}.)
|
|
2154
|
+
"Zmnoprstuwy0123456789WIDEMARKERBODYFILLERZ"
|
|
2155
|
+
];
|
|
2156
|
+
var MARKER_PASS_COUNT = MARKER_TAGS.length;
|
|
2157
|
+
function markerValue(pass, field) {
|
|
2158
|
+
const tag = MARKER_TAGS[pass];
|
|
2159
|
+
return `${tag}::${field}::${tag}`;
|
|
2160
|
+
}
|
|
2161
|
+
function makeValueSentinel(fields, tokenFor, originFor, markerFor, containerOrigin, onAccess, onCoerce) {
|
|
2162
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2163
|
+
return new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
2164
|
+
get(_t, key) {
|
|
2165
|
+
if (typeof key === "symbol") {
|
|
2166
|
+
if (key === Symbol.iterator || key === Symbol.asyncIterator || key === Symbol.toStringTag || typeof Symbol.for === "function" && (key === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || key.description === "nodejs.util.inspect.custom")) {
|
|
2167
|
+
return void 0;
|
|
2168
|
+
}
|
|
2169
|
+
throw new Error(
|
|
2170
|
+
`defineTransaction: unsupported symbol access on ${containerOrigin}. Only declarative field references are allowed; any JS transform / branching / coercion on a parameter is not representable as a declarative transaction template.`
|
|
2171
|
+
);
|
|
2172
|
+
}
|
|
2173
|
+
if (!fields.has(key)) {
|
|
2174
|
+
throw new Error(
|
|
2175
|
+
`defineTransaction: ${containerOrigin} has no field '${key}'. Reference only declared fields; arbitrary property access (or any JS transform / branching on a parameter) is not representable as a declarative transaction template.`
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
onAccess(key);
|
|
2179
|
+
let ref = cache.get(key);
|
|
2180
|
+
if (!ref) {
|
|
2181
|
+
ref = makeFieldRef(
|
|
2182
|
+
tokenFor(key),
|
|
2183
|
+
originFor(key),
|
|
2184
|
+
markerFor(key),
|
|
2185
|
+
() => onCoerce(key)
|
|
2186
|
+
);
|
|
2187
|
+
cache.set(key, ref);
|
|
2188
|
+
}
|
|
2189
|
+
return ref;
|
|
2190
|
+
},
|
|
2191
|
+
set() {
|
|
2192
|
+
throw new Error(
|
|
2193
|
+
`defineTransaction: cannot assign to a field of ${containerOrigin}.`
|
|
2194
|
+
);
|
|
2195
|
+
},
|
|
2196
|
+
defineProperty() {
|
|
2197
|
+
throw new Error(
|
|
2198
|
+
`defineTransaction: cannot define a property on ${containerOrigin}.`
|
|
2199
|
+
);
|
|
2200
|
+
},
|
|
2201
|
+
deleteProperty() {
|
|
2202
|
+
throw new Error(
|
|
2203
|
+
`defineTransaction: cannot delete a field of ${containerOrigin}.`
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
function makeFieldRef(token, origin, marker, onCoerce) {
|
|
2209
|
+
const target = /* @__PURE__ */ Object.create(null);
|
|
2210
|
+
const proxy = new Proxy(target, {
|
|
2211
|
+
get(_t, key) {
|
|
2212
|
+
if (key === TX_REF_BRAND) return true;
|
|
2213
|
+
if (key === "token") return token;
|
|
2214
|
+
if (key === "origin") return origin;
|
|
2215
|
+
if (key === Symbol.toPrimitive) {
|
|
2216
|
+
return (hint) => {
|
|
2217
|
+
onCoerce();
|
|
2218
|
+
return hint === "number" ? NaN : marker;
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
if (key === "toString" || key === Symbol.toStringTag) {
|
|
2222
|
+
return () => {
|
|
2223
|
+
onCoerce();
|
|
2224
|
+
return marker;
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2227
|
+
if (key === "valueOf") {
|
|
2228
|
+
return () => {
|
|
2229
|
+
onCoerce();
|
|
2230
|
+
return NaN;
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
if (key === Symbol.iterator || key === Symbol.asyncIterator || key === "then" || key === "constructor" || key === "prototype" || typeof key === "symbol" && typeof Symbol.for === "function" && (key === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || key.description === "nodejs.util.inspect.custom")) {
|
|
2234
|
+
return void 0;
|
|
2235
|
+
}
|
|
2236
|
+
const name = typeof key === "symbol" ? key.toString() : String(key);
|
|
2237
|
+
throw new Error(
|
|
2238
|
+
`defineTransaction: unsupported operation '${name}' on ${origin}. The static planner (Python bridge) only supports direct field references (\`p.field\` / \`el.field\`) or concrete literals at value positions; any string transform, property access (e.g. \`.length\`, \`[i]\`, \`.toUpperCase()\`), arithmetic, or value branching on a parameter is not representable as a declarative transaction template.`
|
|
2239
|
+
);
|
|
2240
|
+
},
|
|
2241
|
+
set() {
|
|
2242
|
+
throw new Error(`defineTransaction: cannot assign on ${origin}.`);
|
|
2243
|
+
},
|
|
2244
|
+
defineProperty() {
|
|
2245
|
+
throw new Error(`defineTransaction: cannot define a property on ${origin}.`);
|
|
2246
|
+
},
|
|
2247
|
+
deleteProperty() {
|
|
2248
|
+
throw new Error(`defineTransaction: cannot delete on ${origin}.`);
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
return proxy;
|
|
2252
|
+
}
|
|
2253
|
+
var when = {
|
|
2254
|
+
eq(left, right) {
|
|
2255
|
+
assertRef(left, "when.eq");
|
|
2256
|
+
return { op: "eq", left, right };
|
|
2257
|
+
},
|
|
2258
|
+
ne(left, right) {
|
|
2259
|
+
assertRef(left, "when.ne");
|
|
2260
|
+
return { op: "ne", left, right };
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
function assertRef(value, context) {
|
|
2264
|
+
if (!isTransactionRef(value)) {
|
|
2265
|
+
throw new Error(
|
|
2266
|
+
`defineTransaction: ${context} expects a field reference (e.g. \`p.status\` or an element field) on its left-hand side.`
|
|
2267
|
+
);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
function entityRef2(model) {
|
|
2271
|
+
const modelClass = resolveModelClass(model);
|
|
2272
|
+
return { name: modelClass.name, modelClass };
|
|
2273
|
+
}
|
|
2274
|
+
function descriptorOf(p) {
|
|
2275
|
+
if (p.kind === "array") {
|
|
2276
|
+
const element = {};
|
|
2277
|
+
for (const [field, fieldParam] of Object.entries(p.element ?? {})) {
|
|
2278
|
+
element[field] = descriptorOf(fieldParam);
|
|
2279
|
+
}
|
|
2280
|
+
return { kind: "array", required: true, element };
|
|
2281
|
+
}
|
|
2282
|
+
return p.literals === void 0 ? { kind: p.kind, required: true } : { kind: p.kind, literals: p.literals, required: true };
|
|
2283
|
+
}
|
|
2284
|
+
function runBuildPass(params, build, scalarFields, arrayParams, pass, accessed, coerced) {
|
|
2285
|
+
const noteAccess = (token, origin) => {
|
|
2286
|
+
if (!accessed.has(token)) accessed.set(token, { token, origin });
|
|
2287
|
+
};
|
|
2288
|
+
const noteCoerce = (token, origin) => {
|
|
2289
|
+
if (!coerced.has(token)) coerced.set(token, { token, origin });
|
|
2290
|
+
};
|
|
2291
|
+
const scalarSentinel = makeValueSentinel(
|
|
2292
|
+
scalarFields,
|
|
2293
|
+
(f) => `{${f}}`,
|
|
2294
|
+
(f) => `param '${f}'`,
|
|
2295
|
+
(f) => markerValue(pass, f),
|
|
2296
|
+
"params",
|
|
2297
|
+
(f) => noteAccess(`{${f}}`, `param '${f}'`),
|
|
2298
|
+
(f) => noteCoerce(`{${f}}`, `param '${f}'`)
|
|
2299
|
+
);
|
|
2300
|
+
const pProxy = new Proxy(/* @__PURE__ */ Object.create(null), {
|
|
2301
|
+
get(_t, key) {
|
|
2302
|
+
if (typeof key === "symbol") {
|
|
2303
|
+
if (key === Symbol.iterator || key === Symbol.asyncIterator || key === Symbol.toStringTag || typeof Symbol.for === "function" && (key === /* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom") || key.description === "nodejs.util.inspect.custom")) {
|
|
2304
|
+
return void 0;
|
|
2305
|
+
}
|
|
2306
|
+
throw new Error("defineTransaction: unsupported symbol access on params.");
|
|
2307
|
+
}
|
|
2308
|
+
if (arrayParams.has(key)) return params[key];
|
|
2309
|
+
if (scalarFields.has(key)) return scalarSentinel[key];
|
|
2310
|
+
throw new Error(
|
|
2311
|
+
`defineTransaction: unknown parameter '${key}'. Declare it in the params map passed to defineTransaction.`
|
|
2312
|
+
);
|
|
2313
|
+
}
|
|
2314
|
+
});
|
|
2315
|
+
const instructions = [];
|
|
2316
|
+
let currentBody = instructions;
|
|
2317
|
+
let insideForEach = false;
|
|
2318
|
+
const recordWrite = (instr) => {
|
|
2319
|
+
currentBody.push(instr);
|
|
2320
|
+
};
|
|
2321
|
+
const recorder3 = {
|
|
2322
|
+
put(model, item, options) {
|
|
2323
|
+
recordWrite({
|
|
2324
|
+
kind: "write",
|
|
2325
|
+
operation: "put",
|
|
2326
|
+
entity: entityRef2(model),
|
|
2327
|
+
item,
|
|
2328
|
+
...options?.condition !== void 0 ? { condition: options.condition } : {},
|
|
2329
|
+
...options?.when !== void 0 ? { when: options.when } : {}
|
|
2330
|
+
});
|
|
2331
|
+
},
|
|
2332
|
+
update(model, key, changes, options) {
|
|
2333
|
+
recordWrite({
|
|
2334
|
+
kind: "write",
|
|
2335
|
+
operation: "update",
|
|
2336
|
+
entity: entityRef2(model),
|
|
2337
|
+
key,
|
|
2338
|
+
changes,
|
|
2339
|
+
...options?.condition !== void 0 ? { condition: options.condition } : {},
|
|
2340
|
+
...options?.when !== void 0 ? { when: options.when } : {}
|
|
2341
|
+
});
|
|
2342
|
+
},
|
|
2343
|
+
delete(model, key, options) {
|
|
2344
|
+
recordWrite({
|
|
2345
|
+
kind: "write",
|
|
2346
|
+
operation: "delete",
|
|
2347
|
+
entity: entityRef2(model),
|
|
2348
|
+
key,
|
|
2349
|
+
...options?.condition !== void 0 ? { condition: options.condition } : {},
|
|
2350
|
+
...options?.when !== void 0 ? { when: options.when } : {}
|
|
2351
|
+
});
|
|
2352
|
+
},
|
|
2353
|
+
conditionCheck(model, key, options) {
|
|
2354
|
+
if (options?.condition === void 0 || options.condition === null) {
|
|
2355
|
+
throw new Error(
|
|
2356
|
+
"defineTransaction: tx.conditionCheck requires a `condition` (the read-only assertion it makes); e.g. `{ condition: { attributeExists: 'PK' } }`."
|
|
2357
|
+
);
|
|
2358
|
+
}
|
|
2359
|
+
recordWrite({
|
|
2360
|
+
kind: "write",
|
|
2361
|
+
operation: "conditionCheck",
|
|
2362
|
+
entity: entityRef2(model),
|
|
2363
|
+
key,
|
|
2364
|
+
condition: options.condition,
|
|
2365
|
+
...options.when !== void 0 ? { when: options.when } : {}
|
|
2366
|
+
});
|
|
2367
|
+
},
|
|
2368
|
+
forEach(source, body, options) {
|
|
2369
|
+
if (insideForEach) {
|
|
2370
|
+
throw new Error(
|
|
2371
|
+
"defineTransaction: nested forEach is not supported; flatten the loop."
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
if (!isParam(source) || source.kind !== "array") {
|
|
2375
|
+
throw new Error(
|
|
2376
|
+
"defineTransaction: forEach source must be a `param.array(...)` parameter accessed via `p.<name>`."
|
|
2377
|
+
);
|
|
2378
|
+
}
|
|
2379
|
+
const arrayParam = source;
|
|
2380
|
+
const sourceName = arrayParams.size ? Object.keys(params).find((k) => params[k] === arrayParam) : void 0;
|
|
2381
|
+
if (sourceName === void 0) {
|
|
2382
|
+
throw new Error(
|
|
2383
|
+
"defineTransaction: forEach source is not a declared array param."
|
|
2384
|
+
);
|
|
2385
|
+
}
|
|
2386
|
+
const elementFields = new Set(Object.keys(arrayParam.element ?? {}));
|
|
2387
|
+
const elementProxy = makeValueSentinel(
|
|
2388
|
+
elementFields,
|
|
2389
|
+
(f) => `{item.${f}}`,
|
|
2390
|
+
(f) => `element field '${f}' of '${sourceName}'`,
|
|
2391
|
+
(f) => markerValue(pass, `item.${f}`),
|
|
2392
|
+
`element of '${sourceName}'`,
|
|
2393
|
+
(f) => noteAccess(`{item.${f}}`, `element field '${f}' of '${sourceName}'`),
|
|
2394
|
+
(f) => noteCoerce(`{item.${f}}`, `element field '${f}' of '${sourceName}'`)
|
|
2395
|
+
);
|
|
2396
|
+
const block = {
|
|
2397
|
+
kind: "forEach",
|
|
2398
|
+
source: sourceName,
|
|
2399
|
+
...options?.when !== void 0 ? { when: options.when } : {},
|
|
2400
|
+
body: []
|
|
2401
|
+
};
|
|
2402
|
+
const prevBody = currentBody;
|
|
2403
|
+
currentBody = block.body;
|
|
2404
|
+
insideForEach = true;
|
|
2405
|
+
try {
|
|
2406
|
+
body(elementProxy);
|
|
2407
|
+
} finally {
|
|
2408
|
+
insideForEach = false;
|
|
2409
|
+
currentBody = prevBody;
|
|
2410
|
+
}
|
|
2411
|
+
currentBody.push(block);
|
|
2412
|
+
}
|
|
2413
|
+
};
|
|
2414
|
+
build(recorder3, pProxy);
|
|
2415
|
+
return instructions;
|
|
2416
|
+
}
|
|
2417
|
+
function defineTransaction(params, build) {
|
|
2418
|
+
const descriptors = {};
|
|
2419
|
+
for (const [name, value] of Object.entries(params)) {
|
|
2420
|
+
if (!isParam(value)) {
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
`defineTransaction: param '${name}' is not a param.* placeholder.`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2425
|
+
descriptors[name] = descriptorOf(value);
|
|
2426
|
+
}
|
|
2427
|
+
const scalarFields = new Set(
|
|
2428
|
+
Object.keys(params).filter((k) => params[k].kind !== "array")
|
|
2429
|
+
);
|
|
2430
|
+
const arrayParams = new Set(
|
|
2431
|
+
Object.keys(params).filter((k) => params[k].kind === "array")
|
|
2432
|
+
);
|
|
2433
|
+
const accessed = /* @__PURE__ */ new Map();
|
|
2434
|
+
const coerced = /* @__PURE__ */ new Map();
|
|
2435
|
+
const passes = [];
|
|
2436
|
+
for (let pass = 0; pass < MARKER_PASS_COUNT; pass++) {
|
|
2437
|
+
passes.push(
|
|
2438
|
+
runBuildPass(
|
|
2439
|
+
params,
|
|
2440
|
+
build,
|
|
2441
|
+
scalarFields,
|
|
2442
|
+
arrayParams,
|
|
2443
|
+
pass,
|
|
2444
|
+
accessed,
|
|
2445
|
+
coerced
|
|
2446
|
+
)
|
|
2447
|
+
);
|
|
2448
|
+
}
|
|
2449
|
+
if (coerced.size > 0) {
|
|
2450
|
+
const info = [...coerced.values()][0];
|
|
2451
|
+
throw new Error(
|
|
2452
|
+
`defineTransaction: ${info.origin} is coerced to a primitive (e.g. via \`String(${info.origin.includes("element") ? "el" : "p"}.x)\`, a template literal \`\${\u2026}\`, string concatenation, or a comparison) and then consumed by a transform or branch \u2014 for example \`String(p.x).slice(0, n)\`, \`String(p.x)[0]\`, \`String(p.x).includes('\u2026')\`, or \`\${p.x}-suffix\`. At a value position the static planner (Python bridge) only supports a *direct* field reference (stored without coercion) or a concrete literal; a coerced / transformed parameter is not representable as a declarative transaction template, and (because the transform may converge to a constant or a shared marker fragment) could otherwise be silently emitted as a wrong spec. Reference the field directly (\`p.x\` / \`el.x\`) or use a literal.`
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
const surfaced = /* @__PURE__ */ new Set();
|
|
2456
|
+
verifyInstructions(passes, surfaced);
|
|
2457
|
+
for (const [token, info] of accessed) {
|
|
2458
|
+
if (!surfaced.has(token)) {
|
|
2459
|
+
throw new Error(
|
|
2460
|
+
`defineTransaction: ${info.origin} is accessed but never appears as a field reference in the recorded transaction. This happens when a parameter is consumed by a value branch (e.g. \`p.role === 'admin' ? 'A' : 'B'\`) or a string transform (e.g. \`String(p.role).toUpperCase()\`) instead of being referenced directly. The static planner (Python bridge) only supports direct field references or concrete literals at value positions; branching or transforming a parameter is not representable as a declarative transaction template.`
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
return {
|
|
2465
|
+
__isTransactionDefinition: true,
|
|
2466
|
+
params: descriptors,
|
|
2467
|
+
instructions: passes[0]
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function verifyInstructions(passes, surfaced) {
|
|
2471
|
+
const first = passes[0];
|
|
2472
|
+
for (let i = 0; i < first.length; i++) {
|
|
2473
|
+
const instrs = passes.map((p) => p[i]);
|
|
2474
|
+
const kind = first[i].kind;
|
|
2475
|
+
if (instrs.some((x) => x.kind !== kind)) {
|
|
2476
|
+
throw new Error(
|
|
2477
|
+
"defineTransaction: build is not deterministic across evaluation (instruction shape diverged between passes). The callback must be a pure, declarative description of the writes."
|
|
2478
|
+
);
|
|
2479
|
+
}
|
|
2480
|
+
if (kind === "write") {
|
|
2481
|
+
verifyWrite(instrs, surfaced);
|
|
2482
|
+
} else {
|
|
2483
|
+
const blocks = instrs;
|
|
2484
|
+
verifyWhen(
|
|
2485
|
+
blocks.map((b) => b.when),
|
|
2486
|
+
surfaced,
|
|
2487
|
+
`forEach '${blocks[0].source}' when`
|
|
2488
|
+
);
|
|
2489
|
+
verifyInstructions(
|
|
2490
|
+
blocks.map((b) => b.body),
|
|
2491
|
+
surfaced
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2495
|
+
}
|
|
2496
|
+
function verifyWrite(instrs, surfaced) {
|
|
2497
|
+
const op = instrs[0].operation;
|
|
2498
|
+
if (instrs.some((x) => x.operation !== op)) {
|
|
2499
|
+
throw new Error(
|
|
2500
|
+
"defineTransaction: write operation diverged between evaluation passes; the callback must be a pure, declarative description of the writes."
|
|
2501
|
+
);
|
|
2502
|
+
}
|
|
2503
|
+
verifyRecord2(instrs.map((x) => x.item), surfaced, `${op} item`);
|
|
2504
|
+
verifyRecord2(instrs.map((x) => x.key), surfaced, `${op} key`);
|
|
2505
|
+
verifyRecord2(instrs.map((x) => x.changes), surfaced, `${op} changes`);
|
|
2506
|
+
verifyRecord2(
|
|
2507
|
+
instrs.map((x) => x.condition),
|
|
2508
|
+
surfaced,
|
|
2509
|
+
`${op} condition`
|
|
2510
|
+
);
|
|
2511
|
+
verifyWhen(instrs.map((x) => x.when), surfaced, `${op} when`);
|
|
2512
|
+
}
|
|
2513
|
+
function verifyRecord2(records, surfaced, context) {
|
|
2514
|
+
const present = records.filter((r) => r !== void 0);
|
|
2515
|
+
if (present.length === 0) return;
|
|
2516
|
+
if (present.length !== records.length) {
|
|
2517
|
+
throw new Error(
|
|
2518
|
+
`defineTransaction: ${context} present in some evaluation passes but not others; the callback must be a pure, declarative description.`
|
|
2519
|
+
);
|
|
2520
|
+
}
|
|
2521
|
+
const keys = Object.keys(present[0]).sort();
|
|
2522
|
+
for (const r of present) {
|
|
2523
|
+
const k = Object.keys(r).sort();
|
|
2524
|
+
if (k.length !== keys.length || k.some((x, i) => x !== keys[i])) {
|
|
2525
|
+
throw new Error(
|
|
2526
|
+
`defineTransaction: ${context} field set diverged between evaluation passes; the callback must be a pure, declarative description.`
|
|
2527
|
+
);
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
for (const field of keys) {
|
|
2531
|
+
verifyLeaf2(
|
|
2532
|
+
present.map((r) => r[field]),
|
|
2533
|
+
surfaced,
|
|
2534
|
+
`${context} field '${field}'`
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
function verifyWhen(whens, surfaced, context) {
|
|
2539
|
+
const present = whens.filter((w) => w !== void 0);
|
|
2540
|
+
if (present.length === 0) return;
|
|
2541
|
+
if (present.length !== whens.length) {
|
|
2542
|
+
throw new Error(
|
|
2543
|
+
`defineTransaction: ${context} present in some evaluation passes but not others; the callback must be a pure, declarative description.`
|
|
2544
|
+
);
|
|
2545
|
+
}
|
|
2546
|
+
for (const w of present) {
|
|
2547
|
+
if (isTransactionRef(w.left)) surfaced.add(w.left.token);
|
|
2548
|
+
}
|
|
2549
|
+
verifyLeaf2(present.map((w) => w.right), surfaced, `${context} right-hand value`);
|
|
2550
|
+
}
|
|
2551
|
+
function verifyLeaf2(values, surfaced, context) {
|
|
2552
|
+
const refTokens = values.map((v) => isTransactionRef(v) ? v.token : void 0);
|
|
2553
|
+
const anyRef = refTokens.some((t) => t !== void 0);
|
|
2554
|
+
if (anyRef) {
|
|
2555
|
+
if (refTokens.some((t) => t === void 0)) {
|
|
2556
|
+
throw new Error(
|
|
2557
|
+
`defineTransaction: ${context} is a field reference in some evaluation passes but a transformed value in others \u2014 a coercion/transform escape (e.g. \`String(p.x).toUpperCase()\`) was applied. Use a direct field reference or a concrete literal.`
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
const token = refTokens[0];
|
|
2561
|
+
if (refTokens.some((t) => t !== token)) {
|
|
2562
|
+
throw new Error(
|
|
2563
|
+
`defineTransaction: ${context} resolves to different field references across evaluation passes \u2014 the value is not a stable, declarative reference.`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
surfaced.add(token);
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
const norm = values.map(normalizeLiteral2);
|
|
2570
|
+
for (let i = 1; i < norm.length; i++) {
|
|
2571
|
+
if (norm[i] !== norm[0]) {
|
|
2572
|
+
throw new Error(
|
|
2573
|
+
`defineTransaction: ${context} is not a stable concrete literal \u2014 evaluating the callback twice produced different values ('${norm[0]}' vs '${norm[i]}'). This means a parameter was coerced or transformed into the value (e.g. \`String(p.x).toUpperCase()\`, \`\${p.x}-suffix\`, or arithmetic). The static planner (Python bridge) only supports a direct field reference or a concrete literal at a value position.`
|
|
2574
|
+
);
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
function normalizeLiteral2(value) {
|
|
2579
|
+
if (value === null) return "t:null";
|
|
2580
|
+
if (value instanceof Date) return `t:date:${value.toISOString()}`;
|
|
2581
|
+
const t = typeof value;
|
|
2582
|
+
if (t === "string") return `t:s:${value}`;
|
|
2583
|
+
if (t === "boolean") return `t:b:${String(value)}`;
|
|
2584
|
+
if (t === "number") {
|
|
2585
|
+
if (Number.isNaN(value)) {
|
|
2586
|
+
throw new Error(
|
|
2587
|
+
"defineTransaction: a value leaf coerced to NaN, which means arithmetic was performed on a parameter. Only direct field references or concrete literals are allowed at value positions."
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
return `t:n:${String(value)}`;
|
|
2591
|
+
}
|
|
2592
|
+
throw new Error(
|
|
2593
|
+
`defineTransaction: a value leaf has unsupported type '${t}'. Only a direct field reference (\`p.field\` / \`el.field\`) or a concrete literal (string / number / boolean / Date) is allowed at a value position.`
|
|
2594
|
+
);
|
|
2595
|
+
}
|
|
2596
|
+
function defineTransactions(definitions) {
|
|
2597
|
+
for (const [name, def] of Object.entries(definitions)) {
|
|
2598
|
+
if (def === null || typeof def !== "object" || def.__isTransactionDefinition !== true) {
|
|
2599
|
+
throw new Error(
|
|
2600
|
+
`defineTransactions: '${name}' is not a transaction definition. Use defineTransaction(...) to build entries.`
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
return definitions;
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2607
|
+
// src/spec/transaction.ts
|
|
2608
|
+
function paramSpec(d) {
|
|
2609
|
+
if (d.kind === "array") {
|
|
2610
|
+
const element = {};
|
|
2611
|
+
for (const field of Object.keys(d.element ?? {}).sort()) {
|
|
2612
|
+
element[field] = paramSpec(d.element[field]);
|
|
2613
|
+
}
|
|
2614
|
+
return { type: "array", required: d.required, element };
|
|
2615
|
+
}
|
|
2616
|
+
return d.literals === void 0 ? { type: d.kind, required: d.required } : {
|
|
2617
|
+
type: d.kind,
|
|
2618
|
+
required: d.required,
|
|
2619
|
+
literals: d.literals
|
|
2620
|
+
};
|
|
2621
|
+
}
|
|
2622
|
+
function transactionParamSpecs(params) {
|
|
2623
|
+
const out = {};
|
|
2624
|
+
for (const name of Object.keys(params).sort()) {
|
|
2625
|
+
out[name] = paramSpec(params[name]);
|
|
2626
|
+
}
|
|
2627
|
+
return out;
|
|
2628
|
+
}
|
|
2629
|
+
function leafTemplate(value, context) {
|
|
2630
|
+
if (isTransactionRef(value)) return value.token;
|
|
2631
|
+
if (value instanceof Date) return value.toISOString();
|
|
2632
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2633
|
+
return String(value);
|
|
2634
|
+
}
|
|
2635
|
+
throw new Error(
|
|
2636
|
+
`defineTransaction: ${context} must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
function templateRecord2(structure, context) {
|
|
2640
|
+
const out = {};
|
|
2641
|
+
for (const field of Object.keys(structure).sort()) {
|
|
2642
|
+
out[field] = leafTemplate(structure[field], `${context} field '${field}'`);
|
|
2643
|
+
}
|
|
2644
|
+
return out;
|
|
2645
|
+
}
|
|
2646
|
+
function tokenName(token) {
|
|
2647
|
+
return token.startsWith("{") && token.endsWith("}") ? token.slice(1, -1) : token;
|
|
2648
|
+
}
|
|
2649
|
+
function keyConditionTemplate2(metadata, keyStructure, context) {
|
|
2650
|
+
if (!metadata.primaryKey) {
|
|
2651
|
+
throw new Error(`defineTransaction: ${context} entity has no primary key.`);
|
|
2652
|
+
}
|
|
2653
|
+
const nameByField = /* @__PURE__ */ new Map();
|
|
2654
|
+
for (const [field, value] of Object.entries(keyStructure)) {
|
|
2655
|
+
if (!isTransactionRef(value)) {
|
|
2656
|
+
throw new Error(
|
|
2657
|
+
`defineTransaction: ${context} key field '${field}' must be a field reference (from \`p\` or a forEach element); a transaction key may not be a computed or literal value.`
|
|
2658
|
+
);
|
|
2659
|
+
}
|
|
2660
|
+
nameByField.set(field, tokenName(value.token));
|
|
2661
|
+
}
|
|
2662
|
+
const present = new Set(Object.keys(keyStructure));
|
|
2663
|
+
const { pk, sk } = evaluateKey(
|
|
2664
|
+
metadata.primaryKey.segmented,
|
|
2665
|
+
"param",
|
|
2666
|
+
present,
|
|
2667
|
+
(f) => nameByField.get(f) ?? f
|
|
2668
|
+
);
|
|
2669
|
+
const out = { PK: pk };
|
|
2670
|
+
if (sk !== void 0) out.SK = sk;
|
|
2671
|
+
return out;
|
|
2672
|
+
}
|
|
2673
|
+
function whenSpec(when2) {
|
|
2674
|
+
const left = when2.left.token;
|
|
2675
|
+
const right = leafTemplate(when2.right, "when right-hand value");
|
|
2676
|
+
return { op: when2.op, left, right };
|
|
2677
|
+
}
|
|
2678
|
+
function conditionSpec(condition, commandName) {
|
|
2679
|
+
const spec = conditionInputToSpec(
|
|
2680
|
+
condition,
|
|
2681
|
+
commandName,
|
|
2682
|
+
(field, value) => leafTemplate(value, `condition field '${field}'`)
|
|
2683
|
+
);
|
|
2684
|
+
if (spec) assertSupportedCondition(commandName, spec);
|
|
2685
|
+
return spec;
|
|
2686
|
+
}
|
|
2687
|
+
var ITEM_TYPE = {
|
|
2688
|
+
put: "Put",
|
|
2689
|
+
update: "Update",
|
|
2690
|
+
delete: "Delete",
|
|
2691
|
+
conditionCheck: "ConditionCheck"
|
|
2692
|
+
};
|
|
2693
|
+
function buildWriteItem(instr, forEachSource, txName) {
|
|
2694
|
+
const metadata = MetadataRegistry.get(instr.entity.modelClass);
|
|
2695
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
2696
|
+
const entity = instr.entity.name;
|
|
2697
|
+
const condition = conditionSpec(instr.condition, `${txName} (${entity})`);
|
|
2698
|
+
const when2 = instr.when ? whenSpec(instr.when) : void 0;
|
|
2699
|
+
const base = {
|
|
2700
|
+
type: ITEM_TYPE[instr.operation],
|
|
2701
|
+
tableName,
|
|
2702
|
+
entity,
|
|
2703
|
+
...condition ? { condition } : {},
|
|
2704
|
+
...when2 ? { when: when2 } : {},
|
|
2705
|
+
...forEachSource ? { forEach: { source: forEachSource } } : {}
|
|
2706
|
+
};
|
|
2707
|
+
if (instr.operation === "put") {
|
|
2708
|
+
return {
|
|
2709
|
+
...base,
|
|
2710
|
+
item: templateRecord2(instr.item ?? {}, `${txName} put`)
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
if (instr.operation === "delete") {
|
|
2714
|
+
return {
|
|
2715
|
+
...base,
|
|
2716
|
+
keyCondition: keyConditionTemplate2(metadata, instr.key ?? {}, `${txName} delete`)
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
if (instr.operation === "conditionCheck") {
|
|
2720
|
+
if (!condition) {
|
|
2721
|
+
throw new Error(
|
|
2722
|
+
`defineTransaction '${txName}': a ConditionCheck on '${entity}' requires a condition (the read-only assertion it makes).`
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
return {
|
|
2726
|
+
...base,
|
|
2727
|
+
keyCondition: keyConditionTemplate2(
|
|
2728
|
+
metadata,
|
|
2729
|
+
instr.key ?? {},
|
|
2730
|
+
`${txName} conditionCheck`
|
|
2731
|
+
)
|
|
2732
|
+
};
|
|
2733
|
+
}
|
|
2734
|
+
return {
|
|
2735
|
+
...base,
|
|
2736
|
+
keyCondition: keyConditionTemplate2(metadata, instr.key ?? {}, `${txName} update`),
|
|
2737
|
+
changes: templateRecord2(instr.changes ?? {}, `${txName} update changes`)
|
|
2738
|
+
};
|
|
2739
|
+
}
|
|
2740
|
+
function buildTransactionSpec(txName, def) {
|
|
2741
|
+
const items = [];
|
|
2742
|
+
let hasForEach = false;
|
|
2743
|
+
for (const instr of def.instructions) {
|
|
2744
|
+
if (instr.kind === "write") {
|
|
2745
|
+
items.push(buildWriteItem(instr, void 0, txName));
|
|
2746
|
+
} else {
|
|
2747
|
+
hasForEach = true;
|
|
2748
|
+
const block = instr;
|
|
2749
|
+
const blockWhen = block.when ? whenSpec(block.when) : void 0;
|
|
2750
|
+
for (const w of block.body) {
|
|
2751
|
+
const item = buildWriteItem(w, block.source, txName);
|
|
2752
|
+
items.push(blockWhen && !item.when ? { ...item, when: blockWhen } : item);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
if (!hasForEach) {
|
|
2757
|
+
if (items.length === 0) {
|
|
2758
|
+
throw new Error(
|
|
2759
|
+
`defineTransaction '${txName}': a transaction must contain at least one write.`
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
2763
|
+
throw new Error(
|
|
2764
|
+
`defineTransaction '${txName}': ${items.length} write items exceed the DynamoDB TransactWriteItems limit of ${MAX_TRANSACT_ITEMS}.`
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
return {
|
|
2768
|
+
params: transactionParamSpecs(def.params),
|
|
2769
|
+
items,
|
|
2770
|
+
maxItems: items.length
|
|
2771
|
+
};
|
|
2772
|
+
}
|
|
2773
|
+
return {
|
|
2774
|
+
params: transactionParamSpecs(def.params),
|
|
2775
|
+
items
|
|
2776
|
+
};
|
|
2777
|
+
}
|
|
2778
|
+
function buildTransactions(transactions = {}) {
|
|
2779
|
+
const out = {};
|
|
2780
|
+
for (const name of Object.keys(transactions).sort()) {
|
|
2781
|
+
out[name] = buildTransactionSpec(name, transactions[name]);
|
|
2782
|
+
}
|
|
2783
|
+
return out;
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// src/spec/contract-n1-check.ts
|
|
2787
|
+
function isQueryMethodSpec(spec) {
|
|
2788
|
+
return typeof spec.resolution === "string" && spec.resolution !== void 0;
|
|
2789
|
+
}
|
|
2790
|
+
function checkArrayIntoRange(contract, method, spec) {
|
|
2791
|
+
if (spec.resolution === "range" && spec.inputArity !== "single") {
|
|
2792
|
+
return {
|
|
2793
|
+
rule: "array-into-range",
|
|
2794
|
+
contract,
|
|
2795
|
+
method,
|
|
2796
|
+
message: `Contract '${contract}.${method}': a \`range\` resolution accepts an array input (inputArity: '${spec.inputArity}'), but a partition \`Query\` cannot be coalesced across partition keys \u2014 feeding N keys would issue N \`Query\`s (an N+1 fan-out). A \`range\` method must be \`inputArity: 'single'\`. Resolve "just these few" with an application-side loop (\`Promise.all(ids.map((id) => contract.method(id)))\`), keeping the N visible at the call site; there is no bounded-fan-out opt-in.`
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
return void 0;
|
|
2800
|
+
}
|
|
2801
|
+
function checkArrayIntoGsiPoint(contract, method, spec, isGsiPoint) {
|
|
2802
|
+
if (spec.resolution === "point" && isGsiPoint && spec.inputArity !== "single") {
|
|
2803
|
+
return {
|
|
2804
|
+
rule: "array-into-gsi-point",
|
|
2805
|
+
contract,
|
|
2806
|
+
method,
|
|
2807
|
+
message: `Contract '${contract}.${method}': a \`point\` read whose Key resolves via a unique GSI accepts an array input (inputArity: '${spec.inputArity}'), but a GSI point read is a per-key \`Query\` \u2014 \`BatchGetItem\` cannot read a GSI \u2014 so feeding N keys would issue N \`Query\`s (an N+1 fan-out). A unique-GSI \`point\` method must be \`inputArity: 'single'\` (only a base-table primary-key point coalesces into one \`BatchGetItem\`). Resolve "just these few" with an application-side loop (\`Promise.all(keys.map((k) => contract.method(k)))\`), keeping the N visible at the call site; there is no bounded-fan-out opt-in.`
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
return void 0;
|
|
2811
|
+
}
|
|
2812
|
+
function checkComposeChild(contract, method, parentCardinality, node) {
|
|
2813
|
+
const childResolution = node.resolution;
|
|
2814
|
+
if (childResolution !== "range") return void 0;
|
|
2815
|
+
if (parentCardinality === "many") {
|
|
2816
|
+
return {
|
|
2817
|
+
rule: "list-under-list",
|
|
2818
|
+
contract,
|
|
2819
|
+
method,
|
|
2820
|
+
message: `Contract '${contract}.${method}': composed child '${node.as}' is a \`range\` read nested under a parent step whose per-key cardinality is 'many' \u2014 a "list under a list". The parent yields N records and each drives its own partition \`Query\`, an N+1 fan-out, which is rejected at build time. A composed child under a many-yielding parent must be \`point\` (it coalesces to one \`BatchGetItem\`); resolve a true to-many with an application-side loop instead.`
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
return {
|
|
2824
|
+
rule: "compose-range-under-many",
|
|
2825
|
+
contract,
|
|
2826
|
+
method,
|
|
2827
|
+
message: `Contract '${contract}.${method}': composed child '${node.as}' is a \`range\` read (a partition \`Query\`). An External Query / composition child must be \`point\` so it coalesces to one \`BatchGetItem\` across all parent keys; a \`range\` child cannot be batched across the parent's keys and is an N+1 fan-out. Expose the to-many as a single-key \`range\` method and resolve it with an application-side loop, or make the child a \`point\` contract.`
|
|
2828
|
+
};
|
|
2829
|
+
}
|
|
2830
|
+
function collectContractN1Violations(contractName, spec, isGsiPoint = () => false) {
|
|
2831
|
+
const violations = [];
|
|
2832
|
+
if (spec.kind !== "query") return violations;
|
|
2833
|
+
for (const methodName of Object.keys(spec.methods)) {
|
|
2834
|
+
const methodSpec = spec.methods[methodName];
|
|
2835
|
+
if (!isQueryMethodSpec(methodSpec)) continue;
|
|
2836
|
+
const arrayIntoRange = checkArrayIntoRange(contractName, methodName, methodSpec);
|
|
2837
|
+
if (arrayIntoRange) violations.push(arrayIntoRange);
|
|
2838
|
+
const arrayIntoGsiPoint = checkArrayIntoGsiPoint(
|
|
2839
|
+
contractName,
|
|
2840
|
+
methodName,
|
|
2841
|
+
methodSpec,
|
|
2842
|
+
isGsiPoint(methodName)
|
|
2843
|
+
);
|
|
2844
|
+
if (arrayIntoGsiPoint) violations.push(arrayIntoGsiPoint);
|
|
2845
|
+
const parentCardinality = methodSpec.cardinality;
|
|
2846
|
+
for (const child of methodSpec.compose ?? []) {
|
|
2847
|
+
const violation = checkComposeChild(
|
|
2848
|
+
contractName,
|
|
2849
|
+
methodName,
|
|
2850
|
+
parentCardinality,
|
|
2851
|
+
child
|
|
2852
|
+
);
|
|
2853
|
+
if (violation) violations.push(violation);
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return violations;
|
|
2857
|
+
}
|
|
2858
|
+
function assertContractN1Safe(contractName, spec, isGsiPoint) {
|
|
2859
|
+
const violations = collectContractN1Violations(contractName, spec, isGsiPoint);
|
|
2860
|
+
if (violations.length === 0) return;
|
|
2861
|
+
const [first, ...rest] = violations;
|
|
2862
|
+
const suffix = rest.length > 0 ? `
|
|
2863
|
+
|
|
2864
|
+
(${rest.length} further N+1 violation${rest.length === 1 ? "" : "s"} in this contract:
|
|
2865
|
+
${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
|
|
2866
|
+
throw new Error(first.message + suffix);
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
// src/relation/concurrency.ts
|
|
2870
|
+
var RELATION_TRAVERSAL_CONCURRENCY = 16;
|
|
2871
|
+
async function mapWithConcurrency(items, limit, worker) {
|
|
2872
|
+
if (items.length === 0) {
|
|
2873
|
+
return [];
|
|
2874
|
+
}
|
|
2875
|
+
const effectiveLimit = Math.max(1, Math.min(limit, items.length));
|
|
2876
|
+
if (effectiveLimit >= items.length) {
|
|
2877
|
+
return Promise.all(items.map((item, index) => worker(item, index)));
|
|
2878
|
+
}
|
|
2879
|
+
const results = new Array(items.length);
|
|
2880
|
+
let nextIndex = 0;
|
|
2881
|
+
async function runWorker() {
|
|
2882
|
+
while (true) {
|
|
2883
|
+
const index = nextIndex++;
|
|
2884
|
+
if (index >= items.length) {
|
|
2885
|
+
return;
|
|
2886
|
+
}
|
|
2887
|
+
results[index] = await worker(items[index], index);
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
const runners = [];
|
|
2891
|
+
for (let i = 0; i < effectiveLimit; i++) {
|
|
2892
|
+
runners.push(runWorker());
|
|
2893
|
+
}
|
|
2894
|
+
await Promise.all(runners);
|
|
2895
|
+
return results;
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
// src/relation/execution-plan.ts
|
|
2899
|
+
function relationResultPaths(select, metadata, parentPath = "$") {
|
|
2900
|
+
const out = [];
|
|
2901
|
+
const relations = detectRelationFields(select, metadata);
|
|
2902
|
+
for (const rel of [...relations].sort(
|
|
2903
|
+
(a, b) => a.propertyName.localeCompare(b.propertyName)
|
|
2904
|
+
)) {
|
|
2905
|
+
const childSelect = normalizeSelectSpec(select[rel.propertyName]).select ?? {};
|
|
2906
|
+
const targetMeta = MetadataRegistry.get(rel.targetFactory());
|
|
2907
|
+
const childPath = `${parentPath}.${rel.propertyName}`;
|
|
2908
|
+
const isMany = rel.type === "hasMany";
|
|
2909
|
+
const resultPath = isMany ? `${childPath}.items` : childPath;
|
|
2910
|
+
out.push({
|
|
2911
|
+
propertyName: rel.propertyName,
|
|
2912
|
+
parentPath,
|
|
2913
|
+
resultPath,
|
|
2914
|
+
isMany
|
|
2915
|
+
});
|
|
2916
|
+
out.push(...relationResultPaths(childSelect, targetMeta, resultPath));
|
|
2917
|
+
}
|
|
2918
|
+
return out;
|
|
2919
|
+
}
|
|
2920
|
+
function parentResultPath(resultPath) {
|
|
2921
|
+
if (!resultPath.startsWith("$.")) return "$";
|
|
2922
|
+
const tokens = resultPath.slice(2).split(".");
|
|
2923
|
+
if (tokens[tokens.length - 1] === "items") tokens.pop();
|
|
2924
|
+
tokens.pop();
|
|
2925
|
+
return tokens.length === 0 ? "$" : `$.${tokens.join(".")}`;
|
|
2926
|
+
}
|
|
2927
|
+
function deriveExecutionPlan(resultPaths) {
|
|
2928
|
+
if (resultPaths.length <= 1) return void 0;
|
|
2929
|
+
const indexByPath = /* @__PURE__ */ new Map();
|
|
2930
|
+
resultPaths.forEach((path, i) => {
|
|
2931
|
+
indexByPath.set(path, i);
|
|
2932
|
+
});
|
|
2933
|
+
const stageOf = new Array(resultPaths.length);
|
|
2934
|
+
for (let i = 0; i < resultPaths.length; i++) {
|
|
2935
|
+
if (resultPaths[i] === "$") {
|
|
2936
|
+
stageOf[i] = 0;
|
|
2937
|
+
continue;
|
|
2938
|
+
}
|
|
2939
|
+
const parentPath = parentResultPath(resultPaths[i]);
|
|
2940
|
+
const parentIndex = indexByPath.get(parentPath);
|
|
2941
|
+
const parentStage = parentIndex !== void 0 ? stageOf[parentIndex] : 0;
|
|
2942
|
+
stageOf[i] = parentStage + 1;
|
|
2943
|
+
}
|
|
2944
|
+
const stageCount = Math.max(...stageOf) + 1;
|
|
2945
|
+
const groups = Array.from({ length: stageCount }, () => []);
|
|
2946
|
+
for (let i = 0; i < resultPaths.length; i++) {
|
|
2947
|
+
groups[stageOf[i]].push(i);
|
|
2948
|
+
}
|
|
2949
|
+
for (const g of groups) g.sort((a, b) => a - b);
|
|
2950
|
+
return { groups, concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
2951
|
+
}
|
|
2952
|
+
function buildRelationExecutionPlan(select, metadata) {
|
|
2953
|
+
const relationPaths = relationResultPaths(select, metadata);
|
|
2954
|
+
if (relationPaths.length === 0) return void 0;
|
|
2955
|
+
const resultPaths = ["$", ...relationPaths.map((r) => r.resultPath)];
|
|
2956
|
+
const plan = deriveExecutionPlan(resultPaths);
|
|
2957
|
+
if (plan === void 0) return void 0;
|
|
2958
|
+
return { plan, resultPaths };
|
|
2959
|
+
}
|
|
2960
|
+
function deriveCompositionPlan(composeCount) {
|
|
2961
|
+
if (composeCount <= 0) {
|
|
2962
|
+
return { stages: [], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
2963
|
+
}
|
|
2964
|
+
const stage = Array.from({ length: composeCount }, (_unused, i) => i);
|
|
2965
|
+
return { stages: [stage], concurrency: RELATION_TRAVERSAL_CONCURRENCY };
|
|
2966
|
+
}
|
|
2967
|
+
function stageOfResultPath(resolved, resultPath) {
|
|
2968
|
+
const index = resolved.resultPaths.indexOf(resultPath);
|
|
2969
|
+
if (index < 0) return void 0;
|
|
2970
|
+
for (let s = 0; s < resolved.plan.groups.length; s++) {
|
|
2971
|
+
if (resolved.plan.groups[s].includes(index)) return s;
|
|
2972
|
+
}
|
|
2973
|
+
return void 0;
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
// src/spec/contract-boundary-check.ts
|
|
2977
|
+
function buildOwnershipIndex(contexts) {
|
|
2978
|
+
const modelOwner = /* @__PURE__ */ new Map();
|
|
2979
|
+
const contractOwner = /* @__PURE__ */ new Map();
|
|
2980
|
+
const claim = (owner, kind, name, context) => {
|
|
2981
|
+
const existing = owner.get(name);
|
|
2982
|
+
if (existing !== void 0 && existing !== context) {
|
|
2983
|
+
throw new Error(
|
|
2984
|
+
`Context-ownership declaration is ambiguous: ${kind} '${name}' is claimed by both context '${existing}' and context '${context}'. A ${kind} belongs to exactly one bounded context \u2014 remove it from one of the \`${kind === "Model" ? "models" : "contracts"}\` lists.`
|
|
2985
|
+
);
|
|
2986
|
+
}
|
|
2987
|
+
owner.set(name, context);
|
|
2988
|
+
};
|
|
2989
|
+
for (const context of Object.keys(contexts).sort()) {
|
|
2990
|
+
const membership = contexts[context];
|
|
2991
|
+
for (const model of membership.models ?? []) claim(modelOwner, "Model", model, context);
|
|
2992
|
+
for (const contract of membership.contracts ?? [])
|
|
2993
|
+
claim(contractOwner, "Contract", contract, context);
|
|
2994
|
+
}
|
|
2995
|
+
return { modelOwner, contractOwner };
|
|
2996
|
+
}
|
|
2997
|
+
function methodOpsOf(contract) {
|
|
2998
|
+
if (isQueryModelContract(contract) || isCommandModelContract(contract)) {
|
|
2999
|
+
return Object.keys(contract.methods).sort().map((name) => [name, contract.methods[name].op]);
|
|
3000
|
+
}
|
|
3001
|
+
return [];
|
|
3002
|
+
}
|
|
3003
|
+
function collectContractBoundaryViolations(contracts, contexts) {
|
|
3004
|
+
const violations = [];
|
|
3005
|
+
const { modelOwner, contractOwner } = buildOwnershipIndex(contexts);
|
|
3006
|
+
for (const contractName of Object.keys(contracts).sort()) {
|
|
3007
|
+
const contractContext = contractOwner.get(contractName);
|
|
3008
|
+
if (contractContext === void 0) continue;
|
|
3009
|
+
const contract = contracts[contractName];
|
|
3010
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3011
|
+
for (const [method, op] of methodOpsOf(contract)) {
|
|
3012
|
+
const model = op.entity.name;
|
|
3013
|
+
const foreignContext = modelOwner.get(model);
|
|
3014
|
+
if (foreignContext === void 0) continue;
|
|
3015
|
+
if (foreignContext === contractContext) continue;
|
|
3016
|
+
const dedupeKey = `${method}\0${model}`;
|
|
3017
|
+
if (seen.has(dedupeKey)) continue;
|
|
3018
|
+
seen.add(dedupeKey);
|
|
3019
|
+
violations.push({
|
|
3020
|
+
contract: contractName,
|
|
3021
|
+
contractContext,
|
|
3022
|
+
method,
|
|
3023
|
+
foreignModel: model,
|
|
3024
|
+
foreignContext,
|
|
3025
|
+
message: `Contract '${contractName}.${method}' (context '${contractContext}') directly references Model '${model}', which is private to context '${foreignContext}'. A Model is the private implementation of its bounded context; crossing a context boundary is allowed only through the other context's published Contract (an External Query / Command), never its Models. Depend on context '${foreignContext}''s published Contract for '${model}' instead of reaching into the Model directly.`
|
|
3026
|
+
});
|
|
3027
|
+
}
|
|
3028
|
+
}
|
|
3029
|
+
return violations;
|
|
3030
|
+
}
|
|
3031
|
+
function assertContractBoundaries(contracts, contexts) {
|
|
3032
|
+
const violations = collectContractBoundaryViolations(contracts, contexts);
|
|
3033
|
+
if (violations.length === 0) return;
|
|
3034
|
+
const [first, ...rest] = violations;
|
|
3035
|
+
const suffix = rest.length > 0 ? `
|
|
3036
|
+
|
|
3037
|
+
(${rest.length} further context-boundary violation${rest.length === 1 ? "" : "s"}:
|
|
3038
|
+
${rest.map((v) => ` - ${v.message}`).join("\n")})` : "";
|
|
3039
|
+
throw new Error(first.message + suffix);
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// src/spec/operations.ts
|
|
3043
|
+
function paramSpecs(params) {
|
|
3044
|
+
const out = {};
|
|
3045
|
+
for (const name of Object.keys(params).sort()) {
|
|
3046
|
+
const d = params[name];
|
|
3047
|
+
out[name] = d.literals === void 0 ? { type: d.kind, required: d.required } : {
|
|
3048
|
+
type: d.kind,
|
|
3049
|
+
required: d.required,
|
|
3050
|
+
literals: d.literals
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
return out;
|
|
3054
|
+
}
|
|
3055
|
+
function metaFor(def) {
|
|
3056
|
+
return MetadataRegistry.get(def.entity.modelClass);
|
|
3057
|
+
}
|
|
3058
|
+
function templateLeaf(key, value) {
|
|
3059
|
+
if (isContractParamRef(value)) return value.token;
|
|
3060
|
+
if (isContractKeyFieldRef(value)) return `{${value.field}}`;
|
|
3061
|
+
if (isParam(value)) return `{${key}}`;
|
|
3062
|
+
if (value instanceof Date) return value.toISOString();
|
|
3063
|
+
return String(value);
|
|
3064
|
+
}
|
|
3065
|
+
function keyFieldNames(key) {
|
|
3066
|
+
if (key === null || typeof key !== "object") return [];
|
|
3067
|
+
return Object.keys(key);
|
|
3068
|
+
}
|
|
3069
|
+
function buildReadOperations(def) {
|
|
3070
|
+
const metadata = metaFor(def);
|
|
3071
|
+
const fields = keyFieldNames(def.key);
|
|
3072
|
+
const resolved = resolveKey(fields, metadata);
|
|
3073
|
+
const select = def.select ?? {};
|
|
3074
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
3075
|
+
const present = new Set(fields);
|
|
3076
|
+
let type;
|
|
3077
|
+
let indexName;
|
|
3078
|
+
let keyCondition = {};
|
|
3079
|
+
let rangeCondition;
|
|
3080
|
+
if (resolved.type === "pk") {
|
|
3081
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
3082
|
+
const { pk, sk } = evaluateKey(
|
|
3083
|
+
metadata.primaryKey.segmented,
|
|
3084
|
+
"param",
|
|
3085
|
+
present,
|
|
3086
|
+
void 0,
|
|
3087
|
+
resolved.partial
|
|
3088
|
+
);
|
|
3089
|
+
if (resolved.partial) {
|
|
3090
|
+
type = "Query";
|
|
3091
|
+
keyCondition = { PK: pk };
|
|
3092
|
+
if (sk !== void 0 && sk !== "") {
|
|
3093
|
+
rangeCondition = { operator: "begins_with", key: "SK", value: sk };
|
|
3094
|
+
}
|
|
3095
|
+
} else if (sk !== void 0) {
|
|
3096
|
+
type = "GetItem";
|
|
3097
|
+
keyCondition = { PK: pk, SK: sk };
|
|
3098
|
+
} else {
|
|
3099
|
+
type = "Query";
|
|
3100
|
+
keyCondition = { PK: pk };
|
|
3101
|
+
}
|
|
3102
|
+
} else {
|
|
3103
|
+
const gsi = metadata.gsiDefinitions.find(
|
|
3104
|
+
(g) => g.indexName === resolved.indexName
|
|
3105
|
+
);
|
|
3106
|
+
if (!gsi) throw new Error(`GSI '${resolved.indexName}' not found`);
|
|
3107
|
+
const { pk, sk } = evaluateKey(
|
|
3108
|
+
gsi.segmented,
|
|
3109
|
+
"param",
|
|
3110
|
+
present,
|
|
3111
|
+
void 0,
|
|
3112
|
+
resolved.partial
|
|
3113
|
+
);
|
|
3114
|
+
type = "Query";
|
|
3115
|
+
indexName = gsi.indexName;
|
|
3116
|
+
keyCondition = { [`${gsi.indexName}PK`]: pk };
|
|
3117
|
+
if (resolved.partial) {
|
|
3118
|
+
if (sk !== void 0 && sk !== "") {
|
|
3119
|
+
rangeCondition = {
|
|
3120
|
+
operator: "begins_with",
|
|
3121
|
+
key: `${gsi.indexName}SK`,
|
|
3122
|
+
value: sk
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
} else if (sk !== void 0 && sk !== "") {
|
|
3126
|
+
keyCondition[`${gsi.indexName}SK`] = sk;
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
const projection = projectionFields(select);
|
|
3130
|
+
const root = {
|
|
3131
|
+
type,
|
|
3132
|
+
tableName,
|
|
3133
|
+
...indexName ? { indexName } : {},
|
|
3134
|
+
keyCondition,
|
|
3135
|
+
...rangeCondition ? { rangeCondition } : {},
|
|
3136
|
+
projection,
|
|
3137
|
+
resultPath: "$"
|
|
3138
|
+
};
|
|
3139
|
+
const ops = [root];
|
|
3140
|
+
ops.push(...buildRelationOperations(metadata, select, "$"));
|
|
3141
|
+
return ops;
|
|
3142
|
+
}
|
|
3143
|
+
function projectionFields(select) {
|
|
3144
|
+
const out = [];
|
|
3145
|
+
for (const [field, value] of Object.entries(select)) {
|
|
3146
|
+
if (value === true) out.push(field);
|
|
3147
|
+
}
|
|
3148
|
+
return out.sort();
|
|
3149
|
+
}
|
|
3150
|
+
function buildRelationOperations(metadata, select, parentPath) {
|
|
3151
|
+
const ops = [];
|
|
3152
|
+
const relations = detectRelationFields(select, metadata);
|
|
3153
|
+
for (const rel of [...relations].sort(
|
|
3154
|
+
(a, b) => a.propertyName.localeCompare(b.propertyName)
|
|
3155
|
+
)) {
|
|
3156
|
+
const spec = normalizeSelectSpec(select[rel.propertyName]);
|
|
3157
|
+
const childSelect = spec.select ?? {};
|
|
3158
|
+
const targetClass = rel.targetFactory();
|
|
3159
|
+
const targetMeta = MetadataRegistry.get(targetClass);
|
|
3160
|
+
const childPath = `${parentPath}.${rel.propertyName}`;
|
|
3161
|
+
if (rel.type === "hasMany") {
|
|
3162
|
+
const limit = spec.limit ?? rel.options?.limit?.default ?? 20;
|
|
3163
|
+
ops.push(
|
|
3164
|
+
buildHasManyOperation(rel, targetMeta, childSelect, limit, spec.filter, `${childPath}.items`)
|
|
3165
|
+
);
|
|
3166
|
+
ops.push(...buildRelationOperations(targetMeta, childSelect, `${childPath}.items`));
|
|
3167
|
+
} else {
|
|
3168
|
+
ops.push(
|
|
3169
|
+
buildBatchGetOperation(rel, targetMeta, childSelect, childPath)
|
|
3170
|
+
);
|
|
3171
|
+
ops.push(...buildRelationOperations(targetMeta, childSelect, childPath));
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
return ops;
|
|
3175
|
+
}
|
|
3176
|
+
function singleSourceField(rel) {
|
|
3177
|
+
const entries = Object.entries(rel.keyBinding);
|
|
3178
|
+
if (entries.length !== 1) {
|
|
3179
|
+
throw new Error(
|
|
3180
|
+
`Relation '${rel.propertyName}' binds ${entries.length} fields; the static planner currently supports single-field relation key bindings only.`
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
const [targetField, sourceField] = entries[0];
|
|
3184
|
+
return { targetField, sourceField };
|
|
3185
|
+
}
|
|
3186
|
+
function buildHasManyOperation(rel, targetMeta, select, limit, filter, resultPath) {
|
|
3187
|
+
const { targetField, sourceField } = singleSourceField(rel);
|
|
3188
|
+
const resolved = resolveKey([targetField], targetMeta);
|
|
3189
|
+
const tableName = TableMapping.resolve(targetMeta.tableName);
|
|
3190
|
+
const nameOf = (f) => f === targetField ? sourceField : f;
|
|
3191
|
+
const present = /* @__PURE__ */ new Set([targetField]);
|
|
3192
|
+
const op = (() => {
|
|
3193
|
+
if (resolved.type === "pk") {
|
|
3194
|
+
if (!targetMeta.primaryKey) throw new Error("Primary key not defined");
|
|
3195
|
+
const { pk: pk2, sk: sk2 } = evaluateKey(
|
|
3196
|
+
targetMeta.primaryKey.segmented,
|
|
3197
|
+
"result",
|
|
3198
|
+
present,
|
|
3199
|
+
nameOf,
|
|
3200
|
+
resolved.partial
|
|
3201
|
+
);
|
|
3202
|
+
const keyCondition2 = {
|
|
3203
|
+
PK: pk2
|
|
3204
|
+
};
|
|
3205
|
+
const range3 = sk2 !== void 0 && sk2 !== "" ? { operator: "begins_with", key: "SK", value: sk2 } : void 0;
|
|
3206
|
+
return {
|
|
3207
|
+
type: "Query",
|
|
3208
|
+
tableName,
|
|
3209
|
+
keyCondition: keyCondition2,
|
|
3210
|
+
...range3 ? { rangeCondition: range3 } : {},
|
|
3211
|
+
projection: projectionFields(select),
|
|
3212
|
+
limit,
|
|
3213
|
+
resultPath,
|
|
3214
|
+
sourceField
|
|
3215
|
+
};
|
|
3216
|
+
}
|
|
3217
|
+
const gsi = targetMeta.gsiDefinitions.find((g) => g.indexName === resolved.indexName);
|
|
3218
|
+
const { pk, sk } = evaluateKey(
|
|
3219
|
+
gsi.segmented,
|
|
3220
|
+
"result",
|
|
3221
|
+
present,
|
|
3222
|
+
nameOf,
|
|
3223
|
+
resolved.partial
|
|
3224
|
+
);
|
|
3225
|
+
const keyCondition = { [`${gsi.indexName}PK`]: pk };
|
|
3226
|
+
const range2 = sk !== void 0 && sk !== "" ? { operator: "begins_with", key: `${gsi.indexName}SK`, value: sk } : void 0;
|
|
3227
|
+
return {
|
|
3228
|
+
type: "Query",
|
|
3229
|
+
tableName,
|
|
3230
|
+
indexName: gsi.indexName,
|
|
3231
|
+
keyCondition,
|
|
3232
|
+
...range2 ? { rangeCondition: range2 } : {},
|
|
3233
|
+
projection: projectionFields(select),
|
|
3234
|
+
limit,
|
|
3235
|
+
resultPath,
|
|
3236
|
+
sourceField
|
|
3237
|
+
};
|
|
3238
|
+
})();
|
|
3239
|
+
return filter ? { ...op, filter: filterSpec(filter) } : op;
|
|
3240
|
+
}
|
|
3241
|
+
function buildBatchGetOperation(rel, targetMeta, select, resultPath) {
|
|
3242
|
+
const { targetField, sourceField } = singleSourceField(rel);
|
|
3243
|
+
const resolved = resolveKey([targetField], targetMeta);
|
|
3244
|
+
if (resolved.type !== "pk" || resolved.partial) {
|
|
3245
|
+
throw new Error(
|
|
3246
|
+
`Relation '${rel.propertyName}' (${rel.type}) must resolve to a full primary key (BatchGetItem); got ${resolved.type}${resolved.partial ? " (partial)" : ""}.`
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
3249
|
+
if (!targetMeta.primaryKey) throw new Error("Primary key not defined");
|
|
3250
|
+
const nameOf = (f) => f === targetField ? sourceField : f;
|
|
3251
|
+
const { pk, sk } = evaluateKey(
|
|
3252
|
+
targetMeta.primaryKey.segmented,
|
|
3253
|
+
"result",
|
|
3254
|
+
/* @__PURE__ */ new Set([targetField]),
|
|
3255
|
+
nameOf
|
|
3256
|
+
);
|
|
3257
|
+
const keyCondition = {
|
|
3258
|
+
PK: pk
|
|
3259
|
+
};
|
|
3260
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
3261
|
+
return {
|
|
3262
|
+
type: "BatchGetItem",
|
|
3263
|
+
tableName: TableMapping.resolve(targetMeta.tableName),
|
|
3264
|
+
keyCondition,
|
|
3265
|
+
projection: projectionFields(select),
|
|
3266
|
+
resultPath,
|
|
3267
|
+
sourceField
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
function filterSpec(filter) {
|
|
3271
|
+
return { declarative: filter };
|
|
3272
|
+
}
|
|
3273
|
+
function buildCommandSpec(def) {
|
|
3274
|
+
const metadata = metaFor(def);
|
|
3275
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
3276
|
+
const params = paramSpecs(def.params);
|
|
3277
|
+
const entity = def.entity.name;
|
|
3278
|
+
const condition = extractCondition(def);
|
|
3279
|
+
if (def.operation === "put") {
|
|
3280
|
+
const item = templateRecord3(def.key);
|
|
3281
|
+
return {
|
|
3282
|
+
type: "PutItem",
|
|
3283
|
+
tableName,
|
|
3284
|
+
entity,
|
|
3285
|
+
params,
|
|
3286
|
+
item,
|
|
3287
|
+
...condition ? { condition } : {}
|
|
3288
|
+
};
|
|
3289
|
+
}
|
|
3290
|
+
if (def.operation === "delete") {
|
|
3291
|
+
return {
|
|
3292
|
+
type: "DeleteItem",
|
|
3293
|
+
tableName,
|
|
3294
|
+
entity,
|
|
3295
|
+
params,
|
|
3296
|
+
keyCondition: writeKeyCondition(metadata, def.key, entity),
|
|
3297
|
+
...condition ? { condition } : {}
|
|
3298
|
+
};
|
|
3299
|
+
}
|
|
3300
|
+
return {
|
|
3301
|
+
type: "UpdateItem",
|
|
3302
|
+
tableName,
|
|
3303
|
+
entity,
|
|
3304
|
+
params,
|
|
3305
|
+
keyCondition: writeKeyCondition(metadata, def.key, entity),
|
|
3306
|
+
changes: templateRecord3(def.changes),
|
|
3307
|
+
...condition ? { condition } : {}
|
|
3308
|
+
};
|
|
3309
|
+
}
|
|
3310
|
+
function writeKeyCondition(metadata, key, entity) {
|
|
3311
|
+
if (!metadata.primaryKey) throw new Error("Primary key not defined");
|
|
3312
|
+
const fields = Object.keys(key);
|
|
3313
|
+
const resolved = resolveKey(fields, metadata);
|
|
3314
|
+
if (resolved.type === "gsi") {
|
|
3315
|
+
throw new Error(
|
|
3316
|
+
`Command on '${entity}': the contract Key fields ${JSON.stringify(fields)} resolve to the GSI '${resolved.indexName}', but a DynamoDB write (\`UpdateItem\` / \`DeleteItem\`) can target an item only by its base-table primary key \u2014 there is no GSI-keyed write. A command keyed by a secondary index is not expressible. Key the command by the model's primary key (e.g. read the item by its GSI first, then write by the resolved primary key), or model the write against the base-table PK Key fields.`
|
|
3317
|
+
);
|
|
3318
|
+
}
|
|
3319
|
+
const present = new Set(fields);
|
|
3320
|
+
const { pk, sk } = evaluateKey(
|
|
3321
|
+
metadata.primaryKey.segmented,
|
|
3322
|
+
"param",
|
|
3323
|
+
present
|
|
3324
|
+
);
|
|
3325
|
+
const out = { PK: pk };
|
|
3326
|
+
if (sk !== void 0) out.SK = sk;
|
|
3327
|
+
return out;
|
|
3328
|
+
}
|
|
3329
|
+
function templateRecord3(structure) {
|
|
3330
|
+
const out = {};
|
|
3331
|
+
for (const key of Object.keys(structure).sort()) {
|
|
3332
|
+
out[key] = templateLeaf(key, structure[key]);
|
|
3333
|
+
}
|
|
3334
|
+
return out;
|
|
3335
|
+
}
|
|
3336
|
+
function extractCondition(def) {
|
|
3337
|
+
return conditionInputToSpec(
|
|
3338
|
+
def.condition,
|
|
3339
|
+
`Command on '${def.entity.name}'`,
|
|
3340
|
+
(field, value) => templateLeaf(field, value)
|
|
3341
|
+
);
|
|
3342
|
+
}
|
|
3343
|
+
function buildQuerySpec(def) {
|
|
3344
|
+
const operations = buildReadOperations(def);
|
|
3345
|
+
const executionPlan = deriveExecutionPlan(operations.map((op) => op.resultPath));
|
|
3346
|
+
return {
|
|
3347
|
+
params: paramSpecs(def.params),
|
|
3348
|
+
operations,
|
|
3349
|
+
// `defineQuery` → a single entity object; `defineList` → a connection.
|
|
3350
|
+
cardinality: def.operation === "query" ? "one" : "many",
|
|
3351
|
+
...executionPlan !== void 0 ? { executionPlan } : {}
|
|
3352
|
+
};
|
|
3353
|
+
}
|
|
3354
|
+
function mergeSpecs(base, synthesized, kind) {
|
|
3355
|
+
for (const name of Object.keys(synthesized)) {
|
|
3356
|
+
if (Object.prototype.hasOwnProperty.call(base, name)) {
|
|
3357
|
+
throw new Error(
|
|
3358
|
+
`Contract serialization produced ${kind} '${name}', which collides with a hand-written definition of the same name. Contract method ops are named '<Contract>__<method>'; rename the conflicting definition.`
|
|
3359
|
+
);
|
|
3360
|
+
}
|
|
3361
|
+
base[name] = synthesized[name];
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
function buildOperations(queries = {}, commands = {}, transactions = {}, contractInputs = {}) {
|
|
3365
|
+
const querySpecs = {};
|
|
3366
|
+
for (const name of Object.keys(queries).sort()) {
|
|
3367
|
+
querySpecs[name] = buildQuerySpec(queries[name]);
|
|
3368
|
+
}
|
|
3369
|
+
const commandSpecs = {};
|
|
3370
|
+
for (const name of Object.keys(commands).sort()) {
|
|
3371
|
+
const spec = buildCommandSpec(commands[name]);
|
|
3372
|
+
if (spec.condition) assertSupportedCondition(name, spec.condition);
|
|
3373
|
+
commandSpecs[name] = spec;
|
|
3374
|
+
}
|
|
3375
|
+
const transactionSpecs = buildTransactions(transactions);
|
|
3376
|
+
const contractInputMap = contractInputs.contracts ?? {};
|
|
3377
|
+
const contextInputMap = contractInputs.contexts ?? {};
|
|
3378
|
+
assertContractBoundaries(contractInputMap, contextInputMap);
|
|
3379
|
+
const built = buildContracts(contractInputMap);
|
|
3380
|
+
mergeSpecs(querySpecs, built.queries, "query");
|
|
3381
|
+
mergeSpecs(commandSpecs, built.commands, "command");
|
|
3382
|
+
mergeSpecs(transactionSpecs, built.transactions, "transaction");
|
|
3383
|
+
const contractSpecs = built.contracts;
|
|
3384
|
+
const contextSpecs = buildContexts(contextInputMap);
|
|
3385
|
+
return {
|
|
3386
|
+
version: SPEC_VERSION,
|
|
3387
|
+
queries: querySpecs,
|
|
3388
|
+
commands: commandSpecs,
|
|
3389
|
+
...Object.keys(transactionSpecs).length > 0 ? { transactions: transactionSpecs } : {},
|
|
3390
|
+
...Object.keys(contractSpecs).length > 0 ? { contracts: contractSpecs } : {},
|
|
3391
|
+
...Object.keys(contextSpecs).length > 0 ? { contexts: contextSpecs } : {}
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
|
|
3395
|
+
// src/spec/contracts.ts
|
|
3396
|
+
function kindForDynamoType(dynamoType) {
|
|
3397
|
+
return dynamoType === "N" ? "number" : "string";
|
|
3398
|
+
}
|
|
3399
|
+
function recoverKind(metadata, fieldName) {
|
|
3400
|
+
const field = metadata.fields.find((f) => f.propertyName === fieldName);
|
|
3401
|
+
if (field === void 0) return { kind: "string" };
|
|
3402
|
+
if (field.literals !== void 0 && field.literals.length > 0) {
|
|
3403
|
+
return { kind: "literal", literals: field.literals };
|
|
3404
|
+
}
|
|
3405
|
+
return { kind: kindForDynamoType(field.dynamoType) };
|
|
3406
|
+
}
|
|
3407
|
+
function opToDefinition(contractName, methodName, op) {
|
|
3408
|
+
const metadata = MetadataRegistry.get(op.entity.modelClass);
|
|
3409
|
+
const params = {};
|
|
3410
|
+
const place = (paramName, bindField) => {
|
|
3411
|
+
const recovered = recoverKind(metadata, bindField);
|
|
3412
|
+
params[paramName] = recovered.literals === void 0 ? { kind: recovered.kind, required: true } : { kind: recovered.kind, literals: recovered.literals, required: true };
|
|
3413
|
+
return recovered;
|
|
3414
|
+
};
|
|
3415
|
+
let key;
|
|
3416
|
+
if (op.operation === "put") {
|
|
3417
|
+
key = convertStructure(op.item ?? {}, place, `${contractName}.${methodName} item`);
|
|
3418
|
+
} else if (isContractKeyRef(op.keys)) {
|
|
3419
|
+
key = wholeKeyToParams(metadata, op, contractName, methodName, place);
|
|
3420
|
+
} else {
|
|
3421
|
+
key = convertKeyRecord(op.keys, place, `${contractName}.${methodName} key`);
|
|
3422
|
+
}
|
|
3423
|
+
const select = op.select !== void 0 ? assertBooleanProjection(op.select, `${contractName}.${methodName} select`) : void 0;
|
|
3424
|
+
const changes = op.changes !== void 0 ? convertStructure(op.changes, place, `${contractName}.${methodName} changes`) : void 0;
|
|
3425
|
+
const condition = op.condition !== void 0 ? convertCondition(op.condition, place, `${contractName}.${methodName} condition`) : void 0;
|
|
3426
|
+
return {
|
|
3427
|
+
__isOperationDefinition: true,
|
|
3428
|
+
entity: op.entity,
|
|
3429
|
+
operation: op.operation,
|
|
3430
|
+
key,
|
|
3431
|
+
select,
|
|
3432
|
+
changes,
|
|
3433
|
+
...condition !== void 0 ? { condition } : {},
|
|
3434
|
+
params
|
|
3435
|
+
};
|
|
3436
|
+
}
|
|
3437
|
+
function paramOfKind(recovered) {
|
|
3438
|
+
if (recovered.kind === "number") return param.number();
|
|
3439
|
+
if (recovered.kind === "literal" && recovered.literals !== void 0 && recovered.literals.length > 0) {
|
|
3440
|
+
return param.literal(
|
|
3441
|
+
...recovered.literals
|
|
3442
|
+
);
|
|
3443
|
+
}
|
|
3444
|
+
return param.string();
|
|
3445
|
+
}
|
|
3446
|
+
function wholeKeyToParams(metadata, op, contractName, methodName, place) {
|
|
3447
|
+
const fields = wholeKeyFieldNames(metadata, op, contractName, methodName);
|
|
3448
|
+
const key = {};
|
|
3449
|
+
for (const field of fields) {
|
|
3450
|
+
key[field] = paramOfKind(place(field, field));
|
|
3451
|
+
}
|
|
3452
|
+
return key;
|
|
3453
|
+
}
|
|
3454
|
+
function wholeKeyFieldNames(metadata, op, contractName, methodName) {
|
|
3455
|
+
if (op.keyFields !== void 0 && op.keyFields.length > 0) return op.keyFields;
|
|
3456
|
+
if (!metadata.primaryKey) {
|
|
3457
|
+
throw new Error(
|
|
3458
|
+
`Contract '${contractName}.${methodName}': the '${op.operation}' op passes the whole \`keys\` into the key position, but model '${op.entity.name}' has no primary key to resolve the contract Key from. Supply an explicit Key-field list to the factory (e.g. \`publicQueryModel<Key>(['field'])\`).`
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
return metadata.primaryKey.inputFieldNames;
|
|
3462
|
+
}
|
|
3463
|
+
function convertKeyRecord(record, place, context) {
|
|
3464
|
+
const out = {};
|
|
3465
|
+
for (const [field, leaf] of Object.entries(record)) {
|
|
3466
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
3467
|
+
out[field] = paramOfKind(place(leaf.field, field));
|
|
3468
|
+
} else if (isConcreteLiteral3(leaf)) {
|
|
3469
|
+
out[field] = leaf;
|
|
3470
|
+
} else {
|
|
3471
|
+
throw new Error(
|
|
3472
|
+
`${context}: key field '${field}' is neither a \`keys.<field>\` reference nor a concrete literal \u2014 it cannot be serialized.`
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
return out;
|
|
3477
|
+
}
|
|
3478
|
+
function convertStructure(structure, place, context) {
|
|
3479
|
+
const out = {};
|
|
3480
|
+
for (const [field, leaf] of Object.entries(structure)) {
|
|
3481
|
+
out[field] = convertLeaf(leaf, field, place, `${context} field '${field}'`);
|
|
3482
|
+
}
|
|
3483
|
+
return out;
|
|
3484
|
+
}
|
|
3485
|
+
function convertLeaf(leaf, bindField, place, context) {
|
|
3486
|
+
if (isContractParamRef(leaf)) {
|
|
3487
|
+
place(leaf.field, bindField);
|
|
3488
|
+
return leaf;
|
|
3489
|
+
}
|
|
3490
|
+
if (isContractKeyFieldRef(leaf)) {
|
|
3491
|
+
place(leaf.field, bindField);
|
|
3492
|
+
return leaf;
|
|
3493
|
+
}
|
|
3494
|
+
if (isConcreteLiteral3(leaf)) return leaf;
|
|
3495
|
+
throw new Error(
|
|
3496
|
+
`${context}: value is neither a param/key reference nor a concrete literal \u2014 it cannot be serialized as a declarative contract operation.`
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
function convertCondition(condition, place, context) {
|
|
3500
|
+
if (typeof condition === "object" && condition !== null) {
|
|
3501
|
+
const obj = condition;
|
|
3502
|
+
if (obj.notExists === true) return { notExists: true };
|
|
3503
|
+
if (typeof obj.attributeExists === "string") {
|
|
3504
|
+
return { attributeExists: obj.attributeExists };
|
|
3505
|
+
}
|
|
3506
|
+
if (typeof obj.attributeNotExists === "string") {
|
|
3507
|
+
return { attributeNotExists: obj.attributeNotExists };
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
const out = {};
|
|
3511
|
+
for (const [field, leaf] of Object.entries(condition)) {
|
|
3512
|
+
out[field] = convertLeaf(leaf, field, place, `${context} field '${field}'`);
|
|
3513
|
+
}
|
|
3514
|
+
return out;
|
|
3515
|
+
}
|
|
3516
|
+
function assertBooleanProjection(select, context) {
|
|
3517
|
+
const out = {};
|
|
3518
|
+
for (const [field, leaf] of Object.entries(select)) {
|
|
3519
|
+
if (typeof leaf === "boolean") {
|
|
3520
|
+
out[field] = leaf;
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
if (leaf !== null && typeof leaf === "object" && !isContractParamRef(leaf) && !isContractKeyFieldRef(leaf)) {
|
|
3524
|
+
out[field] = leaf;
|
|
3525
|
+
continue;
|
|
3526
|
+
}
|
|
3527
|
+
throw new Error(
|
|
3528
|
+
`${context}: projection field '${field}' must be a boolean flag (\`true\` / \`false\`), but a ${describeLeaf(leaf)} was found. A parameter reference in a projection slot is not a valid select \u2014 refusing to emit it.`
|
|
3529
|
+
);
|
|
3530
|
+
}
|
|
3531
|
+
return out;
|
|
3532
|
+
}
|
|
3533
|
+
function describeLeaf(leaf) {
|
|
3534
|
+
if (isContractParamRef(leaf)) return `param reference (${leaf.token})`;
|
|
3535
|
+
if (isContractKeyFieldRef(leaf)) return `key-field reference (${leaf.token})`;
|
|
3536
|
+
return `value of type '${typeof leaf}'`;
|
|
3537
|
+
}
|
|
3538
|
+
function isConcreteLiteral3(value) {
|
|
3539
|
+
const t = typeof value;
|
|
3540
|
+
return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
|
|
3541
|
+
}
|
|
3542
|
+
function opRefName(contractName, methodName) {
|
|
3543
|
+
return `${contractName}__${methodName}`;
|
|
3544
|
+
}
|
|
3545
|
+
function composeSpecOf(node, contractName, methodName, contracts) {
|
|
3546
|
+
if (isRecordedCompose(node)) {
|
|
3547
|
+
return recordedComposeSpec(node, contractName, methodName, contracts);
|
|
3548
|
+
}
|
|
3549
|
+
const c = node;
|
|
3550
|
+
if (typeof c.as !== "string" || typeof c.contract !== "string" || typeof c.method !== "string") {
|
|
3551
|
+
throw new Error(
|
|
3552
|
+
`Contract '${contractName}.${methodName}': a composition must declare string \`as\` / \`contract\` / \`method\`.`
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
if (c.resolution !== "point" && c.resolution !== "range") {
|
|
3556
|
+
throw new Error(
|
|
3557
|
+
`Contract '${contractName}.${methodName}': composed child '${c.as}' must declare a \`resolution\` of 'point' or 'range'.`
|
|
3558
|
+
);
|
|
3559
|
+
}
|
|
3560
|
+
const bindIn = c.bind ?? {};
|
|
3561
|
+
const bind = {};
|
|
3562
|
+
for (const field of Object.keys(bindIn).sort()) {
|
|
3563
|
+
const v = bindIn[field];
|
|
3564
|
+
if (typeof v !== "string") {
|
|
3565
|
+
throw new Error(
|
|
3566
|
+
`Contract '${contractName}.${methodName}': composition '${c.as}' binding '${field}' must be a \`from\` path string (e.g. "$.billingAccountId").`
|
|
3567
|
+
);
|
|
3568
|
+
}
|
|
3569
|
+
bind[field] = v;
|
|
3570
|
+
}
|
|
3571
|
+
const cardinality = c.cardinality === "many" ? "many" : "one";
|
|
3572
|
+
return {
|
|
3573
|
+
as: c.as,
|
|
3574
|
+
contract: c.contract,
|
|
3575
|
+
method: c.method,
|
|
3576
|
+
...typeof c.context === "string" ? { context: c.context } : {},
|
|
3577
|
+
bind,
|
|
3578
|
+
resolution: c.resolution,
|
|
3579
|
+
cardinality
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function isRecordedCompose(node) {
|
|
3583
|
+
return typeof node === "object" && node !== null && typeof node.as === "string" && typeof node.method === "object" && node.method?.__methodKind === "query";
|
|
3584
|
+
}
|
|
3585
|
+
function recordedComposeSpec(node, contractName, methodName, contracts) {
|
|
3586
|
+
const { contract: refName, method: refMethodName } = resolveComposeRef(
|
|
3587
|
+
node,
|
|
3588
|
+
contractName,
|
|
3589
|
+
methodName,
|
|
3590
|
+
contracts
|
|
3591
|
+
);
|
|
3592
|
+
const refMethod = node.method;
|
|
3593
|
+
const bind = {};
|
|
3594
|
+
for (const field of Object.keys(node.bind).sort()) {
|
|
3595
|
+
bind[field] = node.bind[field].path;
|
|
3596
|
+
}
|
|
3597
|
+
const cardinality = refMethod.resolution === "point" ? "one" : "many";
|
|
3598
|
+
return {
|
|
3599
|
+
as: node.as,
|
|
3600
|
+
contract: refName,
|
|
3601
|
+
method: refMethodName,
|
|
3602
|
+
bind,
|
|
3603
|
+
resolution: refMethod.resolution,
|
|
3604
|
+
cardinality
|
|
3605
|
+
};
|
|
3606
|
+
}
|
|
3607
|
+
function resolveComposeRef(node, contractName, methodName, contracts) {
|
|
3608
|
+
const refMethodName = node.method.__methodName;
|
|
3609
|
+
if (typeof refMethodName !== "string") {
|
|
3610
|
+
throw new Error(
|
|
3611
|
+
`Contract '${contractName}.${methodName}': composed child '${node.as}' references a query method that was not produced by publicQueryModel (no method name). Compose only the value of a publicQueryModel method (e.g. \`query(OtherContract.get, \u2026)\`).`
|
|
3612
|
+
);
|
|
3613
|
+
}
|
|
3614
|
+
const owner = contractOfMethodSpec(node.method);
|
|
3615
|
+
if (owner !== void 0) {
|
|
3616
|
+
for (const name of Object.keys(contracts)) {
|
|
3617
|
+
if (contracts[name] === owner) return { contract: name, method: refMethodName };
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
throw new Error(
|
|
3621
|
+
`Contract '${contractName}.${methodName}': composed child '${node.as}' references a contract method that is not registered in this SSoT. An External Query must reference another contract passed to the generator's contract map (a same-SSoT, in-process reference); add the referenced contract to the \`contracts\` map.`
|
|
3622
|
+
);
|
|
3623
|
+
}
|
|
3624
|
+
function composeOf(op) {
|
|
3625
|
+
const raw = op.compose;
|
|
3626
|
+
return Array.isArray(raw) ? raw : [];
|
|
3627
|
+
}
|
|
3628
|
+
function compositionPlanOf(compose) {
|
|
3629
|
+
if (compose.length === 0) return void 0;
|
|
3630
|
+
const { stages, concurrency } = deriveCompositionPlan(compose.length);
|
|
3631
|
+
return {
|
|
3632
|
+
stages: stages.map((s) => [...s]),
|
|
3633
|
+
concurrency,
|
|
3634
|
+
batchChunkSize: BATCH_GET_MAX_KEYS
|
|
3635
|
+
};
|
|
3636
|
+
}
|
|
3637
|
+
function batchTxName(contractName, methodName) {
|
|
3638
|
+
return `${opRefName(contractName, methodName)}__batch`;
|
|
3639
|
+
}
|
|
3640
|
+
function keyParamNames(op) {
|
|
3641
|
+
return keyFieldsOf(op);
|
|
3642
|
+
}
|
|
3643
|
+
var TX_ITEM_TYPE = {
|
|
3644
|
+
PutItem: "Put",
|
|
3645
|
+
UpdateItem: "Update",
|
|
3646
|
+
DeleteItem: "Delete"
|
|
3647
|
+
};
|
|
3648
|
+
function toElementTemplate(template, keyParams) {
|
|
3649
|
+
return template.replace(
|
|
3650
|
+
/\{([^}]+)\}/g,
|
|
3651
|
+
(match, name) => keyParams.has(name) ? `{item.${name}}` : match
|
|
3652
|
+
);
|
|
3653
|
+
}
|
|
3654
|
+
function toElementRecord(record, keyParams) {
|
|
3655
|
+
const out = {};
|
|
3656
|
+
for (const field of Object.keys(record)) {
|
|
3657
|
+
out[field] = toElementTemplate(record[field], keyParams);
|
|
3658
|
+
}
|
|
3659
|
+
return out;
|
|
3660
|
+
}
|
|
3661
|
+
function toElementCondition(condition, keyParams) {
|
|
3662
|
+
if (condition.kind !== "equals") return condition;
|
|
3663
|
+
return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
|
|
3664
|
+
}
|
|
3665
|
+
function synthesizeBatchTransaction(commandSpec, op) {
|
|
3666
|
+
const keyParams = new Set(keyParamNames(op));
|
|
3667
|
+
const elementFields = {};
|
|
3668
|
+
const sharedParams = {};
|
|
3669
|
+
for (const name of Object.keys(commandSpec.params).sort()) {
|
|
3670
|
+
if (keyParams.has(name)) elementFields[name] = commandSpec.params[name];
|
|
3671
|
+
else sharedParams[name] = commandSpec.params[name];
|
|
3672
|
+
}
|
|
3673
|
+
const params = {
|
|
3674
|
+
...sharedParams,
|
|
3675
|
+
keys: { type: "array", required: true, element: elementFields }
|
|
3676
|
+
};
|
|
3677
|
+
const condition = commandSpec.condition !== void 0 ? toElementCondition(commandSpec.condition, keyParams) : void 0;
|
|
3678
|
+
const item = {
|
|
3679
|
+
type: TX_ITEM_TYPE[commandSpec.type],
|
|
3680
|
+
tableName: commandSpec.tableName,
|
|
3681
|
+
entity: commandSpec.entity,
|
|
3682
|
+
...commandSpec.item !== void 0 ? { item: toElementRecord(commandSpec.item, keyParams) } : {},
|
|
3683
|
+
...commandSpec.keyCondition !== void 0 ? { keyCondition: toElementRecord(commandSpec.keyCondition, keyParams) } : {},
|
|
3684
|
+
...commandSpec.changes !== void 0 ? { changes: toElementRecord(commandSpec.changes, keyParams) } : {},
|
|
3685
|
+
...condition !== void 0 ? { condition } : {},
|
|
3686
|
+
forEach: { source: "keys" }
|
|
3687
|
+
};
|
|
3688
|
+
return { params, items: [item] };
|
|
3689
|
+
}
|
|
3690
|
+
function composeFragmentTransaction(contractName, methodName, ops) {
|
|
3691
|
+
const items = [];
|
|
3692
|
+
const params = {};
|
|
3693
|
+
for (let i = 0; i < ops.length; i++) {
|
|
3694
|
+
const op = ops[i];
|
|
3695
|
+
const def = opToDefinition(contractName, `${methodName}__f${i}`, op);
|
|
3696
|
+
const commandSpec = buildCommandSpec(def);
|
|
3697
|
+
if (commandSpec.condition) {
|
|
3698
|
+
assertSupportedCondition(`${opRefName(contractName, methodName)}#${i}`, commandSpec.condition);
|
|
3699
|
+
}
|
|
3700
|
+
for (const [name, spec] of Object.entries(commandSpec.params)) {
|
|
3701
|
+
params[name] = spec;
|
|
3702
|
+
}
|
|
3703
|
+
const item = {
|
|
3704
|
+
type: TX_ITEM_TYPE[commandSpec.type],
|
|
3705
|
+
tableName: commandSpec.tableName,
|
|
3706
|
+
entity: commandSpec.entity,
|
|
3707
|
+
...commandSpec.item !== void 0 ? { item: commandSpec.item } : {},
|
|
3708
|
+
...commandSpec.keyCondition !== void 0 ? { keyCondition: commandSpec.keyCondition } : {},
|
|
3709
|
+
...commandSpec.changes !== void 0 ? { changes: commandSpec.changes } : {},
|
|
3710
|
+
...commandSpec.condition !== void 0 ? { condition: commandSpec.condition } : {}
|
|
3711
|
+
};
|
|
3712
|
+
items.push(item);
|
|
3713
|
+
}
|
|
3714
|
+
return { params: sortedParams(params), items, maxItems: items.length };
|
|
3715
|
+
}
|
|
3716
|
+
function itemKeySignature(item) {
|
|
3717
|
+
if (item.type === "Put") {
|
|
3718
|
+
const metadata = MetadataRegistry.get(
|
|
3719
|
+
// The entity name matches a manifest entity / a registered model class name.
|
|
3720
|
+
resolveModelByName(item.entity)
|
|
3721
|
+
);
|
|
3722
|
+
if (!metadata.primaryKey) return `${item.entity}#<no-key>`;
|
|
3723
|
+
const present = new Set(Object.keys(item.item ?? {}));
|
|
3724
|
+
const { pk, sk } = evaluateKey(metadata.primaryKey.segmented, "param", present);
|
|
3725
|
+
return `${item.entity}#${pk}#${sk ?? ""}`;
|
|
3726
|
+
}
|
|
3727
|
+
const kc = item.keyCondition ?? {};
|
|
3728
|
+
return `${item.entity}#${kc.PK ?? ""}#${kc.SK ?? ""}`;
|
|
3729
|
+
}
|
|
3730
|
+
function resolveModelByName(name) {
|
|
3731
|
+
for (const modelClass of MetadataRegistry.getAll().keys()) {
|
|
3732
|
+
if (modelClass.name === name) {
|
|
3733
|
+
return modelClass;
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
throw new Error(
|
|
3737
|
+
`Contract serializer: cannot resolve model '${name}' for the edge de-dup key signature \u2014 it is not a registered model.`
|
|
3738
|
+
);
|
|
3739
|
+
}
|
|
3740
|
+
function sortedParams(params) {
|
|
3741
|
+
const out = {};
|
|
3742
|
+
for (const name of Object.keys(params).sort()) out[name] = params[name];
|
|
3743
|
+
return out;
|
|
3744
|
+
}
|
|
3745
|
+
function buildConditionCheckItems(checks, label) {
|
|
3746
|
+
const items = [];
|
|
3747
|
+
const params = {};
|
|
3748
|
+
for (const check of checks) {
|
|
3749
|
+
const metadata = MetadataRegistry.get(
|
|
3750
|
+
check.entity.modelClass
|
|
3751
|
+
);
|
|
3752
|
+
if (!metadata.primaryKey) {
|
|
3753
|
+
throw new Error(
|
|
3754
|
+
`${label}: the referenced entity '${check.entity.name}' has no primary key, so a referential-integrity ConditionCheck cannot be keyed.`
|
|
3755
|
+
);
|
|
3756
|
+
}
|
|
3757
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
3758
|
+
const present = new Set(Object.keys(check.keyBinding));
|
|
3759
|
+
const { pk, sk } = evaluateKey(
|
|
3760
|
+
metadata.primaryKey.segmented,
|
|
3761
|
+
"param",
|
|
3762
|
+
present,
|
|
3763
|
+
(field) => check.keyBinding[field] ?? field
|
|
3764
|
+
);
|
|
3765
|
+
const keyCondition = { PK: pk };
|
|
3766
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
3767
|
+
const condition = { kind: "attributeExists", field: "PK" };
|
|
3768
|
+
assertSupportedCondition(`${label} (${check.entity.name})`, condition);
|
|
3769
|
+
items.push({
|
|
3770
|
+
type: "ConditionCheck",
|
|
3771
|
+
tableName,
|
|
3772
|
+
entity: check.entity.name,
|
|
3773
|
+
keyCondition,
|
|
3774
|
+
condition
|
|
3775
|
+
});
|
|
3776
|
+
for (const [targetField, inputField] of Object.entries(check.keyBinding)) {
|
|
3777
|
+
const recovered = recoverKind(metadata, targetField);
|
|
3778
|
+
params[inputField] = recovered.literals === void 0 ? { type: recovered.kind, required: true } : { type: recovered.kind, required: true, literals: recovered.literals };
|
|
3779
|
+
}
|
|
3780
|
+
}
|
|
3781
|
+
return { items, params };
|
|
3782
|
+
}
|
|
3783
|
+
function buildEdgeWriteItems(edgeWrites2) {
|
|
3784
|
+
const items = [];
|
|
3785
|
+
const params = {};
|
|
3786
|
+
for (const edge of edgeWrites2) {
|
|
3787
|
+
const metadata = MetadataRegistry.get(
|
|
3788
|
+
edge.entity.modelClass
|
|
3789
|
+
);
|
|
3790
|
+
for (const item of edge.items) {
|
|
3791
|
+
items.push(item);
|
|
3792
|
+
collectTemplateParams(item.item, metadata, params);
|
|
3793
|
+
collectTemplateParams(item.keyCondition, metadata, params);
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3796
|
+
return { items, params };
|
|
3797
|
+
}
|
|
3798
|
+
var TEMPLATE_TOKEN_RE = /\{([^{}]+)\}/g;
|
|
3799
|
+
function collectTemplateParams(record, metadata, out) {
|
|
3800
|
+
if (record === void 0) return;
|
|
3801
|
+
for (const template of Object.values(record)) {
|
|
3802
|
+
for (const match of template.matchAll(TEMPLATE_TOKEN_RE)) {
|
|
3803
|
+
const token = match[1];
|
|
3804
|
+
const field = token.startsWith("old.") ? token.slice("old.".length) : token;
|
|
3805
|
+
const recovered = recoverKind(metadata, field);
|
|
3806
|
+
out[token] = recovered.literals === void 0 ? { type: recovered.kind, required: true } : { type: recovered.kind, required: true, literals: recovered.literals };
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
function buildDerivedUpdateItems(derivedUpdates) {
|
|
3811
|
+
const items = [];
|
|
3812
|
+
const params = {};
|
|
3813
|
+
for (const update of derivedUpdates) {
|
|
3814
|
+
const metadata = MetadataRegistry.get(
|
|
3815
|
+
update.entity.modelClass
|
|
3816
|
+
);
|
|
3817
|
+
if (!metadata.primaryKey) {
|
|
3818
|
+
throw new Error(
|
|
3819
|
+
`derived update: the target entity '${update.entity.name}' has no primary key, so a derived counter \`UpdateItem\` cannot be keyed.`
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
const tableName = TableMapping.resolve(metadata.tableName);
|
|
3823
|
+
const present = new Set(Object.keys(update.keyBinding));
|
|
3824
|
+
const { pk, sk } = evaluateKey(
|
|
3825
|
+
metadata.primaryKey.segmented,
|
|
3826
|
+
"param",
|
|
3827
|
+
present,
|
|
3828
|
+
(field) => update.keyBinding[field] ?? field
|
|
3829
|
+
);
|
|
3830
|
+
const keyCondition = { PK: pk };
|
|
3831
|
+
if (sk !== void 0) keyCondition.SK = sk;
|
|
3832
|
+
items.push({
|
|
3833
|
+
type: "Update",
|
|
3834
|
+
tableName,
|
|
3835
|
+
entity: update.entity.name,
|
|
3836
|
+
keyCondition,
|
|
3837
|
+
// The delta is a compile-time constant → a literal numeric template (no param).
|
|
3838
|
+
add: { [update.attribute]: String(update.amount) }
|
|
3839
|
+
});
|
|
3840
|
+
collectTemplateParams(keyCondition, metadata, params);
|
|
3841
|
+
}
|
|
3842
|
+
return { items, params };
|
|
3843
|
+
}
|
|
3844
|
+
function buildUniqueGuardItems(uniqueGuards, entityMetadata) {
|
|
3845
|
+
const items = [];
|
|
3846
|
+
const params = {};
|
|
3847
|
+
for (const guard of uniqueGuards) {
|
|
3848
|
+
for (const item of guard.items) {
|
|
3849
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
3850
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
3851
|
+
collectTemplateParams(item.keyCondition, entityMetadata, params);
|
|
3852
|
+
}
|
|
3853
|
+
}
|
|
3854
|
+
return { items, params };
|
|
3855
|
+
}
|
|
3856
|
+
function buildOutboxEventItems(outboxEvents, entityMetadata) {
|
|
3857
|
+
const items = [];
|
|
3858
|
+
const params = {};
|
|
3859
|
+
for (const event of outboxEvents) {
|
|
3860
|
+
for (const item of event.items) {
|
|
3861
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
3862
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
return { items, params };
|
|
3866
|
+
}
|
|
3867
|
+
function buildIdempotencyGuardItems(guard, entityMetadata) {
|
|
3868
|
+
const items = [];
|
|
3869
|
+
const params = {};
|
|
3870
|
+
for (const item of guard.items) {
|
|
3871
|
+
items.push({ ...item, tableName: TableMapping.resolve(item.tableName) });
|
|
3872
|
+
collectTemplateParams(item.item, entityMetadata, params);
|
|
3873
|
+
}
|
|
3874
|
+
return { items, params };
|
|
3875
|
+
}
|
|
3876
|
+
function synthesizeMutationTransaction(contractName, methodName, ops, checks, edgeWrites2, derivedUpdates, uniqueGuards, outboxEvents, idempotencyGuard) {
|
|
3877
|
+
const label = opRefName(contractName, methodName);
|
|
3878
|
+
const base = composeFragmentTransaction(contractName, methodName, ops);
|
|
3879
|
+
const { items: edgeItemsRaw, params: edgeParams } = buildEdgeWriteItems(edgeWrites2);
|
|
3880
|
+
const { items: updateItems, params: updateParams } = buildDerivedUpdateItems(derivedUpdates);
|
|
3881
|
+
const { items: checkItems, params: checkParams } = buildConditionCheckItems(checks, label);
|
|
3882
|
+
const primaryMetadata = MetadataRegistry.get(
|
|
3883
|
+
ops[0].entity.modelClass
|
|
3884
|
+
);
|
|
3885
|
+
const { items: guardItemsRaw, params: guardParams } = buildUniqueGuardItems(
|
|
3886
|
+
uniqueGuards,
|
|
3887
|
+
primaryMetadata
|
|
3888
|
+
);
|
|
3889
|
+
const { items: outboxItems, params: outboxParams } = buildOutboxEventItems(
|
|
3890
|
+
outboxEvents,
|
|
3891
|
+
primaryMetadata
|
|
3892
|
+
);
|
|
3893
|
+
const { items: idempotencyItems, params: idempotencyParams } = idempotencyGuard !== void 0 ? buildIdempotencyGuardItems(idempotencyGuard, primaryMetadata) : { items: [], params: {} };
|
|
3894
|
+
const baseKeys = new Set(base.items.map(itemKeySignature));
|
|
3895
|
+
for (const update of updateItems) {
|
|
3896
|
+
if (baseKeys.has(itemKeySignature(update))) {
|
|
3897
|
+
throw new Error(
|
|
3898
|
+
`Contract '${contractName}.${methodName}': a derived counter (\`increment\`) targets the same row as the entity write (entity '${update.entity}', key ${JSON.stringify(update.keyCondition ?? {})}) \u2014 a Put+Update on one item is unsupported in a transaction (DynamoDB rejects touching one key twice). A \`w.increment(...)\` must target a DIFFERENT row (e.g. a parent aggregate's counter), not the fragment's own entity row; increment a field on the entity write itself instead of deriving a counter onto it.`
|
|
3899
|
+
);
|
|
3900
|
+
}
|
|
3901
|
+
}
|
|
3902
|
+
const edgeItems = edgeItemsRaw.filter((item) => {
|
|
3903
|
+
const sig = itemKeySignature(item);
|
|
3904
|
+
if (baseKeys.has(sig)) return false;
|
|
3905
|
+
baseKeys.add(sig);
|
|
3906
|
+
return true;
|
|
3907
|
+
});
|
|
3908
|
+
const guardItems = guardItemsRaw;
|
|
3909
|
+
const params = { ...base.params };
|
|
3910
|
+
for (const source of [
|
|
3911
|
+
edgeParams,
|
|
3912
|
+
updateParams,
|
|
3913
|
+
checkParams,
|
|
3914
|
+
guardParams,
|
|
3915
|
+
outboxParams,
|
|
3916
|
+
idempotencyParams
|
|
3917
|
+
]) {
|
|
3918
|
+
for (const [name, spec] of Object.entries(source)) params[name] = spec;
|
|
3919
|
+
}
|
|
3920
|
+
const items = [
|
|
3921
|
+
...base.items,
|
|
3922
|
+
...edgeItems,
|
|
3923
|
+
...updateItems,
|
|
3924
|
+
...guardItems,
|
|
3925
|
+
...outboxItems,
|
|
3926
|
+
...idempotencyItems,
|
|
3927
|
+
...checkItems
|
|
3928
|
+
];
|
|
3929
|
+
if (items.length > MAX_TRANSACT_ITEMS) {
|
|
3930
|
+
throw new Error(
|
|
3931
|
+
`Contract '${contractName}.${methodName}': a mutation composes ${items.length} items (writes + edges + derived updates + uniqueness guards + outbox events + idempotency guard + ConditionChecks) into one TransactWriteItems, exceeding the DynamoDB limit of ${MAX_TRANSACT_ITEMS}. An atomic transaction cannot be split; reduce the fragment / effect count.`
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
return { params: sortedParams(params), items, maxItems: items.length };
|
|
3935
|
+
}
|
|
3936
|
+
function batchTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
|
|
3937
|
+
if (mode === void 0) return void 0;
|
|
3938
|
+
if (mode === "batchWrite") {
|
|
3939
|
+
if (commandSpec.condition !== void 0) {
|
|
3940
|
+
throw new Error(
|
|
3941
|
+
`Contract '${contractName}.${methodName}': a 'batchWrite' batched form cannot carry a write condition \u2014 DynamoDB's BatchWriteItem has no ConditionExpression. Declare \`batch: 'transact'\` for a conditional batch.`
|
|
3942
|
+
);
|
|
3943
|
+
}
|
|
3944
|
+
return { mode: "batchWrite", operation: opName };
|
|
3945
|
+
}
|
|
3946
|
+
const txName = batchTxName(contractName, methodName);
|
|
3947
|
+
transactionSpecs[txName] = synthesizeBatchTransaction(commandSpec, op);
|
|
3948
|
+
return { mode: "transaction", transaction: txName };
|
|
3949
|
+
}
|
|
3950
|
+
function buildContracts(contracts = {}) {
|
|
3951
|
+
const contractSpecs = {};
|
|
3952
|
+
const querySpecs = {};
|
|
3953
|
+
const commandSpecs = {};
|
|
3954
|
+
const transactionSpecs = {};
|
|
3955
|
+
for (const contractName of Object.keys(contracts).sort()) {
|
|
3956
|
+
const contract = contracts[contractName];
|
|
3957
|
+
let spec;
|
|
3958
|
+
if (isQueryModelContract(contract)) {
|
|
3959
|
+
spec = serializeQueryContract(contractName, contract, querySpecs, contracts);
|
|
3960
|
+
} else if (isCommandModelContract(contract)) {
|
|
3961
|
+
spec = serializeCommandContract(
|
|
3962
|
+
contractName,
|
|
3963
|
+
contract,
|
|
3964
|
+
commandSpecs,
|
|
3965
|
+
transactionSpecs,
|
|
3966
|
+
querySpecs
|
|
3967
|
+
);
|
|
3968
|
+
} else {
|
|
3969
|
+
throw new Error(
|
|
3970
|
+
`Contract '${contractName}' is not a publicQueryModel / publicCommandModel result. Pass the value returned by publicQueryModel(...) / publicCommandModel(...).`
|
|
3971
|
+
);
|
|
3972
|
+
}
|
|
3973
|
+
assertContractN1Safe(
|
|
3974
|
+
contractName,
|
|
3975
|
+
spec,
|
|
3976
|
+
(methodName) => isGsiPointOp(querySpecs[opRefName(contractName, methodName)])
|
|
3977
|
+
);
|
|
3978
|
+
contractSpecs[contractName] = spec;
|
|
3979
|
+
}
|
|
3980
|
+
return {
|
|
3981
|
+
contracts: contractSpecs,
|
|
3982
|
+
queries: querySpecs,
|
|
3983
|
+
commands: commandSpecs,
|
|
3984
|
+
transactions: transactionSpecs
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
function serializeQueryContract(contractName, contract, querySpecs, contracts) {
|
|
3988
|
+
const methods = {};
|
|
3989
|
+
const keyFields = /* @__PURE__ */ new Set();
|
|
3990
|
+
for (const methodName of Object.keys(contract.methods).sort()) {
|
|
3991
|
+
const method = contract.methods[methodName];
|
|
3992
|
+
const op = method.op;
|
|
3993
|
+
const def = opToDefinition(contractName, methodName, op);
|
|
3994
|
+
const querySpec = buildQuerySpec(def);
|
|
3995
|
+
const opName = opRefName(contractName, methodName);
|
|
3996
|
+
querySpecs[opName] = querySpec;
|
|
3997
|
+
for (const f of keyFieldsOf(op)) keyFields.add(f);
|
|
3998
|
+
const compose = composeOf(op).map(
|
|
3999
|
+
(n) => composeSpecOf(n, contractName, methodName, contracts)
|
|
4000
|
+
);
|
|
4001
|
+
const compositionPlan = compositionPlanOf(compose);
|
|
4002
|
+
methods[methodName] = {
|
|
4003
|
+
resolution: method.resolution,
|
|
4004
|
+
inputArity: method.inputArity,
|
|
4005
|
+
// Per-key cardinality: a point read is at most one item; a range read is a
|
|
4006
|
+
// connection. Derived from the #58 facts, not re-computed from storage.
|
|
4007
|
+
cardinality: method.resolution === "point" ? "one" : "many",
|
|
4008
|
+
operation: opName,
|
|
4009
|
+
...compose.length > 0 ? { compose } : {},
|
|
4010
|
+
...compositionPlan !== void 0 ? { compositionPlan } : {}
|
|
4011
|
+
};
|
|
4012
|
+
}
|
|
4013
|
+
return {
|
|
4014
|
+
kind: "query",
|
|
4015
|
+
key: { fields: sortedKeyFields(keyFields) },
|
|
4016
|
+
methods
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
function serializeCommandContract(contractName, contract, commandSpecs, transactionSpecs, querySpecs) {
|
|
4020
|
+
const methods = {};
|
|
4021
|
+
const keyFields = /* @__PURE__ */ new Set();
|
|
4022
|
+
for (const methodName of Object.keys(contract.methods).sort()) {
|
|
4023
|
+
const method = contract.methods[methodName];
|
|
4024
|
+
const op = method.op;
|
|
4025
|
+
const def = opToDefinition(contractName, methodName, op);
|
|
4026
|
+
const commandSpec = buildCommandSpec(def);
|
|
4027
|
+
if (commandSpec.condition) assertSupportedCondition(opRefName(contractName, methodName), commandSpec.condition);
|
|
4028
|
+
const opName = opRefName(contractName, methodName);
|
|
4029
|
+
commandSpecs[opName] = commandSpec;
|
|
4030
|
+
const methodKeyFields = keyFieldsOf(op);
|
|
4031
|
+
for (const f of methodKeyFields) keyFields.add(f);
|
|
4032
|
+
const conditionChecks = method.conditionChecks ?? [];
|
|
4033
|
+
const edgeWrites2 = method.edgeWrites ?? [];
|
|
4034
|
+
const derivedUpdates = method.derivedUpdates ?? [];
|
|
4035
|
+
const uniqueGuards = method.uniqueGuards ?? [];
|
|
4036
|
+
const outboxEvents = method.outboxEvents ?? [];
|
|
4037
|
+
const idempotencyGuard = method.idempotencyGuard;
|
|
4038
|
+
const hasDerivedEffects = conditionChecks.length > 0 || edgeWrites2.length > 0 || derivedUpdates.length > 0 || uniqueGuards.length > 0 || outboxEvents.length > 0 || idempotencyGuard !== void 0;
|
|
4039
|
+
const isMultiFragment = method.ops !== void 0 && method.ops.length > 1;
|
|
4040
|
+
const promotedToTransaction = isMultiFragment || hasDerivedEffects;
|
|
4041
|
+
if (promotedToTransaction) {
|
|
4042
|
+
const txName = opName;
|
|
4043
|
+
const ops = method.ops ?? [op];
|
|
4044
|
+
const tx = hasDerivedEffects ? (
|
|
4045
|
+
// #84/#85/#86/#87: writes + ConditionChecks + edge writes + derived updates +
|
|
4046
|
+
// uniqueness guards + outbox events + idempotency guard, one atomic tx.
|
|
4047
|
+
synthesizeMutationTransaction(
|
|
4048
|
+
contractName,
|
|
4049
|
+
methodName,
|
|
4050
|
+
ops,
|
|
4051
|
+
conditionChecks,
|
|
4052
|
+
edgeWrites2,
|
|
4053
|
+
derivedUpdates,
|
|
4054
|
+
uniqueGuards,
|
|
4055
|
+
outboxEvents,
|
|
4056
|
+
idempotencyGuard
|
|
4057
|
+
)
|
|
4058
|
+
) : (
|
|
4059
|
+
// #90: writes only.
|
|
4060
|
+
composeFragmentTransaction(contractName, methodName, ops)
|
|
4061
|
+
);
|
|
4062
|
+
if (tx.items.length > MAX_TRANSACT_ITEMS) {
|
|
4063
|
+
throw new Error(
|
|
4064
|
+
`Contract '${contractName}.${methodName}': a mutation composes ${tx.items.length} items (writes + ConditionChecks) into one TransactWriteItems, exceeding the DynamoDB limit of ${MAX_TRANSACT_ITEMS}. An atomic transaction cannot be split; reduce the fragment / requires count.`
|
|
4065
|
+
);
|
|
4066
|
+
}
|
|
4067
|
+
transactionSpecs[txName] = tx;
|
|
4068
|
+
}
|
|
4069
|
+
const returnSelection = method.returnSelection;
|
|
4070
|
+
if (returnSelection !== void 0 && Object.keys(returnSelection).length > 0) {
|
|
4071
|
+
querySpecs[readbackQueryName(contractName, methodName)] = buildReadbackQuerySpec(
|
|
4072
|
+
contractName,
|
|
4073
|
+
methodName,
|
|
4074
|
+
op,
|
|
4075
|
+
methodKeyFields,
|
|
4076
|
+
returnSelection
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
const batch = batchTargetFor(
|
|
4080
|
+
method.batch,
|
|
4081
|
+
op,
|
|
4082
|
+
commandSpec,
|
|
4083
|
+
contractName,
|
|
4084
|
+
methodName,
|
|
4085
|
+
opName,
|
|
4086
|
+
transactionSpecs
|
|
4087
|
+
);
|
|
4088
|
+
const single = promotedToTransaction ? { mode: "transaction", transaction: opName } : { mode: "op", operation: opName };
|
|
4089
|
+
methods[methodName] = {
|
|
4090
|
+
inputArity: method.inputArity,
|
|
4091
|
+
result: method.result,
|
|
4092
|
+
single,
|
|
4093
|
+
...batch !== void 0 ? { batch } : {},
|
|
4094
|
+
...returnSelection !== void 0 && Object.keys(returnSelection).length > 0 ? { returnSelection: sortedReturnSelection(returnSelection) } : {}
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
return {
|
|
4098
|
+
kind: "command",
|
|
4099
|
+
key: { fields: sortedKeyFields(keyFields) },
|
|
4100
|
+
methods
|
|
4101
|
+
};
|
|
4102
|
+
}
|
|
4103
|
+
function readbackQueryName(contractName, methodName) {
|
|
4104
|
+
return `${opRefName(contractName, methodName)}__readback`;
|
|
4105
|
+
}
|
|
4106
|
+
function sortedReturnSelection(selection) {
|
|
4107
|
+
const out = {};
|
|
4108
|
+
for (const field of Object.keys(selection).sort()) {
|
|
4109
|
+
if (selection[field] === true) out[field] = true;
|
|
4110
|
+
}
|
|
4111
|
+
return out;
|
|
4112
|
+
}
|
|
4113
|
+
function buildReadbackQuerySpec(contractName, methodName, op, keyFields, returnSelection) {
|
|
4114
|
+
if (keyFields.length === 0) {
|
|
4115
|
+
throw new Error(
|
|
4116
|
+
`Contract '${contractName}.${methodName}': a return projection needs the written entity's primary key to read it back, but no contract Key fields were resolved. The target model must declare a primary key.`
|
|
4117
|
+
);
|
|
4118
|
+
}
|
|
4119
|
+
const metadata = MetadataRegistry.get(
|
|
4120
|
+
op.entity.modelClass
|
|
4121
|
+
);
|
|
4122
|
+
const params = {};
|
|
4123
|
+
const key = {};
|
|
4124
|
+
for (const field of keyFields) {
|
|
4125
|
+
const recovered = recoverKind(metadata, field);
|
|
4126
|
+
params[field] = recovered.literals === void 0 ? { kind: recovered.kind, required: true } : { kind: recovered.kind, literals: recovered.literals, required: true };
|
|
4127
|
+
key[field] = paramOfKind(recovered);
|
|
4128
|
+
}
|
|
4129
|
+
const select = {};
|
|
4130
|
+
for (const f of Object.keys(returnSelection).sort()) {
|
|
4131
|
+
if (returnSelection[f] === true) select[f] = true;
|
|
4132
|
+
}
|
|
4133
|
+
const def = {
|
|
4134
|
+
__isOperationDefinition: true,
|
|
4135
|
+
entity: op.entity,
|
|
4136
|
+
operation: "query",
|
|
4137
|
+
key,
|
|
4138
|
+
select,
|
|
4139
|
+
params
|
|
4140
|
+
};
|
|
4141
|
+
return buildQuerySpec(def);
|
|
4142
|
+
}
|
|
4143
|
+
function keyFieldsOf(op) {
|
|
4144
|
+
if (op.operation === "put") {
|
|
4145
|
+
const fields2 = [];
|
|
4146
|
+
for (const leaf of Object.values(op.item ?? {})) {
|
|
4147
|
+
if (isContractKeyFieldRef(leaf)) fields2.push(leaf.field);
|
|
4148
|
+
}
|
|
4149
|
+
return fields2;
|
|
4150
|
+
}
|
|
4151
|
+
if (isContractKeyRef(op.keys)) {
|
|
4152
|
+
if (op.keyFields !== void 0 && op.keyFields.length > 0) return op.keyFields;
|
|
4153
|
+
const metadata = MetadataRegistry.get(
|
|
4154
|
+
op.entity.modelClass
|
|
4155
|
+
);
|
|
4156
|
+
return metadata.primaryKey ? metadata.primaryKey.inputFieldNames : [];
|
|
4157
|
+
}
|
|
4158
|
+
const fields = [];
|
|
4159
|
+
for (const leaf of Object.values(op.keys)) {
|
|
4160
|
+
if (isContractKeyFieldRef(leaf)) fields.push(leaf.field);
|
|
4161
|
+
}
|
|
4162
|
+
return fields;
|
|
4163
|
+
}
|
|
4164
|
+
function sortedKeyFields(fields) {
|
|
4165
|
+
return [...fields].sort();
|
|
4166
|
+
}
|
|
4167
|
+
function isGsiPointOp(querySpec) {
|
|
4168
|
+
if (querySpec === void 0) return false;
|
|
4169
|
+
const root = querySpec.operations[0];
|
|
4170
|
+
return root !== void 0 && root.indexName !== void 0;
|
|
4171
|
+
}
|
|
4172
|
+
function buildContexts(contexts = {}) {
|
|
4173
|
+
const out = {};
|
|
4174
|
+
for (const name of Object.keys(contexts).sort()) {
|
|
4175
|
+
const c = contexts[name];
|
|
4176
|
+
out[name] = {
|
|
4177
|
+
models: [...c.models ?? []].sort(),
|
|
4178
|
+
contracts: [...c.contracts ?? []].sort()
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
return out;
|
|
4182
|
+
}
|
|
4183
|
+
|
|
4184
|
+
// src/spec/index.ts
|
|
4185
|
+
function buildBridgeBundle(queries = {}, commands = {}, registry = MetadataRegistry, transactions = {}, contractInputs = {}) {
|
|
4186
|
+
const bundle = {
|
|
4187
|
+
manifest: buildManifest(registry),
|
|
4188
|
+
operations: buildOperations(queries, commands, transactions, contractInputs)
|
|
4189
|
+
};
|
|
4190
|
+
assertBundleSerializable(bundle);
|
|
4191
|
+
return bundle;
|
|
4192
|
+
}
|
|
4193
|
+
|
|
4194
|
+
export {
|
|
4195
|
+
SPEC_VERSION,
|
|
4196
|
+
MARKER_ROW_ENTITY,
|
|
4197
|
+
buildManifest,
|
|
4198
|
+
isSelectBuilder,
|
|
4199
|
+
normalizeSelectSpec,
|
|
4200
|
+
normalizeTopLevelSelect,
|
|
4201
|
+
buildProject,
|
|
4202
|
+
buildRelation,
|
|
4203
|
+
detectRelationFields,
|
|
4204
|
+
getImplicitKeyFields,
|
|
4205
|
+
param,
|
|
4206
|
+
isParam,
|
|
4207
|
+
LIFECYCLE_CONTRACT_MARKER,
|
|
4208
|
+
isLifecycleContract,
|
|
4209
|
+
ENTITY_WRITES_MARKER,
|
|
4210
|
+
isEntityWritesDefinition,
|
|
4211
|
+
entityWrites,
|
|
4212
|
+
getEntityWrites,
|
|
4213
|
+
lifecyclePhaseForIntent,
|
|
4214
|
+
isMutationInputRef,
|
|
4215
|
+
isMutationFragment,
|
|
4216
|
+
isCommandPlan,
|
|
4217
|
+
mutation,
|
|
4218
|
+
definePlan,
|
|
4219
|
+
OLD_VALUE_NAMESPACE,
|
|
4220
|
+
EDGE_WRITES_MARKER,
|
|
4221
|
+
isEdgeWritesDefinition,
|
|
4222
|
+
edgeWrites,
|
|
4223
|
+
deriveEdgeWriteItemsFor,
|
|
4224
|
+
deriveEdgeWriteItems,
|
|
4225
|
+
getEdgeWrites,
|
|
4226
|
+
deriveModelEdgeWriteItems,
|
|
4227
|
+
resolveLifecycle,
|
|
4228
|
+
compileFragment,
|
|
4229
|
+
compileSingleFragmentPlan,
|
|
4230
|
+
compileMutationPlan,
|
|
4231
|
+
isCommandBatchMode,
|
|
4232
|
+
isContractKeyRef,
|
|
4233
|
+
mintContractKeyFieldRef,
|
|
4234
|
+
wholeKeysSentinel,
|
|
4235
|
+
isContractKeyFieldRef,
|
|
4236
|
+
isContractParamRef,
|
|
4237
|
+
mintContractParamRef,
|
|
4238
|
+
isContractFromRef,
|
|
4239
|
+
isContractComposeNode,
|
|
4240
|
+
from,
|
|
4241
|
+
query,
|
|
4242
|
+
contractOfMethodSpec,
|
|
4243
|
+
recordContractOp,
|
|
4244
|
+
isRecordingContractMethod,
|
|
4245
|
+
point,
|
|
4246
|
+
range,
|
|
4247
|
+
isQueryModelContract,
|
|
4248
|
+
isCommandModelContract,
|
|
4249
|
+
publicQueryModel,
|
|
4250
|
+
publicCommandModel,
|
|
4251
|
+
isPlannedCommandMethod,
|
|
4252
|
+
command,
|
|
4253
|
+
RELATION_TRAVERSAL_CONCURRENCY,
|
|
4254
|
+
mapWithConcurrency,
|
|
4255
|
+
buildRelationExecutionPlan,
|
|
4256
|
+
deriveCompositionPlan,
|
|
4257
|
+
stageOfResultPath,
|
|
4258
|
+
assertSupportedCondition,
|
|
4259
|
+
assertJsonSerializable,
|
|
4260
|
+
assertBundleSerializable,
|
|
4261
|
+
isTransactionRef,
|
|
4262
|
+
when,
|
|
4263
|
+
defineTransaction,
|
|
4264
|
+
defineTransactions,
|
|
4265
|
+
buildTransactionSpec,
|
|
4266
|
+
buildTransactions,
|
|
4267
|
+
collectContractN1Violations,
|
|
4268
|
+
assertContractN1Safe,
|
|
4269
|
+
buildContracts,
|
|
4270
|
+
buildContexts,
|
|
4271
|
+
collectContractBoundaryViolations,
|
|
4272
|
+
assertContractBoundaries,
|
|
4273
|
+
buildQuerySpec,
|
|
4274
|
+
buildOperations,
|
|
4275
|
+
buildBridgeBundle
|
|
4276
|
+
};
|