graphddb 0.7.0 → 0.7.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.
@@ -67,7 +67,7 @@ function evaluateKey(segmented, scope, presentFields, nameOf = (f) => f, partial
67
67
  }
68
68
 
69
69
  // src/spec/types.ts
70
- var SPEC_VERSION = "1.0";
70
+ var SPEC_VERSION = "1.1";
71
71
  var MARKER_ROW_ENTITY = "__marker__";
72
72
 
73
73
  // src/spec/manifest.ts
@@ -144,6 +144,9 @@ function buildFields(metadata) {
144
144
  }
145
145
  return sortedRecord(fields);
146
146
  }
147
+ function buildManifestEntity(metadata) {
148
+ return buildEntity(metadata);
149
+ }
147
150
  function buildEntity(metadata) {
148
151
  return {
149
152
  table: metadata.tableName,
@@ -3532,8 +3535,23 @@ async function persistSingleWriteWithHooks(w, runtime, origins, transaction, ret
3532
3535
  });
3533
3536
  }
3534
3537
  async function runEnvelopeW1(op, runtime, transaction, writeCtxs) {
3535
- const kind = op.fragment.op.operation;
3536
- const keyFields = new Set(op.fragment.keyFields);
3538
+ const r = await runLogicalW1(
3539
+ {
3540
+ kind: op.fragment.op.operation,
3541
+ modelClass: op.fragment.op.entity.modelClass,
3542
+ keyFields: op.fragment.keyFields,
3543
+ params: op.params
3544
+ },
3545
+ runtime,
3546
+ transaction,
3547
+ writeCtxs
3548
+ );
3549
+ if (r.rewritten !== void 0) return { op, rewritten: r.rewritten };
3550
+ return { op: { ...op, params: r.params } };
3551
+ }
3552
+ async function runLogicalW1(op, runtime, transaction, writeCtxs) {
3553
+ const kind = op.kind;
3554
+ const keyFields = new Set(op.keyFields);
3537
3555
  const input = {};
3538
3556
  if (kind === "put") {
3539
3557
  input.item = { ...op.params };
@@ -3547,15 +3565,14 @@ async function runEnvelopeW1(op, runtime, transaction, writeCtxs) {
3547
3565
  input.key = key;
3548
3566
  input.changes = changes;
3549
3567
  }
3550
- const modelClass = op.fragment.op.entity.modelClass;
3551
- const ctx = runtime.writeCtx(kind, ctxModelFor(modelClass), input, transaction);
3568
+ const ctx = runtime.writeCtx(kind, ctxModelFor(op.modelClass), input, transaction);
3552
3569
  await runtime.runWriteBefore(ctx);
3553
3570
  writeCtxs.push(ctx);
3554
3571
  if (ctx.kind !== kind) {
3555
- return { op, rewritten: renderRewrittenWrite(modelClass, ctx) };
3572
+ return { params: op.params, rewritten: renderRewrittenWrite(op.modelClass, ctx) };
3556
3573
  }
3557
3574
  const merged = ctx.kind === "put" ? { ...ctx.input.item ?? {} } : { ...ctx.input.key ?? {}, ...ctx.input.changes ?? {} };
3558
- return { op: { ...op, params: merged } };
3575
+ return { params: merged };
3559
3576
  }
3560
3577
  function renderRewrittenWrite(modelClass, ctx) {
3561
3578
  const modelStatic = modelClass.asModel();
@@ -3646,6 +3663,199 @@ async function executeCompiledParallelWrites(ops, options) {
3646
3663
  ]);
3647
3664
  return results;
3648
3665
  }
