dynamodb-reactive 0.1.0 → 0.1.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/dist/server.js CHANGED
@@ -1,18 +1,1689 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
- for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
1
+ import { SystemTableNames } from './chunk-HZ6JHAJJ.js';
2
+ import { DynamoDBClient, ExecuteStatementCommand } from '@aws-sdk/client-dynamodb';
3
+ import { UpdateCommand, DeleteCommand, PutCommand, GetCommand, DynamoDBDocumentClient, QueryCommand } from '@aws-sdk/lib-dynamodb';
4
+ import { unmarshall } from '@aws-sdk/util-dynamodb';
5
+ import { compare, applyPatch } from 'fast-json-patch';
6
+ import { ApiGatewayManagementApiClient, PostToConnectionCommand, GoneException } from '@aws-sdk/client-apigatewaymanagementapi';
7
+
8
+ // ../server/src/procedure.ts
9
+ var ProcedureBuilder = class _ProcedureBuilder {
10
+ inputSchema;
11
+ constructor(inputSchema) {
12
+ this.inputSchema = inputSchema;
13
+ }
14
+ /**
15
+ * Define the input schema for the procedure
16
+ */
17
+ input(schema) {
18
+ return new _ProcedureBuilder(schema);
19
+ }
20
+ /**
21
+ * Define a query procedure (read-only operation)
22
+ */
23
+ query(resolver) {
24
+ return {
25
+ type: "query",
26
+ inputSchema: this.inputSchema,
27
+ resolver
28
+ };
29
+ }
30
+ /**
31
+ * Define a mutation procedure (write operation)
32
+ */
33
+ mutation(resolver) {
34
+ return {
35
+ type: "mutation",
36
+ inputSchema: this.inputSchema,
37
+ resolver
38
+ };
39
+ }
15
40
  };
