graphddb 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
- export { C as CdcEmulator, M as MAINT_OUTBOX_PK_PREFIX, a as MaintenanceDrain, b as MaintenanceDrainOptions, c as createCdcEmulator, d as createMaintenanceDrain, e as createMaintenanceDrainHandler, p as parseChange } from '../from-change-CFzBy7aU.js';
2
- export { B as BatchResult, C as CdcEmulatorOptions, a as CdcMode, b as ChangeBatch, c as ChangeEvent, d as ChangeEventName, e as ChangeHandler, f as ClockMode, g as ConcurrentRecomputeRef, E as EventLog, F as FaultSpec, R as ReplayOptions, S as ShardId, h as StartingPosition, i as StreamViewType, j as SubscribeHandler, k as SubscribeHandlers, U as Unsubscribe, l as buildSubscribeHandler } from '../maintenance-view-adapter-CFeasCKo.js';
1
+ export { C as CdcEmulator, M as MAINT_OUTBOX_PK_PREFIX, a as MaintenanceDrain, b as MaintenanceDrainOptions, c as createCdcEmulator, d as createMaintenanceDrain, e as createMaintenanceDrainHandler, p as parseChange } from '../from-change-pnURY-cV.js';
2
+ export { B as BatchResult, C as CdcEmulatorOptions, a as CdcMode, b as ChangeBatch, c as ChangeEvent, d as ChangeEventName, e as ChangeHandler, f as ClockMode, g as ConcurrentRecomputeRef, E as EventLog, F as FaultSpec, R as ReplayOptions, S as ShardId, h as StartingPosition, i as StreamViewType, j as SubscribeHandler, k as SubscribeHandlers, U as Unsubscribe, l as buildSubscribeHandler } from '../maintenance-view-adapter-BP2CJDdz.js';
3
3
  import '@aws-sdk/client-dynamodb';
4
4
 