3666
+ var LOGICAL_EFFECT_CATEGORIES = [
3667
+ "edge",
3668
+ "derive",
3669
+ "unique",
3670
+ "outbox",
3671
+ "idempotency",
3672
+ "maintain",
3673
+ "maintainOutbox"
3674
+ ];
3675
+ function renderLogicalWrite(op, params) {
3676
+ const base = {
3677
+ operation: op.kind,
3678
+ modelClass: op.modelClass,
3679
+ modelStatic: op.modelStatic,
3680
+ key: params,
3681
+ ...op.condition !== void 0 ? { condition: op.condition } : {}
3682
+ };
3683
+ if (op.kind === "put") return { ...base, item: params };
3684
+ if (op.kind === "update") {
3685
+ const keyFields = new Set(op.keyFields);
3686
+ const changes = {};
3687
+ for (const [k, v] of Object.entries(params)) {
3688
+ if (!keyFields.has(k)) changes[k] = v;
3689
+ }
3690
+ return { ...base, changes };
3691
+ }
3692
+ return base;
3693
+ }
3694
+ function renderLogicalOp(op, params) {
3695
+ const write = renderLogicalWrite(op, params);
3696
+ const rendered = op.renderEffects(params);
3697
+ const e = rendered.effects;
3698
+ const checkItems = rendered.checks.map((check) => ({ check }));
3699
+ const modelBySignature = new Map(rendered.modelBySignature);
3700
+ const pick = (c) => [...e[c] ?? []];
3701
+ const edgeItems = pick("edge");
3702
+ const updateItems = pick("derive");
3703
+ const guardItems = pick("unique");
3704
+ const outboxItems = pick("outbox");
3705
+ const idempotencyItems = pick("idempotency");
3706
+ const maintainItems = pick("maintain");
3707
+ const maintainOutboxItems = pick("maintainOutbox");
3708
+ return {
3709
+ write,
3710
+ edgeItems,
3711
+ updateItems,
3712
+ guardItems,
3713
+ outboxItems,
3714
+ idempotencyItems,
3715
+ maintainItems,
3716
+ maintainOutboxItems,
3717
+ checkItems,
3718
+ modelBySignature,
3719
+ hasDerivedEffects: edgeItems.length > 0 || updateItems.length > 0 || guardItems.length > 0 || outboxItems.length > 0 || idempotencyItems.length > 0 || maintainItems.length > 0 || maintainOutboxItems.length > 0 || checkItems.length > 0
3720
+ };
3721
+ }
3722
+ function readBackForLogicalOp(op, params) {
3723
+ const sel = op.result?.select;
3724
+ if (sel === void 0 || Object.keys(sel).length === 0) return void 0;
3725
+ const key = {};
3726
+ for (const f of op.keyFields) {
3727
+ if (f in params) key[f] = params[f];
3728
+ }
3729
+ return executeQuery(op.modelClass, key, sel, {
3730
+ consistentRead: op.result?.options?.consistentRead ?? true,
3731
+ maxDepth: op.result?.options?.maxDepth
3732
+ });
3733
+ }
3734
+ async function executeLogicalWriteOps(ops, options) {
3735
+ const label = options.label;
3736
+ const writeMw = buildWriteRuntime(options.context);
3737
+ const writeCtxs = [];
3738
+ const txId = { id: /* @__PURE__ */ Symbol("graphddb.prepared-aot") };
3739
+ const effectiveParams = [];
3740
+ const rewrites = [];
3741
+ if (writeMw.active) {
3742
+ for (const op of ops) {
3743
+ const r = await runLogicalW1(op, writeMw, txId, writeCtxs);
3744
+ effectiveParams.push(r.params);
3745
+ rewrites.push(r.rewritten ?? null);
3746
+ }
3747
+ } else {
3748
+ for (const op of ops) {
3749
+ effectiveParams.push(op.params);
3750
+ rewrites.push(null);
3751
+ }
3752
+ }
3753
+ const renderedOps = ops.map(
3754
+ (op, i) => rewrites[i] !== null ? rewrittenCompiledOp(rewrites[i]) : renderLogicalOp(op, effectiveParams[i])
3755
+ );
3756
+ const rendered = renderedOps.map((r) => r.write);
3757
+ const modelBySignature = /* @__PURE__ */ new Map();
3758
+ const edgeItems = [];
3759
+ const updateItems = [];
3760
+ const guardItems = [];
3761
+ const outboxItems = [];
3762
+ const idempotencyItems = [];
3763
+ const maintainItems = [];
3764
+ const maintainOutboxItems = [];
3765
+ const checkItems = [];
3766
+ for (const r of renderedOps) {
3767
+ for (const [sig, name] of r.modelBySignature) modelBySignature.set(sig, name);
3768
+ edgeItems.push(...r.edgeItems);
3769
+ updateItems.push(...r.updateItems);
3770
+ guardItems.push(...r.guardItems);
3771
+ outboxItems.push(...r.outboxItems);
3772
+ idempotencyItems.push(...r.idempotencyItems);
3773
+ maintainItems.push(...r.maintainItems);
3774
+ maintainOutboxItems.push(...r.maintainOutboxItems);
3775
+ checkItems.push(...r.checkItems);
3776
+ }
3777
+ const hasDerivedEffects = renderedOps.some((r) => r.hasDerivedEffects);
3778
+ const origins = writeMw.active ? rendered.map((w) => ({ model: ctxModelFor(w.modelClass), kind: w.operation })) : [];
3779
+ if (rendered.length === 1 && !hasDerivedEffects) {
3780
+ if (writeMw.active) {
3781
+ await persistSingleWriteWithHooks(rendered[0], writeMw, origins, txId, options.retry);
3782
+ } else {
3783
+ await executeSingleWrite(rendered[0], options.retry);
3784
+ }
3785
+ } else {
3786
+ await executeReferentialTransaction(
3787
+ rendered,
3788
+ edgeItems,
3789
+ updateItems,
3790
+ guardItems,
3791
+ outboxItems,
3792
+ idempotencyItems,
3793
+ maintainItems,
3794
+ maintainOutboxItems,
3795
+ checkItems,
3796
+ label,
3797
+ modelBySignature,
3798
+ options.retry,
3799
+ writeMw.active ? { runtime: writeMw, origins, transaction: txId } : void 0
3800
+ );
3801
+ }
3802
+ if (writeMw.active) {
3803
+ for (let i = writeCtxs.length - 1; i >= 0; i--) {
3804
+ await writeMw.runWriteAfter(writeCtxs[i], {});
3805
+ }
3806
+ }
3807
+ const readBacks = await Promise.all(
3808
+ ops.map((op, i) => readBackForLogicalOp(op, effectiveParams[i]))
3809
+ );
3810
+ return { readBacks };
3811
+ }
3812
+ async function executeLogicalParallelWrites(ops, options) {
3813
+ const label = options.label;
3814
+ const results = new Array(ops.length);
3815
+ const renderedOps = ops.map((op) => renderLogicalOp(op, op.params));
3816
+ const coalescibleIdx = [];
3817
+ const individualIdx = [];
3818
+ const hooksActive = buildWriteRuntime(options.context).active;
3819
+ renderedOps.forEach((r, i) => {
3820
+ const w = r.write;
3821
+ if (!hooksActive && !r.hasDerivedEffects && w.condition === void 0 && (w.operation === "put" || w.operation === "delete")) {
3822
+ coalescibleIdx.push(i);
3823
+ } else {
3824
+ individualIdx.push(i);
3825
+ }
3826
+ });
3827
+ await Promise.all([
3828
+ (async () => {
3829
+ if (coalescibleIdx.length === 0) return;
3830
+ const writes = coalescibleIdx.map((i) => renderedOps[i].write);
3831
+ try {
3832
+ await executeParallelWrites(writes);
3833
+ await Promise.all(
3834
+ coalescibleIdx.map(async (i) => {
3835
+ const value = await readBackForLogicalOp(ops[i], ops[i].params);
3836
+ results[i] = { ok: true, value };
3837
+ })
3838
+ );
3839
+ } catch (e) {
3840
+ const error = e instanceof Error ? e : new Error(String(e));
3841
+ for (const i of coalescibleIdx) results[i] = { ok: false, error };
3842
+ }
3843
+ })(),
3844
+ ...individualIdx.map(async (i) => {
3845
+ try {
3846
+ const { readBacks } = await executeLogicalWriteOps([ops[i]], {
3847
+ label: `${label} (op #${i})`,
3848
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
3849
+ ...options.context !== void 0 ? { context: options.context } : {}
3850
+ });
3851
+ results[i] = { ok: true, value: readBacks[0] };
3852
+ } catch (e) {
3853
+ results[i] = { ok: false, error: e instanceof Error ? e : new Error(String(e)) };
3854
+ }
3855
+ })
3856
+ ]);
3857
+ return results;
3858
+ }
3649
3859
  function withCondition(op, condition) {
3650
3860
  if (op.condition === void 0) return { ...op, condition };
3651
3861
  const base = op.condition;
@@ -4052,7 +4262,7 @@ function classify(routes) {
4052
4262
  }
4053
4263
  return kind;
4054
4264
  }
