graphddb 0.7.6 → 0.7.8

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.
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  BATCH_GET_MAX_KEYS,
3
+ CONDITION_OPERATOR_KEYS,
3
4
  ChangeCaptureRegistry,
4
5
  ClientManager,
5
6
  MAX_TRANSACT_ITEMS,
@@ -21,6 +22,7 @@ import {
21
22
  chunkArray,
22
23
  commitTransaction,
23
24
  compileRawCondition,
25
+ condParamName,
24
26
  ctxModelFor,
25
27
  detectRelationFields,
26
28
  execItemKeySignature,
@@ -49,7 +51,7 @@ import {
49
51
  serializeRawCondition,
50
52
  skTemplate,
51
53
  validateInlineSnapshotSelect
52
- } from "./chunk-F2DI3GTI.js";
54
+ } from "./chunk-HFFIB77D.js";
53
55
  import {
54
56
  TableMapping,
55
57
  createColumnMap,
@@ -201,6 +203,246 @@ function buildManifest(registry = MetadataRegistry) {
201
203
  };
202
204
  }
203
205
 
206
+ // src/spec/guard.ts
207
+ function conditionInputToSpec(condition, context, renderLeaf2, renderTreeLeaf, renderRawSlot) {
208
+ if (condition === void 0 || condition === null) return void 0;
209
+ if (isRawCondition(condition)) {
210
+ const { expression, names, values } = serializeRawCondition(
211
+ condition,
212
+ renderRawSlot
213
+ );
214
+ return { kind: "raw", expression, names, values };
215
+ }
216
+ if (typeof condition !== "object") {
217
+ throw new Error(
218
+ `${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or a declarative field condition).`
219
+ );
220
+ }
221
+ const obj = condition;
222
+ if (obj.notExists === true) return { kind: "notExists" };
223
+ if ("attributeExists" in obj && typeof obj.attributeExists === "string") {
224
+ if (obj.attributeExists.length === 0) {
225
+ throw new Error(
226
+ `${context}: \`attributeExists\` must name a non-empty attribute field.`
227
+ );
228
+ }
229
+ return { kind: "attributeExists", field: obj.attributeExists };
230
+ }
231
+ if ("attributeNotExists" in obj) {
232
+ if (typeof obj.attributeNotExists !== "string" || obj.attributeNotExists.length === 0) {
233
+ throw new Error(
234
+ `${context}: \`attributeNotExists\` must name a non-empty attribute field.`
235
+ );
236
+ }
237
+ return { kind: "attributeNotExists", field: obj.attributeNotExists };
238
+ }
239
+ if (usesOperatorTree(obj)) {
240
+ const leaf = renderTreeLeaf ?? ((value, name) => defaultTreeLeaf(value, name, context));
241
+ return { kind: "expr", declarative: buildTree(obj, context, leaf) };
242
+ }
243
+ const fields = {};
244
+ for (const key of Object.keys(obj).sort()) {
245
+ fields[key] = renderLeaf2(key, obj[key]);
246
+ }
247
+ return { kind: "equals", fields };
248
+ }
249
+ function assertNotNestedRaw(sub, context) {
250
+ if (isRawCondition(sub)) {
251
+ throw new Error(
252
+ `${context}: a raw \`cond\` write condition may be used as a whole condition, but not nested inside an \`and\` / \`or\` / \`not\` group of a serializable (public-contract / Python-bridge) command. Move the \`cond\` to the top level, or express the group declaratively.`
253
+ );
254
+ }
255
+ }
256
+ var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
257
+ var OPERATOR_KEYS = /* @__PURE__ */ new Set([
258
+ "eq",
259
+ "ne",
260
+ "gt",
261
+ "ge",
262
+ "lt",
263
+ "le",
264
+ "between",
265
+ "in",
266
+ "beginsWith",
267
+ "contains",
268
+ "notContains",
269
+ "attributeExists",
270
+ "attributeType",
271
+ "size"
272
+ ]);
273
+ function isOperatorObject(value) {
274
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
275
+ return false;
276
+ }
277
+ const keys = Object.keys(value);
278
+ if (keys.length === 0) return false;
279
+ return keys.every((k) => OPERATOR_KEYS.has(k));
280
+ }
281
+ function usesOperatorTree(obj) {
282
+ for (const [key, value] of Object.entries(obj)) {
283
+ if (LOGICAL_KEYS.has(key)) return true;
284
+ if (isOperatorObject(value)) return true;
285
+ }
286
+ return false;
287
+ }
288
+ function defaultTreeLeaf(value, name, context) {
289
+ void name;
290
+ if (value instanceof Date) return value.toISOString();
291
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
292
+ return value;
293
+ }
294
+ throw new Error(
295
+ `${context}: condition operand must be a concrete literal or a param; got ${typeof value}.`
296
+ );
297
+ }
298
+ function conditionParamName(field, op, index) {
299
+ if (op === "between") return `${field}_${index === 0 ? "lo" : "hi"}`;
300
+ if (op === "in") return `${field}_${index ?? 0}`;
301
+ if (op === "size") return `${field}_size`;
302
+ return field;
303
+ }
304
+ function buildTree(obj, context, renderTreeLeaf) {
305
+ const out = {};
306
+ for (const key of Object.keys(obj).sort()) {
307
+ const value = obj[key];
308
+ if (value === void 0) continue;
309
+ if (key === "and" || key === "or") {
310
+ out[key] = value.map((sub) => {
311
+ assertNotNestedRaw(sub, context);
312
+ return buildTree(sub, context, renderTreeLeaf);
313
+ });
314
+ continue;
315
+ }
316
+ if (key === "not") {
317
+ assertNotNestedRaw(value, context);
318
+ out[key] = buildTree(
319
+ value,
320
+ context,
321
+ renderTreeLeaf
322
+ );
323
+ continue;
324
+ }
325
+ if (!isOperatorObject(value)) {
326
+ out[key] = { eq: renderTreeLeaf(value, conditionParamName(key, "eq")) };
327
+ continue;
328
+ }
329
+ const ops = value;
330
+ const rendered = {};
331
+ for (const op of Object.keys(ops).sort()) {
332
+ const opVal = ops[op];
333
+ if (op === "between") {
334
+ const [lo, hi] = opVal;
335
+ rendered[op] = [
336
+ renderTreeLeaf(lo, conditionParamName(key, op, 0)),
337
+ renderTreeLeaf(hi, conditionParamName(key, op, 1))
338
+ ];
339
+ } else if (op === "in") {
340
+ rendered[op] = opVal.map(
341
+ (v, i) => renderTreeLeaf(v, conditionParamName(key, op, i))
342
+ );
343
+ } else if (op === "attributeExists") {
344
+ rendered[op] = opVal;
345
+ } else if (op === "attributeType") {
346
+ rendered[op] = String(opVal);
347
+ } else {
348
+ rendered[op] = renderTreeLeaf(opVal, conditionParamName(key, op));
349
+ }
350
+ }
351
+ out[key] = rendered;
352
+ }
353
+ return out;
354
+ }
355
+ function assertSupportedCondition(commandName, condition) {
356
+ if (condition.kind === "notExists") return;
357
+ if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
358
+ if (typeof condition.field !== "string" || condition.field.length === 0) {
359
+ throw new Error(
360
+ `Command '${commandName}' has an ${condition.kind} write condition with no attribute field. The Python bridge requires a non-empty field name.`
361
+ );
362
+ }
363
+ return;
364
+ }
365
+ if (condition.kind === "equals") {
366
+ const entries = Object.entries(condition.fields);
367
+ if (entries.length === 0) {
368
+ throw new Error(
369
+ `Command '${commandName}' has an empty equality write condition. The Python bridge supports '{ notExists }' or non-empty field equality conditions only.`
370
+ );
371
+ }
372
+ for (const [field, value] of entries) {
373
+ if (typeof value !== "string") {
374
+ throw new Error(
375
+ `Command '${commandName}' write condition on '${field}' must be a template string value; the Python bridge supports equality on param/literal values only.`
376
+ );
377
+ }
378
+ }
379
+ return;
380
+ }
381
+ if (condition.kind === "raw") {
382
+ if (typeof condition.expression !== "string" || condition.expression.length === 0) {
383
+ throw new Error(
384
+ `Command '${commandName}' has an empty raw \`cond\` write condition.`
385
+ );
386
+ }
387
+ return;
388
+ }
389
+ if (condition.kind === "expr") {
390
+ const tree = condition.declarative;
391
+ if (!tree || Object.keys(tree).length === 0) {
392
+ throw new Error(
393
+ `Command '${commandName}' has an empty declarative write condition.`
394
+ );
395
+ }
396
+ return;
397
+ }
398
+ throw new Error(
399
+ `Command '${commandName}' has an unsupported write condition.`
400
+ );
401
+ }
402
+ function assertJsonSerializable(value, path = "$") {
403
+ if (value === null) return;
404
+ switch (typeof value) {
405
+ case "string":
406
+ case "boolean":
407
+ return;
408
+ case "number":
409
+ if (!Number.isFinite(value)) {
410
+ throw new Error(
411
+ `Bridge spec is not JSON-serializable: non-finite number at ${path}.`
412
+ );
413
+ }
414
+ return;
415
+ case "object":
416
+ break;
417
+ default:
418
+ throw new Error(
419
+ `Bridge spec is not JSON-serializable: '${typeof value}' at ${path}.`
420
+ );
421
+ }
422
+ if (Array.isArray(value)) {
423
+ value.forEach((v, i) => assertJsonSerializable(v, `${path}[${i}]`));
424
+ return;
425
+ }
426
+ const proto = Object.getPrototypeOf(value);
427
+ if (proto !== Object.prototype && proto !== null) {
428
+ throw new Error(
429
+ `Bridge spec is not JSON-serializable: non-plain object (${value.constructor?.name ?? "unknown"}) at ${path}.`
430
+ );
431
+ }
432
+ for (const [k, v] of Object.entries(value)) {
433
+ if (v === void 0) {
434
+ throw new Error(
435
+ `Bridge spec is not JSON-serializable: 'undefined' value at ${path}.${k}.`
436
+ );
437
+ }
438
+ assertJsonSerializable(v, `${path}.${k}`);
439
+ }
440
+ }
441
+ function assertBundleSerializable(bundle) {
442
+ assertJsonSerializable(bundle.manifest, "$.manifest");
443
+ assertJsonSerializable(bundle.operations, "$.operations");
444
+ }
445
+
204
446
  // src/planner/projection.ts