5
5
  /**
@@ -3760,17 +3760,15 @@ function normalizeWriteOp(alias, d) {
3760
3760
  }
3761
3761
  return [{ ...base, key: d.key }];
3762
3762
  }
3763
- function compileWriteOp(op) {
3764
- const keyFieldNames2 = Object.keys(op.key);
3765
- const inputFieldNames = Object.keys(op.input);
3766
- const model = op.model;
3763
+ function compileWriteFragment(input) {
3764
+ const { alias, intent, model, keyFieldNames: keyFieldNames2, inputFieldNames } = input;
3767
3765
  const body = ($) => {
3768
3766
  const descriptor = {
3769
- [op.intent]: (() => model),
3767
+ [intent]: (() => model),
3770
3768
  key: mapToRefs($, keyFieldNames2),
3771
3769
  ...inputFieldNames.length > 0 ? { input: mapToRefs($, inputFieldNames) } : {}
3772
3770
  };
3773
- const map = { [op.alias]: descriptor };
3771
+ const map = { [alias]: descriptor };
3774
3772
  return map;
3775
3773
  };
3776
3774
  const plan2 = mutation(body);
@@ -3778,7 +3776,16 @@ function compileWriteOp(op) {
3778
3776
  throw new Error("DDBModel.mutate: internal error building the mutation plan.");
3779
3777
  }
3780
3778
  const compiled = compileMutationPlan(plan2);
3781
- const fragment = compiled.fragments[0];
3779
+ return compiled.fragments[0];
3780
+ }
3781
+ function compileWriteOp(op) {
3782
+ const fragment = compileWriteFragment({
3783
+ alias: op.alias,
3784
+ intent: op.intent,
3785
+ model: op.model,
3786
+ keyFieldNames: Object.keys(op.key),
3787
+ inputFieldNames: Object.keys(op.input)
3788
+ });
3782
3789
  const params = { ...op.input, ...op.key };
3783
3790
  return {
3784
3791
  fragment,
@@ -3847,6 +3854,445 @@ function resolveModelStatic(ref, alias) {
3847
3854
  return ref;
3848
3855
  }
3849
3856
 
3857
+ // src/runtime/prepared.ts
3858
+ var PREPARE_LABEL = "DDBModel.prepare";
3859
+ var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:preparedParamRef");
3860
+ function isPreparedParamRef(value) {
3861
+ return typeof value === "object" && value !== null && value[PARAM_REF_BRAND] === true;
3862
+ }
3863
+ function makeParamRef(name) {
3864
+ const origin = `the placeholder '${name}' (\`$.${name}\`)`;
3865
+ const target = /* @__PURE__ */ Object.create(null);
3866
+ const proxy = new Proxy(target, {
3867
+ get(_t, prop) {
3868
+ if (prop === PARAM_REF_BRAND) return true;
3869
+ if (prop === "name") return name;
3870
+ if (typeof prop === "symbol") return void 0;
3871
+ throw new Error(
3872
+ `${PREPARE_LABEL}: unsupported access '${String(prop)}' on ${origin}. A \`key\` / \`input\` / option value only supports a direct \`$.name\` param reference or a concrete static literal \u2014 never a transform, a nested access, or a coercion (that would capture a per-call runtime value).`
3873
+ );
3874
+ },
3875
+ set() {
3876
+ throw new Error(`${PREPARE_LABEL}: cannot assign on ${origin}.`);
3877
+ }
3878
+ });
3879
+ return proxy;
3880
+ }
3881
+ function makePreparedInputProxy() {
3882
+ const cache = /* @__PURE__ */ new Map();
3883
+ return new Proxy(/* @__PURE__ */ Object.create(null), {
3884
+ get(_t, prop) {
3885
+ if (typeof prop === "symbol") return void 0;
3886
+ const name = prop;
3887
+ let ref = cache.get(name);
3888
+ if (!ref) {
3889
+ ref = makeParamRef(name);
3890
+ cache.set(name, ref);
3891
+ }
3892
+ return ref;
3893
+ },
3894
+ set() {
3895
+ throw new Error(`${PREPARE_LABEL}: cannot assign to the \`$\` placeholder proxy.`);
3896
+ }
3897
+ });
3898
+ }
3899
+ function assertNoRuntimeCapture(value, where) {
3900
+ if (isPreparedParamRef(value)) return;
3901
+ if (value === null) return;
3902
+ const t = typeof value;
3903
+ if (t === "string" || t === "number" || t === "boolean" || t === "bigint") return;
3904
+ if (value instanceof Date) return;
3905
+ if (t === "undefined") {
3906
+ throw new Error(
3907
+ `${PREPARE_LABEL}: ${where} is undefined. Bind a \`$.name\` param reference or a concrete static literal \u2014 or omit the field from the template entirely.`
3908
+ );
3909
+ }
3910
+ if (t === "function") {
3911
+ throw new Error(
3912
+ `${PREPARE_LABEL}: ${where} is a function. A prepared body may only bind a \`$.name\` param reference or a static literal here; a function is a captured per-call value (no runtime capture \u2014 see the prepared-statement contract).`
3913
+ );
3914
+ }
3915
+ if (t === "symbol") {
3916
+ throw new Error(`${PREPARE_LABEL}: ${where} is a symbol, which is not a bindable value.`);
3917
+ }
3918
+ throw new Error(
3919
+ `${PREPARE_LABEL}: ${where} is a non-literal object. A prepared body may only bind a \`$.name\` param reference or a static literal here \u2014 an object value is a captured per-call runtime value.`
3920
+ );
3921
+ }
3922
+ function recordSlots(binding, where) {
3923
+ const fields = Object.keys(binding);
3924
+ const slots = {};
3925
+ for (const f of fields) {
3926
+ const v = binding[f];
3927
+ if (isPreparedParamRef(v)) {
3928
+ slots[f] = { kind: "param", name: v.name };
3929
+ } else {
3930
+ assertNoRuntimeCapture(v, `${where} field '${f}'`);
3931
+ slots[f] = { kind: "literal", value: v };
3932
+ }
3933
+ }
3934
+ return { fields, slots };
3935
+ }
3936
+ function bindSlots(slots, params) {
3937
+ const out = {};
3938
+ for (const [field, slot] of Object.entries(slots)) {
3939
+ out[field] = slot.kind === "param" ? params[slot.name] : slot.value;
3940
+ }
3941
+ return out;
3942
+ }
3943
+ function bindOptionalSlot(slot, params) {
3944
+ if (slot === void 0) return void 0;
3945
+ return slot.kind === "param" ? params[slot.name] : slot.value;
3946
+ }
3947
+ function recordOptionSlot(value, where) {
3948
+ if (value === void 0) return void 0;
3949
+ if (isPreparedParamRef(value)) return { kind: "param", name: value.name };
3950
+ assertNoRuntimeCapture(value, where);
3951
+ return { kind: "literal", value };
3952
+ }
3953
+ var PreparedWriteStatement = class {
3954
+ /** @internal */
3955
+ constructor(ops) {
3956
+ this.ops = ops;
3957
+ }
3958
+ ops;
3959
+ async execute(params = {}, options = {}) {
3960
+ const compiled = this.ops.map((op) => ({
3961
+ fragment: op.fragment,
3962
+ params: { ...bindSlots(op.inputSlots, params), ...bindSlots(op.keySlots, params) },
3963
+ ...op.conditionSlots !== void 0 ? { condition: bindSlots(op.conditionSlots, params) } : {},
3964
+ ...op.result !== void 0 ? { result: op.result } : {}
3965
+ }));
3966
+ const mode = options.mode ?? "transaction";
3967
+ const runOpts = {
3968
+ label: PREPARE_LABEL,
3969
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
3970
+ ...options.context !== void 0 ? { context: options.context } : {}
3971
+ };
3972
+ if (mode === "transaction") {
3973
+ const { readBacks } = await executeCompiledWriteOp(compiled, runOpts);
3974
+ const out2 = {};
3975
+ this.ops.forEach((op, i) => {
3976
+ if (op.result !== void 0) out2[op.alias] = readBacks[i];
3977
+ });
3978
+ return out2;
3979
+ }
3980
+ const settled = await executeCompiledParallelWrites(compiled, runOpts);
3981
+ const out = {};
3982
+ this.ops.forEach((op, i) => {
3983
+ const res = settled[i];
3984
+ out[op.alias] = res.ok ? { ok: true, value: op.result !== void 0 ? res.value : void 0 } : { ok: false, error: res.error };
3985
+ });
3986
+ return out;
3987
+ }
3988
+ };
3989
+ var PreparedReadStatement = class {
3990
+ /** @internal */
3991
+ constructor(routes) {
3992
+ this.routes = routes;
3993
+ }
3994
+ routes;
3995
+ async execute(params = {}, options = {}) {
3996
+ const results = await Promise.all(
3997
+ this.routes.map((route) => this.runRoute(route, params, options))
3998
+ );
3999
+ const out = {};
4000
+ this.routes.forEach((route, i) => {
4001
+ out[route.alias] = results[i];
4002
+ });
4003
+ return out;
4004
+ }
4005
+ runRoute(route, params, options) {
4006
+ const key = bindSlots(route.keySlots, params);
4007
+ const consistentRead = bindOptionalSlot(route.consistentReadSlot, params);
4008
+ if (route.kind === "query") {
4009
+ return executeQuery(route.modelClass, key, route.select, {
4010
+ consistentRead,
4011
+ maxDepth: route.maxDepth,
4012
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
4013
+ ...options.context !== void 0 ? { context: options.context } : {}
4014
+ });
4015
+ }
4016
+ const limit = bindOptionalSlot(route.limitSlot, params);
4017
+ const after = bindOptionalSlot(route.afterSlot, params);
4018
+ return executeList(route.modelClass, key, route.select, {
4019
+ limit,
4020
+ after,
4021
+ order: route.order,
4022
+ filter: route.filter,
4023
+ ...options.retry !== void 0 ? { retry: options.retry } : {},
4024
+ ...options.context !== void 0 ? { context: options.context } : {}
4025
+ });
4026
+ }
4027
+ };
4028
+ var READ_INTENT_KEYS = ["query", "list"];
4029
+ function classify(routes) {
4030
+ let kind;
4031
+ for (const { alias, route } of routes) {
4032
+ const rec = route;
4033
+ const isRead = READ_INTENT_KEYS.some((k) => rec[k] !== void 0);
4034
+ const isWrite = INTENT_KEYS.some((k) => rec[k] !== void 0);
4035
+ if (isRead === isWrite) {
4036
+ throw new Error(
4037
+ `${PREPARE_LABEL}: the descriptor for alias '${alias}' must carry exactly one of a read intent (\`query\` / \`list\`) OR a write intent (\`create\` / \`update\` / \`remove\`). It declared ${isRead && isWrite ? "both" : "neither"}.`
4038
+ );
4039
+ }
4040
+ const routeKind = isRead ? "read" : "write";
4041
+ if (kind === void 0) kind = routeKind;
4042
+ else if (kind !== routeKind) {
4043
+ throw new Error(
4044
+ `${PREPARE_LABEL}: a prepared body must be all-read or all-write; alias '${alias}' is a ${routeKind} route but a previous alias was a ${kind} route. Split them into two prepared statements.`
4045
+ );
4046
+ }
4047
+ }
4048
+ if (kind === void 0) {
4049
+ throw new Error(
4050
+ `${PREPARE_LABEL}: the prepared body declared no routes. Declare at least one \`{ alias: { query|list|create|update|remove: () => Model, key, ... } }\`.`
4051
+ );
4052
+ }
4053
+ return kind;
4054
+ }
4055
+ function compileWriteRoute(alias, route) {
4056
+ const present = INTENT_KEYS.filter((k) => route[k] !== void 0);
4057
+ if (present.length !== 1) {
4058
+ throw new Error(
4059
+ `${PREPARE_LABEL}: the write descriptor for alias '${alias}' must carry exactly one intent (\`create\` / \`update\` / \`remove\`), but declared ${present.length === 0 ? "none" : present.join(", ")}.`
4060
+ );
4061
+ }
4062
+ const intent = present[0];
4063
+ if (route.key === null || typeof route.key !== "object") {
4064
+ throw new Error(
4065
+ `${PREPARE_LABEL}: the '${intent}' descriptor for alias '${alias}' must declare a \`key\` object binding the target's primary-key fields.`
4066
+ );
4067
+ }
4068
+ const model = resolveModelStatic(route[intent], alias);
4069
+ const keyRec = recordSlots(route.key, `${alias} key`);
4070
+ const inputRec = recordSlots(
4071
+ route.input ?? {},
4072
+ `${alias} input`
4073
+ );
4074
+ const conditionRec = route.condition !== void 0 ? recordSlots(route.condition, `${alias} condition`) : void 0;
4075
+ 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
+ return {
4084
+ alias,
4085
+ intent,
4086
+ fragment,
4087
+ keySlots: keyRec.slots,
4088
+ inputSlots: inputRec.slots,
4089
+ ...conditionRec !== void 0 ? { conditionSlots: conditionRec.slots } : {},
4090
+ ...route.result !== void 0 ? { result: route.result } : {}
4091
+ };
4092
+ }
4093
+ function compileReadRoute(alias, route) {
4094
+ const isQuery = route.query !== void 0;
4095
+ const isList = route.list !== void 0;
4096
+ if (isQuery === isList) {
4097
+ throw new Error(
4098
+ `${PREPARE_LABEL}: the read descriptor for alias '${alias}' must carry exactly one of \`query\` or \`list\`. It declared ${isQuery && isList ? "both" : "neither"}.`
4099
+ );
4100
+ }
4101
+ if (route.key === null || typeof route.key !== "object" || route.select === void 0) {
4102
+ throw new Error(
4103
+ `${PREPARE_LABEL}: the '${isQuery ? "query" : "list"}' descriptor for alias '${alias}' must declare both \`key\` and \`select\`.`
4104
+ );
4105
+ }
4106
+ const model = resolveModelStatic(isQuery ? route.query : route.list, alias);
4107
+ const modelClass = resolveModelClass(model);
4108
+ const keyRec = recordSlots(route.key, `${alias} key`);
4109
+ assertStaticTemplate(route.select, `${alias} select`);
4110
+ const opt = route.options ?? {};
4111
+ if (isQuery) {
4112
+ for (const unsupported of ["limit", "after", "order", "filter"]) {
4113
+ if (opt[unsupported] !== void 0) {
4114
+ throw new Error(
4115
+ `${PREPARE_LABEL}: the 'query' descriptor for alias '${alias}' declares \`options.${unsupported}\`, which a single-entity query does not support (a query supports \`consistentRead\` / \`maxDepth\`). Use a \`list\` route for pagination / ordering / filtering.`
4116
+ );
4117
+ }
4118
+ }
4119
+ } else if (opt.consistentRead !== void 0) {
4120
+ throw new Error(
4121
+ `${PREPARE_LABEL}: the 'list' descriptor for alias '${alias}' declares \`options.consistentRead\`, which \`Model.list\` does not support \u2014 it would be silently ignored. Remove it (a list always reads eventually consistent).`
4122
+ );
4123
+ }
4124
+ if (opt.filter !== void 0) {
4125
+ assertStaticTemplate(opt.filter, `${alias} options.filter`);
4126
+ }
4127
+ return {
4128
+ alias,
4129
+ kind: isQuery ? "query" : "list",
4130
+ modelClass,
4131
+ select: route.select,
4132
+ keySlots: keyRec.slots,
4133
+ ...typeof opt.maxDepth === "number" ? { maxDepth: opt.maxDepth } : {},
4134
+ ...opt.order !== void 0 ? { order: opt.order } : {},
4135
+ ...opt.filter !== void 0 ? { filter: opt.filter } : {},
4136
+ ...(() => {
4137
+ const s = recordOptionSlot(opt.consistentRead, `${alias} options.consistentRead`);
4138
+ return s !== void 0 ? { consistentReadSlot: s } : {};
4139
+ })(),
4140
+ ...(() => {
4141
+ const s = recordOptionSlot(opt.limit, `${alias} options.limit`);
4142
+ return s !== void 0 ? { limitSlot: s } : {};
4143
+ })(),
4144
+ ...(() => {
4145
+ const s = recordOptionSlot(opt.after, `${alias} options.after`);
4146
+ return s !== void 0 ? { afterSlot: s } : {};
4147
+ })()
4148
+ };
4149
+ }
4150
+ function assertStaticTemplate(value, where) {
4151
+ if (isPreparedParamRef(value)) {
4152
+ throw new Error(
4153
+ `${PREPARE_LABEL}: ${where} is a \`$\` param reference. This position is a static projection / template \u2014 only \`key\` (and the dynamic pagination options) may bind params.`
4154
+ );
4155
+ }
4156
+ if (typeof value === "function") {
4157
+ throw new Error(
4158
+ `${PREPARE_LABEL}: ${where} is a function \u2014 a captured per-call value (no runtime capture). This position must be a static, params-independent template.`
4159
+ );
4160
+ }
4161
+ if (value !== null && typeof value === "object") {
4162
+ if (Array.isArray(value)) {
4163
+ value.forEach((v, i) => assertStaticTemplate(v, `${where}[${i}]`));
4164
+ } else {
4165
+ for (const [k, v] of Object.entries(value)) {
4166
+ assertStaticTemplate(v, `${where}.${k}`);
4167
+ }
4168
+ }
4169
+ }
4170
+ }
4171
+ var PREPARE_CACHE_MAX = 256;
4172
+ var BoundedLru = class {
4173
+ constructor(max) {
4174
+ this.max = max;
4175
+ }
4176
+ max;
4177
+ map = /* @__PURE__ */ new Map();
4178
+ get(key) {
4179
+ const v = this.map.get(key);
4180
+ if (v !== void 0) {
4181
+ this.map.delete(key);
4182
+ this.map.set(key, v);
4183
+ }
4184
+ return v;
4185
+ }
4186
+ set(key, value) {
4187
+ if (this.map.has(key)) this.map.delete(key);
4188
+ this.map.set(key, value);
4189
+ if (this.map.size > this.max) {
4190
+ const oldest = this.map.keys().next().value;
4191
+ if (oldest !== void 0) this.map.delete(oldest);
4192
+ }
4193
+ }
4194
+ get size() {
4195
+ return this.map.size;
4196
+ }
4197
+ clear() {
4198
+ this.map.clear();
4199
+ }
4200
+ };
4201
+ var PREPARE_CACHE = new BoundedLru(PREPARE_CACHE_MAX);
4202
+ function structureKey(routes) {
4203
+ const parts = routes.map(({ alias, route }) => {
4204
+ const rec = route;
4205
+ const intent = [...INTENT_KEYS, ...READ_INTENT_KEYS].find((k) => rec[k] !== void 0) ?? "?";
4206
+ const modelRef = rec[intent];
4207
+ const modelId = modelIdentity(modelRef, alias);
4208
+ const key = describeBinding(route.key);
4209
+ const rw = route;
4210
+ const rr = route;
4211
+ const extra = intent === "query" || intent === "list" ? `select=${stableJson(rr.select)};opt=${describeReadOptions(rr.options)}` : `input=${describeBinding(rw.input)};cond=${describeBinding(rw.condition)};result=${stableJson(rw.result)}`;
4212
+ return `${alias}|${intent}|${modelId}|key=${key}|${extra}`;
4213
+ });
4214
+ return parts.join("||");
4215
+ }
4216
+ var MODEL_IDENTITY_IDS = /* @__PURE__ */ new WeakMap();
4217
+ var nextModelIdentityId = 1;
4218
+ function modelIdentity(ref, alias) {
4219
+ try {
4220
+ const model = resolveModelStatic(ref, alias);
4221
+ const cls = resolveModelClass(model);
4222
+ let id = MODEL_IDENTITY_IDS.get(cls);
4223
+ if (id === void 0) {
4224
+ id = nextModelIdentityId++;
4225
+ MODEL_IDENTITY_IDS.set(cls, id);
4226
+ }
4227
+ return `model#${id}`;
4228
+ } catch {
4229
+ return "unresolved";
4230
+ }
4231
+ }
4232
+ function describeBinding(binding) {
4233
+ if (binding === void 0) return "\u2205";
4234
+ const keys = Object.keys(binding).sort();
4235
+ return keys.map((k) => {
4236
+ const v = binding[k];
4237
+ return isPreparedParamRef(v) ? `${k}=$(${v.name})` : `${k}=L(${stableJson(v)})`;
4238
+ }).join(",");
4239
+ }
4240
+ function describeReadOptions(options) {
4241
+ if (options === void 0) return "\u2205";
4242
+ const o = options;
4243
+ const slot = (v) => isPreparedParamRef(v) ? `$(${v.name})` : v === void 0 ? "\u2205" : `L(${stableJson(v)})`;
4244
+ return `cr=${slot(o.consistentRead)};limit=${slot(o.limit)};after=${slot(o.after)};depth=${o.maxDepth ?? "\u2205"};order=${o.order ?? "\u2205"};filter=${stableJson(o.filter)}`;
4245
+ }
4246
+ function stableJson(value) {
4247
+ return JSON.stringify(value, (_k, v) => {
4248
+ if (isPreparedParamRef(v)) return `$${v.name}`;
4249
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
4250
+ const sorted = {};
4251
+ for (const k of Object.keys(v).sort()) {
4252
+ sorted[k] = v[k];
4253
+ }
4254
+ return sorted;
4255
+ }
4256
+ return v;
4257
+ });
4258
+ }
4259
+ function prepare(body) {
4260
+ if (typeof body !== "function") {
4261
+ throw new Error(
4262
+ `${PREPARE_LABEL}: expected a body function \`($) => ({ alias: { ... } })\`.`
4263
+ );
4264
+ }
4265
+ const $ = makePreparedInputProxy();
4266
+ const map = body($);
4267
+ if (map === null || typeof map !== "object") {
4268
+ throw new Error(
4269
+ `${PREPARE_LABEL}: the body must return a descriptor map \`{ alias: { ... } }\`.`
4270
+ );
4271
+ }
4272
+ const routes = Object.keys(map).map((alias) => ({
4273
+ alias,
4274
+ route: map[alias]
4275
+ }));
4276
+ const cacheKey = structureKey(routes);
4277
+ const cached = PREPARE_CACHE.get(cacheKey);
4278
+ if (cached !== void 0) return cached;
4279
+ const kind = classify(routes);
4280
+ let handle;
4281
+ if (kind === "write") {
4282
+ const ops = routes.map(
4283
+ ({ alias, route }) => compileWriteRoute(alias, route)
4284
+ );
4285
+ handle = new PreparedWriteStatement(ops);
4286
+ } else {
4287
+ const compiledRoutes = routes.map(
4288
+ ({ alias, route }) => compileReadRoute(alias, route)
4289
+ );
4290
+ handle = new PreparedReadStatement(compiledRoutes);
4291
+ }
4292
+ PREPARE_CACHE.set(cacheKey, handle);
4293
+ return handle;
4294
+ }
4295
+
3850
4296
  // src/model/DDBModel.ts
