graphddb 0.1.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.
@@ -0,0 +1,898 @@
1
+ import {
2
+ createCdcEmulator
3
+ } from "../chunk-UNRQ5YJT.js";
4
+ import {
5
+ ClientManager,
6
+ MetadataRegistry,
7
+ TableMapping,
8
+ resolveModelClass
9
+ } from "../chunk-347U24SB.js";
10
+
11
+ // src/memory/memory-store.ts
12
+ function deepClone(value) {
13
+ return structuredClone(value);
14
+ }
15
+ function compareSortKey(a, b) {
16
+ if (a < b) return -1;
17
+ if (a > b) return 1;
18
+ return 0;
19
+ }
20
+ var MemoryStore = class {
21
+ /** table → PK → SK → item. */
22
+ tables = /* @__PURE__ */ new Map();
23
+ partition(table, pk) {
24
+ return this.tables.get(table)?.get(pk);
25
+ }
26
+ /** Insert/overwrite one physical item. Returns the prior item if present. */
27
+ putItem(table, item) {
28
+ const pk = String(item.PK);
29
+ const sk = String(item.SK ?? "");
30
+ let tbl = this.tables.get(table);
31
+ if (!tbl) {
32
+ tbl = /* @__PURE__ */ new Map();
33
+ this.tables.set(table, tbl);
34
+ }
35
+ let part = tbl.get(pk);
36
+ if (!part) {
37
+ part = /* @__PURE__ */ new Map();
38
+ tbl.set(pk, part);
39
+ }
40
+ const prior = part.get(sk);
41
+ part.set(sk, deepClone(item));
42
+ return prior ? deepClone(prior) : void 0;
43
+ }
44
+ /** Fetch one physical item by base-table key. */
45
+ getItem(table, pk, sk) {
46
+ const item = this.partition(table, pk)?.get(sk);
47
+ return item ? deepClone(item) : void 0;
48
+ }
49
+ /** Delete one item by base-table key. Returns the removed item if present. */
50
+ deleteItem(table, pk, sk) {
51
+ const part = this.partition(table, pk);
52
+ if (!part) return void 0;
53
+ const prior = part.get(sk);
54
+ part.delete(sk);
55
+ if (part.size === 0) {
56
+ this.tables.get(table)?.delete(pk);
57
+ }
58
+ return prior ? deepClone(prior) : void 0;
59
+ }
60
+ /**
61
+ * All items in one base-table partition, sorted ascending by SK (byte order).
62
+ * Used by the Query path (PK eq + optional SK range).
63
+ */
64
+ partitionItems(table, pk) {
65
+ const part = this.partition(table, pk);
66
+ if (!part) return [];
67
+ return [...part.values()].map((it) => deepClone(it)).sort((a, b) => compareSortKey(String(a.SK ?? ""), String(b.SK ?? "")));
68
+ }
69
+ /**
70
+ * All items in one GSI partition (rows whose `{index}PK` equals `pk`), sorted
71
+ * ascending by the GSI sort key (`{index}SK`, byte order). Filtering the base
72
+ * rows is equivalent to a maintained ALL-projection secondary index.
73
+ */
74
+ gsiPartitionItems(table, indexName, pk) {
75
+ const pkAttr = `${indexName}PK`;
76
+ const skAttr = `${indexName}SK`;
77
+ const tbl = this.tables.get(table);
78
+ if (!tbl) return [];
79
+ const matched = [];
80
+ for (const part of tbl.values()) {
81
+ for (const item of part.values()) {
82
+ if (String(item[pkAttr] ?? "") === pk) {
83
+ matched.push(deepClone(item));
84
+ }
85
+ }
86
+ }
87
+ return matched.sort(
88
+ (a, b) => compareSortKey(String(a[skAttr] ?? ""), String(b[skAttr] ?? ""))
89
+ );
90
+ }
91
+ /** Every physical item in one table (unsorted; inspection only). */
92
+ allItems(table) {
93
+ const tbl = this.tables.get(table);
94
+ if (!tbl) return [];
95
+ const out = [];
96
+ for (const part of tbl.values()) {
97
+ for (const item of part.values()) {
98
+ out.push(deepClone(item));
99
+ }
100
+ }
101
+ return out;
102
+ }
103
+ /** Names of every table that currently holds at least one partition. */
104
+ tableNames() {
105
+ return [...this.tables.keys()];
106
+ }
107
+ /** Item count for one table. */
108
+ count(table) {
109
+ const tbl = this.tables.get(table);
110
+ if (!tbl) return 0;
111
+ let n = 0;
112
+ for (const part of tbl.values()) n += part.size;
113
+ return n;
114
+ }
115
+ /** Clear all tables. */
116
+ reset() {
117
+ this.tables.clear();
118
+ }
119
+ /** Structured-clone the whole store (save). */
120
+ snapshot() {
121
+ const out = {};
122
+ for (const table of this.tables.keys()) {
123
+ out[table] = this.allItems(table);
124
+ }
125
+ return out;
126
+ }
127
+ /** Replace all store contents from a snapshot (load). */
128
+ restore(snapshot) {
129
+ this.tables.clear();
130
+ for (const [table, items] of Object.entries(snapshot)) {
131
+ for (const item of items) {
132
+ this.putItem(table, item);
133
+ }
134
+ }
135
+ }
136
+ };
137
+
138
+ // src/memory/filter-eval.ts
139
+ var FUNCTIONS = /* @__PURE__ */ new Set([
140
+ "begins_with",
141
+ "contains",
142
+ "attribute_exists",
143
+ "attribute_not_exists",
144
+ "attribute_type",
145
+ "size"
146
+ ]);
147
+ function tokenize(expr) {
148
+ const tokens = [];
149
+ let i = 0;
150
+ const n = expr.length;
151
+ while (i < n) {
152
+ const c = expr[i];
153
+ if (c === " ") {
154
+ i++;
155
+ continue;
156
+ }
157
+ if (c === "(") {
158
+ tokens.push({ kind: "lparen" });
159
+ i++;
160
+ continue;
161
+ }
162
+ if (c === ")") {
163
+ tokens.push({ kind: "rparen" });
164
+ i++;
165
+ continue;
166
+ }
167
+ if (c === ",") {
168
+ tokens.push({ kind: "comma" });
169
+ i++;
170
+ continue;
171
+ }
172
+ if (c === "#" || c === ":") {
173
+ let j2 = i + 1;
174
+ while (j2 < n && /[A-Za-z0-9_]/.test(expr[j2])) j2++;
175
+ const ident = expr.slice(i, j2);
176
+ tokens.push({ kind: c === "#" ? "name" : "value", value: ident });
177
+ i = j2;
178
+ continue;
179
+ }
180
+ if (c === "<") {
181
+ if (expr[i + 1] === ">") {
182
+ tokens.push({ kind: "op", value: "<>" });
183
+ i += 2;
184
+ } else if (expr[i + 1] === "=") {
185
+ tokens.push({ kind: "op", value: "<=" });
186
+ i += 2;
187
+ } else {
188
+ tokens.push({ kind: "op", value: "<" });
189
+ i++;
190
+ }
191
+ continue;
192
+ }
193
+ if (c === ">") {
194
+ if (expr[i + 1] === "=") {
195
+ tokens.push({ kind: "op", value: ">=" });
196
+ i += 2;
197
+ } else {
198
+ tokens.push({ kind: "op", value: ">" });
199
+ i++;
200
+ }
201
+ continue;
202
+ }
203
+ if (c === "=") {
204
+ tokens.push({ kind: "op", value: "=" });
205
+ i++;
206
+ continue;
207
+ }
208
+ let j = i;
209
+ while (j < n && /[A-Za-z_]/.test(expr[j])) j++;
210
+ const word = expr.slice(i, j);
211
+ i = j;
212
+ const upper = word.toUpperCase();
213
+ if (upper === "AND") tokens.push({ kind: "and" });
214
+ else if (upper === "OR") tokens.push({ kind: "or" });
215
+ else if (upper === "NOT") tokens.push({ kind: "not" });
216
+ else if (upper === "BETWEEN") tokens.push({ kind: "op", value: "BETWEEN" });
217
+ else if (upper === "IN") tokens.push({ kind: "op", value: "IN" });
218
+ else if (FUNCTIONS.has(word)) tokens.push({ kind: "func", value: word });
219
+ else throw new Error(`memory filter: unexpected token '${word}'`);
220
+ }
221
+ return tokens;
222
+ }
223
+ var FilterEvaluator = class {
224
+ constructor(tokens, ctx) {
225
+ this.tokens = tokens;
226
+ this.ctx = ctx;
227
+ }
228
+ tokens;
229
+ ctx;
230
+ pos = 0;
231
+ evaluate() {
232
+ const result = this.parseOr();
233
+ if (this.pos !== this.tokens.length) {
234
+ throw new Error("memory filter: trailing tokens after expression");
235
+ }
236
+ return result;
237
+ }
238
+ peek() {
239
+ return this.tokens[this.pos];
240
+ }
241
+ next() {
242
+ const t = this.tokens[this.pos];
243
+ if (!t) throw new Error("memory filter: unexpected end of expression");
244
+ this.pos++;
245
+ return t;
246
+ }
247
+ parseOr() {
248
+ let left = this.parseAnd();
249
+ while (this.peek()?.kind === "or") {
250
+ this.next();
251
+ const right = this.parseAnd();
252
+ left = left || right;
253
+ }
254
+ return left;
255
+ }
256
+ parseAnd() {
257
+ let left = this.parseNot();
258
+ while (this.peek()?.kind === "and") {
259
+ this.next();
260
+ const right = this.parseNot();
261
+ left = left && right;
262
+ }
263
+ return left;
264
+ }
265
+ parseNot() {
266
+ if (this.peek()?.kind === "not") {
267
+ this.next();
268
+ return !this.parseNot();
269
+ }
270
+ return this.parsePrimary();
271
+ }
272
+ parsePrimary() {
273
+ const t = this.peek();
274
+ if (!t) throw new Error("memory filter: unexpected end of expression");
275
+ if (t.kind === "lparen") {
276
+ this.next();
277
+ const inner = this.parseOr();
278
+ const close = this.next();
279
+ if (close.kind !== "rparen") {
280
+ throw new Error('memory filter: expected ")"');
281
+ }
282
+ return inner;
283
+ }
284
+ if (t.kind === "func") {
285
+ return this.parseFunction();
286
+ }
287
+ if (t.kind === "name") {
288
+ return this.parseComparison();
289
+ }
290
+ throw new Error(`memory filter: unexpected token kind '${t.kind}'`);
291
+ }
292
+ parseFunction() {
293
+ const fn = this.next();
294
+ if (fn.kind !== "func") throw new Error("memory filter: expected function");
295
+ this.expect("lparen");
296
+ if (fn.value === "attribute_exists" || fn.value === "attribute_not_exists") {
297
+ const column2 = this.readName();
298
+ this.expect("rparen");
299
+ const present = this.attrPresent(column2);
300
+ return fn.value === "attribute_exists" ? present : !present;
301
+ }
302
+ if (fn.value === "size") {
303
+ const column2 = this.readName();
304
+ this.expect("rparen");
305
+ const op = this.next();
306
+ if (op.kind !== "op" || op.value !== "=") {
307
+ throw new Error('memory filter: expected "=" after size()');
308
+ }
309
+ const expected = this.readValue();
310
+ const len = this.sizeOf(this.attr(column2));
311
+ return len === expected;
312
+ }
313
+ const column = this.readName();
314
+ this.expect("comma");
315
+ const value = this.readValue();
316
+ this.expect("rparen");
317
+ const actual = this.attr(column);
318
+ if (fn.value === "begins_with") {
319
+ return typeof actual === "string" && actual.startsWith(String(value));
320
+ }
321
+ if (fn.value === "contains") {
322
+ if (typeof actual === "string") return actual.includes(String(value));
323
+ if (Array.isArray(actual)) return actual.includes(value);
324
+ if (actual instanceof Set) return actual.has(value);
325
+ return false;
326
+ }
327
+ return this.attrType(actual) === value;
328
+ }
329
+ parseComparison() {
330
+ const column = this.readName();
331
+ const op = this.next();
332
+ if (op.kind !== "op") {
333
+ throw new Error("memory filter: expected an operator after a name");
334
+ }
335
+ const actual = this.attr(column);
336
+ if (op.value === "BETWEEN") {
337
+ const lo = this.readValue();
338
+ const and = this.next();
339
+ if (and.kind !== "and") {
340
+ throw new Error("memory filter: expected AND in BETWEEN");
341
+ }
342
+ const hi = this.readValue();
343
+ return compare(actual, lo) >= 0 && compare(actual, hi) <= 0;
344
+ }
345
+ if (op.value === "IN") {
346
+ this.expect("lparen");
347
+ const candidates = [this.readValue()];
348
+ while (this.peek()?.kind === "comma") {
349
+ this.next();
350
+ candidates.push(this.readValue());
351
+ }
352
+ this.expect("rparen");
353
+ return candidates.some((v) => looseEq(actual, v));
354
+ }
355
+ const expected = this.readValue();
356
+ switch (op.value) {
357
+ case "=":
358
+ return looseEq(actual, expected);
359
+ case "<>":
360
+ return !looseEq(actual, expected);
361
+ case ">":
362
+ return compare(actual, expected) > 0;
363
+ case ">=":
364
+ return compare(actual, expected) >= 0;
365
+ case "<":
366
+ return compare(actual, expected) < 0;
367
+ case "<=":
368
+ return compare(actual, expected) <= 0;
369
+ default:
370
+ throw new Error(`memory filter: unknown operator '${op.value}'`);
371
+ }
372
+ }
373
+ expect(kind) {
374
+ const t = this.next();
375
+ if (t.kind !== kind) {
376
+ throw new Error(`memory filter: expected '${kind}', got '${t.kind}'`);
377
+ }
378
+ }
379
+ readName() {
380
+ const t = this.next();
381
+ if (t.kind !== "name") throw new Error("memory filter: expected a name alias");
382
+ const column = this.ctx.names[t.value];
383
+ if (column === void 0) {
384
+ throw new Error(`memory filter: unbound name alias '${t.value}'`);
385
+ }
386
+ return column;
387
+ }
388
+ readValue() {
389
+ const t = this.next();
390
+ if (t.kind !== "value") throw new Error("memory filter: expected a value alias");
391
+ if (!(t.value in this.ctx.values)) {
392
+ throw new Error(`memory filter: unbound value alias '${t.value}'`);
393
+ }
394
+ return this.ctx.values[t.value];
395
+ }
396
+ attr(column) {
397
+ return this.ctx.item[column];
398
+ }
399
+ attrPresent(column) {
400
+ return column in this.ctx.item;
401
+ }
402
+ sizeOf(value) {
403
+ if (typeof value === "string" || Array.isArray(value)) return value.length;
404
+ if (value instanceof Set) return value.size;
405
+ if (value && typeof value === "object") return Object.keys(value).length;
406
+ return void 0;
407
+ }
408
+ attrType(value) {
409
+ if (typeof value === "string") return "S";
410
+ if (typeof value === "number") return "N";
411
+ if (typeof value === "boolean") return "BOOL";
412
+ if (value === null) return "NULL";
413
+ if (Array.isArray(value)) return "L";
414
+ if (value instanceof Set) return "SS";
415
+ return "M";
416
+ }
417
+ };
418
+ function looseEq(a, b) {
419
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
420
+ return a === b;
421
+ }
422
+ function compare(a, b) {
423
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
424
+ if (typeof a === "number" && typeof b === "number") return a - b;
425
+ if (typeof a === "string" && typeof b === "string") {
426
+ return a < b ? -1 : a > b ? 1 : 0;
427
+ }
428
+ if (a === b) return 0;
429
+ return a < b ? -1 : 1;
430
+ }
431
+ function evaluateCompiledFilter(filterExpression, names, values, item) {
432
+ const tokens = tokenize(filterExpression);
433
+ return new FilterEvaluator(tokens, { item, names, values }).evaluate();
434
+ }
435
+
436
+ // src/memory/apply-update.ts
437
+ function resolvePath(pathExpr, names) {
438
+ return pathExpr.split(".").map((alias) => {
439
+ const trimmed = alias.trim();
440
+ const column = names[trimmed];
441
+ if (column === void 0) {
442
+ throw new Error(`memory update: unbound name alias '${trimmed}'`);
443
+ }
444
+ return column;
445
+ });
446
+ }
447
+ function setPath(target, segments, value) {
448
+ let cursor = target;
449
+ for (let i = 0; i < segments.length - 1; i++) {
450
+ const seg = segments[i];
451
+ const existing = cursor[seg];
452
+ if (existing === null || typeof existing !== "object" || Array.isArray(existing)) {
453
+ cursor[seg] = {};
454
+ }
455
+ cursor = cursor[seg];
456
+ }
457
+ cursor[segments[segments.length - 1]] = value;
458
+ }
459
+ function removePath(target, segments) {
460
+ let cursor = target;
461
+ for (let i = 0; i < segments.length - 1; i++) {
462
+ const seg = segments[i];
463
+ const existing = cursor[seg];
464
+ if (existing === null || typeof existing !== "object") return;
465
+ cursor = existing;
466
+ }
467
+ delete cursor[segments[segments.length - 1]];
468
+ }
469
+ function applyUpdateExpression(item, expression, clauses) {
470
+ const setMatch = /SET\s+(.*?)(?:\s+REMOVE\s+|$)/.exec(expression);
471
+ const removeMatch = /REMOVE\s+(.*)$/.exec(expression);
472
+ if (setMatch && setMatch[1].trim().length > 0) {
473
+ const assignments = splitTopLevel(setMatch[1]);
474
+ for (const assignment of assignments) {
475
+ const eq = assignment.indexOf("=");
476
+ if (eq < 0) {
477
+ throw new Error(`memory update: malformed SET clause '${assignment}'`);
478
+ }
479
+ const pathExpr = assignment.slice(0, eq).trim();
480
+ const valueAlias = assignment.slice(eq + 1).trim();
481
+ if (!(valueAlias in clauses.values)) {
482
+ throw new Error(`memory update: unbound value alias '${valueAlias}'`);
483
+ }
484
+ const segments = resolvePath(pathExpr, clauses.names);
485
+ setPath(item, segments, clauses.values[valueAlias]);
486
+ }
487
+ }
488
+ if (removeMatch) {
489
+ const paths = splitTopLevel(removeMatch[1]);
490
+ for (const pathExpr of paths) {
491
+ const trimmed = pathExpr.trim();
492
+ if (trimmed.length === 0) continue;
493
+ removePath(item, resolvePath(trimmed, clauses.names));
494
+ }
495
+ }
496
+ }
497
+ function splitTopLevel(clause) {
498
+ const parts = [];
499
+ let depth = 0;
500
+ let current = "";
501
+ for (const ch of clause) {
502
+ if (ch === "(") depth++;
503
+ else if (ch === ")") depth--;
504
+ if (ch === "," && depth === 0) {
505
+ parts.push(current);
506
+ current = "";
507
+ } else {
508
+ current += ch;
509
+ }
510
+ }
511
+ if (current.trim().length > 0) parts.push(current);
512
+ return parts;
513
+ }
514
+
515
+ // src/memory/apply-condition.ts
516
+ var ConditionalCheckFailed = class extends Error {
517
+ // Mirror the AWS SDK error name so existing catch/`name`-based handling and
518
+ // tests that assert on a conditional failure recognize it.
519
+ name = "ConditionalCheckFailedException";
520
+ constructor(message = "The conditional request failed") {
521
+ super(message);
522
+ }
523
+ };
524
+ function evaluateCondition(expression, names, values, item) {
525
+ const effectiveItem = item ?? {};
526
+ const effectiveNames = { ...names ?? {} };
527
+ let expr = expression;
528
+ expr = expr.replace(
529
+ /(attribute_exists|attribute_not_exists)\((PK|SK)\)/g,
530
+ (_m, fn, col) => {
531
+ const alias = `#__cond_${col}`;
532
+ effectiveNames[alias] = col;
533
+ return `${fn}(${alias})`;
534
+ }
535
+ );
536
+ return evaluateCompiledFilter(expr, effectiveNames, values ?? {}, effectiveItem);
537
+ }
538
+
539
+ // src/memory/memory-executor.ts
540
+ var MemoryExecutor = class {
541
+ constructor(store) {
542
+ this.store = store;
543
+ }
544
+ store;
545
+ // ── reads ────────────────────────────────────────────────────────────────
546
+ async execute(operation) {
547
+ if (operation.type === "GetItem") {
548
+ return this.getItem(operation);
549
+ }
550
+ if (operation.type === "BatchGetItem") {
551
+ return this.batchGet(operation);
552
+ }
553
+ return this.query(operation);
554
+ }
555
+ async batchGet(input) {
556
+ const items = [];
557
+ for (const key of input.keys) {
558
+ const found = this.store.getItem(
559
+ input.tableName,
560
+ String(key.PK),
561
+ String(key.SK ?? "")
562
+ );
563
+ if (found) {
564
+ items.push(projectItem(found, input.projectionExpression, input.expressionAttributeNames));
565
+ }
566
+ }
567
+ return { items };
568
+ }
569
+ getItem(operation) {
570
+ const key = operation.keyCondition;
571
+ const found = this.store.getItem(
572
+ operation.tableName,
573
+ String(key.PK),
574
+ String(key.SK ?? "")
575
+ );
576
+ if (!found) return { items: [] };
577
+ return {
578
+ items: [
579
+ projectItem(
580
+ found,
581
+ operation.projectionExpression,
582
+ operation.expressionAttributeNames
583
+ )
584
+ ]
585
+ };
586
+ }
587
+ query(operation) {
588
+ const { keyCondition, indexName } = operation;
589
+ const pkAttr = indexName ? `${indexName}PK` : "PK";
590
+ const skAttr = indexName ? `${indexName}SK` : "SK";
591
+ const pkValue = String(keyCondition[pkAttr]);
592
+ let rows = indexName ? this.store.gsiPartitionItems(operation.tableName, indexName, pkValue) : this.store.partitionItems(operation.tableName, pkValue);
593
+ const skEq = keyCondition[skAttr];
594
+ if (skEq !== void 0) {
595
+ rows = rows.filter((it) => String(it[skAttr] ?? "") === String(skEq));
596
+ } else if (operation.rangeCondition) {
597
+ const prefix = String(operation.rangeCondition.value);
598
+ rows = rows.filter(
599
+ (it) => String(it[operation.rangeCondition.key] ?? "").startsWith(prefix)
600
+ );
601
+ }
602
+ if (operation.scanIndexForward === false) {
603
+ rows = rows.slice().reverse();
604
+ }
605
+ if (operation.exclusiveStartKey) {
606
+ const startSk = String(operation.exclusiveStartKey[skAttr] ?? "");
607
+ const startPk = String(operation.exclusiveStartKey[pkAttr] ?? pkValue);
608
+ const idx = rows.findIndex(
609
+ (it) => String(it[pkAttr] ?? "") === startPk && String(it[skAttr] ?? "") === startSk
610
+ );
611
+ if (idx >= 0) {
612
+ rows = rows.slice(idx + 1);
613
+ }
614
+ }
615
+ if (operation.filterExpression) {
616
+ rows = rows.filter(
617
+ (it) => evaluateCompiledFilter(
618
+ operation.filterExpression,
619
+ operation.filterExpressionAttributeNames ?? {},
620
+ operation.filterExpressionAttributeValues ?? {},
621
+ it
622
+ )
623
+ );
624
+ }
625
+ let lastEvaluatedKey;
626
+ if (operation.limit != null && rows.length > operation.limit) {
627
+ const lastRow = rows[operation.limit - 1];
628
+ rows = rows.slice(0, operation.limit);
629
+ lastEvaluatedKey = { PK: lastRow.PK, SK: lastRow.SK ?? "" };
630
+ if (indexName) {
631
+ lastEvaluatedKey[pkAttr] = lastRow[pkAttr];
632
+ lastEvaluatedKey[skAttr] = lastRow[skAttr];
633
+ }
634
+ }
635
+ const projected = rows.map(
636
+ (it) => projectItem(
637
+ it,
638
+ operation.projectionExpression,
639
+ operation.expressionAttributeNames
640
+ )
641
+ );
642
+ return lastEvaluatedKey ? { items: projected, lastEvaluatedKey } : { items: projected };
643
+ }
644
+ // ── writes ─────────────────────────────────────────────────────────────────
645
+ async put(input, options) {
646
+ const prior = this.store.getItem(
647
+ input.TableName,
648
+ String(input.Item.PK),
649
+ String(input.Item.SK ?? "")
650
+ );
651
+ this.assertCondition(input.ConditionExpression, input, prior);
652
+ this.store.putItem(input.TableName, input.Item);
653
+ return options?.returnOldImage && prior ? { oldItem: prior } : {};
654
+ }
655
+ async update(input, options) {
656
+ const pk = String(input.Key.PK);
657
+ const sk = String(input.Key.SK ?? "");
658
+ const prior = this.store.getItem(input.TableName, pk, sk);
659
+ this.assertCondition(input.ConditionExpression, input, prior);
660
+ const next = prior ? { ...prior } : { PK: pk, SK: sk };
661
+ applyUpdateExpression(next, input.UpdateExpression, {
662
+ names: input.ExpressionAttributeNames,
663
+ values: input.ExpressionAttributeValues ?? {}
664
+ });
665
+ this.store.putItem(input.TableName, next);
666
+ return options?.returnOldImage && prior ? { oldItem: prior } : {};
667
+ }
668
+ async delete(input, options) {
669
+ const pk = String(input.Key.PK);
670
+ const sk = String(input.Key.SK ?? "");
671
+ const prior = this.store.getItem(input.TableName, pk, sk);
672
+ this.assertCondition(input.ConditionExpression, input, prior);
673
+ this.store.deleteItem(input.TableName, pk, sk);
674
+ return options?.returnOldImage && prior ? { oldItem: prior } : {};
675
+ }
676
+ async batchWrite(tableName, items) {
677
+ for (const item of items) {
678
+ if (item.type === "put") {
679
+ this.store.putItem(tableName, item.item);
680
+ } else {
681
+ this.store.deleteItem(
682
+ tableName,
683
+ String(item.key.PK),
684
+ String(item.key.SK ?? "")
685
+ );
686
+ }
687
+ }
688
+ }
689
+ async transactWrite(items) {
690
+ if (items.length === 0) return;
691
+ const touchedTables = /* @__PURE__ */ new Set();
692
+ for (const it of items) {
693
+ if ("Put" in it) touchedTables.add(it.Put.TableName);
694
+ else if ("Update" in it) touchedTables.add(it.Update.TableName);
695
+ else if ("Delete" in it) touchedTables.add(it.Delete.TableName);
696
+ else touchedTables.add(it.ConditionCheck.TableName);
697
+ }
698
+ const backup = /* @__PURE__ */ new Map();
699
+ for (const table of touchedTables) {
700
+ backup.set(table, this.store.allItems(table));
701
+ }
702
+ try {
703
+ for (const it of items) {
704
+ if ("Put" in it) {
705
+ await this.put(it.Put);
706
+ } else if ("Update" in it) {
707
+ await this.update(it.Update);
708
+ } else if ("Delete" in it) {
709
+ await this.delete(it.Delete);
710
+ } else {
711
+ this.conditionCheck(it.ConditionCheck);
712
+ }
713
+ }
714
+ } catch (err) {
715
+ for (const [table, snapshot] of backup) {
716
+ this.restoreTable(table, snapshot);
717
+ }
718
+ if (err instanceof ConditionalCheckFailed) {
719
+ throw new ConditionalCheckFailed(
720
+ "Transaction cancelled, please refer cancellation reasons for specific reasons"
721
+ );
722
+ }
723
+ throw err;
724
+ }
725
+ }
726
+ restoreTable(table, items) {
727
+ for (const existing of this.store.allItems(table)) {
728
+ this.store.deleteItem(
729
+ table,
730
+ String(existing.PK),
731
+ String(existing.SK ?? "")
732
+ );
733
+ }
734
+ for (const item of items) {
735
+ this.store.putItem(table, item);
736
+ }
737
+ }
738
+ /**
739
+ * Evaluate a transaction `ConditionCheck` (issue #81): read the keyed item and
740
+ * assert its `ConditionExpression` holds. Mutates nothing; a failed assertion
741
+ * throws {@link ConditionalCheckFailed}, which {@link transactWrite} catches to
742
+ * roll the whole transaction back — mirroring DynamoDB's atomic cancellation.
743
+ */
744
+ conditionCheck(input) {
745
+ const prior = this.store.getItem(
746
+ input.TableName,
747
+ String(input.Key.PK),
748
+ String(input.Key.SK ?? "")
749
+ );
750
+ this.assertCondition(input.ConditionExpression, input, prior);
751
+ }
752
+ assertCondition(expression, input, item) {
753
+ if (!expression) return;
754
+ const ok = evaluateCondition(
755
+ expression,
756
+ input.ExpressionAttributeNames,
757
+ input.ExpressionAttributeValues,
758
+ item
759
+ );
760
+ if (!ok) {
761
+ throw new ConditionalCheckFailed();
762
+ }
763
+ }
764
+ };
765
+ function projectItem(item, projectionExpression, names) {
766
+ if (!projectionExpression) return item;
767
+ const paths = projectionExpression.split(",").map((p) => p.trim());
768
+ const out = {};
769
+ for (const pathExpr of paths) {
770
+ const segments = pathExpr.split(".").map((alias) => {
771
+ const trimmed = alias.trim();
772
+ return names?.[trimmed] ?? trimmed;
773
+ });
774
+ copyPath(item, out, segments);
775
+ }
776
+ return out;
777
+ }
778
+ function copyPath(src, dest, segments) {
779
+ let s = src;
780
+ for (const seg of segments) {
781
+ if (s === null || typeof s !== "object") return;
782
+ s = s[seg];
783
+ if (s === void 0) return;
784
+ }
785
+ let cursor = dest;
786
+ for (let i = 0; i < segments.length - 1; i++) {
787
+ const seg = segments[i];
788
+ if (cursor[seg] === void 0 || cursor[seg] === null || typeof cursor[seg] !== "object") {
789
+ cursor[seg] = {};
790
+ }
791
+ cursor = cursor[seg];
792
+ }
793
+ cursor[segments[segments.length - 1]] = s;
794
+ }
795
+
796
+ // src/testing/index.ts
797
+ function physicalTableName(model) {
798
+ const modelClass = toClass(model);
799
+ const meta = MetadataRegistry.get(modelClass);
800
+ return TableMapping.resolve(meta.tableName);
801
+ }
802
+ function toClass(model) {
803
+ try {
804
+ return resolveModelClass(model);
805
+ } catch {
806
+ return model;
807
+ }
808
+ }
809
+ function createTestDB(options = {}) {
810
+ const store = new MemoryStore();
811
+ const executor = new MemoryExecutor(store);
812
+ ClientManager.setExecutor(executor);
813
+ const logicalToPhysical = /* @__PURE__ */ new Map();
814
+ for (const model of options.models ?? []) {
815
+ const modelClass = toClass(model);
816
+ const meta = MetadataRegistry.get(modelClass);
817
+ logicalToPhysical.set(meta.tableName, physicalTableName(model));
818
+ }
819
+ function resolveTable(table) {
820
+ return logicalToPhysical.get(table) ?? TableMapping.resolve(table);
821
+ }
822
+ let emulator = null;
823
+ if (options.cdc) {
824
+ emulator = createCdcEmulator({
825
+ mode: "record",
826
+ models: (options.models ?? []).map(toClass)
827
+ });
828
+ }
829
+ const inspector = {
830
+ dump() {
831
+ const out = {};
832
+ for (const table of store.tableNames()) {
833
+ out[table] = store.allItems(table);
834
+ }
835
+ return out;
836
+ },
837
+ dumpTable(table) {
838
+ return store.allItems(resolveTable(table));
839
+ },
840
+ scan(table, filter) {
841
+ const physical = resolveTable(table);
842
+ const index = filter?.index;
843
+ const pkAttr = index ? `${index}PK` : "PK";
844
+ const skAttr = index ? `${index}SK` : "SK";
845
+ let rows = store.allItems(physical);
846
+ if (filter?.pk !== void 0) {
847
+ rows = rows.filter((it) => String(it[pkAttr] ?? "") === filter.pk);
848
+ }
849
+ if (filter?.skPrefix !== void 0) {
850
+ rows = rows.filter(
851
+ (it) => String(it[skAttr] ?? "").startsWith(filter.skPrefix)
852
+ );
853
+ }
854
+ return rows;
855
+ },
856
+ count(table) {
857
+ return store.count(resolveTable(table));
858
+ },
859
+ getRaw(table, key) {
860
+ return store.getItem(resolveTable(table), key.PK, key.SK) ?? null;
861
+ },
862
+ seed(table, items) {
863
+ const physical = resolveTable(table);
864
+ for (const item of items) {
865
+ store.putItem(physical, item);
866
+ }
867
+ },
868
+ reset() {
869
+ store.reset();
870
+ emulator?.reset();
871
+ },
872
+ snapshot() {
873
+ return store.snapshot();
874
+ },
875
+ restore(snapshot) {
876
+ store.restore(snapshot);
877
+ },
878
+ changes() {
879
+ if (!emulator) {
880
+ throw new Error(
881
+ "createTestDB: changes() requires { cdc: true }. Pass it to createTestDB."
882
+ );
883
+ }
884
+ return emulator.record().events;
885
+ },
886
+ close() {
887
+ emulator?.close();
888
+ emulator = null;
889
+ ClientManager.resetExecutor();
890
+ }
891
+ };
892
+ return { testing: inspector, store };
893
+ }
894
+ export {
895
+ MemoryExecutor,
896
+ MemoryStore,
897
+ createTestDB
898
+ };