graphddb 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -37,6 +37,64 @@ A partition key is a graph entry point, a relation an explicit traversal path, a
37
37
  selection set. GraphDDB does not invent a query language — it executes well-designed access
38
38
  patterns directly and makes ambiguity visible through plans, limits, and relation resolution.
39
39
 
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install graphddb
44
+ ```
45
+
46
+ The AWS SDK v3 is a peer dependency — install the clients you use:
47
+
48
+ ```bash
49
+ npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
50
+ ```
51
+
52
+ `tsx` is an optional peer dependency, needed only to run the code-generation CLI
53
+ (`graphddb generate …`) directly against TypeScript definition files. Library
54
+ consumers don't need it (so it never pulls esbuild into your runtime bundle):
55
+
56
+ ```bash
57
+ npm install -D tsx
58
+ ```
59
+
60
+ Requires Node.js ≥ 22.
61
+
62
+ ## Quick Start
63
+
64
+ ```ts
65
+ import { DDBModel, model, string, key, k } from 'graphddb';
66
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
67
+
68
+ @model({ table: 'AppTable', prefix: 'USER' })
69
+ class UserModel extends DDBModel {
70
+ static readonly keys = key<{ userId: string }>((c) => ({
71
+ pk: k`USER#${c.userId}`,
72
+ sk: k`PROFILE`,
73
+ }));
74
+
75
+ @string userId!: string;
76
+ @string name!: string;
77
+ }
78
+ const User = UserModel.asModel();
79
+
80
+ // Wire up the DynamoDB client once (configure retries/region on the client itself).
81
+ DDBModel.setClient(new DynamoDBClient({}));
82
+
83
+ // Read — only the selected fields appear in the result type.
84
+ const user = await User.query({ userId: 'alice' }, { name: true });
85
+
86
+ // Single-item write — raw base op, named after the DynamoDB API.
87
+ await User.putItem({ userId: 'alice', name: 'Alice' });
88
+
89
+ // Multi-item atomic write — the unified envelope (default `mode: 'transaction'`).
90
+ await DDBModel.mutate({
91
+ user: { update: User, key: { userId: 'alice' }, input: { name: 'Alice B.' } },
92
+ });
93
+ ```
94
+
95
+ For cross-service contracts, the Python bridge, multi-fragment atomic mutations,
96
+ and more, see the sections below and [`docs/spec.md`](./docs/spec.md).
97
+
40
98
  ## User Permissions Example
41
99
 
42
100
  The [user-permissions](./examples/user-permissions/) example models users, groups, memberships, and permissions in a single DynamoDB table.
@@ -272,6 +330,10 @@ are aliased (`ExpressionAttributeNames`) — there is no literal interpolation,
272
330
  the compiled expression is injection-free, and it is attached independently of
273
331
  the `KeyConditionExpression` / `ProjectionExpression`.
274
332
 
333
+ The same `cond` fragment is also accepted as a write `condition` (on
334
+ `putItem` / `updateItem` / `deleteItem`, transaction items, and public commands),
335
+ not just as a read filter.
336
+
275
337
  > **RCU note:** A `FilterExpression` does **not** reduce read capacity —
276
338
  > DynamoDB reads matching keys first, then filters. `limit` is applied
277
339
  > **before** the filter, so a single page may return **fewer** than `limit`
@@ -339,7 +401,11 @@ const res = await DDBModel.mutate({
339
401
  - **`key`** is a single object or an **array of objects** (key-array bulk). A
340
402
  transaction compiles to `TransactWriteItems`; parallel mode compiles to
341
403
  `BatchWriteItem` (chunked, with `UnprocessedItems` retry).
342
- - **`condition`** is an equality subset that gates the write.
404
+ - **`condition`** is a `WriteCondition` write gate the same declarative
405
+ operator subset as read filters (`eq`/`ne`/comparisons/`between`/`in`/string
406
+ ops/`size`/`attributeType` + `and`/`or`/`not`), the legacy existence primitives
407
+ (`notExists` / `attributeExists` / `attributeNotExists`), or a raw `cond`
408
+ fragment.
343
409
  - **`result: { select, options? }`** reads the written item back; omit it and the
344
410
  route returns void.
345
411
  - **`mode: 'transaction'`** (DEFAULT) is one atomic `TransactWriteItems`:
@@ -515,7 +581,6 @@ Layer 5: Hydrator
515
581
  | Structured keys | Keys are built from `k` segment templates; partial keys compile to `begins_with` at a segment boundary. |
516
582
  | Typed consistentRead | `consistentRead` is only available for PK queries. GSI queries are rejected at compile time. |
517
583
  | Server-side filter | `filter` is a declarative DynamoDB `FilterExpression` (AppSync `ModelFilterInput`-compatible), typed against the full entity and able to reference unprojected attributes. It does not reduce RCU, and `limit` is applied before it. |
518
- | Post-load filtering | No built-in post-load predicate; use `result.items.filter(...)` on the typed projection. Efficient narrowing belongs in key design. |
519
584
 
520
585
  ## Architectural Boundaries
521
586
 
@@ -583,7 +648,7 @@ The query / relation / write API above is the core layer. GraphDDB also ships:
583
648
  - Entity metadata / decorators
584
649
  - DDBModel base class / connection management
585
650
  - Type system (SelectableOf, QueryResult, QueryKeyOf)
586
- - CRUD (put, update, delete, conditional writes)
651
+ - CRUD (put, update, delete, conditional writes — `WriteCondition` shares the full declarative filter operator set plus the `cond` raw escape hatch)
587
652
  - Query / List (planner, executor, hydrator, cursor pagination)
588
653
  - Type-safe filtering: declarative server-side `filter` (FilterExpression / AppSync `ModelFilterInput`-compatible), plus `Model.col` column refs and the `cond` raw escape hatch
589
654
  - Structured segment keys (`k` tag): partial keys compile to `begins_with` at a segment boundary
@@ -13,6 +13,7 @@ import {
13
13
  buildUpdateInput,
14
14
  captureWrite,
15
15
  commitTransaction,
16
+ compileRawCondition,
16
17
  createColumnMap,
17
18
  execItemKeySignature,
18
19
  executeDelete,
@@ -20,14 +21,18 @@ import {
20
21
  executeTransaction,
21
22
  executeUpdate,
22
23
  isColumn,
24
+ isParam,
25
+ isRawCondition,
26
+ param,
23
27
  pkTemplate,
24
28
  resolveKey,
25
29
  resolveModelClass,
26
30
  resolveSegmentedKey,
27
31
  segmentFieldNames,
28
32
  serializeFieldValue,
33
+ serializeRawCondition,
29
34
  skTemplate
30
- } from "./chunk-347U24SB.js";
35
+ } from "./chunk-PWV7JDMR.js";
31
36
 
32
37
  // src/spec/types.ts
33
38
  var SPEC_VERSION = "1.0";
@@ -236,51 +241,6 @@ function getImplicitKeyFields(select, metadata) {
236
241
  return [...needed];
237
242
  }
238
243
 
239
- // src/define/param.ts
240
- function makeParam(kind, literals) {
241
- return literals === void 0 ? { kind } : { kind, literals };
242
- }
243
- var param = {
244
- /** A placeholder for a `string` value. */
245
- string() {
246
- return makeParam("string");
247
- },
248
- /** A placeholder for a `number` value. */
249
- number() {
250
- return makeParam("number");
251
- },
252
- /**
253
- * A placeholder constrained to one of the given literal values. The value
254
- * type of the resulting {@link Param} is the **union of the literals**, so it
255
- * is preserved precisely in the IR and the inferred definition types.
256
- *
257
- * @example
258
- * ```ts
259
- * param.literal('active', 'disabled'); // Param<'active' | 'disabled'>
260
- * ```
261
- */
262
- literal(...values) {
263
- return makeParam("literal", values);
264
- },
265
- /**
266
- * A placeholder for an **array** parameter whose elements have the given field
267
- * shape. Used by `defineTransaction`'s `tx.forEach(p.<arrayParam>, …)` to bind
268
- * each element's fields to `{item.<field>}` templates.
269
- *
270
- * @example
271
- * ```ts
272
- * param.array({ userId: param.string(), role: param.string() });
273
- * // Param<{ userId: string; role: string }[]>
274
- * ```
275
- */
276
- array(element) {
277
- return { kind: "array", element };
278
- }
279
- };
280
- function isParam(value) {
281
- return typeof value === "object" && value !== null && "kind" in value && (value.kind === "string" || value.kind === "number" || value.kind === "literal" || value.kind === "array");
282
- }
283
-
284
244
  // src/planner/projection.ts
285
245
  function buildProjection(select, additionalFields) {
286
246
  const paths = [];
@@ -321,32 +281,6 @@ function buildProjection(select, additionalFields) {
321
281
  };
322
282
  }
323
283
 
324
- // src/select/cond.ts
325
- var RAW_COND = /* @__PURE__ */ Symbol.for("graphddb.rawCond");
326
- function isRawCondition(value) {
327
- return typeof value === "object" && value !== null && value[RAW_COND] === true;
328
- }
329
- function cond(template, ...parts) {
330
- return {
331
- [RAW_COND]: true,
332
- template,
333
- parts
334
- };
335
- }
336
- function compileRawCondition(raw, allocName, allocValue) {
337
- let out = raw.template[0];
338
- for (let i = 0; i < raw.parts.length; i++) {
339
- const part = raw.parts[i];
340
- if (isColumn(part)) {
341
- out += allocName(part.name);
342
- } else {
343
- out += allocValue(part);
344
- }
345
- out += raw.template[i + 1];
346
- }
347
- return out;
348
- }
349
-
350
284
  // src/expression/filter-expression.ts
351
285
  var LOGICAL_KEYS = /* @__PURE__ */ new Set(["and", "or", "not"]);
352
286
  var OPERATOR_KEYS = /* @__PURE__ */ new Set([
@@ -1916,6 +1850,12 @@ function renderRecord(structure, key, params, context) {
1916
1850
  }
1917
1851
  function renderCondition(condition, key, params, context) {
1918
1852
  if (condition === void 0) return void 0;
1853
+ if (isRawCondition(condition)) {
1854
+ const parts = condition.parts.map(
1855
+ (part) => isColumn(part) ? part : renderLeaf(part, key, params, `${context} condition`)
1856
+ );
1857
+ return { ...condition, parts };
1858
+ }
1919
1859
  if (typeof condition === "object" && condition !== null) {
1920
1860
  const obj = condition;
1921
1861
  if (obj.notExists === true) return { notExists: true };
@@ -2003,14 +1943,14 @@ function renderConditionCheck(check, key, params, contextLabel) {
2003
1943
  modelClass,
2004
1944
  referencedKey
2005
1945
  );
2006
- const cond2 = buildConditionExpression({ attributeExists: "PK" });
1946
+ const cond = buildConditionExpression({ attributeExists: "PK" });
2007
1947
  const checkInput = {
2008
1948
  TableName,
2009
1949
  Key,
2010
- ConditionExpression: cond2.expression
1950
+ ConditionExpression: cond.expression
2011
1951
  };
2012
- if (Object.keys(cond2.names).length > 0) checkInput.ExpressionAttributeNames = cond2.names;
2013
- if (Object.keys(cond2.values).length > 0) checkInput.ExpressionAttributeValues = cond2.values;
1952
+ if (Object.keys(cond.names).length > 0) checkInput.ExpressionAttributeNames = cond.names;
1953
+ if (Object.keys(cond.values).length > 0) checkInput.ExpressionAttributeValues = cond.values;
2014
1954
  return checkInput;
2015
1955
  }
2016
1956
  var RUNTIME_TOKEN_RE = /\{([^{}]+)\}/g;
@@ -2093,10 +2033,10 @@ function renderUniqueGuardItems(guard, key, params, contextLabel) {
2093
2033
  if (item.type === "Put") {
2094
2034
  const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} guard Put`);
