graphddb 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,7 +17,7 @@ import {
17
17
  normalizeTopLevelSelect,
18
18
  parseChange,
19
19
  validateInlineSnapshotSelect
20
- } from "./chunk-HNY2EJPV.js";
20
+ } from "./chunk-7OCXY4R6.js";
21
21
  import {
22
22
  EXPR_VERSION,
23
23
  MARKER_ROW_ENTITY,
@@ -26,7 +26,7 @@ import {
26
26
  isScpExprConditionInput,
27
27
  isTransactionDefinition,
28
28
  isTransactionRef
29
- } from "./chunk-L4QRCHRQ.js";
29
+ } from "./chunk-WOFRHRXY.js";
30
30
  import {
31
31
  BATCH_GET_MAX_KEYS,
32
32
  CONDITION_OPERATOR_KEYS,
@@ -61,7 +61,7 @@ import {
61
61
  serializeFieldValue,
62
62
  serializeRawCondition,
63
63
  skTemplate
64
- } from "./chunk-L2NEDS7U.js";
64
+ } from "./chunk-GWWRXIHF.js";
65
65
  import {
66
66
  TableMapping,
67
67
  createColumnMap,
@@ -2520,7 +2520,12 @@ function renderWrite(op, key, params, contextLabel) {
2520
2520
  return { ...base, item: renderRecord(op.item, key, params, `${contextLabel} item`) };
2521
2521
  }
2522
2522
  if (op.operation === "update") {
2523
- return { ...base, changes: renderRecord(op.changes, key, params, `${contextLabel} changes`) };
2523
+ const add = op.add !== void 0 && Object.keys(op.add).length > 0 ? renderRecord(op.add, key, params, `${contextLabel} add`) : void 0;
2524
+ return {
2525
+ ...base,
2526
+ changes: renderRecord(op.changes, key, params, `${contextLabel} changes`),
2527
+ ...add !== void 0 ? { add } : {}
2528
+ };
2524
2529
  }
2525
2530
  return base;
2526
2531
  }
@@ -2534,11 +2539,34 @@ async function executeSingleWrite(w, retry) {
2534
2539
  return;
2535
2540
  }
2536
2541
  if (w.operation === "update") {
2542
+ if (w.add !== void 0) {
2543
+ const primaryOptions = { ...options ?? {}, add: w.add };
2544
+ if (w.condition === void 0) {
2545
+ await executeUpdate(w.modelClass, w.key, w.changes ?? {}, primaryOptions);
2546
+ return;
2547
+ }
2548
+ try {
2549
+ await executeUpdate(w.modelClass, w.key, w.changes ?? {}, primaryOptions);
2550
+ } catch (e) {
2551
+ if (!isConditionalCheckFailure(e)) throw e;
2552
+ const fallbackOptions = {
2553
+ add: w.add,
2554
+ ...retry !== void 0 ? { retry } : {}
2555
+ };
2556
+ await executeUpdate(w.modelClass, w.key, {}, fallbackOptions);
2557
+ }
2558
+ return;
2559
+ }
2537
2560
  await executeUpdate(w.modelClass, w.key, w.changes ?? {}, options);
2538
2561
  return;
2539
2562
  }
2540
2563
  await executeDelete(w.modelClass, w.key, options);
2541
2564
  }