16
- Object.defineProperty(exports, "__esModule", { value: true });
17
- __exportStar(require("@dynamodb-reactive/server"), exports);
41
+ function isProcedure(value) {
42
+ return typeof value === "object" && value !== null && "type" in value && "resolver" in value && (value.type === "query" || value.type === "mutation");
43
+ }
44
+ async function executeProcedure(procedure, ctx, rawInput) {
45
+ let input;
46
+ if (procedure.inputSchema) {
47
+ const parseResult = procedure.inputSchema.safeParse(rawInput);
48
+ if (!parseResult.success) {
49
+ throw new Error(`Invalid input: ${parseResult.error.message}`);
50
+ }
51
+ input = parseResult.data;
52
+ } else {
53
+ input = rawInput;
54
+ }
55
+ return procedure.resolver({ ctx, input });
56
+ }
57
+
58
+ // ../server/src/router.ts
59
+ var Router = class {
60
+ definition;
61
+ constructor(definition) {
62
+ this.definition = definition;
63
+ }
64
+ /**
65
+ * Get a procedure by its path (e.g., "todos.list")
66
+ */
67
+ getProcedure(path) {
68
+ const parts = path.split(".");
69
+ let current = this.definition;
70
+ for (const part of parts) {
71
+ if (typeof current !== "object" || current === null) {
72
+ return null;
73
+ }
74
+ current = current[part];
75
+ }
76
+ if (isProcedure(current)) {
77
+ return current;
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * Execute a procedure by its path
83
+ */
84
+ async execute(path, ctx, input) {
85
+ const procedure = this.getProcedure(path);
86
+ if (!procedure) {
87
+ throw new Error(`Procedure not found: ${path}`);
88
+ }
89
+ return executeProcedure(procedure, ctx, input);
90
+ }
91
+ /**
92
+ * Get all procedure paths in the router
93
+ */
94
+ getProcedurePaths() {
95
+ const paths = [];
96
+ function traverse(obj, prefix) {
97
+ if (typeof obj !== "object" || obj === null) return;
98
+ for (const [key, value] of Object.entries(obj)) {
99
+ const path = prefix ? `${prefix}.${key}` : key;
100
+ if (isProcedure(value)) {
101
+ paths.push(path);
102
+ } else {
103
+ traverse(value, path);
104
+ }
105
+ }
106
+ }
107
+ traverse(this.definition, "");
108
+ return paths;
109
+ }
110
+ /**
111
+ * Check if a path is a query procedure
112
+ */
113
+ isQuery(path) {
114
+ const procedure = this.getProcedure(path);
115
+ return procedure?.type === "query";
116
+ }
117
+ /**
118
+ * Check if a path is a mutation procedure
119
+ */
120
+ isMutation(path) {
121
+ const procedure = this.getProcedure(path);
122
+ return procedure?.type === "mutation";
123
+ }
124
+ };
125
+ function createRouter(definition) {
126
+ return new Router(definition);
127
+ }
128
+ function mergeRouters(...routers) {
129
+ const merged = {};
130
+ for (const router of routers) {
131
+ Object.assign(merged, router.definition);
132
+ }
133
+ return new Router(merged);
134
+ }
135
+
136
+ // ../server/src/reactive.ts
137
+ function initReactive() {
138
+ return {
139
+ procedure: new ProcedureBuilder(),
140
+ router(definition) {
141
+ return createRouter(definition);
142
+ }
143
+ };
144
+ }
145
+
146
+ // ../server/src/partiql-builder.ts
147
+ function conditionToPartiQL(condition, parameters) {
148
+ switch (condition.type) {
149
+ case "comparison": {
150
+ parameters.push(condition.value);
151
+ return `"${condition.field}" ${condition.operator} ?`;
152
+ }
153
+ case "function": {
154
+ if (condition.operator === "BETWEEN") {
155
+ parameters.push(condition.value);
156
+ parameters.push(condition.value2);
157
+ return `"${condition.field}" BETWEEN ? AND ?`;
158
+ }
159
+ if (condition.operator === "begins_with") {
160
+ parameters.push(condition.value);
161
+ return `begins_with("${condition.field}", ?)`;
162
+ }
163
+ if (condition.operator === "contains") {
164
+ parameters.push(condition.value);
165
+ return `contains("${condition.field}", ?)`;
166
+ }
167
+ throw new Error(`Unknown function operator: ${condition.operator}`);
168
+ }
169
+ case "logical": {
170
+ if (!condition.conditions || condition.conditions.length === 0) {
171
+ throw new Error(
172
+ "Logical condition requires at least one sub-condition"
173
+ );
174
+ }
175
+ if (condition.operator === "NOT") {
176
+ const subClause = conditionToPartiQL(
177
+ condition.conditions[0],
178
+ parameters
179
+ );
180
+ return `NOT (${subClause})`;
181
+ }
182
+ const subClauses = condition.conditions.map(
183
+ (c) => conditionToPartiQL(c, parameters)
184
+ );
185
+ return `(${subClauses.join(` ${condition.operator} `)})`;
186
+ }
187
+ default:
188
+ throw new Error(`Unknown condition type: ${condition.type}`);
189
+ }
190
+ }
191
+ function buildSelectStatement(operation) {
192
+ const parameters = [];
193
+ let statement = `SELECT * FROM "${operation.tableName}"`;
194
+ if (operation.indexName) {
195
+ statement += `."${operation.indexName}"`;
196
+ }
197
+ if (operation.filters.length > 0) {
198
+ const whereClauses = operation.filters.map(
199
+ (f) => conditionToPartiQL(f, parameters)
200
+ );
201
+ statement += ` WHERE ${whereClauses.join(" AND ")}`;
202
+ }
203
+ return { statement, parameters };
204
+ }
205
+ function buildInsertStatement(tableName, item) {
206
+ const parameters = [item];
207
+ const statement = `INSERT INTO "${tableName}" VALUE ?`;
208
+ return { statement, parameters };
209
+ }
210
+ function buildUpdateStatement(tableName, key, updates) {
211
+ const parameters = [];
212
+ const setClauses = [];
213
+ for (const [field, value] of Object.entries(updates)) {
214
+ if (field in key) continue;
215
+ setClauses.push(`"${field}" = ?`);
216
+ parameters.push(value);
217
+ }
218
+ if (setClauses.length === 0) {
219
+ throw new Error("No fields to update");
220
+ }
221
+ let statement = `UPDATE "${tableName}" SET ${setClauses.join(", ")}`;
222
+ const whereClauses = Object.entries(key).map(([field, value]) => {
223
+ parameters.push(value);
224
+ return `"${field}" = ?`;
225
+ });
226
+ statement += ` WHERE ${whereClauses.join(" AND ")}`;
227
+ return { statement, parameters };
228
+ }
229
+ function buildDeleteStatement(tableName, key) {
230
+ const parameters = [];
231
+ const whereClauses = Object.entries(key).map(([field, value]) => {
232
+ parameters.push(value);
233
+ return `"${field}" = ?`;
234
+ });
235
+ const statement = `DELETE FROM "${tableName}" WHERE ${whereClauses.join(" AND ")}`;
236
+ return { statement, parameters };
237
+ }
238
+ function buildGetStatement(tableName, key) {
239
+ const parameters = [];
240
+ const whereClauses = Object.entries(key).map(([field, value]) => {
241
+ parameters.push(value);
242
+ return `"${field}" = ?`;
243
+ });
244
+ const statement = `SELECT * FROM "${tableName}" WHERE ${whereClauses.join(" AND ")}`;
245
+ return { statement, parameters };
246
+ }
247
+
248
+ // ../server/src/query-builder.ts
249
+ function createCondition(type, operator, field, value, value2) {
250
+ return { type, operator, field, value, value2 };
251
+ }
252
+ var FilterBuilderImpl = class {
253
+ eq(field, value) {
254
+ return createCondition("comparison", "=", field.fieldName, value);
255
+ }
256
+ ne(field, value) {
257
+ return createCondition("comparison", "<>", field.fieldName, value);
258
+ }
259
+ gt(field, value) {
260
+ return createCondition("comparison", ">", field.fieldName, value);
261
+ }
262
+ gte(field, value) {
263
+ return createCondition("comparison", ">=", field.fieldName, value);
264
+ }
265
+ lt(field, value) {
266
+ return createCondition("comparison", "<", field.fieldName, value);
267
+ }
268
+ lte(field, value) {
269
+ return createCondition("comparison", "<=", field.fieldName, value);
270
+ }
271
+ between(field, lower, upper) {
272
+ return createCondition(
273
+ "function",
274
+ "BETWEEN",
275
+ field.fieldName,
276
+ lower,
277
+ upper
278
+ );
279
+ }
280
+ beginsWith(field, prefix) {
281
+ return createCondition("function", "begins_with", field.fieldName, prefix);
282
+ }
283
+ contains(field, substring) {
284
+ return createCondition("function", "contains", field.fieldName, substring);
285
+ }
286
+ and(...conditions) {
287
+ return { type: "logical", operator: "AND", conditions };
288
+ }
289
+ or(...conditions) {
290
+ return { type: "logical", operator: "OR", conditions };
291
+ }
292
+ not(condition) {
293
+ return { type: "logical", operator: "NOT", conditions: [condition] };
294
+ }
295
+ };
296
+ var QueryBuilderImpl = class {
297
+ table;
298
+ filters = [];
299
+ indexName;
300
+ limit;
301
+ startKey;
302
+ ascending = true;
303
+ executor;
304
+ operationTracker;
305
+ constructor(table, executor, operationTracker) {
306
+ this.table = table;
307
+ this.executor = executor;
308
+ this.operationTracker = operationTracker;
309
+ }
310
+ filter(fn) {
311
+ const builder = new FilterBuilderImpl();
312
+ const condition = fn(builder);
313
+ this.filters.push(condition);
314
+ return this;
315
+ }
316
+ useIndex(indexName) {
317
+ this.indexName = indexName;
318
+ return this;
319
+ }
320
+ take(limit) {
321
+ this.limit = limit;
322
+ return this;
323
+ }
324
+ startFrom(key) {
325
+ this.startKey = key;
326
+ return this;
327
+ }
328
+ sortAscending() {
329
+ this.ascending = true;
330
+ return this;
331
+ }
332
+ sortDescending() {
333
+ this.ascending = false;
334
+ return this;
335
+ }
336
+ async execute() {
337
+ const operation = {
338
+ tableName: this.table.tableName,
339
+ filters: this.filters,
340
+ indexName: this.indexName,
341
+ pkField: this.table.pk,
342
+ skField: this.table.sk,
343
+ sortField: this.table.sk,
344
+ // Default sort by SK if available
345
+ sortOrder: this.ascending ? "asc" : "desc",
346
+ limit: this.limit
347
+ };
348
+ if (this.operationTracker) {
349
+ this.operationTracker(operation);
350
+ }
351
+ const options = {
352
+ limit: this.limit,
353
+ startKey: this.startKey,
354
+ ascending: this.ascending
355
+ };
356
+ return this.executor(operation, options);
357
+ }
358
+ /**
359
+ * Get the current operation without executing
360
+ * Used for dependency extraction
361
+ */
362
+ getOperation() {
363
+ return {
364
+ tableName: this.table.tableName,
365
+ filters: this.filters,
366
+ indexName: this.indexName,
367
+ pkField: this.table.pk,
368
+ skField: this.table.sk,
369
+ sortField: this.table.sk,
370
+ sortOrder: this.ascending ? "asc" : "desc",
371
+ limit: this.limit
372
+ };
373
+ }
374
+ };
375
+ function createFilterBuilder() {
376
+ return new FilterBuilderImpl();
377
+ }
378
+
379
+ // ../server/src/db-context.ts
380
+ function createDocClient(config) {
381
+ const client = config.client ?? new DynamoDBClient({
382
+ region: config.region ?? process.env.AWS_REGION ?? "us-east-1",
383
+ endpoint: config.endpoint
384
+ });
385
+ return DynamoDBDocumentClient.from(client, {
386
+ marshallOptions: {
387
+ removeUndefinedValues: true,
388
+ convertEmptyValues: false
389
+ },
390
+ unmarshallOptions: {
391
+ wrapNumbers: false
392
+ }
393
+ });
394
+ }
395
+ function toAttributeValue(value) {
396
+ if (typeof value === "string") return { S: value };
397
+ if (typeof value === "number") return { N: String(value) };
398
+ if (typeof value === "boolean") return { BOOL: value };
399
+ if (value === null || value === void 0) return { NULL: true };
400
+ if (Array.isArray(value)) {
401
+ return { L: value.map(toAttributeValue) };
402
+ }
403
+ if (typeof value === "object") {
404
+ const m = {};
405
+ for (const [k, v] of Object.entries(value)) {
406
+ m[k] = toAttributeValue(v);
407
+ }
408
+ return { M: m };
409
+ }
410
+ return { S: String(value) };
411
+ }
412
+ async function executePartiQL(docClient, statement) {
413
+ const command = new ExecuteStatementCommand({
414
+ Statement: statement.statement,
415
+ Parameters: statement.parameters.map(toAttributeValue)
416
+ });
417
+ const response = await docClient.send(command);
418
+ return (response.Items ?? []).map(
419
+ (item) => unmarshall(item)
420
+ );
421
+ }
422
+ function createDbContext(config, dependencyTracker) {
423
+ const docClient = createDocClient(config);
424
+ async function executeQuery(operation, options) {
425
+ const statement = buildSelectStatement(operation);
426
+ const items = await executePartiQL(docClient, statement);
427
+ if (options.limit && items.length > options.limit) {
428
+ return items.slice(0, options.limit);
429
+ }
430
+ return items;
431
+ }
432
+ return {
433
+ query(table) {
434
+ return new QueryBuilderImpl(
435
+ table,
436
+ executeQuery,
437
+ dependencyTracker?.track.bind(dependencyTracker)
438
+ );
439
+ },
440
+ async get(table, key) {
441
+ const command = new GetCommand({
442
+ TableName: table.tableName,
443
+ Key: key
444
+ });
445
+ const response = await docClient.send(command);
446
+ return response.Item ?? null;
447
+ },
448
+ async put(table, item) {
449
+ table.validate(item);
450
+ const command = new PutCommand({
451
+ TableName: table.tableName,
452
+ Item: item
453
+ });
454
+ await docClient.send(command);
455
+ },
456
+ async delete(table, key) {
457
+ const command = new DeleteCommand({
458
+ TableName: table.tableName,
459
+ Key: key
460
+ });
461
+ await docClient.send(command);
462
+ },
463
+ async update(table, key, updates) {
464
+ const updateExpressions = [];
465
+ const expressionAttributeNames = {};
466
+ const expressionAttributeValues = {};
467
+ let i = 0;
468
+ for (const [field, value] of Object.entries(updates)) {
469
+ if (field in key) continue;
470
+ const nameKey = `#f${i}`;
471
+ const valueKey = `:v${i}`;
472
+ updateExpressions.push(`${nameKey} = ${valueKey}`);
473
+ expressionAttributeNames[nameKey] = field;
474
+ expressionAttributeValues[valueKey] = value;
475
+ i++;
476
+ }
477
+ if (updateExpressions.length === 0) {
478
+ const current = await this.get(table, key);
479
+ if (!current) {
480
+ throw new Error("Item not found");
481
+ }
482
+ return current;
483
+ }
484
+ const command = new UpdateCommand({
485
+ TableName: table.tableName,
486
+ Key: key,
487
+ UpdateExpression: `SET ${updateExpressions.join(", ")}`,
488
+ ExpressionAttributeNames: expressionAttributeNames,
489
+ ExpressionAttributeValues: expressionAttributeValues,
490
+ ReturnValues: "ALL_NEW"
491
+ });
492
+ const response = await docClient.send(command);
493
+ return response.Attributes;
494
+ }
495
+ };
496
+ }
497
+
498
+ // ../server/src/dependency-extractor.ts
499
+ function extractFromCondition(tableName, condition, indexName) {
500
+ const dependencies = [];
501
+ switch (condition.type) {
502
+ case "comparison": {
503
+ if (condition.operator === "=" && condition.field && condition.value !== void 0) {
504
+ dependencies.push({
505
+ tableName,
506
+ fieldName: condition.field,
507
+ fieldValue: String(condition.value),
508
+ indexName
509
+ });
510
+ }
511
+ break;
512
+ }
513
+ case "function": {
514
+ if (condition.operator === "begins_with" && condition.field && condition.value) {
515
+ dependencies.push({
516
+ tableName,
517
+ fieldName: condition.field,
518
+ fieldValue: `prefix:${String(condition.value)}`,
519
+ indexName
520
+ });
521
+ }
522
+ break;
523
+ }
524
+ case "logical": {
525
+ if (condition.conditions) {
526
+ for (const subCondition of condition.conditions) {
527
+ dependencies.push(
528
+ ...extractFromCondition(tableName, subCondition, indexName)
529
+ );
530
+ }
531
+ }
532
+ break;
533
+ }
534
+ }
535
+ return dependencies;
536
+ }
537
+ function extractDependencies(operation) {
538
+ const dependencies = [];
539
+ for (const filter of operation.filters) {
540
+ dependencies.push(
541
+ ...extractFromCondition(operation.tableName, filter, operation.indexName)
542
+ );
543
+ }
544
+ return dependencies;
545
+ }
546
+ function createDependencyKey(dependency) {
547
+ return `${dependency.tableName}#${dependency.fieldName}#${dependency.fieldValue}`;
548
+ }
549
+ function parseDependencyKey(key) {
550
+ const parts = key.split("#");
551
+ if (parts.length < 3) return null;
552
+ return {
553
+ tableName: parts[0],
554
+ fieldName: parts[1],
555
+ fieldValue: parts.slice(2).join("#")
556
+ // Handle values that contain #
557
+ };
558
+ }
559
+ function extractAffectedKeys(tableName, item) {
560
+ const keys = [];
561
+ for (const [fieldName, fieldValue] of Object.entries(item)) {
562
+ if (fieldValue !== null && fieldValue !== void 0) {
563
+ keys.push(`${tableName}#${fieldName}#${String(fieldValue)}`);
564
+ if (typeof fieldValue === "string") {
565
+ for (let i = 1; i <= fieldValue.length; i++) {
566
+ keys.push(
567
+ `${tableName}#${fieldName}#prefix:${fieldValue.substring(0, i)}`
568
+ );
569
+ }
570
+ }
571
+ }
572
+ }
573
+ return keys;
574
+ }
575
+ function operationToQueryMetadata(operation) {
576
+ const normalizeFilters = (filters) => {
577
+ return filters.map((f) => normalizeFilter(f));
578
+ };
579
+ const normalizeFilter = (filter) => {
580
+ if (filter.type === "comparison") {
581
+ const operatorMap = {
582
+ "=": "eq",
583
+ "<>": "ne",
584
+ ">": "gt",
585
+ ">=": "gte",
586
+ "<": "lt",
587
+ "<=": "lte"
588
+ };
589
+ return {
590
+ ...filter,
591
+ operator: operatorMap[filter.operator ?? ""] ?? filter.operator
592
+ };
593
+ }
594
+ if (filter.type === "function") {
595
+ const operatorMap = {
596
+ begins_with: "beginsWith",
597
+ BETWEEN: "between"
598
+ };
599
+ return {
600
+ ...filter,
601
+ operator: operatorMap[filter.operator ?? ""] ?? filter.operator
602
+ };
603
+ }
604
+ if (filter.type === "logical" && filter.conditions) {
605
+ const operatorMap = {
606
+ AND: "and",
607
+ OR: "or",
608
+ NOT: "not"
609
+ };
610
+ return {
611
+ ...filter,
612
+ operator: operatorMap[filter.operator ?? ""] ?? filter.operator,
613
+ conditions: normalizeFilters(filter.conditions)
614
+ };
615
+ }
616
+ return filter;
617
+ };
618
+ return {
619
+ tableName: operation.tableName,
620
+ indexName: operation.indexName,
621
+ filterConditions: normalizeFilters(operation.filters),
622
+ sortField: operation.sortField,
623
+ sortOrder: operation.sortOrder,
624
+ limit: operation.limit
625
+ };
626
+ }
627
+ var DependencyTracker = class {
628
+ operations = [];
629
+ /**
630
+ * Track a query operation
631
+ */
632
+ track(operation) {
633
+ this.operations.push(operation);
634
+ }
635
+ /**
636
+ * Get all tracked operations
637
+ */
638
+ getOperations() {
639
+ return [...this.operations];
640
+ }
641
+ /**
642
+ * Extract all dependencies from tracked operations
643
+ */
644
+ extractAll() {
645
+ const dependencies = [];
646
+ for (const operation of this.operations) {
647
+ dependencies.push(...extractDependencies(operation));
648
+ }
649
+ return dependencies;
650
+ }
651
+ /**
652
+ * Get all dependency keys for the inverted index
653
+ */
654
+ getDependencyKeys() {
655
+ return this.extractAll().map(createDependencyKey);
656
+ }
657
+ /**
658
+ * Get query metadata for the first tracked operation.
659
+ * Used for storing subscription state.
660
+ */
661
+ getQueryMetadata() {
662
+ if (this.operations.length === 0) return null;
663
+ return operationToQueryMetadata(this.operations[0]);
664
+ }
665
+ /**
666
+ * Get the primary key field from the first operation
667
+ */
668
+ getPkField() {
669
+ if (this.operations.length === 0) return null;
670
+ return this.operations[0].pkField;
671
+ }
672
+ /**
673
+ * Get the sort key field from the first operation
674
+ */
675
+ getSkField() {
676
+ if (this.operations.length === 0) return void 0;
677
+ return this.operations[0].skField;
678
+ }
679
+ /**
680
+ * Clear tracked operations
681
+ */
682
+ clear() {
683
+ this.operations = [];
684
+ }
685
+ };
686
+ function generatePatches(oldValue, newValue) {
687
+ const operations = compare(
688
+ oldValue,
689
+ newValue
690
+ );
691
+ return operations.map((op) => ({
692
+ op: op.op,
693
+ path: op.path,
694
+ value: "value" in op ? op.value : void 0,
695
+ from: "from" in op ? op.from : void 0
696
+ }));
697
+ }
698
+ function applyPatches(document, patches) {
699
+ const operations = patches.map((patch) => {
700
+ const op = {
701
+ op: patch.op,
702
+ path: patch.path
703
+ };
704
+ if ("value" in patch && patch.value !== void 0) {
705
+ op.value = patch.value;
706
+ }
707
+ if ("from" in patch && patch.from !== void 0) {
708
+ op.from = patch.from;
709
+ }
710
+ return op;
711
+ });
712
+ const result = applyPatch(
713
+ structuredClone(document),
714
+ operations,
715
+ true,
716
+ // Validate operations
717
+ false
718
+ // Don't mutate the original
719
+ );
720
+ return result.newDocument;
721
+ }
722
+ function hasChanges(oldValue, newValue) {
723
+ const patches = generatePatches(oldValue, newValue);
724
+ return patches.length > 0;
725
+ }
726
+ function optimizePatches(patches) {
727
+ const seen = /* @__PURE__ */ new Set();
728
+ const optimized = [];
729
+ for (let i = patches.length - 1; i >= 0; i--) {
730
+ const patch = patches[i];
731
+ if (!seen.has(patch.path)) {
732
+ seen.add(patch.path);
733
+ optimized.unshift(patch);
734
+ }
735
+ }
736
+ return optimized;
737
+ }
738
+ function batchPatches(patchSets) {
739
+ const allPatches = patchSets.flat();
740
+ return optimizePatches(allPatches);
741
+ }
742
+ function createReactiveHandler(config) {
743
+ const ttlSeconds = config.ttlSeconds ?? 3600;
744
+ const connectionsTable = config.connectionsTableName ?? SystemTableNames.connections;
745
+ const dependenciesTable = config.dependenciesTableName ?? SystemTableNames.dependencies;
746
+ const queriesTable = config.queriesTableName ?? SystemTableNames.queries;
747
+ const ddbClient = new DynamoDBClient({
748
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
749
+ });
750
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
751
+ async function handleRequest(connectionId, request) {
752
+ try {
753
+ const ctx = await config.getContext(connectionId);
754
+ const dependencyTracker = new DependencyTracker();
755
+ const db = createDbContext(config.dbConfig ?? {}, dependencyTracker);
756
+ const fullCtx = { ...ctx, db };
757
+ switch (request.type) {
758
+ case "subscribe":
759
+ return handleSubscribe(
760
+ connectionId,
761
+ request,
762
+ fullCtx,
763
+ dependencyTracker
764
+ );
765
+ case "unsubscribe":
766
+ return handleUnsubscribe(connectionId, request);
767
+ case "call":
768
+ return handleCall(request, fullCtx);
769
+ default:
770
+ return {
771
+ type: "error",
772
+ message: `Unknown request type: ${request.type}`
773
+ };
774
+ }
775
+ } catch (error) {
776
+ return {
777
+ type: "error",
778
+ message: error instanceof Error ? error.message : "Unknown error",
779
+ subscriptionId: "subscriptionId" in request ? request.subscriptionId : void 0
780
+ };
781
+ }
782
+ }
783
+ async function handleSubscribe(connectionId, request, ctx, dependencyTracker) {
784
+ const result = await config.router.execute(
785
+ request.path,
786
+ ctx,
787
+ request.input
788
+ );
789
+ const queryMetadata = dependencyTracker.getQueryMetadata();
790
+ const dependencyKeys = dependencyTracker.getDependencyKeys();
791
+ if (!queryMetadata) {
792
+ console.warn("No query metadata captured for subscription");
793
+ }
794
+ const now = Date.now();
795
+ const ttl = Math.floor(now / 1e3) + ttlSeconds;
796
+ const queryEntry = {
797
+ pk: connectionId,
798
+ sk: request.subscriptionId,
799
+ connectionId,
800
+ subscriptionId: request.subscriptionId,
801
+ queryMetadata: queryMetadata ?? {
802
+ tableName: "",
803
+ filterConditions: []
804
+ },
805
+ lastResult: Array.isArray(result) ? result : [result],
806
+ dependencies: dependencyKeys,
807
+ createdAt: now,
808
+ updatedAt: now,
809
+ ttl
810
+ };
811
+ await docClient.send(
812
+ new PutCommand({
813
+ TableName: queriesTable,
814
+ Item: queryEntry
815
+ })
816
+ );
817
+ for (const key of dependencyKeys) {
818
+ await docClient.send(
819
+ new PutCommand({
820
+ TableName: dependenciesTable,
821
+ Item: {
822
+ pk: key,
823
+ sk: `${connectionId}#${request.subscriptionId}`,
824
+ connectionId,
825
+ subscriptionId: request.subscriptionId,
826
+ ttl
827
+ }
828
+ })
829
+ );
830
+ }
831
+ console.log("Subscription created:", {
832
+ connectionId,
833
+ subscriptionId: request.subscriptionId,
834
+ queryMetadata: queryMetadata?.tableName,
835
+ dependencies: dependencyKeys
836
+ });
837
+ return {
838
+ type: "snapshot",
839
+ subscriptionId: request.subscriptionId,
840
+ data: result
841
+ };
842
+ }
843
+ async function handleUnsubscribe(connectionId, request) {
844
+ const subResponse = await docClient.send(
845
+ new GetCommand({
846
+ TableName: queriesTable,
847
+ Key: { pk: connectionId, sk: request.subscriptionId }
848
+ })
849
+ );
850
+ if (subResponse.Item) {
851
+ const queryEntry = subResponse.Item;
852
+ for (const key of queryEntry.dependencies ?? []) {
853
+ await docClient.send(
854
+ new DeleteCommand({
855
+ TableName: dependenciesTable,
856
+ Key: { pk: key, sk: `${connectionId}#${request.subscriptionId}` }
857
+ })
858
+ );
859
+ }
860
+ }
861
+ await docClient.send(
862
+ new DeleteCommand({
863
+ TableName: queriesTable,
864
+ Key: { pk: connectionId, sk: request.subscriptionId }
865
+ })
866
+ );
867
+ console.log("Subscription removed:", {
868
+ connectionId,
869
+ subscriptionId: request.subscriptionId
870
+ });
871
+ return {
872
+ type: "result",
873
+ data: { success: true }
874
+ };
875
+ }
876
+ async function handleCall(request, ctx) {
877
+ const result = await config.router.execute(
878
+ request.path,
879
+ ctx,
880
+ request.input
881
+ );
882
+ return {
883
+ type: "result",
884
+ data: result
885
+ };
886
+ }
887
+ async function registerConnection(connectionId, context) {
888
+ const now = Date.now();
889
+ const ttl = Math.floor(now / 1e3) + ttlSeconds;
890
+ const connectionEntry = {
891
+ connectionId,
892
+ context,
893
+ connectedAt: now,
894
+ ttl
895
+ };
896
+ await docClient.send(
897
+ new PutCommand({
898
+ TableName: connectionsTable,
899
+ Item: connectionEntry
900
+ })
901
+ );
902
+ console.log("Connection registered:", connectionEntry);
903
+ }
904
+ async function unregisterConnection(connectionId) {
905
+ await docClient.send(
906
+ new DeleteCommand({
907
+ TableName: connectionsTable,
908
+ Key: { connectionId }
909
+ })
910
+ );
911
+ console.log("Connection unregistered:", connectionId);
912
+ }
913
+ return {
914
+ handleRequest,
915
+ registerConnection,
916
+ unregisterConnection
917
+ };
918
+ }
919
+ function createStreamHandler(config) {
920
+ const dependenciesTable = config.dependenciesTableName ?? SystemTableNames.dependencies;
921
+ const queriesTable = config.queriesTableName ?? SystemTableNames.queries;
922
+ const ddbClient = new DynamoDBClient({
923
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
924
+ });
925
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
926
+ const apiClient = new ApiGatewayManagementApiClient({
927
+ endpoint: config.apiGatewayEndpoint
928
+ });
929
+ async function handler(event) {
930
+ const affectedSubscriptions = /* @__PURE__ */ new Map();
931
+ for (const record of event.Records) {
932
+ if (!record.dynamodb) continue;
933
+ const tableName = extractTableName(record);
934
+ if (!tableName) continue;
935
+ const newImage = record.dynamodb.NewImage ? unmarshall(record.dynamodb.NewImage) : null;
936
+ const oldImage = record.dynamodb.OldImage ? unmarshall(record.dynamodb.OldImage) : null;
937
+ const affectedKeys = /* @__PURE__ */ new Set();
938
+ if (newImage) {
939
+ for (const key of extractAffectedKeys(tableName, newImage)) {
940
+ affectedKeys.add(key);
941
+ }
942
+ }
943
+ if (oldImage) {
944
+ for (const key of extractAffectedKeys(tableName, oldImage)) {
945
+ affectedKeys.add(key);
946
+ }
947
+ }
948
+ for (const key of affectedKeys) {
949
+ const subscriptions = await findAffectedSubscriptions(key);
950
+ for (const sub of subscriptions) {
951
+ const connId = sub.connectionId;
952
+ const subId = sub.subscriptionId;
953
+ if (!affectedSubscriptions.has(connId)) {
954
+ affectedSubscriptions.set(connId, /* @__PURE__ */ new Set());
955
+ }
956
+ affectedSubscriptions.get(connId).add(subId);
957
+ }
958
+ }
959
+ }
960
+ const sendPromises = [];
961
+ for (const [connectionId, subscriptionIds] of affectedSubscriptions) {
962
+ for (const subscriptionId of subscriptionIds) {
963
+ sendPromises.push(processSubscription(connectionId, subscriptionId));
964
+ }
965
+ }
966
+ await Promise.allSettled(sendPromises);
967
+ }
968
+ function extractTableName(record) {
969
+ const arn = record.eventSourceARN;
970
+ if (!arn) return null;
971
+ const match = arn.match(/table\/([^/]+)/);
972
+ return match ? match[1] : null;
973
+ }
974
+ async function findAffectedSubscriptions(dependencyKey) {
975
+ try {
976
+ const response = await docClient.send(
977
+ new QueryCommand({
978
+ TableName: dependenciesTable,
979
+ KeyConditionExpression: "pk = :pk",
980
+ ExpressionAttributeValues: {
981
+ ":pk": dependencyKey
982
+ }
983
+ })
984
+ );
985
+ return (response.Items ?? []).map((item) => ({
986
+ connectionId: item.connectionId,
987
+ subscriptionId: item.subscriptionId
988
+ }));
989
+ } catch (error) {
990
+ console.error("Error finding affected subscriptions:", error);
991
+ return [];
992
+ }
993
+ }
994
+ async function processSubscription(connectionId, subscriptionId) {
995
+ try {
996
+ const queryState = await getQueryState(connectionId, subscriptionId);
997
+ if (!queryState) {
998
+ console.warn(
999
+ `Subscription not found: ${connectionId}/${subscriptionId}`
1000
+ );
1001
+ return;
1002
+ }
1003
+ const newResult = await executeQueryFromMetadata(
1004
+ queryState.queryMetadata
1005
+ );
1006
+ if (!hasChanges(queryState.lastResult, newResult)) {
1007
+ return;
1008
+ }
1009
+ const patches = generatePatches(queryState.lastResult, newResult);
1010
+ await updateQueryState(connectionId, subscriptionId, newResult);
1011
+ await sendPatch(connectionId, subscriptionId, patches);
1012
+ } catch (error) {
1013
+ if (error instanceof GoneException) {
1014
+ await cleanupConnection(connectionId);
1015
+ } else {
1016
+ console.error(
1017
+ `Error processing subscription ${connectionId}/${subscriptionId}:`,
1018
+ error
1019
+ );
1020
+ }
1021
+ }
1022
+ }
1023
+ async function getQueryState(connectionId, subscriptionId) {
1024
+ try {
1025
+ const response = await docClient.send(
1026
+ new GetCommand({
1027
+ TableName: queriesTable,
1028
+ Key: {
1029
+ pk: connectionId,
1030
+ sk: subscriptionId
1031
+ }
1032
+ })
1033
+ );
1034
+ return response.Item ?? null;
1035
+ } catch (error) {
1036
+ console.error("Error getting query state:", error);
1037
+ return null;
1038
+ }
1039
+ }
1040
+ async function executeQueryFromMetadata(metadata) {
1041
+ const { tableName, filterConditions, sortOrder, limit } = metadata;
1042
+ const whereClause = buildWhereClause(filterConditions);
1043
+ const orderClause = sortOrder === "desc" ? "ORDER BY SK DESC" : "";
1044
+ const limitClause = limit ? `LIMIT ${limit}` : "";
1045
+ const statement = `SELECT * FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim();
1046
+ try {
1047
+ const result = await ddbClient.send(
1048
+ new ExecuteStatementCommand({
1049
+ Statement: statement
1050
+ })
1051
+ );
1052
+ return (result.Items ?? []).map(
1053
+ (item) => unmarshall(item)
1054
+ );
1055
+ } catch (error) {
1056
+ console.error("Error executing query from metadata:", error);
1057
+ console.error("Statement:", statement);
1058
+ return [];
1059
+ }
1060
+ }
1061
+ function buildWhereClause(conditions) {
1062
+ if (conditions.length === 0) return "";
1063
+ const clauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1064
+ if (clauses.length === 0) return "";
1065
+ return `WHERE ${clauses.join(" AND ")}`;
1066
+ }
1067
+ function buildConditionClause(condition) {
1068
+ const { type, operator, field, value, value2, conditions } = condition;
1069
+ if (type === "comparison" && field) {
1070
+ const escapedValue = escapeValue(value);
1071
+ switch (operator) {
1072
+ case "eq":
1073
+ return `"${field}" = ${escapedValue}`;
1074
+ case "ne":
1075
+ return `"${field}" <> ${escapedValue}`;
1076
+ case "gt":
1077
+ return `"${field}" > ${escapedValue}`;
1078
+ case "gte":
1079
+ return `"${field}" >= ${escapedValue}`;
1080
+ case "lt":
1081
+ return `"${field}" < ${escapedValue}`;
1082
+ case "lte":
1083
+ return `"${field}" <= ${escapedValue}`;
1084
+ case "between":
1085
+ return `"${field}" BETWEEN ${escapedValue} AND ${escapeValue(value2)}`;
1086
+ default:
1087
+ return "";
1088
+ }
1089
+ }
1090
+ if (type === "function" && field) {
1091
+ const escapedValue = escapeValue(value);
1092
+ switch (operator) {
1093
+ case "beginsWith":
1094
+ return `begins_with("${field}", ${escapedValue})`;
1095
+ case "contains":
1096
+ return `contains("${field}", ${escapedValue})`;
1097
+ default:
1098
+ return "";
1099
+ }
1100
+ }
1101
+ if (type === "logical" && conditions) {
1102
+ const subclauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1103
+ if (subclauses.length === 0) return "";
1104
+ switch (operator) {
1105
+ case "and":
1106
+ return `(${subclauses.join(" AND ")})`;
1107
+ case "or":
1108
+ return `(${subclauses.join(" OR ")})`;
1109
+ case "not":
1110
+ return subclauses.length > 0 ? `NOT (${subclauses[0]})` : "";
1111
+ default:
1112
+ return "";
1113
+ }
1114
+ }
1115
+ return "";
1116
+ }
1117
+ function escapeValue(value) {
1118
+ if (value === null || value === void 0) return "NULL";
1119
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
1120
+ if (typeof value === "number") return String(value);
1121
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1122
+ return `'${String(value).replace(/'/g, "''")}'`;
1123
+ }
1124
+ async function updateQueryState(connectionId, subscriptionId, newResult) {
1125
+ try {
1126
+ const existing = await getQueryState(connectionId, subscriptionId);
1127
+ if (!existing) return;
1128
+ await docClient.send(
1129
+ new GetCommand({
1130
+ TableName: queriesTable,
1131
+ Key: { pk: connectionId, sk: subscriptionId }
1132
+ })
1133
+ );
1134
+ const { PutCommand: PutCommand4 } = await import('@aws-sdk/lib-dynamodb');
1135
+ await docClient.send(
1136
+ new PutCommand4({
1137
+ TableName: queriesTable,
1138
+ Item: {
1139
+ ...existing,
1140
+ lastResult: newResult,
1141
+ updatedAt: Date.now()
1142
+ }
1143
+ })
1144
+ );
1145
+ } catch (error) {
1146
+ console.error("Error updating query state:", error);
1147
+ }
1148
+ }
1149
+ async function sendPatch(connectionId, subscriptionId, patches) {
1150
+ const message = JSON.stringify({
1151
+ type: "patch",
1152
+ subscriptionId,
1153
+ patches
1154
+ });
1155
+ try {
1156
+ await apiClient.send(
1157
+ new PostToConnectionCommand({
1158
+ ConnectionId: connectionId,
1159
+ Data: Buffer.from(message)
1160
+ })
1161
+ );
1162
+ } catch (error) {
1163
+ if (error instanceof GoneException) {
1164
+ throw error;
1165
+ }
1166
+ console.error(`Error sending patch to ${connectionId}:`, error);
1167
+ }
1168
+ }
1169
+ async function cleanupConnection(connectionId) {
1170
+ console.log("Cleaning up disconnected connection:", connectionId);
1171
+ }
1172
+ return { handler };
1173
+ }
1174
+ function createConnectHandler(config) {
1175
+ const connectionsTable = config.connectionsTableName ?? SystemTableNames.connections;
1176
+ const ddbClient = new DynamoDBClient({
1177
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
1178
+ });
1179
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
1180
+ return async function handler(event) {
1181
+ const connectionId = event.requestContext.connectionId;
1182
+ try {
1183
+ await docClient.send(
1184
+ new QueryCommand({
1185
+ TableName: connectionsTable,
1186
+ KeyConditionExpression: "connectionId = :cid",
1187
+ ExpressionAttributeValues: {
1188
+ ":cid": connectionId
1189
+ }
1190
+ })
1191
+ );
1192
+ console.log("Connection established:", connectionId);
1193
+ return { statusCode: 200 };
1194
+ } catch (error) {
1195
+ console.error("Error creating connection:", error);
1196
+ return { statusCode: 500 };
1197
+ }
1198
+ };
1199
+ }
1200
+ function createDisconnectHandler(config) {
1201
+ const connectionsTable = config.connectionsTableName ?? SystemTableNames.connections;
1202
+ const ddbClient = new DynamoDBClient({
1203
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
1204
+ });
1205
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
1206
+ return async function handler(event) {
1207
+ const connectionId = event.requestContext.connectionId;
1208
+ try {
1209
+ await docClient.send(
1210
+ new DeleteCommand({
1211
+ TableName: connectionsTable,
1212
+ Key: { connectionId }
1213
+ })
1214
+ );
1215
+ console.log("Connection removed:", connectionId);
1216
+ return { statusCode: 200 };
1217
+ } catch (error) {
1218
+ console.error("Error removing connection:", error);
1219
+ return { statusCode: 500 };
1220
+ }
1221
+ };
1222
+ }
1223
+
1224
+ // ../server/src/harness.ts
1225
+ function createReactiveHarness(config) {
1226
+ return {
1227
+ getContext: config.getContext ?? (async () => ({})),
1228
+ dbConfig: config.dbConfig
1229
+ };
1230
+ }
1231
+ function createLambdaHandlers() {
1232
+ const connectionsTable = process.env.CONNECTIONS_TABLE ?? SystemTableNames.connections;
1233
+ const dependenciesTable = process.env.DEPENDENCIES_TABLE ?? SystemTableNames.dependencies;
1234
+ const queriesTable = process.env.QUERIES_TABLE ?? SystemTableNames.queries;
1235
+ const wsEndpoint = process.env.WEBSOCKET_ENDPOINT ?? "";
1236
+ const ddbClient = new DynamoDBClient({
1237
+ region: process.env.AWS_REGION
1238
+ });
1239
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
1240
+ const getApiClient = () => new ApiGatewayManagementApiClient({
1241
+ endpoint: wsEndpoint
1242
+ });
1243
+ async function connectHandler(event) {
1244
+ const connectionId = event.requestContext.connectionId;
1245
+ const now = Date.now();
1246
+ const ttl = Math.floor(now / 1e3) + 3600;
1247
+ try {
1248
+ const connectionEntry = {
1249
+ connectionId,
1250
+ context: event.requestContext.authorizer,
1251
+ connectedAt: now,
1252
+ ttl
1253
+ };
1254
+ await docClient.send(
1255
+ new PutCommand({
1256
+ TableName: connectionsTable,
1257
+ Item: connectionEntry
1258
+ })
1259
+ );
1260
+ console.log("Connection established:", connectionId);
1261
+ return { statusCode: 200, body: "Connected" };
1262
+ } catch (error) {
1263
+ console.error("Error creating connection:", error);
1264
+ return { statusCode: 500, body: "Failed to connect" };
1265
+ }
1266
+ }
1267
+ async function disconnectHandler(event) {
1268
+ const connectionId = event.requestContext.connectionId;
1269
+ try {
1270
+ await docClient.send(
1271
+ new DeleteCommand({
1272
+ TableName: connectionsTable,
1273
+ Key: { connectionId }
1274
+ })
1275
+ );
1276
+ console.log("Connection removed:", connectionId);
1277
+ return { statusCode: 200, body: "Disconnected" };
1278
+ } catch (error) {
1279
+ console.error("Error removing connection:", error);
1280
+ return { statusCode: 500, body: "Failed to disconnect" };
1281
+ }
1282
+ }
1283
+ async function messageHandler(event) {
1284
+ const connectionId = event.requestContext.connectionId;
1285
+ try {
1286
+ const body = JSON.parse(event.body ?? "{}");
1287
+ const { type, subscriptionId } = body;
1288
+ let response;
1289
+ switch (type) {
1290
+ case "unsubscribe": {
1291
+ const subResponse = await docClient.send(
1292
+ new GetCommand({
1293
+ TableName: queriesTable,
1294
+ Key: { pk: connectionId, sk: subscriptionId }
1295
+ })
1296
+ );
1297
+ if (subResponse.Item) {
1298
+ const queryEntry = subResponse.Item;
1299
+ for (const key of queryEntry.dependencies ?? []) {
1300
+ await docClient.send(
1301
+ new DeleteCommand({
1302
+ TableName: dependenciesTable,
1303
+ Key: { pk: key, sk: `${connectionId}#${subscriptionId}` }
1304
+ })
1305
+ );
1306
+ }
1307
+ }
1308
+ await docClient.send(
1309
+ new DeleteCommand({
1310
+ TableName: queriesTable,
1311
+ Key: { pk: connectionId, sk: subscriptionId }
1312
+ })
1313
+ );
1314
+ response = { type: "result", data: { success: true } };
1315
+ break;
1316
+ }
1317
+ default:
1318
+ response = {
1319
+ type: "error",
1320
+ message: `Message type '${type}' should be handled by the app API route`
1321
+ };
1322
+ }
1323
+ const apiClient = getApiClient();
1324
+ await apiClient.send(
1325
+ new PostToConnectionCommand({
1326
+ ConnectionId: connectionId,
1327
+ Data: Buffer.from(JSON.stringify(response))
1328
+ })
1329
+ );
1330
+ return { statusCode: 200, body: "OK" };
1331
+ } catch (error) {
1332
+ console.error("Error handling message:", error);
1333
+ return { statusCode: 500, body: "Internal server error" };
1334
+ }
1335
+ }
1336
+ async function streamHandler(event) {
1337
+ const affectedSubscriptions = /* @__PURE__ */ new Map();
1338
+ for (const record of event.Records) {
1339
+ if (!record.dynamodb) continue;
1340
+ const tableName = extractTableName(record);
1341
+ if (!tableName) continue;
1342
+ const newImage = record.dynamodb.NewImage ? unmarshall(record.dynamodb.NewImage) : null;
1343
+ const oldImage = record.dynamodb.OldImage ? unmarshall(record.dynamodb.OldImage) : null;
1344
+ const affectedKeys = /* @__PURE__ */ new Set();
1345
+ if (newImage) {
1346
+ for (const key of extractAffectedKeys(tableName, newImage)) {
1347
+ affectedKeys.add(key);
1348
+ }
1349
+ }
1350
+ if (oldImage) {
1351
+ for (const key of extractAffectedKeys(tableName, oldImage)) {
1352
+ affectedKeys.add(key);
1353
+ }
1354
+ }
1355
+ for (const key of affectedKeys) {
1356
+ const subscriptions = await findAffectedSubscriptions(key);
1357
+ for (const sub of subscriptions) {
1358
+ const connId = sub.connectionId;
1359
+ const subId = sub.subscriptionId;
1360
+ if (!affectedSubscriptions.has(connId)) {
1361
+ affectedSubscriptions.set(connId, /* @__PURE__ */ new Map());
1362
+ }
1363
+ const connSubs = affectedSubscriptions.get(connId);
1364
+ if (!connSubs.has(subId)) {
1365
+ connSubs.set(subId, { oldImage, newImage });
1366
+ }
1367
+ }
1368
+ }
1369
+ }
1370
+ const sendPromises = [];
1371
+ for (const [connectionId, subscriptions] of affectedSubscriptions) {
1372
+ for (const [subscriptionId] of subscriptions) {
1373
+ sendPromises.push(processSubscription(connectionId, subscriptionId));
1374
+ }
1375
+ }
1376
+ await Promise.allSettled(sendPromises);
1377
+ }
1378
+ function extractTableName(record) {
1379
+ const arn = record.eventSourceARN;
1380
+ if (!arn) return null;
1381
+ const match = arn.match(/table\/([^/]+)/);
1382
+ return match ? match[1] : null;
1383
+ }
1384
+ async function findAffectedSubscriptions(dependencyKey) {
1385
+ try {
1386
+ const response = await docClient.send(
1387
+ new QueryCommand({
1388
+ TableName: dependenciesTable,
1389
+ KeyConditionExpression: "pk = :pk",
1390
+ ExpressionAttributeValues: {
1391
+ ":pk": dependencyKey
1392
+ }
1393
+ })
1394
+ );
1395
+ return (response.Items ?? []).map((item) => ({
1396
+ connectionId: item.connectionId,
1397
+ subscriptionId: item.subscriptionId
1398
+ }));
1399
+ } catch (error) {
1400
+ console.error("Error finding affected subscriptions:", error);
1401
+ return [];
1402
+ }
1403
+ }
1404
+ async function processSubscription(connectionId, subscriptionId) {
1405
+ try {
1406
+ const response = await docClient.send(
1407
+ new GetCommand({
1408
+ TableName: queriesTable,
1409
+ Key: { pk: connectionId, sk: subscriptionId }
1410
+ })
1411
+ );
1412
+ const queryState = response.Item;
1413
+ if (!queryState) {
1414
+ console.warn(
1415
+ `Subscription not found: ${connectionId}/${subscriptionId}`
1416
+ );
1417
+ return;
1418
+ }
1419
+ const newResult = await executeQueryFromMetadata(queryState.queryMetadata);
1420
+ if (!hasChanges(queryState.lastResult, newResult)) {
1421
+ return;
1422
+ }
1423
+ const patches = generatePatches(queryState.lastResult, newResult);
1424
+ await docClient.send(
1425
+ new PutCommand({
1426
+ TableName: queriesTable,
1427
+ Item: {
1428
+ ...queryState,
1429
+ lastResult: newResult,
1430
+ updatedAt: Date.now()
1431
+ }
1432
+ })
1433
+ );
1434
+ await sendPatch(connectionId, subscriptionId, patches);
1435
+ } catch (error) {
1436
+ if (error instanceof GoneException) {
1437
+ await cleanupConnection(connectionId);
1438
+ } else {
1439
+ console.error(
1440
+ `Error processing subscription ${connectionId}/${subscriptionId}:`,
1441
+ error
1442
+ );
1443
+ }
1444
+ }
1445
+ }
1446
+ async function executeQueryFromMetadata(metadata) {
1447
+ const { tableName, filterConditions, sortOrder, limit } = metadata;
1448
+ const whereClause = buildWhereClause(filterConditions);
1449
+ const orderClause = sortOrder === "desc" ? "ORDER BY SK DESC" : "";
1450
+ const limitClause = limit ? `LIMIT ${limit}` : "";
1451
+ const statement = `SELECT * FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim();
1452
+ try {
1453
+ const { ExecuteStatementCommand: ExecuteStatementCommand3 } = await import('@aws-sdk/client-dynamodb');
1454
+ const result = await ddbClient.send(
1455
+ new ExecuteStatementCommand3({
1456
+ Statement: statement
1457
+ })
1458
+ );
1459
+ return (result.Items ?? []).map(
1460
+ (item) => unmarshall(item)
1461
+ );
1462
+ } catch (error) {
1463
+ console.error("Error executing query from metadata:", error);
1464
+ console.error("Statement:", statement);
1465
+ return [];
1466
+ }
1467
+ }
1468
+ function buildWhereClause(conditions) {
1469
+ if (conditions.length === 0) return "";
1470
+ const clauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1471
+ if (clauses.length === 0) return "";
1472
+ return `WHERE ${clauses.join(" AND ")}`;
1473
+ }
1474
+ function buildConditionClause(condition) {
1475
+ const { type, operator, field, value, value2, conditions } = condition;
1476
+ if (type === "comparison" && field) {
1477
+ const escapedValue = escapeValue(value);
1478
+ switch (operator) {
1479
+ case "eq":
1480
+ return `"${field}" = ${escapedValue}`;
1481
+ case "ne":
1482
+ return `"${field}" <> ${escapedValue}`;
1483
+ case "gt":
1484
+ return `"${field}" > ${escapedValue}`;
1485
+ case "gte":
1486
+ return `"${field}" >= ${escapedValue}`;
1487
+ case "lt":
1488
+ return `"${field}" < ${escapedValue}`;
1489
+ case "lte":
1490
+ return `"${field}" <= ${escapedValue}`;
1491
+ case "between":
1492
+ return `"${field}" BETWEEN ${escapedValue} AND ${escapeValue(value2)}`;
1493
+ default:
1494
+ return "";
1495
+ }
1496
+ }
1497
+ if (type === "function" && field) {
1498
+ const escapedValue = escapeValue(value);
1499
+ switch (operator) {
1500
+ case "beginsWith":
1501
+ return `begins_with("${field}", ${escapedValue})`;
1502
+ case "contains":
1503
+ return `contains("${field}", ${escapedValue})`;
1504
+ default:
1505
+ return "";
1506
+ }
1507
+ }
1508
+ if (type === "logical" && conditions) {
1509
+ const subclauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1510
+ if (subclauses.length === 0) return "";
1511
+ switch (operator) {
1512
+ case "and":
1513
+ return `(${subclauses.join(" AND ")})`;
1514
+ case "or":
1515
+ return `(${subclauses.join(" OR ")})`;
1516
+ case "not":
1517
+ return subclauses.length > 0 ? `NOT (${subclauses[0]})` : "";
1518
+ default:
1519
+ return "";
1520
+ }
1521
+ }
1522
+ return "";
1523
+ }
1524
+ function escapeValue(value) {
1525
+ if (value === null || value === void 0) return "NULL";
1526
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
1527
+ if (typeof value === "number") return String(value);
1528
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1529
+ return `'${String(value).replace(/'/g, "''")}'`;
1530
+ }
1531
+ async function sendPatch(connectionId, subscriptionId, patches) {
1532
+ const message = JSON.stringify({
1533
+ type: "patch",
1534
+ subscriptionId,
1535
+ patches
1536
+ });
1537
+ try {
1538
+ const apiClient = getApiClient();
1539
+ await apiClient.send(
1540
+ new PostToConnectionCommand({
1541
+ ConnectionId: connectionId,
1542
+ Data: Buffer.from(message)
1543
+ })
1544
+ );
1545
+ } catch (error) {
1546
+ if (error instanceof GoneException) {
1547
+ throw error;
1548
+ }
1549
+ console.error(`Error sending patch to ${connectionId}:`, error);
1550
+ }
1551
+ }
1552
+ async function cleanupConnection(connectionId) {
1553
+ console.log("Cleaning up disconnected connection:", connectionId);
1554
+ try {
1555
+ await docClient.send(
1556
+ new DeleteCommand({
1557
+ TableName: connectionsTable,
1558
+ Key: { connectionId }
1559
+ })
1560
+ );
1561
+ } catch (error) {
1562
+ console.error("Error cleaning up connection:", error);
1563
+ }
1564
+ }
1565
+ return {
1566
+ connectHandler,
1567
+ disconnectHandler,
1568
+ messageHandler,
1569
+ streamHandler
1570
+ };
1571
+ }
1572
+
1573
+ // ../server/src/filter-evaluator.ts
1574
+ function evaluateFilter(filter, record) {
1575
+ switch (filter.type) {
1576
+ case "comparison":
1577
+ return evaluateComparison(filter, record);
1578
+ case "logical":
1579
+ return evaluateLogical(filter, record);
1580
+ case "function":
1581
+ return evaluateFunction(filter, record);
1582
+ default:
1583
+ console.warn(`Unknown filter type: ${filter.type}`);
1584
+ return false;
1585
+ }
1586
+ }
1587
+ function evaluateFilters(filters, record) {
1588
+ if (filters.length === 0) return true;
1589
+ return filters.every((filter) => evaluateFilter(filter, record));
1590
+ }
1591
+ function evaluateComparison(filter, record) {
1592
+ const { operator, field, value, value2 } = filter;
1593
+ if (!field || !operator) return false;
1594
+ const fieldValue = getFieldValue(record, field);
1595
+ switch (operator) {
1596
+ case "eq":
1597
+ return fieldValue === value;
1598
+ case "ne":
1599
+ return fieldValue !== value;
1600
+ case "gt":
1601
+ return compareValues(fieldValue, value) > 0;
1602
+ case "gte":
1603
+ return compareValues(fieldValue, value) >= 0;
1604
+ case "lt":
1605
+ return compareValues(fieldValue, value) < 0;
1606
+ case "lte":
1607
+ return compareValues(fieldValue, value) <= 0;
1608
+ case "between":
1609
+ return compareValues(fieldValue, value) >= 0 && compareValues(fieldValue, value2) <= 0;
1610
+ default:
1611
+ console.warn(`Unknown comparison operator: ${operator}`);
1612
+ return false;
1613
+ }
1614
+ }
1615
+ function evaluateLogical(filter, record) {
1616
+ const { operator, conditions } = filter;
1617
+ if (!operator || !conditions) return false;
1618
+ switch (operator) {
1619
+ case "and":
1620
+ return conditions.every((c) => evaluateFilter(c, record));
1621
+ case "or":
1622
+ return conditions.some((c) => evaluateFilter(c, record));
1623
+ case "not":
1624
+ return conditions.length > 0 && !evaluateFilter(conditions[0], record);
1625
+ default:
1626
+ console.warn(`Unknown logical operator: ${operator}`);
1627
+ return false;
1628
+ }
1629
+ }
1630
+ function evaluateFunction(filter, record) {
1631
+ const { operator, field, value } = filter;
1632
+ if (!field || !operator) return false;
1633
+ const fieldValue = getFieldValue(record, field);
1634
+ switch (operator) {
1635
+ case "beginsWith":
1636
+ return typeof fieldValue === "string" && typeof value === "string" && fieldValue.startsWith(value);
1637
+ case "contains":
1638
+ return typeof fieldValue === "string" && typeof value === "string" && fieldValue.includes(value);
1639
+ default:
1640
+ console.warn(`Unknown function operator: ${operator}`);
1641
+ return false;
1642
+ }
1643
+ }
1644
+ function getFieldValue(record, field) {
1645
+ const parts = field.split(".");
1646
+ let value = record;
1647
+ for (const part of parts) {
1648
+ if (value === null || value === void 0) return void 0;
1649
+ if (typeof value !== "object") return void 0;
1650
+ value = value[part];
1651
+ }
1652
+ return value;
1653
+ }
1654
+ function compareValues(a, b) {
1655
+ if (a === null || a === void 0) {
1656
+ return b === null || b === void 0 ? 0 : -1;
1657
+ }
1658
+ if (b === null || b === void 0) {
1659
+ return 1;
1660
+ }
1661
+ if (typeof a === "number" && typeof b === "number") {
1662
+ return a - b;
1663
+ }
1664
+ if (typeof a === "string" && typeof b === "string") {
1665
+ return a.localeCompare(b);
1666
+ }
1667
+ if (typeof a === "boolean" && typeof b === "boolean") {
1668
+ return a === b ? 0 : a ? 1 : -1;
1669
+ }
1670
+ return String(a).localeCompare(String(b));
1671
+ }
1672
+ function sortRecords(records, sortField, sortOrder = "asc") {
1673
+ if (!sortField) return records;
1674
+ return [...records].sort((a, b) => {
1675
+ const aValue = getFieldValue(a, sortField);
1676
+ const bValue = getFieldValue(b, sortField);
1677
+ const comparison = compareValues(aValue, bValue);
1678
+ return sortOrder === "desc" ? -comparison : comparison;
1679
+ });
1680
+ }
1681
+ function getRecordKey(record, pkField, skField) {
1682
+ const pk = record[pkField];
1683
+ const sk = skField ? record[skField] : void 0;
1684
+ return sk !== void 0 ? `${pk}#${sk}` : String(pk);
1685
+ }
1686
+
1687
+ export { DependencyTracker, ProcedureBuilder, QueryBuilderImpl, Router, applyPatches, batchPatches, buildDeleteStatement, buildGetStatement, buildInsertStatement, buildSelectStatement, buildUpdateStatement, createConnectHandler, createDbContext, createDependencyKey, createDisconnectHandler, createFilterBuilder, createLambdaHandlers, createReactiveHandler, createReactiveHarness, createRouter, createStreamHandler, evaluateFilter, evaluateFilters, executeProcedure, extractAffectedKeys, extractDependencies, generatePatches, getRecordKey, hasChanges, initReactive, isProcedure, mergeRouters, operationToQueryMetadata, optimizePatches, parseDependencyKey, sortRecords };
1688
+ //# sourceMappingURL=server.js.map
18
1689
  //# sourceMappingURL=server.js.map