4055
- function compileWriteRoute(alias, route) {
4265
+ function analyzeWriteRoute(alias, route) {
4056
4266
  const present = INTENT_KEYS.filter((k) => route[k] !== void 0);
4057
4267
  if (present.length !== 1) {
4058
4268
  throw new Error(
@@ -4073,23 +4283,37 @@ function compileWriteRoute(alias, route) {
4073
4283
  );
4074
4284
  const conditionRec = route.condition !== void 0 ? recordSlots(route.condition, `${alias} condition`) : void 0;
4075
4285
  if (route.result !== void 0) assertStaticTemplate(route.result, `${alias} result`);
4076
- const fragment = compileWriteFragment({
4077
- alias,
4078
- intent,
4079
- model,
4080
- keyFieldNames: keyRec.fields,
4081
- inputFieldNames: inputRec.fields
4082
- });
4083
4286
  return {
4084
4287
  alias,
4085
4288
  intent,
4086
- fragment,
4289
+ model,
4290
+ keyFields: keyRec.fields,
4291
+ inputFields: inputRec.fields,
4087
4292
  keySlots: keyRec.slots,
4088
4293
  inputSlots: inputRec.slots,
4089
4294
  ...conditionRec !== void 0 ? { conditionSlots: conditionRec.slots } : {},
4090
4295
  ...route.result !== void 0 ? { result: route.result } : {}
4091
4296
  };
4092
4297
  }
4298
+ function compileWriteRoute(alias, route) {
4299
+ const analyzed = analyzeWriteRoute(alias, route);
4300
+ const fragment = compileWriteFragment({
4301
+ alias,
4302
+ intent: analyzed.intent,
4303
+ model: analyzed.model,
4304
+ keyFieldNames: analyzed.keyFields,
4305
+ inputFieldNames: analyzed.inputFields
4306
+ });
4307
+ return {
4308
+ alias,
4309
+ intent: analyzed.intent,
4310
+ fragment,
4311
+ keySlots: analyzed.keySlots,
4312
+ inputSlots: analyzed.inputSlots,
4313
+ ...analyzed.conditionSlots !== void 0 ? { conditionSlots: analyzed.conditionSlots } : {},
4314
+ ...analyzed.result !== void 0 ? { result: analyzed.result } : {}
4315
+ };
4316
+ }
4093
4317
  function compileReadRoute(alias, route) {
4094
4318
  const isQuery = route.query !== void 0;
4095
4319
  const isList = route.list !== void 0;
@@ -4256,6 +4480,25 @@ function stableJson(value) {
4256
4480
  return v;
4257
4481
  });
4258
4482
  }
4483
+ function evaluatePreparedRoutes(body) {
4484
+ if (typeof body !== "function") {
4485
+ throw new Error(
4486
+ `${PREPARE_LABEL}: expected a body function \`($) => ({ alias: { ... } })\`.`
4487
+ );
4488
+ }
4489
+ const $ = makePreparedInputProxy();
4490
+ const map = body($);
4491
+ if (map === null || typeof map !== "object") {
4492
+ throw new Error(
4493
+ `${PREPARE_LABEL}: the body must return a descriptor map \`{ alias: { ... } }\`.`
4494
+ );
4495
+ }
4496
+ const routes = Object.keys(map).map((alias) => ({
4497
+ alias,
4498
+ route: map[alias]
4499
+ }));
4500
+ return { kind: classify(routes), routes };
4501
+ }
4259
4502
  function prepare(body) {
4260
4503
  if (typeof body !== "function") {
4261
4504
  throw new Error(
@@ -6839,12 +7082,42 @@ function synthesizeMutationTransaction(contractName, methodName, ops, checks, ed
6839
7082
  ...maintainOutboxItems,
6840
7083
  ...checkItems
6841
7084
  ];
7085
+ const categories = [
7086
+ ...base.items.map(() => "base"),
7087
+ ...edgeItems.map(() => "edge"),
7088
+ ...updateItems.map(() => "derive"),
7089
+ ...guardItems.map(() => "unique"),
7090
+ ...outboxItems.map(() => "outbox"),
7091
+ ...idempotencyItems.map(() => "idempotency"),
7092
+ ...maintainItems.map(() => "maintain"),
7093
+ ...maintainOutboxItems.map(() => "maintainOutbox"),
7094
+ ...checkItems.map(() => "check")
7095
+ ];
6842
7096
  if (items.length > MAX_TRANSACT_ITEMS) {
6843
7097
  throw new Error(
6844
7098
  `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.`
6845
7099
  );
6846
7100
  }
6847
- return { params: sortedParams(params), items, maxItems: items.length };
7101
+ return {
7102
+ spec: { params: sortedParams(params), items, maxItems: items.length },
7103
+ categories
7104
+ };
7105
+ }
7106
+ function serializePreparedWriteFragment(label, fragment) {
7107
+ const { spec, categories } = synthesizeMutationTransaction(
7108
+ label,
7109
+ "plan",
7110
+ [fragment.op],
7111
+ fragment.conditionChecks ?? [],
7112
+ fragment.edgeWrites ?? [],
7113
+ fragment.derivedUpdates ?? [],
7114
+ fragment.uniqueGuards ?? [],
7115
+ fragment.outboxEvents ?? [],
7116
+ fragment.idempotencyGuard,
7117
+ fragment.maintainWrites ?? [],
7118
+ fragment.maintainOutbox ?? []
7119
+ );
7120
+ return { transaction: spec, categories };
6848
7121
  }
6849
7122
  function modeTargetFor(mode, op, commandSpec, contractName, methodName, opName, transactionSpecs) {
6850
7123
  if (mode === "parallel") {
@@ -6967,7 +7240,7 @@ function serializeCommandContract(contractName, contract, commandSpecs, transact
6967
7240
  idempotencyGuard,
6968
7241
  maintainWrites,
6969
7242
  maintainOutbox
6970
- )
7243
+ ).spec
6971
7244
  ) : (
6972
7245
  // #90: writes only.
6973
7246
  composeFragmentTransaction(contractName, methodName, ops)
@@ -7263,12 +7536,12 @@ function buildReadOperations(def) {
7263
7536
  };
7264
7537
  const ops = [root];
7265
7538
  ops.push(...buildRelationOperations(metadata, select, "$"));
7266
- return ops;
7539
+ return injectRefsSourceProjections(ops);
7267
7540
  }
7268
7541
  function projectionFields(select) {
7269
7542
  const out = [];
7270
7543
  for (const [field, value] of Object.entries(select)) {
7271
- if (value === true) out.push(field);
7544
+ if (value === true || isInlineSnapshotSpec(value)) out.push(field);
7272
7545
  }
7273
7546
  return out.sort();
7274
7547
  }
@@ -7284,9 +7557,16 @@ function buildRelationOperations(metadata, select, parentPath) {
7284
7557
  const targetMeta = MetadataRegistry.get(targetClass);
7285
7558
  const childPath = `${parentPath}.${rel.propertyName}`;
7286
7559
  if (rel.type === "refs") {
7287
- throw new Error(
7288
- `Relation '${rel.propertyName}' is a 'refs' relation (issue #197): its read fans an inline parent-row id-list ('${rel.refs?.from}[].${rel.refs?.key}') out into one BatchGet. The static operations spec / Python codegen cannot represent a parent-list key fan-out (operation key conditions bind a single scalar '{result.*}'), so a 'refs' relation is resolvable ONLY through the TS in-process runtime (Model.query). Remove '${rel.propertyName}' from selects that are compiled to operations.json / generated Python, or resolve it with a separate BatchGet in the generated-code consumer.`
7560
+ if (spec.filter !== void 0) {
7561
+ throw new Error(
7562
+ `Relation '${rel.propertyName}' (refs) declares a \`filter\`, which the static operations spec cannot apply (a BatchGetItem has no server-side FilterExpression and the spec runtimes run no client-side filter stage). Remove the filter from selects compiled to operations.json / generated Python, or filter in the consumer.`
7563
+ );
7564
+ }
7565
+ ops.push(
7566
+ buildRefsOperation(rel, targetMeta, childSelect, `${childPath}.items`)
7289
7567
  );
7568
+ ops.push(...buildRelationOperations(targetMeta, childSelect, `${childPath}.items`));
7569
+ continue;
7290
7570
  }
7291
7571
  if (rel.type === "hasMany") {
7292
7572
  const limit = spec.limit ?? rel.options?.limit?.default ?? 20;
@@ -7397,6 +7677,65 @@ function buildBatchGetOperation(rel, targetMeta, select, resultPath) {
7397
7677
  sourceField
7398
7678
  };
7399
7679
  }
7680
+ function buildRefsOperation(rel, targetMeta, select, resultPath) {
7681
+ const refs = rel.refs;
7682
+ if (!refs) {
7683
+ throw new Error(
7684
+ `Relation '${rel.propertyName}' is type 'refs' but carries no refs binding.`
7685
+ );
7686
+ }
7687
+ const { targetField, sourceField } = singleSourceField(rel);
7688
+ const resolved = resolveKey([targetField], targetMeta);
7689
+ if (resolved.type !== "pk" || resolved.partial) {
7690
+ throw new Error(
7691
+ `Relation '${rel.propertyName}' (refs) must resolve to a full primary key (BatchGetItem); got ${resolved.type}${resolved.partial ? " (partial)" : ""}.`
7692
+ );
7693
+ }
7694
+ if (!targetMeta.primaryKey) throw new Error("Primary key not defined");
7695
+ const nameOf = (f) => f === targetField ? sourceField : f;
7696
+ const { pk, sk } = evaluateKey(
7697
+ targetMeta.primaryKey.segmented,
7698
+ "result",
7699
+ /* @__PURE__ */ new Set([targetField]),
7700
+ nameOf
7701
+ );
7702
+ const keyCondition = { PK: pk };
7703
+ if (sk !== void 0) keyCondition.SK = sk;
7704
+ return {
7705
+ type: "BatchGetItem",
7706
+ tableName: TableMapping.resolve(targetMeta.tableName),
7707
+ keyCondition,
7708
+ projection: projectionFields(select),
7709
+ resultPath,
7710
+ sourceField,
7711
+ sourceList: { from: refs.from, key: refs.key }
7712
+ };
7713
+ }
7714
+ function injectRefsSourceProjections(ops) {
7715
+ for (let i = 0; i < ops.length; i++) {
7716
+ const op = ops[i];
7717
+ if (op.sourceList === void 0) continue;
7718
+ const tokens = op.resultPath.split(".");
7719
+ const parentPath = tokens.slice(0, -2).join(".") || "$";
7720
+ const parentIndex = ops.findIndex((p) => p.resultPath === parentPath);
7721
+ if (parentIndex === -1) {
7722
+ throw new Error(
7723
+ `refs fan-out at '${op.resultPath}': no parent operation at '${parentPath}'.`
7724
+ );
7725
+ }
7726
+ const parent = ops[parentIndex];
7727
+ const from2 = op.sourceList.from;
7728
+ if (parent.projection.includes(from2)) {
7729
+ continue;
7730
+ }
7731
+ ops[parentIndex] = {
7732
+ ...parent,
7733
+ projection: [...parent.projection, from2].sort()
7734
+ };
7735
+ ops[i] = { ...op, sourceList: { ...op.sourceList, implicit: true } };
7736
+ }
7737
+ return ops;
7738
+ }
7400
7739
  function filterSpec(filter) {
7401
7740
  return { declarative: filter };
7402
7741
  }
@@ -7551,6 +7890,216 @@ function buildOperations(queries = {}, commands = {}, transactions = {}, contrac
7551
7890
  };
7552
7891
  }
7553
7892
 
7893
+ // src/spec/prepared.ts
7894
+ import { createHash } from "crypto";
7895
+ var PREPARED_FORMAT_VERSION = "1";
7896
+ function canonicalJson(value) {
7897
+ return JSON.stringify(value, (_k, v) => {
7898
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
7899
+ const sorted = {};
7900
+ for (const key of Object.keys(v).sort()) {
7901
+ sorted[key] = v[key];
7902
+ }
7903
+ return sorted;
7904
+ }
7905
+ return v;
7906
+ });
7907
+ }
7908
+ function entityFingerprint(metadata) {
7909
+ return createHash("sha256").update(canonicalJson(buildManifestEntity(metadata))).digest("hex");
7910
+ }
7911
+ function planFingerprint(plan2) {
7912
+ return createHash("sha256").update(canonicalJson(plan2)).digest("hex");
7913
+ }
7914
+ function bindOfSlot(slot, where) {
7915
+ if (slot.kind === "param") return { param: slot.name };
7916
+ const v = slot.value;
7917
+ if (v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
7918
+ return { literal: v };
7919
+ }
7920
+ if (v instanceof Date) return { literalDate: v.toISOString() };
7921
+ if (typeof v === "bigint") return { literalBigInt: v.toString() };
7922
+ throw new Error(
7923
+ `prepared AOT: ${where} carries a non-serializable literal (${typeof v}).`
7924
+ );
7925
+ }
7926
+ function bindMapOf(slots, where) {
7927
+ const out = {};
7928
+ for (const [field, slot] of Object.entries(slots)) {
7929
+ out[field] = bindOfSlot(slot, `${where} field '${field}'`);
7930
+ }
7931
+ return out;
7932
+ }
7933
+ function paramKindForField(metadata, field) {
7934
+ const f = metadata.fields.find((x) => x.propertyName === field);
7935
+ return f?.dynamoType === "N" ? "number" : "string";
7936
+ }
7937
+ function addParam(params, name, kind) {
7938
+ if (!(name in params)) params[name] = { type: kind, required: true };
7939
+ }
7940
+ function readRouteQuerySpec(route, metadata) {
7941
+ const params = {};
7942
+ const key = {};
7943
+ for (const field of Object.keys(route.keySlots)) {
7944
+ const kind = paramKindForField(metadata, field);
7945
+ params[field] = { kind, required: true };
7946
+ key[field] = kind === "number" ? param.number() : param.string();
7947
+ }
7948
+ const def = {
7949
+ __isOperationDefinition: true,
7950
+ entity: { name: route.modelClass.name, modelClass: route.modelClass },
7951
+ operation: route.kind === "query" ? "query" : "list",
7952
+ key,
7953
+ select: route.select,
7954
+ params
7955
+ };
7956
+ return buildQuerySpec(def);
7957
+ }
7958
+ function compileReadPlan(routes) {
7959
+ const specs = [];
7960
+ const params = {};
7961
+ const entities = {};
7962
+ for (const { alias, route } of routes) {
7963
+ const compiled = compileReadRoute(alias, route);
7964
+ const metadata = MetadataRegistry.get(compiled.modelClass);
7965
+ const entityName = compiled.modelClass.name;
7966
+ entities[entityName] = entityFingerprint(metadata);
7967
+ for (const [field, slot] of Object.entries(compiled.keySlots)) {
7968
+ if (slot.kind === "param") {
7969
+ addParam(params, slot.name, paramKindForField(metadata, field));
7970
+ }
7971
+ }
7972
+ const dynamicParam = (slot, kind) => {
7973
+ if (slot?.kind === "param") addParam(params, slot.name, kind);
7974
+ };
7975
+ dynamicParam(compiled.limitSlot, "number");
7976
+ dynamicParam(compiled.afterSlot, "string");
7977
+ dynamicParam(compiled.consistentReadSlot, "string");
7978
+ specs.push({
7979
+ alias,
7980
+ op: compiled.kind,
7981
+ entity: entityName,
7982
+ select: compiled.select,
7983
+ key: bindMapOf(compiled.keySlots, `${alias} key`),
7984
+ ...compiled.maxDepth !== void 0 ? { maxDepth: compiled.maxDepth } : {},
7985
+ ...compiled.order !== void 0 ? { order: compiled.order } : {},
7986
+ ...compiled.filter !== void 0 ? { filter: compiled.filter } : {},
7987
+ ...compiled.consistentReadSlot !== void 0 ? { consistentRead: bindOfSlot(compiled.consistentReadSlot, `${alias} consistentRead`) } : {},
7988
+ ...compiled.limitSlot !== void 0 ? { limit: bindOfSlot(compiled.limitSlot, `${alias} limit`) } : {},
7989
+ ...compiled.afterSlot !== void 0 ? { after: bindOfSlot(compiled.afterSlot, `${alias} after`) } : {},
7990
+ query: readRouteQuerySpec(compiled, metadata)
7991
+ });
7992
+ }
7993
+ return { kind: "read", params, entities, reads: specs };
7994
+ }
7995
+ function compileWritePlan(routes, planLabel) {
7996
+ const specs = [];
7997
+ const params = {};
7998
+ const entities = {};
7999
+ for (const { alias, route } of routes) {
8000
+ const analyzed = analyzeWriteRoute(alias, route);
8001
+ if (analyzed.intent === "create" && analyzed.conditionSlots !== void 0) {
8002
+ throw new Error(
8003
+ `prepared AOT (${planLabel} '${alias}'): a \`condition\` on a 'create' is not supported \u2014 a create already guards with attribute_not_exists(PK). Use an 'update' with a condition, or drop the condition.`
8004
+ );
8005
+ }
8006
+ const modelClass = resolveModelClass(analyzed.model);
8007
+ const metadata = MetadataRegistry.get(modelClass);
8008
+ const entityName = modelClass.name;
8009
+ entities[entityName] = entityFingerprint(metadata);
8010
+ const fragment = compileWriteFragment({
8011
+ alias,
8012
+ intent: analyzed.intent,
8013
+ model: analyzed.model,
8014
+ keyFieldNames: analyzed.keyFields,
8015
+ inputFieldNames: analyzed.inputFields
8016
+ });
8017
+ const { transaction, categories } = serializePreparedWriteFragment(
8018
+ `${planLabel}.${alias}`,
8019
+ fragment
8020
+ );
8021
+ for (const item of transaction.items) {
8022
+ if (item.literalKey === true || item.entity === MARKER_ROW_ENTITY) continue;
8023
+ if (item.entity in entities) continue;
8024
+ const cls = resolveRegisteredModel(item.entity, `${planLabel}.${alias}`);
8025
+ entities[item.entity] = entityFingerprint(MetadataRegistry.get(cls));
8026
+ }
8027
+ for (const [field, slot] of Object.entries({
8028
+ ...analyzed.inputSlots,
8029
+ ...analyzed.keySlots,
8030
+ ...analyzed.conditionSlots ?? {}
8031
+ })) {
8032
+ if (slot.kind === "param") {
8033
+ addParam(params, slot.name, paramKindForField(metadata, field));
8034
+ }
8035
+ }
8036
+ let readback;
8037
+ const resultSelect = analyzed.result?.select;
8038
+ if (resultSelect !== void 0 && Object.keys(resultSelect).length > 0) {
8039
+ const kf = {};
8040
+ const kp = {};
8041
+ for (const field of fragment.keyFields) {
8042
+ const kind = paramKindForField(metadata, field);
8043
+ kp[field] = { kind, required: true };
8044
+ kf[field] = kind === "number" ? param.number() : param.string();
8045
+ }
8046
+ readback = buildQuerySpec({
8047
+ __isOperationDefinition: true,
8048
+ entity: { name: entityName, modelClass },
8049
+ operation: "query",
8050
+ key: kf,
8051
+ select: resultSelect,
8052
+ params: kp
8053
+ });
8054
+ }
8055
+ specs.push({
8056
+ alias,
8057
+ entity: entityName,
8058
+ intent: analyzed.intent,
8059
+ keyFields: fragment.keyFields,
8060
+ key: bindMapOf(analyzed.keySlots, `${alias} key`),
8061
+ input: bindMapOf(analyzed.inputSlots, `${alias} input`),
8062
+ ...analyzed.conditionSlots !== void 0 ? { condition: bindMapOf(analyzed.conditionSlots, `${alias} condition`) } : {},
8063
+ ...analyzed.result !== void 0 ? { result: analyzed.result } : {},
8064
+ ...readback !== void 0 ? { readback } : {},
8065
+ transaction,
8066
+ categories
8067
+ });
8068
+ }
8069
+ return { kind: "write", params, entities, writes: specs };
8070
+ }
8071
+ function resolveRegisteredModel(name, label) {
8072
+ for (const cls of MetadataRegistry.getAll().keys()) {
8073
+ if (cls.name === name) {
8074
+ return cls;
8075
+ }
8076
+ }
8077
+ throw new Error(
8078
+ `prepared AOT (${label}): a derived effect targets entity '${name}', which is not a registered model.`
8079
+ );
8080
+ }
8081
+ function compilePreparedPlan(body, planLabel = "prepared") {
8082
+ const { kind, routes } = evaluatePreparedRoutes(body);
8083
+ if (kind === "read") {
8084
+ return compileReadPlan(
8085
+ routes
8086
+ );
8087
+ }
8088
+ return compileWritePlan(
8089
+ routes,
8090
+ planLabel
8091
+ );
8092
+ }
8093
+ function buildPreparedPlanDocument(plans) {
8094
+ const sorted = {};
8095
+ for (const id of Object.keys(plans).sort()) sorted[id] = plans[id];
8096
+ return {
8097
+ formatVersion: PREPARED_FORMAT_VERSION,
8098
+ specVersion: SPEC_VERSION,
8099
+ plans: sorted
8100
+ };
8101
+ }
8102
+
7554
8103
  // src/spec/index.ts
7555
8104
  function buildBridgeBundle(queries = {}, commands = {}, registry = MetadataRegistry, transactions = {}, contractInputs = {}) {
7556
8105
  const bundle = {
@@ -7564,6 +8113,7 @@ function buildBridgeBundle(queries = {}, commands = {}, registry = MetadataRegis
7564
8113
  export {
7565
8114
  SPEC_VERSION,
7566
8115
  MARKER_ROW_ENTITY,
8116
+ buildManifestEntity,
7567
8117
  buildManifest,
7568
8118
  buildProjection,
7569
8119
  compileFilterExpression,
@@ -7601,6 +8151,9 @@ export {
7601
8151
  MAX_TRANSACT_COMPOSE_ITEMS,
7602
8152
  compileMutationPlan,
7603
8153
  executeCommandMethod,
8154
+ LOGICAL_EFFECT_CATEGORIES,
8155
+ executeLogicalWriteOps,
8156
+ executeLogicalParallelWrites,
7604
8157
  isPreparedParamRef,
7605
8158
  PreparedWriteStatement,
7606
8159
  PreparedReadStatement,
@@ -7646,5 +8199,11 @@ export {
7646
8199
  assertContractBoundaries,
7647
8200
  buildQuerySpec,
7648
8201
  buildOperations,
8202
+ PREPARED_FORMAT_VERSION,
8203
+ canonicalJson,
8204
+ entityFingerprint,
8205
+ planFingerprint,
8206
+ compilePreparedPlan,
8207
+ buildPreparedPlanDocument,
7649
8208
  buildBridgeBundle
7650
8209
  };