2565
+ function isConditionalCheckFailure(e) {
2566
+ if (e === null || typeof e !== "object") return false;
2567
+ const name = e.name;
2568
+ return name === "ConditionalCheckFailedException" || name === "ConditionalCheckFailed";
2569
+ }
2542
2570
  function renderConditionCheck(check, key, params, contextLabel) {
2543
2571
  const modelClass = check.entity.modelClass;
2544
2572
  const referencedKey = {};
@@ -4428,8 +4456,17 @@ function isConcreteLiteral(value) {
4428
4456
  const t = typeof value;
4429
4457
  return value === null || value instanceof Date || t === "string" || t === "number" || t === "boolean";
4430
4458
  }
4431
- function makeFragment(intent, entity, input, use, condition) {
4459
+ function makeFragment(intent, entity, input, use, condition, add) {
4432
4460
  const checked = assertFaithfulInput(input ?? {}, intent, entity.name);
4461
+ let checkedAdd;
4462
+ if (add !== void 0 && Object.keys(add).length > 0) {
4463
+ if (intent !== "update") {
4464
+ throw new Error(
4465
+ `mutation: the '${intent}' fragment on '${entity.name}' declares \`add\`, but an atomic \`ADD\` (issue #301) is only valid on an \`update\` fragment.`
4466
+ );
4467
+ }
4468
+ checkedAdd = assertFaithfulInput(add, intent, entity.name);
4469
+ }
4433
4470
  if (use !== void 0 && !isEntityWritesDefinition(use)) {
4434
4471
  throw new Error(
4435
4472
  `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.`
@@ -4441,6 +4478,7 @@ function makeFragment(intent, entity, input, use, condition) {
4441
4478
  intent,
4442
4479
  entity,
4443
4480
  input: checked,
4481
+ ...checkedAdd !== void 0 ? { add: checkedAdd } : {},
4444
4482
  ...use !== void 0 ? { use } : {},
4445
4483
  ...checkedCondition !== void 0 ? { condition: checkedCondition } : {}
4446
4484
  };
@@ -4536,7 +4574,8 @@ function mutation(nameOrBody, maybeBody) {
4536
4574
  const { intent, entity } = resolveDescriptorTarget(descriptor, alias);
4537
4575
  const merged = mergeBindings(descriptor, intent, alias);
4538
4576
  const resolved = resolveAliasRefs(merged, aliasToIndex, alias);
4539
- return makeFragment(intent, entity, resolved, descriptor.use, descriptor.condition);
4577
+ const add = descriptor.add !== void 0 ? resolveAliasRefs(descriptor.add, aliasToIndex, alias) : void 0;
4578
+ return makeFragment(intent, entity, resolved, descriptor.use, descriptor.condition, add);
4540
4579
  });
4541
4580
  return { [COMMAND_PLAN_BRAND]: true, name, fragments };
4542
4581
  }
@@ -4680,8 +4719,16 @@ function buildCommandContract(methods) {
4680
4719
  const resolved = {};
4681
4720
  for (const [name, body] of Object.entries(methods)) {
4682
4721
  if (isPublicWriteDescriptor(body)) {
4683
- const { plan: plan2, select, condition, mode, description } = writeDescriptorToPlan(name, body);
4684
- resolved[name] = finalizeDescriptorCommand(name, plan2, select, condition, mode, description);
4722
+ const { plan: plan2, select, condition, mode, batchWriteEligible, description } = writeDescriptorToPlan(name, body);
4723
+ resolved[name] = finalizeDescriptorCommand(
4724
+ name,
4725
+ plan2,
4726
+ select,
4727
+ condition,
4728
+ mode,
4729
+ description,
4730
+ batchWriteEligible
4731
+ );
4685
4732
  continue;
4686
4733
  }
4687
4734
  if (isPublicCompositeDescriptor(body)) {
@@ -4771,7 +4818,7 @@ function buildPlannedCommandMethod(name, planned) {
4771
4818
  ...returnSelection !== void 0 ? { returnSelection } : {}
4772
4819
  };
4773
4820
  }
4774
- function finalizeDescriptorCommand(name, plan2, select, condition, mode, description) {
4821
+ function finalizeDescriptorCommand(name, plan2, select, condition, declaredMode, description, batchWriteEligible = false) {
4775
4822
  const planned = {
4776
4823
  [PLANNED_COMMAND_BRAND]: true,
4777
4824
  plan: plan2,
@@ -4779,6 +4826,13 @@ function finalizeDescriptorCommand(name, plan2, select, condition, mode, descrip
4779
4826
  select
4780
4827
  };
4781
4828
  const built = buildPlannedCommandMethod(name, planned);
4829
+ const promoted = built.ops !== void 0 && built.ops.length > 1 || built.conditionChecks !== void 0 || built.edgeWrites !== void 0 || built.derivedUpdates !== void 0 || built.uniqueGuards !== void 0 || built.outboxEvents !== void 0 || built.idempotencyGuard !== void 0 || built.maintainWrites !== void 0 || built.maintainOutbox !== void 0;
4830
+ if (declaredMode === "batchWrite" && (!batchWriteEligible || promoted)) {
4831
+ throw new Error(
4832
+ `publishCommand: method '${name}' declares mode: 'batchWrite', but DynamoDB's BatchWriteItem carries no ConditionExpression, no Update and no composed derived effects \u2014 only an UNCONDITIONAL put/delete qualifies (an \`upsert\` or a condition-free \`remove\` with no derived effects). This method is ${!batchWriteEligible ? "conditional / not an unconditional put/delete" : "promoted to an atomic TransactWriteItems by its derived effects"}. Use mode: 'transaction' (atomic TransactWriteItems, condition-capable, \u226425) \u2014 or the transaction forEach form for atomic per-item values.`
4833
+ );
4834
+ }
4835
+ const mode = declaredMode ?? (batchWriteEligible && !promoted ? "batchWrite" : "transaction");
4782
4836
  const builtOp = built.op;
4783
4837
  const op = condition !== void 0 ? { ...builtOp, condition } : builtOp;
4784
4838
  return {
@@ -4992,6 +5046,35 @@ function writeDescriptorToPlan(name, d) {
4992
5046
  const model = resolveDescriptorModel(d[intent], name);
4993
5047
  const keyFieldNames = Object.keys(d.key);
4994
5048
  const inputFieldNames = d.input !== void 0 ? Object.keys(d.input) : [];
5049
+ const addFieldNames = d.add !== void 0 ? Object.keys(d.add) : [];
5050
+ if (addFieldNames.length > 0) {
5051
+ if (intent !== "update") {
5052
+ throw new Error(
5053
+ `publishCommand: method '${name}' declares \`add\` on a '${intent}' descriptor, but an atomic \`ADD\` (issue #301) is only valid on an \`update\` (a standalone UpdateItem). A create / upsert / remove cannot carry \`add\`.`
5054
+ );
5055
+ }
5056
+ if (d.add === null || typeof d.add !== "object") {
5057
+ throw new Error(`publishCommand: method '${name}' has a non-object \`add\`.`);
5058
+ }
5059
+ for (const field of addFieldNames) {
5060
+ const v = d.add[field];
5061
+ if (!isParam(v) || v.kind !== "number") {
5062
+ throw new Error(
5063
+ `publishCommand: method '${name}' binds \`add.${field}\` to a non-\`param.number()\` value. An atomic \`ADD\` delta (issue #301) is a caller \`param.number()\` \u2014 a compile-time constant or a non-numeric param is not a valid atomic increment.`
5064
+ );
5065
+ }
5066
+ if (keyFieldNames.includes(field)) {
5067
+ throw new Error(
5068
+ `publishCommand: method '${name}' binds field '${field}' in both \`key\` and \`add\`. A primary-key field cannot be atomically incremented \u2014 it belongs in \`key\` only.`
5069
+ );
5070
+ }
5071
+ if (inputFieldNames.includes(field)) {
5072
+ throw new Error(
5073
+ `publishCommand: method '${name}' binds field '${field}' in both \`input\` (SET) and \`add\` (ADD). A field is either overwritten (\`input\`) or atomically incremented (\`add\`), never both.`
5074
+ );
5075
+ }
5076
+ }
5077
+ }
4995
5078
  const plan2 = mutation(name, ($) => {
4996
5079
  const keyRefs = (fields) => {
4997
5080
  const out = {};
@@ -5009,13 +5092,17 @@ function writeDescriptorToPlan(name, d) {
5009
5092
  const descriptor = {
5010
5093
  [intent]: (() => model),
5011
5094
  key: keyRefs(keyFieldNames),
5012
- ...inputFieldNames.length > 0 ? { input: inputRefs(d.input) } : {}
5095
+ ...inputFieldNames.length > 0 ? { input: inputRefs(d.input) } : {},
5096
+ // #301: `add` deltas are always `param.number()` (validated above), so each binds
5097
+ // to a same-named `$.field` placeholder — the runtime resolves the caller delta.
5098
+ ...addFieldNames.length > 0 ? { add: keyRefs(addFieldNames) } : {}
5013
5099
  };
5014
5100
  return { [name]: descriptor };
5015
5101
  });
5016
5102
  const select = d.result !== void 0 && d.result.select !== void 0 ? d.result.select : void 0;
5017
5103
  const condition = d.condition !== void 0 ? descriptorConditionRecord(d.condition, name) : void 0;
5018
- return { plan: plan2, select, condition, mode: d.mode ?? "transaction", description: d.description };
5104
+ const batchWriteEligible = (intent === "upsert" || intent === "remove") && d.condition === void 0;
5105
+ return { plan: plan2, select, condition, mode: d.mode, batchWriteEligible, description: d.description };
5019
5106
  }
5020
5107
 
5021
5108
  // src/spec/mutation-command.ts
@@ -5782,6 +5869,12 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
5782
5869
  if (keyFieldSet.has(field)) continue;
5783
5870
  changes[field] = renderInputLeaf(leaf, false, index, field, resolveEntityRef);
5784
5871
  }
5872
+ const add = {};
5873
+ if (fragment.add !== void 0) {
5874
+ for (const [field, leaf] of Object.entries(fragment.add)) {
5875
+ add[field] = renderInputLeaf(leaf, false, index, field, resolveEntityRef);
5876
+ }
5877
+ }
5785
5878
  op = {
5786
5879
  __isContractMethodOp: true,
5787
5880
  entity: fragment.entity,
@@ -5789,6 +5882,7 @@ function compileFragment(fragment, index = 0, resolveEntityRef = null, maintenan
5789
5882
  keys: wholeKeysSentinel(),
5790
5883
  keyFields,
5791
5884
  ...fragment.intent === "update" ? { changes } : {},
5885
+ ...fragment.intent === "update" && Object.keys(add).length > 0 ? { add } : {},
5792
5886
  // #242: a conditioned update / remove — the write is a CAS-style gate (the
5793
5887
  // row must satisfy the condition or the write is rejected, not silently
5794
5888
  // committed). Absent → an unconditional update / remove (unchanged).