3851
4297
  var DDBModel = class {
3852
4298
  /**
@@ -3921,6 +4367,43 @@ var DDBModel = class {
3921
4367
  static mutate(envelope, options) {
3922
4368
  return executeWriteEnvelope(envelope, options);
3923
4369
  }
4370
+ /**
4371
+ * **Prepared statement** (issue #205 / design #203) — the read/write-unified
4372
+ * middle tier between the ad-hoc `DDBModel.mutate` / `DDBModel.query` (per-call
4373
+ * recompiled) and the public CQRS contract (`publicCommandModel` /
4374
+ * `publicQueryModel`, precompiled but contract-welded).
4375
+ *
4376
+ * `prepare($ => ({...}))` **compiles the declarative route body once** and returns
4377
+ * a handle whose `.execute(params)` binds the per-call values and runs through the
4378
+ * SAME execution cores those two surfaces use — so a write's derived maintainers /
4379
+ * atomic transaction semantics are identical to `DDBModel.mutate`, and a read's
4380
+ * projection / relation resolution / pagination are identical to `Model.query` /
4381
+ * `list`. A body is **all-read or all-write**:
4382
+ *
4383
+ * ```ts
4384
+ * const createPost = DDBModel.prepare(($) => ({
4385
+ * post: { create: () => Post, key: { threadId: $.threadId, postId: $.postId },
4386
+ * input: { body: $.body } },
4387
+ * }));
4388
+ * await createPost.execute({ threadId, postId, body });
4389
+ *
4390
+ * const userById = DDBModel.prepare(($) => ({
4391
+ * user: { query: () => User, key: { userId: $.userId },
4392
+ * select: { userId: true, name: true } },
4393
+ * }));
4394
+ * await userById.execute({ userId });
4395
+ * ```
4396
+ *
4397
+ * The body may reference only `$` placeholders (per-call values) and module-static
4398
+ * model refs — a captured per-call runtime value is a **loud reject** (the runtime
4399
+ * half of the no-runtime-capture rule; the static build lint is phase 2 / #206). A
4400
+ * handle hoisted to module scope recompiles nothing; an inline `prepare(fn)` hits a
4401
+ * bounded structural cache after the first compile (placement-independent safety
4402
+ * net).
4403
+ */
4404
+ static prepare(body) {
4405
+ return prepare(body);
4406
+ }
3924
4407
  /**
3925
4408
  * Parse a single CDC {@link ChangeEvent} into the `[oldRecord, newRecord]` tuple
3926
4409
  * for THIS model (issue #153 — the pure `(event) => [old, new]` typed mapper).
@@ -4309,12 +4792,12 @@ function wholeKeysSentinel() {
4309
4792
  function isContractKeyFieldRef(value) {
4310
4793
  return typeof value === "object" && value !== null && value[KEY_FIELD_REF_BRAND] === true;
4311
4794
  }
4312
- var PARAM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
4795
+ var PARAM_REF_BRAND2 = /* @__PURE__ */ Symbol("graphddb:contractParamRef");
4313
4796
  function isContractParamRef(value) {
4314
- return typeof value === "object" && value !== null && value[PARAM_REF_BRAND] === true;
4797
+ return typeof value === "object" && value !== null && value[PARAM_REF_BRAND2] === true;
4315
4798
  }
4316
4799
  function mintContractParamRef(field) {
4317
- return { [PARAM_REF_BRAND]: true, field, token: `{${field}}` };
4800
+ return { [PARAM_REF_BRAND2]: true, field, token: `{${field}}` };
4318
4801
  }
4319
4802
  var FROM_REF_BRAND = /* @__PURE__ */ Symbol("graphddb:contractFromRef");
4320
4803
  var COMPOSE_NODE_BRAND = /* @__PURE__ */ Symbol("graphddb:contractComposeNode");
@@ -4571,13 +5054,13 @@ function normalizeReturnSelection(select) {
4571
5054
  }
4572
5055
  return Object.keys(out).length > 0 ? out : void 0;
4573
5056
  }
