@crvouga/mockingbird-service-dynamodb 3.10.2 → 3.10.3

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/CHANGELOG.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # Changelog — @crvouga/mockingbird-service-dynamodb
2
2
 
3
- ## 3.10.2 (2026-11-02)
3
+ ## 3.10.3 (2026-11-02)
4
4
 
5
5
  ### Fixes and improvements
6
6
 
7
- - reopen IndexedDB durability connection after the browser force-closes it ([d0b45aa](https://github.com/crvouga/mockingbird/commit/d0b45aa1d12a55967e72b8ddc6573f5998c377ce))
7
+ - parse condition, filter and key-condition expressions by DynamoDB's grammar ([e6085d3](https://github.com/crvouga/mockingbird/commit/e6085d35866b318c7e2b9ee99b5d738352150a99))
8
+
9
+ ## 3.10.2 (2026-11-02)
8
10
 
9
- ### Dependencies
11
+ ### Fixes and improvements
10
12
 
11
- - `@crvouga/mockingbird-service-sqlite`
13
+ - reopen IndexedDB durability connection after the browser force-closes it ([d0b45aa](https://github.com/crvouga/mockingbird/commit/d0b45aa1d12a55967e72b8ddc6573f5998c377ce))
12
14
 
13
15
  ## 3.10.1 (2026-11-02)
14
16
 
package/README.md CHANGED
@@ -46,7 +46,7 @@ const { Item } = await db.send(new GetCommand({ TableName: "records", Key: { pk:
46
46
  await emulator.close() // after the test
47
47
  ```
48
48
 
49
- Supported operations are CreateTable, DescribeTable, GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan, BatchGetItem, BatchWriteItem, TransactGetItems, and TransactWriteItems. The expression subset includes expression name/value aliases, SET/ADD/REMOVE/DELETE, `if_not_exists`, `attribute_exists`, `attribute_not_exists`, `begins_with`, comparisons, BETWEEN, AND/OR, projection, conditions, limits, cursors, index ordering, and return values.
49
+ Supported operations are CreateTable, DescribeTable, GetItem, PutItem, UpdateItem, DeleteItem, Query, Scan, BatchGetItem, BatchWriteItem, TransactGetItems, and TransactWriteItems. The expression subset includes expression name/value aliases, SET/ADD/REMOVE/DELETE, `if_not_exists`; condition, filter and key-condition expressions parsed by DynamoDB's grammar (comparisons between values or paths, BETWEEN, IN, AND/OR/NOT with parentheses, `attribute_exists`, `attribute_not_exists`, `attribute_type`, `begins_with`, `contains`, `size`); projection, conditions, limits, cursors, index ordering, and return values.
50
50
 
51
51
  ### Admin and deterministic controls
52
52
 
@@ -9,7 +9,7 @@ import {
9
9
  parseFaultHeader,
10
10
  resolveAdminPrefix,
11
11
  restoreClock
12
- } from "./chunk-JAVDTXSC.js";
12
+ } from "./chunk-WWR6WDQX.js";
13
13
 
14
14
  // ../../adapters/node/dist/cli.js
15
15
  import { readFile as readFile5 } from "node:fs/promises";
@@ -3057,7 +3057,7 @@ async function preflightFleet(config, options) {
3057
3057
  break;
3058
3058
  }
3059
3059
  }
3060
- targets.set(name, { ...target, packageVersion: packageVersion ?? "3.10.2" });
3060
+ targets.set(name, { ...target, packageVersion: packageVersion ?? "3.10.3" });
3061
3061
  } catch (error) {
3062
3062
  throw new Error(`service ${name}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
3063
3063
  }
@@ -5012,4 +5012,4 @@ export {
5012
5012
  createServer2 as createServer,
5013
5013
  serveTarget
5014
5014
  };
5015
- //# sourceMappingURL=chunk-CMCJXLNA.js.map
5015
+ //# sourceMappingURL=chunk-KVPS4PKK.js.map
@@ -1037,14 +1037,14 @@ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
1037
1037
  const capacity = Math.max(0, Math.floor(size));
1038
1038
  const rings = /* @__PURE__ */ new Map();
1039
1039
  let sequence = 0;
1040
- const order = /* @__PURE__ */ new WeakMap();
1040
+ const order2 = /* @__PURE__ */ new WeakMap();
1041
1041
  const inOrder = (ring) => ring.entries.length < capacity ? ring.entries : [...ring.entries.slice(ring.next), ...ring.entries.slice(0, ring.next)];
1042
1042
  return {
1043
1043
  size: capacity,
1044
1044
  record(entry) {
1045
1045
  if (capacity === 0)
1046
1046
  return;
1047
- order.set(entry, sequence++);
1047
+ order2.set(entry, sequence++);
1048
1048
  let ring = rings.get(entry.namespace);
1049
1049
  if (!ring) {
1050
1050
  ring = { entries: [], next: 0 };
@@ -1058,7 +1058,7 @@ var createJournal = (size = DEFAULT_JOURNAL_SIZE) => {
1058
1058
  }
1059
1059
  },
1060
1060
  list(query = {}) {
1061
- const source = query.namespace !== void 0 ? inOrder(rings.get(query.namespace) ?? { entries: [], next: 0 }) : [...rings.values()].flatMap(inOrder).sort((a, b) => (order.get(a) ?? 0) - (order.get(b) ?? 0));
1061
+ const source = query.namespace !== void 0 ? inOrder(rings.get(query.namespace) ?? { entries: [], next: 0 }) : [...rings.values()].flatMap(inOrder).sort((a, b) => (order2.get(a) ?? 0) - (order2.get(b) ?? 0));
1062
1062
  const matched = source.filter((entry) => (query.operationId === void 0 || entry.operationId === query.operationId) && (query.method === void 0 || entry.method.toUpperCase() === query.method.toUpperCase()) && (query.path === void 0 || entry.path === query.path) && (query.faultId === void 0 || entry.faultId === query.faultId) && (query.status === void 0 || entry.status === query.status) && (query.since === void 0 || Date.parse(entry.at) >= query.since));
1063
1063
  if (query.limit === void 0)
1064
1064
  return matched;
@@ -1991,7 +1991,7 @@ var listing = (entry) => ({
1991
1991
  hits: entry.hits,
1992
1992
  seen: entry.seen
1993
1993
  });
1994
- var createFaultRegistry = (random = createRng(0), sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) => {
1994
+ var createFaultRegistry = (random = createRng(0), sleep = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))) => {
1995
1995
  const entries = [];
1996
1996
  const counters = /* @__PURE__ */ new Map();
1997
1997
  const own = /* @__PURE__ */ new Map();
@@ -2287,7 +2287,7 @@ var createNamespaceControls = (options) => {
2287
2287
  if (change.autoAdvanceMs !== void 0)
2288
2288
  settingsOf(namespace).autoAdvanceMs = change.autoAdvanceMs;
2289
2289
  };
2290
- const resolve = (namespace, input, now) => {
2290
+ const resolve2 = (namespace, input, now) => {
2291
2291
  if (!isRecord2(input))
2292
2292
  throw new ControlError("fault: expected an object");
2293
2293
  if (typeof input.preset !== "string") {
@@ -2339,7 +2339,7 @@ var createNamespaceControls = (options) => {
2339
2339
  for (const each of plan.faults ?? []) {
2340
2340
  if (isRecord2(each) && each.namespace === "*")
2341
2341
  throw new ControlError('faults: a control document cannot hold a "*" rule; add it with POST /faults');
2342
- resolve("default", each, 0);
2342
+ resolve2("default", each, 0);
2343
2343
  }
2344
2344
  };
2345
2345
  const commit = (namespace, plan, replace) => {
@@ -2356,7 +2356,7 @@ var createNamespaceControls = (options) => {
2356
2356
  faults.clear(namespace);
2357
2357
  const now = options.clock(namespace).now();
2358
2358
  for (const each of plan.faults)
2359
- store(namespace, resolve(namespace, { ...each, namespace }, now));
2359
+ store(namespace, resolve2(namespace, { ...each, namespace }, now));
2360
2360
  }
2361
2361
  };
2362
2362
  const bootPlan = (namespace) => ({
@@ -2433,7 +2433,7 @@ var createNamespaceControls = (options) => {
2433
2433
  },
2434
2434
  addFault(namespace, input) {
2435
2435
  ensure(namespace);
2436
- return store(namespace, resolve(namespace, input, options.clock(namespace).now()));
2436
+ return store(namespace, resolve2(namespace, input, options.clock(namespace).now()));
2437
2437
  },
2438
2438
  scriptFaults(namespace, request) {
2439
2439
  const rules = [];
@@ -2443,7 +2443,7 @@ var createNamespaceControls = (options) => {
2443
2443
  if (chooses)
2444
2444
  throw new ControlError(`fault: ${chooses} has no meaning here; a fault in a header is for its own request`);
2445
2445
  }
2446
- for (const { rule } of resolve(namespace, request.fault, 0).rules) {
2446
+ for (const { rule } of resolve2(namespace, request.fault, 0).rules) {
2447
2447
  const action = { id: "request" };
2448
2448
  for (const key of ACTION)
2449
2449
  if (rule[key] !== void 0)
@@ -3646,12 +3646,12 @@ var inspectState = (scope) => {
3646
3646
  ...declaration?.meta !== void 0 ? { meta: declaration.meta } : {}
3647
3647
  });
3648
3648
  }
3649
- const order = /* @__PURE__ */ new Map();
3649
+ const order2 = /* @__PURE__ */ new Map();
3650
3650
  for (const [index, item] of (scope.declarations ?? []).entries())
3651
- order.set(item.name, index);
3651
+ order2.set(item.name, index);
3652
3652
  collections.sort((a, b) => {
3653
- const left = order.get(a.name);
3654
- const right = order.get(b.name);
3653
+ const left = order2.get(a.name);
3654
+ const right = order2.get(b.name);
3655
3655
  if (left !== void 0 && right !== void 0)
3656
3656
  return left - right;
3657
3657
  if (left !== void 0)
@@ -3673,14 +3673,14 @@ var pageOf = (sqlite, storageNamespace, name2, query) => {
3673
3673
  if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
3674
3674
  return fail(400, "limit must be an integer from 1 to 200");
3675
3675
  }
3676
- const order = query.order ?? "newest";
3677
- if (order !== "newest" && order !== "oldest")
3676
+ const order2 = query.order ?? "newest";
3677
+ if (order2 !== "newest" && order2 !== "oldest")
3678
3678
  return fail(400, 'order must be "newest" or "oldest"');
3679
3679
  const collection = collectionOf(sqlite, storageNamespace, name2);
3680
- const rows = collection.list({ order }).filter((row) => {
3680
+ const rows = collection.list({ order: order2 }).filter((row) => {
3681
3681
  if (query.after === void 0)
3682
3682
  return true;
3683
- return order === "newest" ? row.seq < query.after : row.seq > query.after;
3683
+ return order2 === "newest" ? row.seq < query.after : row.seq > query.after;
3684
3684
  });
3685
3685
  const slice = rows.slice(0, limit);
3686
3686
  const last = slice.at(-1);
@@ -3795,11 +3795,11 @@ var stateAdminRoutes = (routes) => {
3795
3795
  return adminError2(400, "limit: expected an integer");
3796
3796
  if (after === "bad")
3797
3797
  return adminError2(400, "after: expected a sequence number");
3798
- const order = url.searchParams.get("order");
3798
+ const order2 = url.searchParams.get("order");
3799
3799
  return fromResult(readStatePage(scope.value, params.collection ?? "", {
3800
3800
  ...limit !== void 0 ? { limit } : {},
3801
3801
  ...after !== void 0 ? { after } : {},
3802
- ...order === "newest" || order === "oldest" ? { order } : {}
3802
+ ...order2 === "newest" || order2 === "oldest" ? { order: order2 } : {}
3803
3803
  }), 200);
3804
3804
  },
3805
3805
  "GET /state/:collection/:id": ({ namespace, params }) => {
@@ -3861,7 +3861,7 @@ var stateAdminRoutes = (routes) => {
3861
3861
  };
3862
3862
 
3863
3863
  // ../core/dist/version.js
3864
- var PACKAGE_VERSION = true ? "3.10.2" : UNRELEASED_VERSION;
3864
+ var PACKAGE_VERSION = true ? "3.10.3" : UNRELEASED_VERSION;
3865
3865
 
3866
3866
  // ../core/dist/webhooks.js
3867
3867
  var json4 = (status, body) => new Response(JSON.stringify(body), {
@@ -4016,7 +4016,7 @@ var DroppedConnectionError = class extends TypeError {
4016
4016
  this.reason = reason;
4017
4017
  }
4018
4018
  };
4019
- var realSleep = (ms, signal) => new Promise((resolve, reject) => {
4019
+ var realSleep = (ms, signal) => new Promise((resolve2, reject) => {
4020
4020
  if (signal?.aborted) {
4021
4021
  reject(signal.reason);
4022
4022
  return;
@@ -4027,26 +4027,26 @@ var realSleep = (ms, signal) => new Promise((resolve, reject) => {
4027
4027
  };
4028
4028
  const timer = setTimeout(() => {
4029
4029
  signal?.removeEventListener("abort", abort);
4030
- resolve();
4030
+ resolve2();
4031
4031
  }, ms);
4032
4032
  signal?.addEventListener("abort", abort, { once: true });
4033
4033
  });
4034
4034
  var NULL_BODY_STATUS = /* @__PURE__ */ new Set([101, 204, 205, 304]);
4035
- var truncateResponse = (response, bytes) => {
4035
+ var truncateResponse = (response, bytes2) => {
4036
4036
  if (NULL_BODY_STATUS.has(response.status))
4037
4037
  return response;
4038
4038
  const reader = response.body?.getReader();
4039
4039
  let sent = 0;
4040
4040
  const body = new ReadableStream({
4041
4041
  async pull(controller) {
4042
- const chunk = sent < bytes && reader ? await reader.read() : void 0;
4042
+ const chunk = sent < bytes2 && reader ? await reader.read() : void 0;
4043
4043
  if (!chunk || chunk.done) {
4044
4044
  await reader?.cancel().catch(() => {
4045
4045
  });
4046
4046
  controller.error(new DroppedConnectionError("truncate"));
4047
4047
  return;
4048
4048
  }
4049
- const part = chunk.value.subarray(0, bytes - sent);
4049
+ const part = chunk.value.subarray(0, bytes2 - sent);
4050
4050
  sent += part.length;
4051
4051
  controller.enqueue(part);
4052
4052
  },
@@ -5141,6 +5141,291 @@ var credentialRoutes = (registry) => ({
5141
5141
  }
5142
5142
  });
5143
5143
 
5144
+ // src/expression.ts
5145
+ var COMPARATORS = /* @__PURE__ */ new Set(["=", "<>", "<", "<=", ">", ">="]);
5146
+ var FUNCTIONS = /* @__PURE__ */ new Set([
5147
+ "attribute_exists",
5148
+ "attribute_not_exists",
5149
+ "attribute_type",
5150
+ "begins_with",
5151
+ "contains"
5152
+ ]);
5153
+ var TYPES = /* @__PURE__ */ new Set(["S", "SS", "N", "NS", "B", "BS", "BOOL", "NULL", "L", "M"]);
5154
+ var tokenize = (source) => {
5155
+ const tokens = [];
5156
+ const pattern = /\s+|(<>|<=|>=|[=<>(),.[\]])|(#[A-Za-z0-9_]+)|(:[A-Za-z0-9_]+)|([0-9]+)(?![A-Za-z_])|([A-Za-z_][A-Za-z0-9_-]*)/y;
5157
+ let index = 0;
5158
+ while (index < source.length) {
5159
+ pattern.lastIndex = index;
5160
+ const match2 = pattern.exec(source);
5161
+ if (!match2)
5162
+ throw new SyntaxError(`Invalid ConditionExpression: Syntax error; token: "${source[index]}"`);
5163
+ index = pattern.lastIndex;
5164
+ if (match2[1]) tokens.push({ type: "punct", text: match2[1] });
5165
+ else if (match2[2]) tokens.push({ type: "name", text: match2[2] });
5166
+ else if (match2[3]) tokens.push({ type: "value", text: match2[3] });
5167
+ else if (match2[4]) tokens.push({ type: "number", text: match2[4] });
5168
+ else if (match2[5]) tokens.push({ type: "word", text: match2[5] });
5169
+ }
5170
+ return tokens;
5171
+ };
5172
+ var Parser = class {
5173
+ constructor(tokens, names, values) {
5174
+ this.tokens = tokens;
5175
+ this.names = names;
5176
+ this.values = values;
5177
+ }
5178
+ tokens;
5179
+ names;
5180
+ values;
5181
+ index = 0;
5182
+ parse() {
5183
+ const node = this.or();
5184
+ if (this.peek()) this.fail();
5185
+ return node;
5186
+ }
5187
+ peek(offset = 0) {
5188
+ return this.tokens[this.index + offset];
5189
+ }
5190
+ fail() {
5191
+ const token = this.peek();
5192
+ throw new SyntaxError(
5193
+ `Invalid ConditionExpression: Syntax error; token: "${token?.text ?? "<EOF>"}"`
5194
+ );
5195
+ }
5196
+ keyword(word) {
5197
+ const token = this.peek();
5198
+ if (token?.type === "word" && token.text.toUpperCase() === word) {
5199
+ this.index++;
5200
+ return true;
5201
+ }
5202
+ return false;
5203
+ }
5204
+ punct(text) {
5205
+ if (this.peek()?.type === "punct" && this.peek()?.text === text) {
5206
+ this.index++;
5207
+ return true;
5208
+ }
5209
+ return false;
5210
+ }
5211
+ expect(text) {
5212
+ if (!this.punct(text)) this.fail();
5213
+ }
5214
+ or() {
5215
+ let node = this.and();
5216
+ while (this.keyword("OR")) node = { kind: "or", left: node, right: this.and() };
5217
+ return node;
5218
+ }
5219
+ and() {
5220
+ let node = this.not();
5221
+ while (this.keyword("AND")) node = { kind: "and", left: node, right: this.not() };
5222
+ return node;
5223
+ }
5224
+ not() {
5225
+ if (this.keyword("NOT")) return { kind: "not", operand: this.not() };
5226
+ return this.primary();
5227
+ }
5228
+ primary() {
5229
+ if (this.punct("(")) {
5230
+ const node = this.or();
5231
+ this.expect(")");
5232
+ return node;
5233
+ }
5234
+ const token = this.peek();
5235
+ if (token?.type === "word" && FUNCTIONS.has(token.text) && this.peek(1)?.type === "punct" && this.peek(1)?.text === "(") {
5236
+ this.index += 2;
5237
+ const args = [this.operand()];
5238
+ while (this.punct(",")) args.push(this.operand());
5239
+ this.expect(")");
5240
+ const arity = token.text === "attribute_exists" || token.text === "attribute_not_exists" ? 1 : 2;
5241
+ if (args.length !== arity || args[0]?.kind !== "path")
5242
+ throw new SyntaxError(`Invalid ConditionExpression: Incorrect operands for ${token.text}`);
5243
+ return { kind: "call", name: token.text, args };
5244
+ }
5245
+ const left = this.operand();
5246
+ if (this.keyword("BETWEEN")) {
5247
+ const low = this.operand();
5248
+ if (!this.keyword("AND")) this.fail();
5249
+ return { kind: "between", subject: left, low, high: this.operand() };
5250
+ }
5251
+ if (this.keyword("IN")) {
5252
+ this.expect("(");
5253
+ const candidates = [this.operand()];
5254
+ while (this.punct(",")) candidates.push(this.operand());
5255
+ this.expect(")");
5256
+ return { kind: "in", subject: left, candidates };
5257
+ }
5258
+ const op = this.peek();
5259
+ if (op?.type !== "punct" || !COMPARATORS.has(op.text)) this.fail();
5260
+ this.index++;
5261
+ return { kind: "compare", op: op.text, left, right: this.operand() };
5262
+ }
5263
+ operand() {
5264
+ const token = this.peek();
5265
+ if (token?.type === "value") {
5266
+ this.index++;
5267
+ const value = this.values[token.text];
5268
+ if (!value)
5269
+ throw new SyntaxError(
5270
+ `Invalid ConditionExpression: An expression attribute value used in expression is not defined; attribute value: ${token.text}`
5271
+ );
5272
+ return { kind: "value", value };
5273
+ }
5274
+ if (token?.type === "word" && token.text === "size" && this.peek(1)?.type === "punct" && this.peek(1)?.text === "(") {
5275
+ this.index += 2;
5276
+ const path = this.path();
5277
+ this.expect(")");
5278
+ return { kind: "size", path };
5279
+ }
5280
+ return { kind: "path", path: this.path() };
5281
+ }
5282
+ segment() {
5283
+ const token = this.peek();
5284
+ if (token?.type === "name") {
5285
+ this.index++;
5286
+ const name2 = this.names[token.text];
5287
+ if (name2 === void 0)
5288
+ throw new SyntaxError(
5289
+ `Invalid ConditionExpression: An expression attribute name used in the document path is not defined; attribute name: ${token.text}`
5290
+ );
5291
+ return name2;
5292
+ }
5293
+ if (token?.type === "word") {
5294
+ this.index++;
5295
+ return token.text;
5296
+ }
5297
+ return this.fail();
5298
+ }
5299
+ path() {
5300
+ const path = [this.segment()];
5301
+ for (; ; ) {
5302
+ if (this.punct(".")) path.push(this.segment());
5303
+ else if (this.punct("[")) {
5304
+ const token = this.peek();
5305
+ if (token?.type !== "number") this.fail();
5306
+ this.index++;
5307
+ path.push(Number(token.text));
5308
+ this.expect("]");
5309
+ } else return path;
5310
+ }
5311
+ }
5312
+ };
5313
+ var resolve = (item, path) => {
5314
+ let value = item[path[0]];
5315
+ for (const segment of path.slice(1))
5316
+ value = typeof segment === "number" ? value?.L?.[segment] : value?.M?.[segment];
5317
+ return value;
5318
+ };
5319
+ var typeOf = (value) => Object.keys(value)[0];
5320
+ var bytes = (base64) => Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
5321
+ var order = (left, right) => {
5322
+ if (left?.N !== void 0 && right?.N !== void 0) return Number(left.N) - Number(right.N);
5323
+ if (left?.S !== void 0 && right?.S !== void 0)
5324
+ return left.S < right.S ? -1 : left.S > right.S ? 1 : 0;
5325
+ if (left?.B !== void 0 && right?.B !== void 0) {
5326
+ const a = bytes(String(left.B));
5327
+ const b = bytes(String(right.B));
5328
+ for (let i = 0; i < Math.min(a.length, b.length); i++)
5329
+ if (a[i] !== b[i]) return a[i] - b[i];
5330
+ return a.length - b.length;
5331
+ }
5332
+ return void 0;
5333
+ };
5334
+ var sameSet = (left, right) => left.length === right.length && left.every((entry) => right.includes(entry));
5335
+ var equal = (left, right) => {
5336
+ if (left === void 0 || right === void 0) return false;
5337
+ if (left.N !== void 0 && right.N !== void 0) return Number(left.N) === Number(right.N);
5338
+ for (const set2 of ["SS", "NS", "BS"]) {
5339
+ const a = left[set2];
5340
+ const b = right[set2];
5341
+ if (a && b) return sameSet(a, b);
5342
+ }
5343
+ return JSON.stringify(left) === JSON.stringify(right);
5344
+ };
5345
+ var operandValue = (item, operand) => {
5346
+ if (operand.kind === "value") return operand.value;
5347
+ const value = resolve(item, operand.path);
5348
+ if (operand.kind === "path" || value === void 0) return value;
5349
+ if (value.S !== void 0) return { N: String([...value.S].length) };
5350
+ if (value.B !== void 0) return { N: String(bytes(String(value.B)).length) };
5351
+ const collection = value.SS ?? value.NS ?? value.BS ?? value.L;
5352
+ if (collection) return { N: String(collection.length) };
5353
+ if (value.M) return { N: String(Object.keys(value.M).length) };
5354
+ return void 0;
5355
+ };
5356
+ var call = (item, name2, args) => {
5357
+ const subject = operandValue(item, args[0]);
5358
+ const argument = args[1] ? operandValue(item, args[1]) : void 0;
5359
+ switch (name2) {
5360
+ case "attribute_exists":
5361
+ return subject !== void 0;
5362
+ case "attribute_not_exists":
5363
+ return subject === void 0;
5364
+ case "attribute_type":
5365
+ if (argument?.S === void 0 || !TYPES.has(argument.S))
5366
+ throw new SyntaxError(
5367
+ `Invalid ConditionExpression: Invalid attribute type name found in type`
5368
+ );
5369
+ return subject !== void 0 && typeOf(subject) === argument.S;
5370
+ case "begins_with":
5371
+ if (subject?.S !== void 0 && argument?.S !== void 0)
5372
+ return subject.S.startsWith(argument.S);
5373
+ if (subject?.B !== void 0 && argument?.B !== void 0) {
5374
+ const whole = bytes(String(subject.B));
5375
+ const prefix = bytes(String(argument.B));
5376
+ return prefix.every((byte, i) => whole[i] === byte);
5377
+ }
5378
+ return false;
5379
+ case "contains":
5380
+ if (subject === void 0 || argument === void 0) return false;
5381
+ if (subject.S !== void 0 && argument.S !== void 0) return subject.S.includes(argument.S);
5382
+ if (subject.SS && argument.S !== void 0) return subject.SS.includes(argument.S);
5383
+ if (subject.NS && argument.N !== void 0)
5384
+ return subject.NS.some((entry) => Number(entry) === Number(argument.N));
5385
+ if (subject.BS && argument.B !== void 0) return subject.BS.includes(argument.B);
5386
+ if (subject.L) return subject.L.some((entry) => equal(entry, argument));
5387
+ return false;
5388
+ }
5389
+ return false;
5390
+ };
5391
+ var evaluate = (node, item) => {
5392
+ switch (node.kind) {
5393
+ case "or":
5394
+ return evaluate(node.left, item) || evaluate(node.right, item);
5395
+ case "and":
5396
+ return evaluate(node.left, item) && evaluate(node.right, item);
5397
+ case "not":
5398
+ return !evaluate(node.operand, item);
5399
+ case "call":
5400
+ return call(item, node.name, node.args);
5401
+ case "in": {
5402
+ const subject = operandValue(item, node.subject);
5403
+ return node.candidates.some((candidate) => equal(subject, operandValue(item, candidate)));
5404
+ }
5405
+ case "between": {
5406
+ const subject = operandValue(item, node.subject);
5407
+ const low = order(subject, operandValue(item, node.low));
5408
+ const high = order(subject, operandValue(item, node.high));
5409
+ return low !== void 0 && high !== void 0 && low >= 0 && high <= 0;
5410
+ }
5411
+ case "compare": {
5412
+ const left = operandValue(item, node.left);
5413
+ const right = operandValue(item, node.right);
5414
+ if (node.op === "=") return equal(left, right);
5415
+ if (node.op === "<>") return !equal(left, right);
5416
+ const sign = order(left, right);
5417
+ if (sign === void 0) return false;
5418
+ return node.op === "<" ? sign < 0 : node.op === "<=" ? sign <= 0 : node.op === ">" ? sign > 0 : sign >= 0;
5419
+ }
5420
+ }
5421
+ };
5422
+ var compileCondition = (expression, names, values) => {
5423
+ if (typeof expression !== "string" || !expression.trim()) return () => true;
5424
+ const tree = new Parser(tokenize(expression), names, values).parse();
5425
+ return (item) => evaluate(tree, item ?? {});
5426
+ };
5427
+ var condition = (expression, item, names, values) => compileCondition(expression, names, values)(item);
5428
+
5144
5429
  // src/generated/openapi.ts
5145
5430
  var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Amazon DynamoDB (Mockingbird subset)","version":"2012-08-10","description":"AWS JSON DynamoDB contract used by AWS SDK v3."},"servers":[{"url":"https://dynamodb.us-east-1.amazonaws.com"}],"paths":{"/":{"post":{"operationId":"DynamoDbRpc","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"parameters":[{"name":"X-Amz-Target","in":"header","required":true,"schema":{"type":"string","pattern":"^DynamoDB_20120810\\\\\\\\.[A-Za-z]+$"}}],"requestBody":{"required":true,"content":{"application/x-amz-json-1.0":{"schema":{"type":"object","additionalProperties":true}},"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"responses":{"200":{"description":"Operation response","content":{"application/x-amz-json-1.0":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"DynamoDB exception","content":{"application/x-amz-json-1.0":{"schema":{"type":"object","additionalProperties":true}}}}}}}}}`);
5146
5431
  var operationIds = ["DynamoDbRpc"];
@@ -5231,7 +5516,6 @@ var DYNAMODB_NAMESPACE = "dynamodb";
5231
5516
  var accessKeyCredential = sigV4AccessKeyId;
5232
5517
  var clone = (value) => structuredClone(value);
5233
5518
  var scalar = (value) => value?.S ?? (value?.N !== void 0 ? Number(value.N) : value?.BOOL ?? value?.B);
5234
- var equal = (left, right) => JSON.stringify(left) === JSON.stringify(right);
5235
5519
  var resolveName = (name2, names) => name2.split(".").map((part) => names[part] ?? part).join(".");
5236
5520
  var get = (item, path, names) => {
5237
5521
  const parts = resolveName(path.trim(), names).split(".");
@@ -5277,52 +5561,6 @@ var splitTop = (value, separator = ",") => {
5277
5561
  out.push(value.slice(start).trim());
5278
5562
  return out.filter(Boolean);
5279
5563
  };
5280
- var condition = (expression, item, names, values) => {
5281
- if (typeof expression !== "string" || !expression.trim()) return true;
5282
- const source = expression.trim().replace(/^\((.*)\)$/, "$1");
5283
- const ors = source.split(/\s+OR\s+/i);
5284
- if (ors.length > 1) return ors.some((part) => condition(part, item, names, values));
5285
- const ands = source.split(/\s+AND\s+/i);
5286
- if (ands.length > 1 && !/\s+BETWEEN\s+/i.test(source))
5287
- return ands.every((part) => condition(part, item, names, values));
5288
- const exists = /^attribute_(not_)?exists\s*\(([^)]+)\)$/i.exec(source);
5289
- if (exists)
5290
- return exists[1] ? get(item ?? {}, exists[2], names) === void 0 : get(item ?? {}, exists[2], names) !== void 0;
5291
- const begins = /^begins_with\s*\(([^,]+),\s*(:\w+)\)$/i.exec(source);
5292
- if (begins)
5293
- return String(scalar(get(item ?? {}, begins[1], names)) ?? "").startsWith(
5294
- String(scalar(values[begins[2]]) ?? "")
5295
- );
5296
- const between = /^(.+?)\s+BETWEEN\s+(:\w+)\s+AND\s+(:\w+)$/i.exec(source);
5297
- if (between) {
5298
- const actual = scalar(get(item ?? {}, between[1], names));
5299
- const low = scalar(values[between[2]]);
5300
- const high = scalar(values[between[3]]);
5301
- return actual !== void 0 && low !== void 0 && high !== void 0 && actual >= low && actual <= high;
5302
- }
5303
- const comparison = /^(.+?)\s*(=|<>|<=|>=|<|>)\s*(:\w+)$/.exec(source);
5304
- if (comparison) {
5305
- const leftValue = get(item ?? {}, comparison[1], names);
5306
- const rightValue = values[comparison[3]];
5307
- const left = scalar(leftValue);
5308
- const right = scalar(rightValue);
5309
- switch (comparison[2]) {
5310
- case "=":
5311
- return equal(leftValue, rightValue);
5312
- case "<>":
5313
- return !equal(leftValue, rightValue);
5314
- case "<":
5315
- return left !== void 0 && right !== void 0 && left < right;
5316
- case "<=":
5317
- return left !== void 0 && right !== void 0 && left <= right;
5318
- case ">":
5319
- return left !== void 0 && right !== void 0 && left > right;
5320
- case ">=":
5321
- return left !== void 0 && right !== void 0 && left >= right;
5322
- }
5323
- }
5324
- throw new SyntaxError(`Unsupported expression: ${expression}`);
5325
- };
5326
5564
  var project = (item, expression, names) => typeof expression !== "string" ? clone(item) : Object.fromEntries(
5327
5565
  splitTop(expression).map((path) => [resolveName(path, names), get(item, path, names)]).filter((entry) => entry[1] !== void 0)
5328
5566
  );
@@ -5548,14 +5786,12 @@ var DynamoAPI = class {
5548
5786
  const found = rows.findIndex((row) => row.key === start);
5549
5787
  if (found >= 0) rows = rows.slice(found + 1);
5550
5788
  }
5551
- const matching = rows.filter(
5552
- (row) => !query || condition(input.KeyConditionExpression, row.value, names, values)
5553
- );
5789
+ const keyCondition = compileCondition(query ? input.KeyConditionExpression : "", names, values);
5790
+ const filter = compileCondition(input.FilterExpression, names, values);
5791
+ const matching = rows.filter((row) => keyCondition(row.value));
5554
5792
  const limit = input.Limit === void 0 ? matching.length : Math.max(0, Number(input.Limit));
5555
5793
  const evaluated = matching.slice(0, limit);
5556
- const filtered = evaluated.filter(
5557
- (row) => condition(input.FilterExpression, row.value, names, values)
5558
- );
5794
+ const filtered = evaluated.filter((row) => filter(row.value));
5559
5795
  const last = matching.length > evaluated.length ? evaluated.at(-1) : void 0;
5560
5796
  return {
5561
5797
  Items: filtered.map((row) => project(row.value, input.ProjectionExpression, names)),
@@ -5763,4 +5999,4 @@ export {
5763
5999
  accessKeyCredential,
5764
6000
  DynamoAPI
5765
6001
  };
5766
- //# sourceMappingURL=chunk-JAVDTXSC.js.map
6002
+ //# sourceMappingURL=chunk-WWR6WDQX.js.map