2095
2035
  const put = { TableName: TableMapping.resolve(item.tableName), Item };
2096
- const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
2097
- put.ConditionExpression = cond2.expression;
2098
- if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
2099
- if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
2036
+ const cond = buildConditionExpression({ attributeNotExists: "PK" });
2037
+ put.ConditionExpression = cond.expression;
2038
+ if (Object.keys(cond.names).length > 0) put.ExpressionAttributeNames = cond.names;
2039
+ if (Object.keys(cond.values).length > 0) put.ExpressionAttributeValues = cond.values;
2100
2040
  return { Put: put };
2101
2041
  }
2102
2042
  const Key = renderTemplateRecord(item.keyCondition, key, params, `${contextLabel} guard Delete`);
@@ -2115,10 +2055,10 @@ function renderIdempotencyGuardItems(guard, key, params, contextLabel) {
2115
2055
  return guard.items.map((item) => {
2116
2056
  const Item = renderTemplateRecord(item.item, key, params, `${contextLabel} idempotency Put`);
2117
2057
  const put = { TableName: TableMapping.resolve(item.tableName), Item };
2118
- const cond2 = buildConditionExpression({ attributeNotExists: "PK" });
2119
- put.ConditionExpression = cond2.expression;
2120
- if (Object.keys(cond2.names).length > 0) put.ExpressionAttributeNames = cond2.names;
2121
- if (Object.keys(cond2.values).length > 0) put.ExpressionAttributeValues = cond2.values;
2058
+ const cond = buildConditionExpression({ attributeNotExists: "PK" });
2059
+ put.ConditionExpression = cond.expression;
2060
+ if (Object.keys(cond.names).length > 0) put.ExpressionAttributeNames = cond.names;
2061
+ if (Object.keys(cond.values).length > 0) put.ExpressionAttributeValues = cond.values;
2122
2062
  return { Put: put };
2123
2063
  });
2124
2064
  }