4574
- var READ_INTENT_KEYS = ["query", "list"];
5057
+ var READ_INTENT_KEYS2 = ["query", "list"];
4575
5058
  var WRITE_INTENT_KEYS = ["create", "update", "remove"];
4576
5059
  function isPublicComposeDescriptor(value) {
4577
5060
  return typeof value === "object" && value !== null && isCommandPlan(value.command);
4578
5061
  }
4579
5062
  function isPublicReadDescriptor(value) {
4580
- return typeof value === "object" && value !== null && READ_INTENT_KEYS.some((k) => value[k] !== void 0);
5063
+ return typeof value === "object" && value !== null && READ_INTENT_KEYS2.some((k) => value[k] !== void 0);
4581
5064
  }
4582
5065
  function isPublicWriteDescriptor(value) {
4583
5066
  return typeof value === "object" && value !== null && WRITE_INTENT_KEYS.some((k) => value[k] !== void 0);
@@ -7118,6 +7601,11 @@ export {
7118
7601
  MAX_TRANSACT_COMPOSE_ITEMS,
7119
7602
  compileMutationPlan,
7120
7603
  executeCommandMethod,
7604
+ isPreparedParamRef,
7605
+ PreparedWriteStatement,
7606
+ PreparedReadStatement,
7607
+ PREPARE_CACHE_MAX,
7608
+ prepare,
7121
7609
  DDBModel,
7122
7610
  isMutationInputRef,
7123
7611
  isMutationFragment,