205
447
  function buildProjection(select, additionalFields) {
206
448
  const paths = [];
@@ -244,8 +486,8 @@ function buildProjection(select, additionalFields) {
244
486
  }
245
487
 
246
488
  // src/expression/filter-expression.ts
247
- var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
248
- var OPERATOR_KEYS = /* @__PURE__ */ new Set([
489
+ var LOGICAL_KEYS2 = /* @__PURE__ */ new Set(["and", "or", "not"]);
490
+ var OPERATOR_KEYS2 = /* @__PURE__ */ new Set([
249
491
  "eq",
250
492
  "ne",
251
493
  "gt",
@@ -277,7 +519,7 @@ function valueAlias(ctx, field, raw) {
277
519
  }
278
520
  function compileField(ctx, field, condition) {
279
521
  const nAlias = nameAlias(ctx, field);
280
- if (!isOperatorObject(condition)) {
522
+ if (!isOperatorObject2(condition)) {
281
523
  const vAlias = valueAlias(ctx, field, condition);
282
524
  return `${nAlias} = ${vAlias}`;
283
525
  }
@@ -346,13 +588,13 @@ function compileField(ctx, field, condition) {
346
588
  }
347
589
  return joinAnd(clauses);
348
590
  }
349
- function isOperatorObject(value) {
591
+ function isOperatorObject2(value) {
350
592
  if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || value instanceof Uint8Array || value instanceof Set) {
351
593
  return false;
352
594
  }
353
595
  const keys = Object.keys(value);
354
596
  if (keys.length === 0) return false;
355
- return keys.every((k) => OPERATOR_KEYS.has(k));
597
+ return keys.every((k) => OPERATOR_KEYS2.has(k));
356
598
  }
357
599
  function joinAnd(clauses) {
358
600
  if (clauses.length === 1) return clauses[0];
@@ -395,7 +637,7 @@ function compileNode(ctx, node) {
395
637
  const clauses = [];
396
638
  for (const [key, value] of Object.entries(node)) {
397
639
  if (value === void 0) continue;
398
- if (LOGICAL_KEYS.has(key)) {
640
+ if (LOGICAL_KEYS2.has(key)) {
399
641
  if (key === "and" || key === "or") {
400
642
  const parts = value.map((sub) => compileNode(ctx, sub)).filter((s) => s.length > 0);
401
643
  if (parts.length === 0) continue;
@@ -476,7 +718,7 @@ function eq(a, b) {
476
718
  return a === b;
477
719
  }
478
720
  function evaluateFieldCondition(actual, item, field, condition) {
479
- if (!isOperatorObject(condition)) {
721
+ if (!isOperatorObject2(condition)) {
480
722
  return eq(actual, condition);
481
723
  }
482
724
  const ops = condition;
@@ -2597,18 +2839,66 @@ function renderInputLeaf(leaf, isKeyFieldInPut, consumerIndex, consumerField, re
2597
2839
  }
2598
2840
  return leaf;
2599
2841
  }
2600
- function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenanceGraph) {
2601
- const keyFields = primaryKeyFields(fragment);
2602
- const keyFieldSet = new Set(keyFields);
2603
- const operation = INTENT_OPERATION[fragment.intent];
2604
- const lifecycle = resolveLifecycle(fragment);
2605
- for (const field of keyFields) {
2606
- if (!(field in fragment.input)) {
2607
- throw new Error(
2608
- `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 }\`.`
2609
- );
2610
- }
2842
+ function isConditionOperatorObject(value) {
2843
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isMutationInputRef(value)) {
2844
+ return false;
2845
+ }
2846
+ const keys = Object.keys(value);
2847
+ if (keys.length === 0) return false;
2848
+ return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
2849
+ }
2850
+ function lowerConditionLeaf(leaf) {
2851
+ if (isMutationInputRef(leaf)) return mintContractParamRef(leaf.field);
2852
+ return leaf;
2853
+ }
2854
+ function lowerFragmentCondition(condition) {
2855
+ const obj = condition;
2856
+ const out = {};
2857
+ for (const [key, value] of Object.entries(obj)) {
2858
+ if (value === void 0) continue;
2859
+ if (key === "and" || key === "or") {
2860
+ out[key] = value.map((sub) => lowerFragmentCondition(sub));
2861
+ continue;
2862
+ }
2863
+ if (key === "not") {
2864
+ out[key] = lowerFragmentCondition(value);
2865
+ continue;
2866
+ }
2867
+ if (!isConditionOperatorObject(value)) {
2868
+ out[key] = lowerConditionLeaf(value);
2869
+ continue;
2870
+ }
2871
+ const ops = value;
2872
+ const rendered = {};
2873
+ for (const [op, opVal] of Object.entries(ops)) {
2874
+ if (op === "between") {
2875
+ const [lo, hi] = opVal;
2876
+ rendered[op] = [lowerConditionLeaf(lo), lowerConditionLeaf(hi)];
2877
+ } else if (op === "in") {
2878
+ rendered[op] = opVal.map((v) => lowerConditionLeaf(v));
2879
+ } else if (op === "attributeExists" || op === "attributeType") {
2880
+ rendered[op] = opVal;
2881
+ } else {
2882
+ rendered[op] = lowerConditionLeaf(opVal);
2883
+ }
2884
+ }
2885
+ out[key] = rendered;
2886
+ }
2887
+ return out;
2888
+ }
2889
+ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenanceGraph) {
2890
+ const keyFields = primaryKeyFields(fragment);
2891
+ const keyFieldSet = new Set(keyFields);
2892
+ const operation = INTENT_OPERATION[fragment.intent];
2893
+ const lifecycle = resolveLifecycle(fragment);
2894
+ for (const field of keyFields) {
2895
+ if (!(field in fragment.input)) {
2896
+ throw new Error(
2897
+ `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 }\`.`
2898
+ );
2899
+ }
2611
2900
  }
2901
+ const userCondition = fragment.condition !== void 0 ? lowerFragmentCondition(fragment.condition) : void 0;
2612
2902
  let op;
2613
2903
  if (fragment.intent === "create") {
2614
2904
  const item = {};
@@ -2621,6 +2911,7 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
2621
2911
  resolveEntityRef
2622
2912
  );
2623
2913
  }
2914
+ const condition = userCondition !== void 0 ? { and: [{ PK: { attributeExists: false } }, userCondition] } : { notExists: true };
2624
2915
  op = {
2625
2916
  __isContractMethodOp: true,
2626
2917
  entity: fragment.entity,
@@ -2628,9 +2919,7 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
2628
2919
  keys: wholeKeysSentinel(),
2629
2920
  // a `put` derives identity from the item.
2630
2921
  item,
2631
- // The base `create` op guards against clobbering an existing row, exactly as
2632
- // the proposal specifies (create → Put `attribute_not_exists(PK)`).
2633
- condition: { notExists: true }
2922
+ condition
2634
2923
  };
2635
2924
  } else {
2636
2925
  const changes = {};
@@ -2644,7 +2933,11 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
2644
2933
  operation,
2645
2934
  keys: wholeKeysSentinel(),
2646
2935
  keyFields,
2647
- ...fragment.intent === "update" ? { changes } : {}
2936
+ ...fragment.intent === "update" ? { changes } : {},
2937
+ // #242: a conditioned update / remove — the write is a CAS-style gate (the
2938
+ // row must satisfy the condition or the write is rejected, not silently
2939
+ // committed). Absent → an unconditional update / remove (unchanged).
2940
+ ...userCondition !== void 0 ? { condition: userCondition } : {}
2648
2941
  };
2649
2942
  }
2650
2943
  const conditionChecks = lifecycle?.effects.requires !== void 0 && lifecycle.effects.requires.length > 0 ? deriveConditionChecks(fragment, lifecycle.effects.requires) : void 0;
@@ -2832,13 +3125,62 @@ function renderCondition(condition, key, params, context) {
2832
3125
  return { attributeNotExists: obj.attributeNotExists };
2833
3126
  }
2834
3127
  }
2835
- return renderRecord(
3128
+ return renderConditionTree(
2836
3129
  condition,
2837
3130
  key,
2838
3131
  params,
2839
3132
  `${context} condition`
2840
3133
  );
2841
3134
  }
3135
+ function isConditionOperatorObject2(value) {
3136
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isContractParamRef(value) || isContractKeyFieldRef(value)) {
3137
+ return false;
3138
+ }
3139
+ const keys = Object.keys(value);
3140
+ if (keys.length === 0) return false;
3141
+ return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
3142
+ }
3143
+ function renderConditionTree(node, key, params, context) {
3144
+ const out = {};
3145
+ for (const [k, value] of Object.entries(node)) {
3146
+ if (value === void 0) continue;
3147
+ if (k === "and" || k === "or") {
3148
+ out[k] = value.map(
3149
+ (sub) => renderConditionTree(sub, key, params, `${context} ${k}`)
3150
+ );
3151
+ continue;
3152
+ }
3153
+ if (k === "not") {
3154
+ out[k] = renderConditionTree(value, key, params, `${context} not`);
3155
+ continue;
3156
+ }
3157
+ if (!isConditionOperatorObject2(value)) {
3158
+ out[k] = renderLeaf(value, key, params, `${context} field '${k}'`);
3159
+ continue;
3160
+ }
3161
+ const ops = value;
3162
+ const rendered = {};
3163
+ for (const [op, opVal] of Object.entries(ops)) {
3164
+ if (op === "between") {
3165
+ const [lo, hi] = opVal;
3166
+ rendered[op] = [
3167
+ renderLeaf(lo, key, params, `${context} field '${k}' between[0]`),
3168
+ renderLeaf(hi, key, params, `${context} field '${k}' between[1]`)
3169
+ ];
3170
+ } else if (op === "in") {
3171
+ rendered[op] = opVal.map(
3172
+ (v, i) => renderLeaf(v, key, params, `${context} field '${k}' in[${i}]`)
3173
+ );
3174
+ } else if (op === "attributeExists" || op === "attributeType") {
3175
+ rendered[op] = opVal;
3176
+ } else {
3177
+ rendered[op] = renderLeaf(opVal, key, params, `${context} field '${k}' ${op}`);
3178
+ }
3179
+ }
3180
+ out[k] = rendered;
3181
+ }
3182
+ return out;
3183
+ }
2842
3184
  function renderWrite(op, key, params, contextLabel) {
2843
3185
  if (!isContractKeyRef(op.keys) && op.operation !== "put") {
2844
3186
  throw new Error(
@@ -2924,6 +3266,7 @@ function renderConditionCheck(check, key, params, contextLabel) {
2924
3266
  }
2925
3267
  var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
2926
3268
  function renderTemplate(template, key, params, contextLabel) {
3269
+ if (typeof template !== "string") return template;
2927
3270
  const resolveToken = (token) => {
2928
3271
  if (token in params) return params[token];
2929
3272
  if (token in key) return key[token];
@@ -4579,6 +4922,19 @@ var DDBModel = class {
4579
4922
  static setClient(client) {
4580
4923
  ClientManager.setClient(client);
4581
4924
  }
4925
+ /**
4926
+ * Read back the registered `DynamoDBClient` — the resolved client last passed to
4927
+ * {@link setClient}. Symmetric with {@link setClient} and backed by the same
4928
+ * {@link ClientManager} store, so it always reflects the client GraphDDB sends
4929
+ * through.
4930
+ *
4931
+ * Throws a clear error if no client has been registered yet (call
4932
+ * {@link setClient} first) — matching {@link ClientManager.getClient}'s "loud,
4933
+ * never silently `undefined`" convention shared with the rest of the client API.
4934
+ */
4935
+ static getClient() {
4936
+ return ClientManager.getClient();
4937
+ }
4582
4938
  static setTableMapping(mapping) {
4583
4939
  TableMapping.set(mapping);
4584
4940
  }
@@ -4932,23 +5288,107 @@ function assertFaithfulInput(input, intent, entityName) {
4932
5288
  }
4933
5289
  return input;
4934
5290
  }
5291
+ function isConditionOperatorObject3(value) {
5292
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isMutationInputRef(value)) {
5293
+ return false;
5294
+ }
5295
+ const keys = Object.keys(value);
5296
+ if (keys.length === 0) return false;
5297
+ return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
5298
+ }
5299
+ function assertFaithfulConditionLeaf(leaf, intent, entityName, where) {
5300
+ if (isMutationInputRef(leaf) || isConcreteLiteral(leaf)) return;
5301
+ if (isAliasFieldRef(leaf)) {
5302
+ throw new Error(
5303
+ `mutation: the '${intent}' fragment on '${entityName}' uses a \`$.alias.field\` cross-fragment reference in its \`condition\` (${where}). A write condition gates a single row against the command INPUT (or the row's own persisted attributes) \u2014 a \`TransactWriteItems\` cannot read another item's value mid-transaction to gate this write. Use a \`$.field\` input reference or a literal.`
5304
+ );
5305
+ }
5306
+ throw new Error(
5307
+ `mutation: the '${intent}' fragment on '${entityName}' binds a \`condition\` operand (${where}) to a value that is neither a \`$.field\` input reference nor a concrete literal. A write condition is declarative \u2014 only \`$.field\` references or literals are allowed, never a computed value.`
5308
+ );
5309
+ }
5310
+ function assertFaithfulCondition(condition, intent, entityName) {
5311
+ if (condition === null || typeof condition !== "object" || Array.isArray(condition)) {
5312
+ throw new Error(
5313
+ `mutation: the '${intent}' fragment on '${entityName}' has a non-object \`condition\`. A write condition is a declarative field-clause / logical-group object (e.g. \`{ status: 'PENDING', version: { in: [$.v] } }\` or \`{ and: [ \u2026 ] }\`).`
5314
+ );
5315
+ }
5316
+ const obj = condition;
5317
+ if (Object.keys(obj).length === 0) {
5318
+ throw new Error(
5319
+ `mutation: the '${intent}' fragment on '${entityName}' declared an EMPTY \`condition\` object. Omit \`condition\` entirely for an unconditional write, or declare a real gate.`
5320
+ );
5321
+ }
5322
+ for (const [key, value] of Object.entries(obj)) {
5323
+ if (value === void 0) continue;
5324
+ if (key === "and" || key === "or") {
5325
+ if (!Array.isArray(value) || value.length === 0) {
5326
+ throw new Error(
5327
+ `mutation: the '${intent}' fragment on '${entityName}' \`condition\` \`${key}\` must be a non-empty array of sub-conditions.`
5328
+ );
5329
+ }
5330
+ for (const sub of value) assertFaithfulCondition(sub, intent, entityName);
5331
+ continue;
5332
+ }
5333
+ if (key === "not") {
5334
+ assertFaithfulCondition(value, intent, entityName);
5335
+ continue;
5336
+ }
5337
+ if (!isConditionOperatorObject3(value)) {
5338
+ assertFaithfulConditionLeaf(value, intent, entityName, `field '${key}'`);
5339
+ continue;
5340
+ }
5341
+ const ops = value;
5342
+ for (const [op, opVal] of Object.entries(ops)) {
5343
+ if (op === "between") {
5344
+ if (!Array.isArray(opVal) || opVal.length !== 2) {
5345
+ throw new Error(
5346
+ `mutation: the '${intent}' fragment on '${entityName}' \`condition\` field '${key}' \`between\` takes a \`[lo, hi]\` pair.`
5347
+ );
5348
+ }
5349
+ assertFaithfulConditionLeaf(opVal[0], intent, entityName, `field '${key}' between[0]`);
5350
+ assertFaithfulConditionLeaf(opVal[1], intent, entityName, `field '${key}' between[1]`);
5351
+ } else if (op === "in") {
5352
+ if (!Array.isArray(opVal) || opVal.length === 0) {
5353
+ throw new Error(
5354
+ `mutation: the '${intent}' fragment on '${entityName}' \`condition\` field '${key}' \`in\` takes a non-empty array of operands.`
5355
+ );
5356
+ }
5357
+ opVal.forEach(
5358
+ (v, i) => assertFaithfulConditionLeaf(v, intent, entityName, `field '${key}' in[${i}]`)
5359
+ );
5360
+ } else if (op === "attributeExists" || op === "attributeType") {
5361
+ if (isMutationInputRef(opVal)) {
5362
+ throw new Error(
5363
+ `mutation: the '${intent}' fragment on '${entityName}' \`condition\` field '${key}' \`${op}\` was given a \`$.field\` reference. \`${op}\` takes a ${op === "attributeExists" ? "concrete boolean" : "concrete DynamoDB type-code string"} (a structural operand, not a value-bound param). Use a literal.`
5364
+ );
5365
+ }
5366
+ } else {
5367
+ assertFaithfulConditionLeaf(opVal, intent, entityName, `field '${key}' ${op}`);
5368
+ }
5369
+ }
5370
+ }
5371
+ return condition;
5372
+ }
4935
5373
  function isConcreteLiteral(value) {
4936
5374
  const t = typeof value;
4937
5375
  return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
4938
5376
  }
4939
- function makeFragment(intent, entity, input, use) {
5377
+ function makeFragment(intent, entity, input, use, condition) {
4940
5378
  const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
4941
5379
  if (use !== void 0 && !isEntityWritesDefinition(use)) {
4942
5380
  throw new Error(
4943
5381
  `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.`
4944
5382
  );
4945
5383
  }
5384
+ const checkedCondition = condition !== void 0 ? assertFaithfulCondition(condition, intent, entity.name) : void 0;
4946
5385
  return {
4947
5386
  [FRAGMENT_BRAND]: true,
4948
5387
  intent,
4949
5388
  entity,
4950
5389
  input: checked,
4951
- ...use !== void 0 ? { use } : {}
5390
+ ...use !== void 0 ? { use } : {},
5391
+ ...checkedCondition !== void 0 ? { condition: checkedCondition } : {}
4952
5392
  };
4953
5393
  }
4954
5394
  var COMMAND_PLAN_BRAND = /* @__PURE__ */ Symbol("graphddb:commandPlan");
@@ -5042,7 +5482,7 @@ function mutation(nameOrBody, maybeBody) {
5042
5482
  const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
5043
5483
  const merged = mergeBindings(descriptor, intent, alias);
5044
5484
  const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
5045
- return makeFragment(intent, entity, resolved, descriptor.use);
5485
+ return makeFragment(intent, entity, resolved, descriptor.use, descriptor.condition);
5046
5486
  });
5047
5487
  return { [COMMAND_PLAN_BRAND]: true, name, fragments };
5048
5488
  }
@@ -5362,320 +5802,148 @@ function descriptorKeyRecord(record, name, what) {
5362
5802
  }
5363
5803
  return out;
5364
5804
  }
5365
- function descriptorConditionRecord(record, name) {
5366
- const out = {};
5367
- for (const [field, leaf] of Object.entries(record)) {
5368
- if (isParam(leaf)) {
5369
- out[field] = mintContractParamRef(field);
5370
- } else {
5371
- out[field] = leaf;
5372
- }
5373
- }
5374
- return out;
5375
- }
5376
- function readDescriptorToOp(name, d) {
5377
- const isQuery = d.query !== void 0;
5378
- const isList = d.list !== void 0;
5379
- if (isQuery === isList) {
5380
- throw new Error(
5381
- `publicQueryModel: method '${name}' must carry exactly one of \`query\` or \`list\` (the target model); it declared ${isQuery && isList ? "both" : "neither"}.`
5382
- );
5383
- }
5384
- if (d.key === void 0 || d.select === void 0) {
5385
- throw new Error(
5386
- `publicQueryModel: the '${isQuery ? "query" : "list"}' descriptor for method '${name}' must declare both \`key\` and \`select\`.`
5387
- );
5388
- }
5389
- const model = resolveDescriptorModel(isQuery ? d.query : d.list, name);
5390
- const entity = entityRef(model);
5391
- const operation = isQuery ? "query" : "list";
5392
- const keyFieldNames2 = Object.keys(d.key);
5393
- const keyRecord = descriptorKeyRecord(d.key, name, `${operation} key`);
5394
- const { select, compose } = extractCompose({ ...d.select }, operation, entity.name);
5395
- const keys = isQuery ? wholeKeysSentinel() : keyRecord;
5396
- return {
5397
- __isContractMethodOp: true,
5398
- entity,
5399
- operation,
5400
- keys,
5401
- ...isQuery ? { keyFields: keyFieldNames2 } : {},
5402
- ...select !== void 0 ? { select } : {},
5403
- ...compose.length > 0 ? { compose } : {},
5404
- ...d.description !== void 0 ? { description: d.description } : {}
5405
- };
5406
- }
5407
- function writeDescriptorToPlan(name, d) {
5408
- const present = WRITE_INTENT_KEYS.filter((k) => d[k] !== void 0);
5409
- if (present.length !== 1) {
5410
- throw new Error(
5411
- `publicCommandModel: method '${name}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
5412
- );
5413
- }
5414
- const intent = present[0];
5415
- if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
5416
- throw new Error(
5417
- `publicCommandModel: the '${intent}' descriptor for method '${name}' must declare a \`key\` object binding the target's primary-key fields.`
5418
- );
5419
- }
5420
- const model = resolveDescriptorModel(d[intent], name);
5421
- const keyFieldNames2 = Object.keys(d.key);
5422
- const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
5423
- const plan2 = mutation(name, ($) => {
5424
- const refMap = (fields) => {
5425
- const out = {};
5426
- for (const f of fields) out[f] = $[f];
5427
- return out;
5428
- };
5429
- const descriptor = {
5430
- [intent]: (() => model),
5431
- key: refMap(keyFieldNames2),
5432
- ...inputFieldNames.length > 0 ? { input: refMap(inputFieldNames) } : {}
5433
- };
5434
- return { [name]: descriptor };
5435
- });
5436
- const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
5437
- const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
5438
- return { plan: plan2, select, condition, mode: d.mode ?? "transaction", description: d.description };
5439
- }
5440
-
5441
- // src/spec/guard.ts
5442
- function conditionInputToSpec(condition, context, renderLeaf2, renderTreeLeaf, renderRawSlot) {
5443
- if (condition === void 0 || condition === null) return void 0;
5444
- if (isRawCondition(condition)) {
5445
- const { expression, names, values } = serializeRawCondition(
5446
- condition,
5447
- renderRawSlot
5448
- );
5449
- return { kind: "raw", expression, names, values };
5450
- }
5451
- if (typeof condition !== "object") {
5452
- throw new Error(
5453
- `${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or a declarative field condition).`
5454
- );
5455
- }
5456
- const obj = condition;
5457
- if (obj.notExists === true) return { kind: "notExists" };
5458
- if ("attributeExists" in obj && typeof obj.attributeExists === "string") {
5459
- if (obj.attributeExists.length === 0) {
5460
- throw new Error(
5461
- `${context}: \`attributeExists\` must name a non-empty attribute field.`
5462
- );
5463
- }
5464
- return { kind: "attributeExists", field: obj.attributeExists };
5465
- }
5466
- if ("attributeNotExists" in obj) {
5467
- if (typeof obj.attributeNotExists !== "string" || obj.attributeNotExists.length === 0) {
5468
- throw new Error(
5469
- `${context}: \`attributeNotExists\` must name a non-empty attribute field.`
5470
- );
5471
- }
5472
- return { kind: "attributeNotExists", field: obj.attributeNotExists };
5473
- }
5474
- if (usesOperatorTree(obj)) {
5475
- const leaf = renderTreeLeaf ?? ((value, name) => defaultTreeLeaf(value, name, context));
5476
- return { kind: "expr", declarative: buildTree(obj, context, leaf) };
5477
- }
5478
- const fields = {};
5479
- for (const key of Object.keys(obj).sort()) {
5480
- fields[key] = renderLeaf2(key, obj[key]);
5481
- }
5482
- return { kind: "equals", fields };
5483
- }
5484
- function assertNotNestedRaw(sub, context) {
5485
- if (isRawCondition(sub)) {
5486
- throw new Error(
5487
- `${context}: a raw \`cond\` write condition may be used as a whole condition, but not nested inside an \`and\` / \`or\` / \`not\` group of a serializable (public-contract / Python-bridge) command. Move the \`cond\` to the top level, or express the group declaratively.`
5488
- );
5489
- }
5490
- }
5491
- var LOGICAL_KEYS2 = /* @__PURE__ */ new Set(["and", "or", "not"]);
5492
- var OPERATOR_KEYS2 = /* @__PURE__ */ new Set([
5493
- "eq",
5494
- "ne",
5495
- "gt",
5496
- "ge",
5497
- "lt",
5498
- "le",
5499
- "between",
5500
- "in",
5501
- "beginsWith",
5502
- "contains",
5503
- "notContains",
5504
- "attributeExists",
5505
- "attributeType",
5506
- "size"
5507
- ]);
5508
- function isOperatorObject2(value) {
5509
- if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
5805
+ function isConditionOperatorObject4(value) {
5806
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isParam(value) || isContractParamRef(value) || isContractKeyFieldRef(value)) {
5510
5807
  return false;
5511
5808
  }
5512
5809
  const keys = Object.keys(value);
5513
5810
  if (keys.length === 0) return false;
5514
- return keys.every((k) => OPERATOR_KEYS2.has(k));
5811
+ return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
5515
5812
  }
5516
- function usesOperatorTree(obj) {
5517
- for (const [key, value] of Object.entries(obj)) {
5518
- if (LOGICAL_KEYS2.has(key)) return true;
5519
- if (isOperatorObject2(value)) return true;
5520
- }
5521
- return false;
5813
+ function mintConditionOperand(leaf, field, op, index) {
5814
+ if (isParam(leaf)) return mintContractParamRef(conditionParamName(field, op, index));
5815
+ return leaf;
5522
5816
  }
5523
- function defaultTreeLeaf(value, name, context) {
5524
- void name;
5525
- if (value instanceof Date) return value.toISOString();
5526
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
5527
- return value;
5528
- }
5529
- throw new Error(
5530
- `${context}: condition operand must be a concrete literal or a param; got ${typeof value}.`
5531
- );
5817
+ function mintRawConditionParams(raw) {
5818
+ let paramIndex = 0;
5819
+ const parts = raw.parts.map((part) => {
5820
+ if (isColumn(part)) return part;
5821
+ if (isParam(part)) return mintContractParamRef(condParamName(paramIndex++));
5822
+ return part;
5823
+ });
5824
+ return { ...raw, parts };
5532
5825
  }
5533
- function conditionParamName(field, op, index) {
5534
- if (op === "between") return `${field}_${index === 0 ? "lo" : "hi"}`;
5535
- if (op === "in") return `${field}_${index ?? 0}`;
5536
- if (op === "size") return `${field}_size`;
5537
- return field;
5826
+ function descriptorConditionRecord(record, name) {
5827
+ if (isRawCondition(record)) {
5828
+ return mintRawConditionParams(record);
5829
+ }
5830
+ const top = record;
5831
+ if (top.notExists === true || typeof top.attributeExists === "string" || typeof top.attributeNotExists === "string") {
5832
+ return { ...top };
5833
+ }
5834
+ return descriptorConditionTree(top, name, `publicCommandModel: method '${name}' condition`);
5538
5835
  }
5539
- function buildTree(obj, context, renderTreeLeaf) {
5836
+ function descriptorConditionTree(obj, name, context) {
5540
5837
  const out = {};
5541
- for (const key of Object.keys(obj).sort()) {
5542
- const value = obj[key];
5838
+ for (const [key, value] of Object.entries(obj)) {
5543
5839
  if (value === void 0) continue;
5544
5840
  if (key === "and" || key === "or") {
5545
5841
  out[key] = value.map((sub) => {
5546
- assertNotNestedRaw(sub, context);
5547
- return buildTree(sub, context, renderTreeLeaf);
5842
+ assertNoNestedRawCond(sub, name);
5843
+ return descriptorConditionTree(sub, name, `${context} ${key}`);
5548
5844
  });
5549
5845
  continue;
5550
5846
  }
5551
5847
  if (key === "not") {
5552
- assertNotNestedRaw(value, context);
5553
- out[key] = buildTree(
5554
- value,
5555
- context,
5556
- renderTreeLeaf
5557
- );
5848
+ assertNoNestedRawCond(value, name);
5849
+ out[key] = descriptorConditionTree(value, name, `${context} not`);
5558
5850
  continue;
5559
5851
  }
5560
- if (!isOperatorObject2(value)) {
5561
- out[key] = { eq: renderTreeLeaf(value, conditionParamName(key, "eq")) };
5852
+ if (!isConditionOperatorObject4(value)) {
5853
+ out[key] = mintConditionOperand(value, key, "eq");
5562
5854
  continue;
5563
5855
  }
5564
5856
  const ops = value;
5565
5857
  const rendered = {};
5566
- for (const op of Object.keys(ops).sort()) {
5567
- const opVal = ops[op];
5858
+ for (const [op, opVal] of Object.entries(ops)) {
5568
5859
  if (op === "between") {
5569
5860
  const [lo, hi] = opVal;
5570
5861
  rendered[op] = [
5571
- renderTreeLeaf(lo, conditionParamName(key, op, 0)),
5572
- renderTreeLeaf(hi, conditionParamName(key, op, 1))
5862
+ mintConditionOperand(lo, key, op, 0),
5863
+ mintConditionOperand(hi, key, op, 1)
5573
5864
  ];
5574
5865
  } else if (op === "in") {
5575
- rendered[op] = opVal.map(
5576
- (v, i) => renderTreeLeaf(v, conditionParamName(key, op, i))
5577
- );
5578
- } else if (op === "attributeExists") {
5866
+ rendered[op] = opVal.map((v, i) => mintConditionOperand(v, key, op, i));
5867
+ } else if (op === "attributeExists" || op === "attributeType") {
5579
5868
  rendered[op] = opVal;
5580
- } else if (op === "attributeType") {
5581
- rendered[op] = String(opVal);
5582
- } else {
5583
- rendered[op] = renderTreeLeaf(opVal, conditionParamName(key, op));
5584
- }
5585
- }
5586
- out[key] = rendered;
5587
- }
5588
- return out;
5589
- }
5590
- function assertSupportedCondition(commandName, condition) {
5591
- if (condition.kind === "notExists") return;
5592
- if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
5593
- if (typeof condition.field !== "string" || condition.field.length === 0) {
5594
- throw new Error(
5595
- `Command '${commandName}' has an ${condition.kind} write condition with no attribute field. The Python bridge requires a non-empty field name.`
5596
- );
5597
- }
5598
- return;
5599
- }
5600
- if (condition.kind === "equals") {
5601
- const entries = Object.entries(condition.fields);
5602
- if (entries.length === 0) {
5603
- throw new Error(
5604
- `Command '${commandName}' has an empty equality write condition. The Python bridge supports '{ notExists }' or non-empty field equality conditions only.`
5605
- );
5606
- }
5607
- for (const [field, value] of entries) {
5608
- if (typeof value !== "string") {
5609
- throw new Error(
5610
- `Command '${commandName}' write condition on '${field}' must be a template string value; the Python bridge supports equality on param/literal values only.`
5611
- );
5869
+ } else {
5870
+ rendered[op] = mintConditionOperand(opVal, key, op);
5612
5871
  }
5613
5872
  }
5614
- return;
5615
- }
5616
- if (condition.kind === "raw") {
5617
- if (typeof condition.expression !== "string" || condition.expression.length === 0) {
5618
- throw new Error(
5619
- `Command '${commandName}' has an empty raw \`cond\` write condition.`
5620
- );
5621
- }
5622
- return;
5873
+ out[key] = rendered;
5623
5874
  }
5624
- if (condition.kind === "expr") {
5625
- const tree = condition.declarative;
5626
- if (!tree || Object.keys(tree).length === 0) {
5627
- throw new Error(
5628
- `Command '${commandName}' has an empty declarative write condition.`
5629
- );
5630
- }
5631
- return;
5875
+ return out;
5876
+ }
5877
+ function assertNoNestedRawCond(sub, name) {
5878
+ if (isRawCondition(sub)) {
5879
+ throw new Error(
5880
+ `publicCommandModel: method '${name}' condition nests a raw \`cond\` fragment inside an \`and\` / \`or\` / \`not\` group, which is not serializable. Use a \`cond\` as the whole condition, or express the group with declarative operators.`
5881
+ );
5632
5882
  }
5633
- throw new Error(
5634
- `Command '${commandName}' has an unsupported write condition.`
5635
- );
5636
5883
  }
5637
- function assertJsonSerializable(value, path = "$") {
5638
- if (value === null) return;
5639
- switch (typeof value) {
5640
- case "string":
5641
- case "boolean":
5642
- return;
5643
- case "number":
5644
- if (!Number.isFinite(value)) {
5645
- throw new Error(
5646
- `Bridge spec is not JSON-serializable: non-finite number at ${path}.`
5647
- );
5648
- }
5649
- return;
5650
- case "object":
5651
- break;
5652
- default:
5653
- throw new Error(
5654
- `Bridge spec is not JSON-serializable: '${typeof value}' at ${path}.`
5655
- );
5884
+ function readDescriptorToOp(name, d) {
5885
+ const isQuery = d.query !== void 0;
5886
+ const isList = d.list !== void 0;
5887
+ if (isQuery === isList) {
5888
+ throw new Error(
5889
+ `publicQueryModel: method '${name}' must carry exactly one of \`query\` or \`list\` (the target model); it declared ${isQuery && isList ? "both" : "neither"}.`
5890
+ );
5656
5891
  }
5657
- if (Array.isArray(value)) {
5658
- value.forEach((v, i) => assertJsonSerializable(v, `${path}[${i}]`));
5659
- return;
5892
+ if (d.key === void 0 || d.select === void 0) {
5893
+ throw new Error(
5894
+ `publicQueryModel: the '${isQuery ? "query" : "list"}' descriptor for method '${name}' must declare both \`key\` and \`select\`.`
5895
+ );
5660
5896
  }
5661
- const proto = Object.getPrototypeOf(value);
5662
- if (proto !== Object.prototype && proto !== null) {
5897
+ const model = resolveDescriptorModel(isQuery ? d.query : d.list, name);
5898
+ const entity = entityRef(model);
5899
+ const operation = isQuery ? "query" : "list";
5900
+ const keyFieldNames2 = Object.keys(d.key);
5901
+ const keyRecord = descriptorKeyRecord(d.key, name, `${operation} key`);
5902
+ const { select, compose } = extractCompose({ ...d.select }, operation, entity.name);
5903
+ const keys = isQuery ? wholeKeysSentinel() : keyRecord;
5904
+ return {
5905
+ __isContractMethodOp: true,
5906
+ entity,
5907
+ operation,
5908
+ keys,
5909
+ ...isQuery ? { keyFields: keyFieldNames2 } : {},
5910
+ ...select !== void 0 ? { select } : {},
5911
+ ...compose.length > 0 ? { compose } : {},
5912
+ ...d.description !== void 0 ? { description: d.description } : {}
5913
+ };
5914
+ }
5915
+ function writeDescriptorToPlan(name, d) {
5916
+ const present = WRITE_INTENT_KEYS.filter((k) => d[k] !== void 0);
5917
+ if (present.length !== 1) {
5663
5918
  throw new Error(
5664
- `Bridge spec is not JSON-serializable: non-plain object (${value.constructor?.name ?? "unknown"}) at ${path}.`
5919
+ `publicCommandModel: method '${name}' must carry exactly one intent key (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
5665
5920
  );
5666
5921
  }
5667
- for (const [k, v] of Object.entries(value)) {
5668
- if (v === void 0) {
5669
- throw new Error(
5670
- `Bridge spec is not JSON-serializable: 'undefined' value at ${path}.${k}.`
5671
- );
5672
- }
5673
- assertJsonSerializable(v, `${path}.${k}`);
5922
+ const intent = present[0];
5923
+ if (d.key === void 0 || d.key === null || typeof d.key !== "object") {
5924
+ throw new Error(
5925
+ `publicCommandModel: the '${intent}' descriptor for method '${name}' must declare a \`key\` object binding the target's primary-key fields.`
5926
+ );
5674
5927
  }
5675
- }
5676
- function assertBundleSerializable(bundle) {
5677
- assertJsonSerializable(bundle.manifest, "$.manifest");
5678
- assertJsonSerializable(bundle.operations, "$.operations");
5928
+ const model = resolveDescriptorModel(d[intent], name);
5929
+ const keyFieldNames2 = Object.keys(d.key);
5930
+ const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
5931
+ const plan2 = mutation(name, ($) => {
5932
+ const refMap = (fields) => {
5933
+ const out = {};
5934
+ for (const f of fields) out[f] = $[f];
5935
+ return out;
5936
+ };
5937
+ const descriptor = {
5938
+ [intent]: (() => model),
5939
+ key: refMap(keyFieldNames2),
5940
+ ...inputFieldNames.length > 0 ? { input: refMap(inputFieldNames) } : {}
5941
+ };
5942
+ return { [name]: descriptor };
5943
+ });
5944
+ const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
5945
+ const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
5946
+ return { plan: plan2, select, condition, mode: d.mode ?? "transaction", description: d.description };
5679
5947
  }
5680
5948
 
5681
5949
  // src/define/transaction.ts
@@ -6047,7 +6315,7 @@ function verifyWrite(instrs, surfaced) {
6047
6315
  verifyRecord(instrs.map((x) => x.item), surfaced, `${op} item`);
6048
6316
  verifyRecord(instrs.map((x) => x.key), surfaced, `${op} key`);
6049
6317
  verifyRecord(instrs.map((x) => x.changes), surfaced, `${op} changes`);
6050
- verifyRecord(
6318
+ verifyCondition(
6051
6319
  instrs.map((x) => x.condition),
6052
6320
  surfaced,
6053
6321
  `${op} condition`
@@ -6079,6 +6347,148 @@ function verifyRecord(records, surfaced, context) {
6079
6347
  );
6080
6348
  }
6081
6349
  }
6350
+ function isConditionOperatorObject5(value) {
6351
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isTransactionRef(value)) {
6352
+ return false;
6353
+ }
6354
+ const keys = Object.keys(value);
6355
+ if (keys.length === 0) return false;
6356
+ return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
6357
+ }
6358
+ function verifyCondition(conditions, surfaced, context) {
6359
+ const present = conditions.filter(
6360
+ (c) => c !== void 0 && c !== null
6361
+ );
6362
+ if (present.length === 0) return;
6363
+ if (present.length !== conditions.length) {
6364
+ throw new Error(
6365
+ `defineTransaction: ${context} present in some evaluation passes but not others; the callback must be a pure, declarative description.`
6366
+ );
6367
+ }
6368
+ if (present.some((c) => isRawCondition(c))) return;
6369
+ if (present.some((c) => typeof c !== "object" || Array.isArray(c))) {
6370
+ throw new Error(
6371
+ `defineTransaction: ${context} must be a declarative condition object (field clause / operator object / \`and\` / \`or\` / \`not\` group).`
6372
+ );
6373
+ }
6374
+ verifyConditionTree(
6375
+ present.map((c) => c),
6376
+ surfaced,
6377
+ context
6378
+ );
6379
+ }
6380
+ function verifyConditionTree(nodes, surfaced, context) {
6381
+ const keys = Object.keys(nodes[0]).sort();
6382
+ for (const n of nodes) {
6383
+ const k = Object.keys(n).sort();
6384
+ if (k.length !== keys.length || k.some((x, i) => x !== keys[i])) {
6385
+ throw new Error(
6386
+ `defineTransaction: ${context} key set diverged between evaluation passes; the callback must be a pure, declarative description.`
6387
+ );
6388
+ }
6389
+ }
6390
+ for (const key of keys) {
6391
+ const values = nodes.map((n) => n[key]);
6392
+ if (key === "and" || key === "or") {
6393
+ const arrays = values.map((v) => {
6394
+ if (!Array.isArray(v) || v.length === 0) {
6395
+ throw new Error(
6396
+ `defineTransaction: ${context} \`${key}\` must be a non-empty array of sub-conditions.`
6397
+ );
6398
+ }
6399
+ return v;
6400
+ });
6401
+ const len = arrays[0].length;
6402
+ if (arrays.some((a) => a.length !== len)) {
6403
+ throw new Error(
6404
+ `defineTransaction: ${context} \`${key}\` length diverged between evaluation passes; the callback must be declarative.`
6405
+ );
6406
+ }
6407
+ for (let i = 0; i < len; i++) {
6408
+ verifyCondition(
6409
+ arrays.map((a) => a[i]),
6410
+ surfaced,
6411
+ `${context} ${key}[${i}]`
6412
+ );
6413
+ }
6414
+ continue;
6415
+ }
6416
+ if (key === "not") {
6417
+ verifyCondition(
6418
+ values,
6419
+ surfaced,
6420
+ `${context} not`
6421
+ );
6422
+ continue;
6423
+ }
6424
+ if (!values.every((v) => isConditionOperatorObject5(v))) {
6425
+ if (values.some((v) => isConditionOperatorObject5(v))) {
6426
+ throw new Error(
6427
+ `defineTransaction: ${context} field '${key}' is an operator object in some evaluation passes but a bare value in others; the callback must be declarative.`
6428
+ );
6429
+ }
6430
+ verifyLeaf(values, surfaced, `${context} field '${key}'`);
6431
+ continue;
6432
+ }
6433
+ verifyOperatorObject(
6434
+ values.map((v) => v),
6435
+ surfaced,
6436
+ `${context} field '${key}'`
6437
+ );
6438
+ }
6439
+ }
6440
+ function verifyOperatorObject(objs, surfaced, context) {
6441
+ const ops = Object.keys(objs[0]).sort();
6442
+ for (const o of objs) {
6443
+ const k = Object.keys(o).sort();
6444
+ if (k.length !== ops.length || k.some((x, i) => x !== ops[i])) {
6445
+ throw new Error(
6446
+ `defineTransaction: ${context} operator set diverged between evaluation passes; the callback must be a pure, declarative description.`
6447
+ );
6448
+ }
6449
+ }
6450
+ for (const op of ops) {
6451
+ const opVals = objs.map((o) => o[op]);
6452
+ if (op === "between") {
6453
+ const pairs = opVals.map((v) => {
6454
+ if (!Array.isArray(v) || v.length !== 2) {
6455
+ throw new Error(
6456
+ `defineTransaction: ${context} \`between\` takes a \`[lo, hi]\` pair.`
6457
+ );
6458
+ }
6459
+ return v;
6460
+ });
6461
+ verifyLeaf(pairs.map((p) => p[0]), surfaced, `${context} between[0]`);
6462
+ verifyLeaf(pairs.map((p) => p[1]), surfaced, `${context} between[1]`);
6463
+ } else if (op === "in") {
6464
+ const arrays = opVals.map((v) => {
6465
+ if (!Array.isArray(v) || v.length === 0) {
6466
+ throw new Error(
6467
+ `defineTransaction: ${context} \`in\` takes a non-empty array of operands.`
6468
+ );
6469
+ }
6470
+ return v;
6471
+ });
6472
+ const len = arrays[0].length;
6473
+ if (arrays.some((a) => a.length !== len)) {
6474
+ throw new Error(
6475
+ `defineTransaction: ${context} \`in\` length diverged between evaluation passes; the callback must be declarative.`
6476
+ );
6477
+ }
6478
+ for (let i = 0; i < len; i++) {
6479
+ verifyLeaf(arrays.map((a) => a[i]), surfaced, `${context} in[${i}]`);
6480
+ }
6481
+ } else if (op === "attributeExists" || op === "attributeType") {
6482
+ if (opVals.some((v) => isTransactionRef(v))) {
6483
+ throw new Error(
6484
+ `defineTransaction: ${context} \`${op}\` was given a field reference. \`${op}\` takes a ${op === "attributeExists" ? "concrete boolean" : "concrete DynamoDB type-code string"} (a structural operand, not a value-bound param); use a literal.`
6485
+ );
6486
+ }
6487
+ } else {
6488
+ verifyLeaf(opVals, surfaced, `${context} ${op}`);
6489
+ }
6490
+ }
6491
+ }
6082
6492
  function verifyWhen(whens, surfaced, context) {
6083
6493
  const present = whens.filter((w) => w !== void 0);
6084
6494
  if (present.length === 0) return;
@@ -6191,10 +6601,19 @@ function conditionTreeLeaf(value, name, context) {
6191
6601
  `defineTransaction: ${context} condition operand must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
6192
6602
  );
6193
6603
  }
6194
- function templateRecord2(structure, context) {
6604
+ function valueLeaf(value, context) {
6605
+ if (isTransactionRef(value)) return value.token;
6606
+ if (value instanceof Date) return value.toISOString();
6607
+ if (typeof value === "number" || typeof value === "boolean") return value;
6608
+ if (typeof value === "string") return value;
6609
+ throw new Error(
6610
+ `defineTransaction: ${context} must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
6611
+ );
6612
+ }
6613
+ function valueRecord(structure, context) {
6195
6614
  const out = {};
6196
6615
  for (const field of Object.keys(structure).sort()) {
6197
- out[field] = leafTemplate(structure[field], `${context} field '${field}'`);
6616
+ out[field] = valueLeaf(structure[field], `${context} field '${field}'`);
6198
6617
  }
6199
6618
  return out;
6200
6619
  }
@@ -6267,7 +6686,7 @@ function buildWriteItem(instr, forEachSource, txName) {
6267
6686
  if (instr.operation === "put") {
6268
6687
  return {
6269
6688
  ...base,
6270
- item: templateRecord2(instr.item ?? {}, `${txName} put`)
6689
+ item: valueRecord(instr.item ?? {}, `${txName} put`)
6271
6690
  };
6272
6691
  }
6273
6692
  if (instr.operation === "delete") {
@@ -6291,7 +6710,7 @@ function buildWriteItem(instr, forEachSource, txName) {
6291
6710
  )
6292
6711
  };
6293
6712
  }
6294
- const changes = templateRecord2(instr.changes ?? {}, `${txName} update changes`);
6713
+ const changes = valueRecord(instr.changes ?? {}, `${txName} update changes`);
6295
6714
  injectTransactionGsiRederive(
6296
6715
  metadata,
6297
6716
  instr.key ?? {},
@@ -6480,8 +6899,8 @@ function recoverKind(metadata, fieldName) {
6480
6899
  function opToDefinition(contractName, methodName, op) {
6481
6900
  const metadata = MetadataRegistry.get(op.entity.modelClass);
6482
6901
  const params = {};
6483
- const place = (paramName, bindField) => {
6484
- const recovered = recoverKind(metadata, bindField);
6902
+ const place = (paramName, bindField, kindOverride) => {
6903
+ const recovered = kindOverride !== void 0 ? { kind: kindOverride } : recoverKind(metadata, bindField);
6485
6904
  params[paramName] = recovered.literals === void 0 ? { kind: recovered.kind, required: true } : { kind: recovered.kind, literals: recovered.literals, required: true };
6486
6905
  return recovered;
6487
6906
  };
@@ -6555,13 +6974,13 @@ function convertStructure(structure, place, context) {
6555
6974
  }
6556
6975
  return out;
6557
6976
  }
6558
- function convertLeaf(leaf, bindField, place, context) {
6977
+ function convertLeaf(leaf, bindField, place, context, kindOverride) {
6559
6978
  if (isContractParamRef(leaf)) {
6560
- place(leaf.field, bindField);
6979
+ place(leaf.field, bindField, kindOverride);
6561
6980
  return leaf;
6562
6981
  }
6563
6982
  if (isContractKeyFieldRef(leaf)) {
6564
- place(leaf.field, bindField);
6983
+ place(leaf.field, bindField, kindOverride);
6565
6984
  return leaf;
6566
6985
  }
6567
6986
  if (isConcreteLiteral3(leaf)) return leaf;
@@ -6569,24 +6988,118 @@ function convertLeaf(leaf, bindField, place, context) {
6569
6988
  `${context}: value is neither a param/key reference nor a concrete literal \u2014 it cannot be serialized as a declarative contract operation.`
6570
6989
  );
6571
6990
  }
6991
+ function nearestColumnFields(condition) {
6992
+ const parts = condition.parts;
6993
+ const columnAt = (i) => isColumn(parts[i]) ? parts[i].name : void 0;
6994
+ const out = [];
6995
+ for (let i = 0; i < parts.length; i++) {
6996
+ const part = parts[i];
6997
+ if (!isContractParamRef(part) && !isContractKeyFieldRef(part)) continue;
6998
+ let found;
6999
+ for (let d = 1; d < parts.length && found === void 0; d++) {
7000
+ found = columnAt(i - d) ?? columnAt(i + d);
7001
+ }
7002
+ out.push(found);
7003
+ }
7004
+ return out;
7005
+ }
6572
7006
  function convertCondition(condition, place, context) {
6573
- if (isRawCondition(condition)) return condition;
7007
+ if (isRawCondition(condition)) {
7008
+ const compareFieldForSlot = nearestColumnFields(condition);
7009
+ let slotIndex = 0;
7010
+ for (const part of condition.parts) {
7011
+ if (isContractParamRef(part) || isContractKeyFieldRef(part)) {
7012
+ const compareField = compareFieldForSlot[slotIndex];
7013
+ if (compareField === void 0) {
7014
+ throw new Error(
7015
+ `${context}: a raw \`cond\` value slot ('${part.field}') has no adjacent column, so its param type cannot be inferred. A \`cond\` param slot must be compared against a \`Model.col.<field>\` reference (e.g. \`cond\`\${Model.col.x} < \${param.number()}\`\`) so the parameter inherits that column's type; refusing to emit an un-typed (silently 'string') param.`
7016
+ );
7017
+ }
7018
+ place(part.field, compareField);
7019
+ slotIndex++;
7020
+ }
7021
+ }
7022
+ return condition;
7023
+ }
6574
7024
  if (typeof condition === "object" && condition !== null) {
6575
- const obj = condition;
6576
- if (obj.notExists === true) return { notExists: true };
6577
- if (typeof obj.attributeExists === "string") {
6578
- return { attributeExists: obj.attributeExists };
7025
+ const obj2 = condition;
7026
+ if (obj2.notExists === true) return { notExists: true };
7027
+ if (typeof obj2.attributeExists === "string") {
7028
+ return { attributeExists: obj2.attributeExists };
6579
7029
  }
6580
- if (typeof obj.attributeNotExists === "string") {
6581
- return { attributeNotExists: obj.attributeNotExists };
7030
+ if (typeof obj2.attributeNotExists === "string") {
7031
+ return { attributeNotExists: obj2.attributeNotExists };
6582
7032
  }
6583
7033
  }
7034
+ const obj = condition;
7035
+ if (usesConditionTree(obj)) {
7036
+ return convertConditionTree(obj, place, context);
7037
+ }
6584
7038
  const out = {};
6585
- for (const [field, leaf] of Object.entries(condition)) {
7039
+ for (const [field, leaf] of Object.entries(obj)) {
6586
7040
  out[field] = convertLeaf(leaf, field, place, `${context} field '${field}'`);
6587
7041
  }
6588
7042
  return out;
6589
7043
  }
7044
+ var CONDITION_LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
7045
+ function isConditionOperatorObject6(value) {
7046
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date || isContractParamRef(value) || isContractKeyFieldRef(value)) {
7047
+ return false;
7048
+ }
7049
+ const keys = Object.keys(value);
7050
+ if (keys.length === 0) return false;
7051
+ return keys.every((k) => CONDITION_OPERATOR_KEYS.has(k));
7052
+ }
7053
+ function usesConditionTree(obj) {
7054
+ for (const [key, value] of Object.entries(obj)) {
7055
+ if (CONDITION_LOGICAL_KEYS.has(key)) return true;
7056
+ if (isConditionOperatorObject6(value)) return true;
7057
+ }
7058
+ return false;
7059
+ }
7060
+ function convertConditionTree(obj, place, context) {
7061
+ const out = {};
7062
+ for (const [key, value] of Object.entries(obj)) {
7063
+ if (value === void 0) continue;
7064
+ if (key === "and" || key === "or") {
7065
+ out[key] = value.map(
7066
+ (sub) => convertConditionTree(sub, place, `${context} ${key}`)
7067
+ );
7068
+ continue;
7069
+ }
7070
+ if (key === "not") {
7071
+ out[key] = convertConditionTree(value, place, `${context} not`);
7072
+ continue;
7073
+ }
7074
+ if (!isConditionOperatorObject6(value)) {
7075
+ out[key] = convertLeaf(value, key, place, `${context} field '${key}'`);
7076
+ continue;
7077
+ }
7078
+ const ops = value;
7079
+ const rendered = {};
7080
+ for (const [op, opVal] of Object.entries(ops)) {
7081
+ if (op === "between") {
7082
+ const [lo, hi] = opVal;
7083
+ rendered[op] = [
7084
+ convertLeaf(lo, key, place, `${context} field '${key}' between[0]`),
7085
+ convertLeaf(hi, key, place, `${context} field '${key}' between[1]`)
7086
+ ];
7087
+ } else if (op === "in") {
7088
+ rendered[op] = opVal.map(
7089
+ (v, i) => convertLeaf(v, key, place, `${context} field '${key}' in[${i}]`)
7090
+ );
7091
+ } else if (op === "attributeExists" || op === "attributeType") {
7092
+ rendered[op] = opVal;
7093
+ } else if (op === "size") {
7094
+ rendered[op] = convertLeaf(opVal, key, place, `${context} field '${key}' ${op}`, "number");
7095
+ } else {
7096
+ rendered[op] = convertLeaf(opVal, key, place, `${context} field '${key}' ${op}`);
7097
+ }
7098
+ }
7099
+ out[key] = rendered;
7100
+ }
7101
+ return out;
7102
+ }
6590
7103
  function assertBooleanProjection(select, context) {
6591
7104
  const out = {};
6592
7105
  for (const [field, leaf] of Object.entries(select)) {
@@ -6897,6 +7410,7 @@ var TEMPLATE_TOKEN_RE = /\{([^{}]+)\}/g;
6897
7410
  function collectTemplateParams(record, metadata, out) {
6898
7411
  if (record === void 0) return;
6899
7412
  for (const template of Object.values(record)) {
7413
+ if (typeof template !== "string") continue;
6900
7414
  for (const match of template.matchAll(TEMPLATE_TOKEN_RE)) {
6901
7415
  const token = match[1];
6902
7416
  const field = token.startsWith("old.") ? token.slice("old.".length) : token;
@@ -7775,7 +8289,7 @@ function buildCommandSpec(def) {
7775
8289
  const condition = extractCondition(def);
7776
8290
  const description = def.description !== void 0 ? { description: def.description } : {};
7777
8291
  if (def.operation === "put") {
7778
- const item = templateRecord3(def.key);
8292
+ const item = templateRecord2(def.key);
7779
8293
  return {
7780
8294
  type: "PutItem",
7781
8295
  tableName,
@@ -7803,7 +8317,7 @@ function buildCommandSpec(def) {
7803
8317
  entity,
7804
8318
  params,
7805
8319
  keyCondition: writeKeyCondition(metadata, def.key, entity),
7806
- changes: templateRecord3(def.changes),
8320
+ changes: templateRecord2(def.changes),
7807
8321
  ...condition ? { condition } : {},
7808
8322
  ...description
7809
8323
  };
@@ -7827,7 +8341,7 @@ function writeKeyCondition(metadata, key, entity) {
7827
8341
  if (sk !== void 0) out.SK = sk;
7828
8342
  return out;
7829
8343
  }
7830
- function templateRecord3(structure) {
8344
+ function templateRecord2(structure) {
7831
8345
  const out = {};
7832
8346
  for (const key of Object.keys(structure).sort()) {
7833
8347
  out[key] = templateLeaf(key, structure[key]);
@@ -8158,6 +8672,10 @@ export {
8158
8672
  MARKER_ROW_ENTITY,
8159
8673
  buildManifestEntity,
8160
8674
  buildManifest,
8675
+ conditionParamName,
8676
+ assertSupportedCondition,
8677
+ assertJsonSerializable,
8678
+ assertBundleSerializable,
8161
8679
  buildProjection,
8162
8680
  compileFilterExpression,
8163
8681
  evaluateFilter,
@@ -8225,10 +8743,6 @@ export {
8225
8743
  publicQueryModel,
8226
8744
  publicCommandModel,
8227
8745
  isPlannedCommandMethod,
8228
- conditionParamName,
8229
- assertSupportedCondition,
8230
- assertJsonSerializable,
8231
- assertBundleSerializable,
8232
8746
  isTransactionRef,
8233
8747
  when,
8234
8748
  defineTransaction,