@@ -3891,17 +3831,24 @@ function describeKey(fragment) {
3891
3831
  }
3892
3832
 
3893
3833
  // src/spec/guard.ts
3894
- function conditionInputToSpec(condition, context, renderLeaf2) {
3834
+ function conditionInputToSpec(condition, context, renderLeaf2, renderTreeLeaf, renderRawSlot) {
3895
3835
  if (condition === void 0 || condition === null) return void 0;
3836
+ if (isRawCondition(condition)) {
3837
+ const { expression, names, values } = serializeRawCondition(
3838
+ condition,
3839
+ renderRawSlot
3840
+ );
3841
+ return { kind: "raw", expression, names, values };
3842
+ }
3896
3843
  if (typeof condition !== "object") {
3897
3844
  throw new Error(
3898
- `${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or field equality).`
3845
+ `${context}: write condition must be an object (\`{ notExists: true }\`, \`{ attributeExists: '<field>' }\`, \`{ attributeNotExists: '<field>' }\`, or a declarative field condition).`
3899
3846
  );
3900
3847
  }
3901
3848
  const obj = condition;
3902
3849
  if (obj.notExists === true) return { kind: "notExists" };
3903
- if ("attributeExists" in obj) {
3904
- if (typeof obj.attributeExists !== "string" || obj.attributeExists.length === 0) {
3850
+ if ("attributeExists" in obj && typeof obj.attributeExists === "string") {
3851
+ if (obj.attributeExists.length === 0) {
3905
3852
  throw new Error(
3906
3853
  `${context}: \`attributeExists\` must name a non-empty attribute field.`
3907
3854
  );
@@ -3916,12 +3863,122 @@ function conditionInputToSpec(condition, context, renderLeaf2) {
3916
3863
  }
3917
3864
  return { kind: "attributeNotExists", field: obj.attributeNotExists };
3918
3865
  }
3866
+ if (usesOperatorTree(obj)) {
3867
+ const leaf = renderTreeLeaf ?? ((value, name) => defaultTreeLeaf(value, name, context));
3868
+ return { kind: "expr", declarative: buildTree(obj, context, leaf) };
3869
+ }
3919
3870
  const fields = {};
3920
3871
  for (const key of Object.keys(obj).sort()) {
3921
3872
  fields[key] = renderLeaf2(key, obj[key]);
3922
3873
  }
3923
3874
  return { kind: "equals", fields };
3924
3875
  }
3876
+ function assertNotNestedRaw(sub, context) {
3877
+ if (isRawCondition(sub)) {
3878
+ throw new Error(
3879
+ `${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.`
3880
+ );
3881
+ }
3882
+ }
3883
+ var LOGICAL_KEYS2 = /* @__PURE__ */ new Set(["and", "or", "not"]);
3884
+ var OPERATOR_KEYS2 = /* @__PURE__ */ new Set([
3885
+ "eq",
3886
+ "ne",
3887
+ "gt",
3888
+ "ge",
3889
+ "lt",
3890
+ "le",
3891
+ "between",
3892
+ "in",
3893
+ "beginsWith",
3894
+ "contains",
3895
+ "notContains",
3896
+ "attributeExists",
3897
+ "attributeType",
3898
+ "size"
3899
+ ]);
3900
+ function isOperatorObject2(value) {
3901
+ if (typeof value !== "object" || value === null || Array.isArray(value) || value instanceof Date) {
3902
+ return false;
3903
+ }
3904
+ const keys = Object.keys(value);
3905
+ if (keys.length === 0) return false;
3906
+ return keys.every((k) => OPERATOR_KEYS2.has(k));
3907
+ }
3908
+ function usesOperatorTree(obj) {
3909
+ for (const [key, value] of Object.entries(obj)) {
3910
+ if (LOGICAL_KEYS2.has(key)) return true;
3911
+ if (isOperatorObject2(value)) return true;
3912
+ }
3913
+ return false;
3914
+ }
3915
+ function defaultTreeLeaf(value, name, context) {
3916
+ void name;
3917
+ if (value instanceof Date) return value.toISOString();
3918
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
3919
+ return value;
3920
+ }
3921
+ throw new Error(
3922
+ `${context}: condition operand must be a concrete literal or a param; got ${typeof value}.`
3923
+ );
3924
+ }
3925
+ function conditionParamName(field, op, index) {
3926
+ if (op === "between") return `${field}_${index === 0 ? "lo" : "hi"}`;
3927
+ if (op === "in") return `${field}_${index ?? 0}`;
3928
+ if (op === "size") return `${field}_size`;
3929
+ return field;
3930
+ }
3931
+ function buildTree(obj, context, renderTreeLeaf) {
3932
+ const out = {};
3933
+ for (const key of Object.keys(obj).sort()) {
3934
+ const value = obj[key];
3935
+ if (value === void 0) continue;
3936
+ if (key === "and" || key === "or") {
3937
+ out[key] = value.map((sub) => {
3938
+ assertNotNestedRaw(sub, context);
3939
+ return buildTree(sub, context, renderTreeLeaf);
3940
+ });
3941
+ continue;
3942
+ }
3943
+ if (key === "not") {
3944
+ assertNotNestedRaw(value, context);
3945
+ out[key] = buildTree(
3946
+ value,
3947
+ context,
3948
+ renderTreeLeaf
3949
+ );
3950
+ continue;
3951
+ }
3952
+ if (!isOperatorObject2(value)) {
3953
+ out[key] = { eq: renderTreeLeaf(value, conditionParamName(key, "eq")) };
3954
+ continue;
3955
+ }
3956
+ const ops = value;
3957
+ const rendered = {};
3958
+ for (const op of Object.keys(ops).sort()) {
3959
+ const opVal = ops[op];
3960
+ if (op === "between") {
3961
+ const [lo, hi] = opVal;
3962
+ rendered[op] = [
3963
+ renderTreeLeaf(lo, conditionParamName(key, op, 0)),
3964
+ renderTreeLeaf(hi, conditionParamName(key, op, 1))
3965
+ ];
3966
+ } else if (op === "in") {
3967
+ rendered[op] = opVal.map(
3968
+ (v, i) => renderTreeLeaf(v, conditionParamName(key, op, i))
3969
+ );
3970
+ } else if (op === "attributeExists") {
3971
+ rendered[op] = opVal;
3972
+ } else if (op === "attributeType") {
3973
+ rendered[op] = String(opVal);
3974
+ } else {
3975
+ rendered[op] = renderTreeLeaf(opVal, conditionParamName(key, op));
3976
+ }
3977
+ }
3978
+ out[key] = rendered;
3979
+ }
3980
+ return out;
3981
+ }
3925
3982
  function assertSupportedCondition(commandName, condition) {
3926
3983
  if (condition.kind === "notExists") return;
3927
3984
  if (condition.kind === "attributeExists" || condition.kind === "attributeNotExists") {
@@ -3948,8 +4005,25 @@ function assertSupportedCondition(commandName, condition) {
3948
4005
  }
3949
4006
  return;
3950
4007
  }
4008
+ if (condition.kind === "raw") {
4009
+ if (typeof condition.expression !== "string" || condition.expression.length === 0) {
4010
+ throw new Error(
4011
+ `Command '${commandName}' has an empty raw \`cond\` write condition.`
4012
+ );
4013
+ }
4014
+ return;
4015
+ }
4016
+ if (condition.kind === "expr") {
4017
+ const tree = condition.declarative;
4018
+ if (!tree || Object.keys(tree).length === 0) {
4019
+ throw new Error(
4020
+ `Command '${commandName}' has an empty declarative write condition.`
4021
+ );
4022
+ }
4023
+ return;
4024
+ }
3951
4025
  throw new Error(
3952
- `Command '${commandName}' has an unsupported write condition. The Python bridge supports '{ notExists }' or field equality only.`
4026
+ `Command '${commandName}' has an unsupported write condition.`
3953
4027
  );
3954
4028
  }
3955
4029
  function assertJsonSerializable(value, path = "$") {
@@ -4498,6 +4572,17 @@ function leafTemplate(value, context) {
4498
4572
  `defineTransaction: ${context} must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
4499
4573
  );
4500
4574
  }
4575
+ function conditionTreeLeaf(value, name, context) {
4576
+ void name;
4577
+ if (isTransactionRef(value)) return { $param: tokenName(value.token) };
4578
+ if (value instanceof Date) return value.toISOString();
4579
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
4580
+ return value;
4581
+ }
4582
+ throw new Error(
4583
+ `defineTransaction: ${context} condition operand must be a field reference (from \`p\` or a forEach element) or a concrete literal; got ${typeof value}.`
4584
+ );
4585
+ }
4501
4586
  function templateRecord2(structure, context) {
4502
4587
  const out = {};
4503
4588
  for (const field of Object.keys(structure).sort()) {
@@ -4541,7 +4626,12 @@ function conditionSpec(condition, commandName) {
4541
4626
  const spec = conditionInputToSpec(
4542
4627
  condition,
4543
4628
  commandName,
4544
- (field, value) => leafTemplate(value, `condition field '${field}'`)
4629
+ (field, value) => leafTemplate(value, `condition field '${field}'`),
4630
+ (value, name) => conditionTreeLeaf(value, name, commandName),
4631
+ // A raw `cond` value slot in a transaction is a field reference
4632
+ // (`p.*` / forEach element) → a `{ $param }` marker carrying its token name;
4633
+ // a concrete literal falls through to the default renderer (issue #114-B).
4634
+ (part) => isTransactionRef(part) ? { $param: tokenName(part.token) } : void 0
4545
4635
  );
4546
4636
  if (spec) assertSupportedCondition(commandName, spec);
4547
4637
  return spec;
@@ -5092,9 +5182,32 @@ function extractCondition(def) {
5092
5182
  return conditionInputToSpec(
5093
5183
  def.condition,
5094
5184
  `Command on '${def.entity.name}'`,
5095
- (field, value) => templateLeaf(field, value)
5185
+ (field, value) => templateLeaf(field, value),
5186
+ (value, name) => conditionTreeLeaf2(value, name),
5187
+ // A raw `cond` value slot (issue #114-B): a contract param / key ref carries
5188
+ // its own name; a plain `param.*` falls through to the positional default.
5189
+ (part) => {
5190
+ if (isContractParamRef(part)) return { $param: tokenName2(part.token) };
5191
+ if (isContractKeyFieldRef(part)) return { $param: part.field };
5192
+ return void 0;
5193
+ }
5194
+ );
5195
+ }
5196
+ function conditionTreeLeaf2(value, name) {
5197
+ if (isContractParamRef(value)) return { $param: tokenName2(value.token) };
5198
+ if (isContractKeyFieldRef(value)) return { $param: value.field };
5199
+ if (isParam(value)) return { $param: name };
5200
+ if (value instanceof Date) return value.toISOString();
5201
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
5202
+ return value;
5203
+ }
5204
+ throw new Error(
5205
+ `A declarative write-condition operand must be a concrete literal or a param reference; got ${typeof value}.`
5096
5206
  );
5097
5207
  }
5208
+ function tokenName2(token) {
5209
+ return token.startsWith("{") && token.endsWith("}") ? token.slice(1, -1) : token;
5210
+ }
5098
5211
  function buildQuerySpec(def) {
5099
5212
  const operations = buildReadOperations(def);
5100
5213
  const executionPlan = deriveExecutionPlan(operations.map((op) => op.resultPath));
@@ -5252,6 +5365,7 @@ function convertLeaf(leaf, bindField, place, context) {
5252
5365
  );
5253
5366
  }
5254
5367
  function convertCondition(condition, place, context) {
5368
+ if (isRawCondition(condition)) return condition;
5255
5369
  if (typeof condition === "object" && condition !== null) {
5256
5370
  const obj = condition;
5257
5371
  if (obj.notExists === true) return { notExists: true };
@@ -5414,8 +5528,32 @@ function toElementRecord(record, keyParams) {
5414
5528
  return out;
5415
5529
  }
5416
5530
  function toElementCondition(condition, keyParams) {
5417
- if (condition.kind !== "equals") return condition;
5418
- return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
5531
+ if (condition.kind === "equals") {
5532
+ return { kind: "equals", fields: toElementRecord(condition.fields, keyParams) };
5533
+ }
5534
+ if (condition.kind === "expr") {
5535
+ return { kind: "expr", declarative: toElementTree(condition.declarative, keyParams) };
5536
+ }
5537
+ if (condition.kind === "raw") {
5538
+ return {
5539
+ kind: "raw",
5540
+ expression: condition.expression,
5541
+ names: condition.names,
5542
+ values: toElementTree(condition.values, keyParams)
5543
+ };
5544
+ }
5545
+ return condition;
5546
+ }
5547
+ function toElementTree(node, keyParams) {
5548
+ if (Array.isArray(node)) return node.map((n) => toElementTree(n, keyParams));
5549
+ if (node === null || typeof node !== "object") return node;
5550
+ const obj = node;
5551
+ if (typeof obj.$param === "string" && !obj.$param.startsWith("item.")) {
5552
+ return keyParams.has(obj.$param) ? { $param: `item.${obj.$param}` } : obj;
5553
+ }
5554
+ const out = {};
5555
+ for (const [k, v] of Object.entries(obj)) out[k] = toElementTree(v, keyParams);
5556
+ return out;
5419
5557
  }
5420
5558
  function synthesizeBatchTransaction(commandSpec, op) {
5421
5559
  const keyParams = new Set(keyParamNames(op));
@@ -5948,10 +6086,7 @@ export {
5948
6086
  normalizeSelectSpec,
5949
6087
  detectRelationFields,
5950
6088
  getImplicitKeyFields,
5951
- param,
5952
- isParam,
5953
6089
  buildProjection,
5954
- cond,
5955
6090
  compileFilterExpression,
5956
6091
  evaluateFilter,
5957
6092
  plan,
@@ -6013,6 +6148,7 @@ export {
6013
6148
  publicQueryModel,
6014
6149
  publicCommandModel,
6015
6150
  isPlannedCommandMethod,
6151
+ conditionParamName,
6016
6152
  assertSupportedCondition,
6017
6153
  assertJsonSerializable,
6018
6154
  assertBundleSerializable,