dynamodb-reactive 0.1.0 → 0.1.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/dist/server.js CHANGED
@@ -1,18 +1,1698 @@
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 jsonpatch 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
+ var { applyPatch, compare } = jsonpatch;
687
+ function generatePatches(oldValue, newValue) {
688
+ const operations = compare(
689
+ oldValue,
690
+ newValue
691
+ );
692
+ return operations.map((op) => ({
693
+ op: op.op,
694
+ path: op.path,
695
+ value: "value" in op ? op.value : void 0,
696
+ from: "from" in op ? op.from : void 0
697
+ }));
698
+ }
699
+ function applyPatches(document, patches) {
700
+ const operations = patches.map((patch) => {
701
+ const op = {
702
+ op: patch.op,
703
+ path: patch.path
704
+ };
705
+ if ("value" in patch && patch.value !== void 0) {
706
+ op.value = patch.value;
707
+ }
708
+ if ("from" in patch && patch.from !== void 0) {
709
+ op.from = patch.from;
710
+ }
711
+ return op;
712
+ });
713
+ const result = applyPatch(
714
+ structuredClone(document),
715
+ operations,
716
+ true,
717
+ // Validate operations
718
+ false
719
+ // Don't mutate the original
720
+ );
721
+ return result.newDocument;
722
+ }
723
+ function hasChanges(oldValue, newValue) {
724
+ const patches = generatePatches(oldValue, newValue);
725
+ return patches.length > 0;
726
+ }
727
+ function optimizePatches(patches) {
728
+ const seen = /* @__PURE__ */ new Set();
729
+ const optimized = [];
730
+ for (let i = patches.length - 1; i >= 0; i--) {
731
+ const patch = patches[i];
732
+ if (!seen.has(patch.path)) {
733
+ seen.add(patch.path);
734
+ optimized.unshift(patch);
735
+ }
736
+ }
737
+ return optimized;
738
+ }
739
+ function batchPatches(patchSets) {
740
+ const allPatches = patchSets.flat();
741
+ return optimizePatches(allPatches);
742
+ }
743
+ function createReactiveHandler(config) {
744
+ const ttlSeconds = config.ttlSeconds ?? 3600;
745
+ const connectionsTable = config.connectionsTableName ?? SystemTableNames.connections;
746
+ const dependenciesTable = config.dependenciesTableName ?? SystemTableNames.dependencies;
747
+ const queriesTable = config.queriesTableName ?? SystemTableNames.queries;
748
+ const ddbClient = new DynamoDBClient({
749
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
750
+ });
751
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
752
+ async function handleRequest(connectionId, request) {
753
+ try {
754
+ const ctx = await config.getContext(connectionId);
755
+ const dependencyTracker = new DependencyTracker();
756
+ const db = createDbContext(config.dbConfig ?? {}, dependencyTracker);
757
+ const fullCtx = { ...ctx, db };
758
+ switch (request.type) {
759
+ case "subscribe":
760
+ return handleSubscribe(
761
+ connectionId,
762
+ request,
763
+ fullCtx,
764
+ dependencyTracker
765
+ );
766
+ case "unsubscribe":
767
+ return handleUnsubscribe(connectionId, request);
768
+ case "call":
769
+ return handleCall(request, fullCtx);
770
+ default:
771
+ return {
772
+ type: "error",
773
+ message: `Unknown request type: ${request.type}`
774
+ };
775
+ }
776
+ } catch (error) {
777
+ return {
778
+ type: "error",
779
+ message: error instanceof Error ? error.message : "Unknown error",
780
+ subscriptionId: "subscriptionId" in request ? request.subscriptionId : void 0
781
+ };
782
+ }
783
+ }
784
+ async function handleSubscribe(connectionId, request, ctx, dependencyTracker) {
785
+ const result = await config.router.execute(
786
+ request.path,
787
+ ctx,
788
+ request.input
789
+ );
790
+ const queryMetadata = dependencyTracker.getQueryMetadata();
791
+ const dependencyKeys = dependencyTracker.getDependencyKeys();
792
+ if (!queryMetadata) {
793
+ console.warn("No query metadata captured for subscription");
794
+ }
795
+ const now = Date.now();
796
+ const ttl = Math.floor(now / 1e3) + ttlSeconds;
797
+ const queryEntry = {
798
+ pk: connectionId,
799
+ sk: request.subscriptionId,
800
+ connectionId,
801
+ subscriptionId: request.subscriptionId,
802
+ queryMetadata: queryMetadata ?? {
803
+ tableName: "",
804
+ filterConditions: []
805
+ },
806
+ lastResult: Array.isArray(result) ? result : [result],
807
+ dependencies: dependencyKeys,
808
+ createdAt: now,
809
+ updatedAt: now,
810
+ ttl
811
+ };
812
+ await docClient.send(
813
+ new PutCommand({
814
+ TableName: queriesTable,
815
+ Item: queryEntry
816
+ })
817
+ );
818
+ for (const key of dependencyKeys) {
819
+ await docClient.send(
820
+ new PutCommand({
821
+ TableName: dependenciesTable,
822
+ Item: {
823
+ pk: key,
824
+ sk: `${connectionId}#${request.subscriptionId}`,
825
+ connectionId,
826
+ subscriptionId: request.subscriptionId,
827
+ ttl
828
+ }
829
+ })
830
+ );
831
+ }
832
+ console.log("Subscription created:", {
833
+ connectionId,
834
+ subscriptionId: request.subscriptionId,
835
+ queryMetadata: queryMetadata?.tableName,
836
+ dependencies: dependencyKeys
837
+ });
838
+ return {
839
+ type: "snapshot",
840
+ subscriptionId: request.subscriptionId,
841
+ data: result
842
+ };
843
+ }
844
+ async function handleUnsubscribe(connectionId, request) {
845
+ const subResponse = await docClient.send(
846
+ new GetCommand({
847
+ TableName: queriesTable,
848
+ Key: { pk: connectionId, sk: request.subscriptionId }
849
+ })
850
+ );
851
+ if (subResponse.Item) {
852
+ const queryEntry = subResponse.Item;
853
+ for (const key of queryEntry.dependencies ?? []) {
854
+ await docClient.send(
855
+ new DeleteCommand({
856
+ TableName: dependenciesTable,
857
+ Key: { pk: key, sk: `${connectionId}#${request.subscriptionId}` }
858
+ })
859
+ );
860
+ }
861
+ }
862
+ await docClient.send(
863
+ new DeleteCommand({
864
+ TableName: queriesTable,
865
+ Key: { pk: connectionId, sk: request.subscriptionId }
866
+ })
867
+ );
868
+ console.log("Subscription removed:", {
869
+ connectionId,
870
+ subscriptionId: request.subscriptionId
871
+ });
872
+ return {
873
+ type: "result",
874
+ data: { success: true }
875
+ };
876
+ }
877
+ async function handleCall(request, ctx) {
878
+ const result = await config.router.execute(
879
+ request.path,
880
+ ctx,
881
+ request.input
882
+ );
883
+ return {
884
+ type: "result",
885
+ data: result
886
+ };
887
+ }
888
+ async function registerConnection(connectionId, context) {
889
+ const now = Date.now();
890
+ const ttl = Math.floor(now / 1e3) + ttlSeconds;
891
+ const connectionEntry = {
892
+ connectionId,
893
+ context,
894
+ connectedAt: now,
895
+ ttl
896
+ };
897
+ await docClient.send(
898
+ new PutCommand({
899
+ TableName: connectionsTable,
900
+ Item: connectionEntry
901
+ })
902
+ );
903
+ console.log("Connection registered:", connectionEntry);
904
+ }
905
+ async function unregisterConnection(connectionId) {
906
+ await docClient.send(
907
+ new DeleteCommand({
908
+ TableName: connectionsTable,
909
+ Key: { connectionId }
910
+ })
911
+ );
912
+ console.log("Connection unregistered:", connectionId);
913
+ }
914
+ return {
915
+ handleRequest,
916
+ registerConnection,
917
+ unregisterConnection
918
+ };
919
+ }
920
+ function createStreamHandler(config) {
921
+ const dependenciesTable = config.dependenciesTableName ?? SystemTableNames.dependencies;
922
+ const queriesTable = config.queriesTableName ?? SystemTableNames.queries;
923
+ const ddbClient = new DynamoDBClient({
924
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
925
+ });
926
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
927
+ const apiClient = new ApiGatewayManagementApiClient({
928
+ endpoint: config.apiGatewayEndpoint
929
+ });
930
+ async function handler(event) {
931
+ const affectedSubscriptions = /* @__PURE__ */ new Map();
932
+ for (const record of event.Records) {
933
+ if (!record.dynamodb) continue;
934
+ const tableName = extractTableName(record);
935
+ if (!tableName) continue;
936
+ const newImage = record.dynamodb.NewImage ? unmarshall(record.dynamodb.NewImage) : null;
937
+ const oldImage = record.dynamodb.OldImage ? unmarshall(record.dynamodb.OldImage) : null;
938
+ const affectedKeys = /* @__PURE__ */ new Set();
939
+ if (newImage) {
940
+ for (const key of extractAffectedKeys(tableName, newImage)) {
941
+ affectedKeys.add(key);
942
+ }
943
+ }
944
+ if (oldImage) {
945
+ for (const key of extractAffectedKeys(tableName, oldImage)) {
946
+ affectedKeys.add(key);
947
+ }
948
+ }
949
+ for (const key of affectedKeys) {
950
+ const subscriptions = await findAffectedSubscriptions(key);
951
+ for (const sub of subscriptions) {
952
+ const connId = sub.connectionId;
953
+ const subId = sub.subscriptionId;
954
+ if (!affectedSubscriptions.has(connId)) {
955
+ affectedSubscriptions.set(connId, /* @__PURE__ */ new Set());
956
+ }
957
+ affectedSubscriptions.get(connId).add(subId);
958
+ }
959
+ }
960
+ }
961
+ const sendPromises = [];
962
+ for (const [connectionId, subscriptionIds] of affectedSubscriptions) {
963
+ for (const subscriptionId of subscriptionIds) {
964
+ sendPromises.push(processSubscription(connectionId, subscriptionId));
965
+ }
966
+ }
967
+ await Promise.allSettled(sendPromises);
968
+ }
969
+ function extractTableName(record) {
970
+ const arn = record.eventSourceARN;
971
+ if (!arn) return null;
972
+ const match = arn.match(/table\/([^/]+)/);
973
+ return match ? match[1] : null;
974
+ }
975
+ async function findAffectedSubscriptions(dependencyKey) {
976
+ try {
977
+ const response = await docClient.send(
978
+ new QueryCommand({
979
+ TableName: dependenciesTable,
980
+ KeyConditionExpression: "pk = :pk",
981
+ ExpressionAttributeValues: {
982
+ ":pk": dependencyKey
983
+ }
984
+ })
985
+ );
986
+ return (response.Items ?? []).map((item) => ({
987
+ connectionId: item.connectionId,
988
+ subscriptionId: item.subscriptionId
989
+ }));
990
+ } catch (error) {
991
+ console.error("Error finding affected subscriptions:", error);
992
+ return [];
993
+ }
994
+ }
995
+ async function processSubscription(connectionId, subscriptionId) {
996
+ try {
997
+ const queryState = await getQueryState(connectionId, subscriptionId);
998
+ if (!queryState) {
999
+ console.warn(
1000
+ `Subscription not found: ${connectionId}/${subscriptionId}`
1001
+ );
1002
+ return;
1003
+ }
1004
+ const newResult = await executeQueryFromMetadata(
1005
+ queryState.queryMetadata
1006
+ );
1007
+ if (!hasChanges(queryState.lastResult, newResult)) {
1008
+ return;
1009
+ }
1010
+ const patches = generatePatches(queryState.lastResult, newResult);
1011
+ await updateQueryState(connectionId, subscriptionId, newResult);
1012
+ await sendPatch(connectionId, subscriptionId, patches);
1013
+ } catch (error) {
1014
+ if (error instanceof GoneException) {
1015
+ await cleanupConnection(connectionId);
1016
+ } else {
1017
+ console.error(
1018
+ `Error processing subscription ${connectionId}/${subscriptionId}:`,
1019
+ error
1020
+ );
1021
+ }
1022
+ }
1023
+ }
1024
+ async function getQueryState(connectionId, subscriptionId) {
1025
+ try {
1026
+ const response = await docClient.send(
1027
+ new GetCommand({
1028
+ TableName: queriesTable,
1029
+ Key: {
1030
+ pk: connectionId,
1031
+ sk: subscriptionId
1032
+ }
1033
+ })
1034
+ );
1035
+ return response.Item ?? null;
1036
+ } catch (error) {
1037
+ console.error("Error getting query state:", error);
1038
+ return null;
1039
+ }
1040
+ }
1041
+ async function executeQueryFromMetadata(metadata) {
1042
+ const { tableName, filterConditions, sortOrder, limit } = metadata;
1043
+ const whereClause = buildWhereClause(filterConditions);
1044
+ const orderClause = sortOrder === "desc" ? "ORDER BY SK DESC" : "";
1045
+ const limitClause = limit ? `LIMIT ${limit}` : "";
1046
+ const statement = `SELECT * FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim();
1047
+ try {
1048
+ const result = await ddbClient.send(
1049
+ new ExecuteStatementCommand({
1050
+ Statement: statement
1051
+ })
1052
+ );
1053
+ return (result.Items ?? []).map(
1054
+ (item) => unmarshall(item)
1055
+ );
1056
+ } catch (error) {
1057
+ console.error("Error executing query from metadata:", error);
1058
+ console.error("Statement:", statement);
1059
+ return [];
1060
+ }
1061
+ }
1062
+ function buildWhereClause(conditions) {
1063
+ if (conditions.length === 0) return "";
1064
+ const clauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1065
+ if (clauses.length === 0) return "";
1066
+ return `WHERE ${clauses.join(" AND ")}`;
1067
+ }
1068
+ function buildConditionClause(condition) {
1069
+ const { type, operator, field, value, value2, conditions } = condition;
1070
+ if (type === "comparison" && field) {
1071
+ const escapedValue = escapeValue(value);
1072
+ switch (operator) {
1073
+ case "eq":
1074
+ return `"${field}" = ${escapedValue}`;
1075
+ case "ne":
1076
+ return `"${field}" <> ${escapedValue}`;
1077
+ case "gt":
1078
+ return `"${field}" > ${escapedValue}`;
1079
+ case "gte":
1080
+ return `"${field}" >= ${escapedValue}`;
1081
+ case "lt":
1082
+ return `"${field}" < ${escapedValue}`;
1083
+ case "lte":
1084
+ return `"${field}" <= ${escapedValue}`;
1085
+ case "between":
1086
+ return `"${field}" BETWEEN ${escapedValue} AND ${escapeValue(value2)}`;
1087
+ case void 0:
1088
+ default:
1089
+ return "";
1090
+ }
1091
+ }
1092
+ if (type === "function" && field) {
1093
+ const escapedValue = escapeValue(value);
1094
+ switch (operator) {
1095
+ case "beginsWith":
1096
+ return `begins_with("${field}", ${escapedValue})`;
1097
+ case "contains":
1098
+ return `contains("${field}", ${escapedValue})`;
1099
+ case void 0:
1100
+ default:
1101
+ return "";
1102
+ }
1103
+ }
1104
+ if (type === "logical" && conditions) {
1105
+ const subclauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1106
+ if (subclauses.length === 0) return "";
1107
+ switch (operator) {
1108
+ case "and":
1109
+ return `(${subclauses.join(" AND ")})`;
1110
+ case "or":
1111
+ return `(${subclauses.join(" OR ")})`;
1112
+ case "not":
1113
+ return subclauses.length > 0 ? `NOT (${subclauses[0]})` : "";
1114
+ case void 0:
1115
+ default:
1116
+ return "";
1117
+ }
1118
+ }
1119
+ return "";
1120
+ }
1121
+ function escapeValue(value) {
1122
+ if (value === null || value === void 0) return "NULL";
1123
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
1124
+ if (typeof value === "number") return String(value);
1125
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1126
+ return `'${String(value).replace(/'/g, "''")}'`;
1127
+ }
1128
+ async function updateQueryState(connectionId, subscriptionId, newResult) {
1129
+ try {
1130
+ const existing = await getQueryState(connectionId, subscriptionId);
1131
+ if (!existing) return;
1132
+ await docClient.send(
1133
+ new GetCommand({
1134
+ TableName: queriesTable,
1135
+ Key: { pk: connectionId, sk: subscriptionId }
1136
+ })
1137
+ );
1138
+ const { PutCommand: PutCommand4 } = await import('@aws-sdk/lib-dynamodb');
1139
+ await docClient.send(
1140
+ new PutCommand4({
1141
+ TableName: queriesTable,
1142
+ Item: {
1143
+ ...existing,
1144
+ lastResult: newResult,
1145
+ updatedAt: Date.now()
1146
+ }
1147
+ })
1148
+ );
1149
+ } catch (error) {
1150
+ console.error("Error updating query state:", error);
1151
+ }
1152
+ }
1153
+ async function sendPatch(connectionId, subscriptionId, patches) {
1154
+ const message = JSON.stringify({
1155
+ type: "patch",
1156
+ subscriptionId,
1157
+ patches
1158
+ });
1159
+ try {
1160
+ await apiClient.send(
1161
+ new PostToConnectionCommand({
1162
+ ConnectionId: connectionId,
1163
+ Data: Buffer.from(message)
1164
+ })
1165
+ );
1166
+ } catch (error) {
1167
+ if (error instanceof GoneException) {
1168
+ throw error;
1169
+ }
1170
+ console.error(`Error sending patch to ${connectionId}:`, error);
1171
+ }
1172
+ }
1173
+ async function cleanupConnection(connectionId) {
1174
+ console.log("Cleaning up disconnected connection:", connectionId);
1175
+ }
1176
+ return { handler };
1177
+ }
1178
+ function createConnectHandler(config) {
1179
+ const connectionsTable = config.connectionsTableName ?? SystemTableNames.connections;
1180
+ const ddbClient = new DynamoDBClient({
1181
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
1182
+ });
1183
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
1184
+ return async function handler(event) {
1185
+ const connectionId = event.requestContext.connectionId;
1186
+ try {
1187
+ await docClient.send(
1188
+ new QueryCommand({
1189
+ TableName: connectionsTable,
1190
+ KeyConditionExpression: "connectionId = :cid",
1191
+ ExpressionAttributeValues: {
1192
+ ":cid": connectionId
1193
+ }
1194
+ })
1195
+ );
1196
+ console.log("Connection established:", connectionId);
1197
+ return { statusCode: 200 };
1198
+ } catch (error) {
1199
+ console.error("Error creating connection:", error);
1200
+ return { statusCode: 500 };
1201
+ }
1202
+ };
1203
+ }
1204
+ function createDisconnectHandler(config) {
1205
+ const connectionsTable = config.connectionsTableName ?? SystemTableNames.connections;
1206
+ const ddbClient = new DynamoDBClient({
1207
+ region: config.dbConfig?.region ?? process.env.AWS_REGION
1208
+ });
1209
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
1210
+ return async function handler(event) {
1211
+ const connectionId = event.requestContext.connectionId;
1212
+ try {
1213
+ await docClient.send(
1214
+ new DeleteCommand({
1215
+ TableName: connectionsTable,
1216
+ Key: { connectionId }
1217
+ })
1218
+ );
1219
+ console.log("Connection removed:", connectionId);
1220
+ return { statusCode: 200 };
1221
+ } catch (error) {
1222
+ console.error("Error removing connection:", error);
1223
+ return { statusCode: 500 };
1224
+ }
1225
+ };
1226
+ }
1227
+
1228
+ // ../server/src/harness.ts
1229
+ function createReactiveHarness(config) {
1230
+ return {
1231
+ getContext: config.getContext ?? (async () => ({})),
1232
+ dbConfig: config.dbConfig
1233
+ };
1234
+ }
1235
+ function createLambdaHandlers() {
1236
+ const connectionsTable = process.env.CONNECTIONS_TABLE ?? SystemTableNames.connections;
1237
+ const dependenciesTable = process.env.DEPENDENCIES_TABLE ?? SystemTableNames.dependencies;
1238
+ const queriesTable = process.env.QUERIES_TABLE ?? SystemTableNames.queries;
1239
+ const wsEndpoint = process.env.WEBSOCKET_ENDPOINT ?? "";
1240
+ const ddbClient = new DynamoDBClient({
1241
+ region: process.env.AWS_REGION
1242
+ });
1243
+ const docClient = DynamoDBDocumentClient.from(ddbClient);
1244
+ const getApiClient = () => new ApiGatewayManagementApiClient({
1245
+ endpoint: wsEndpoint
1246
+ });
1247
+ async function connectHandler(event) {
1248
+ const connectionId = event.requestContext.connectionId;
1249
+ const now = Date.now();
1250
+ const ttl = Math.floor(now / 1e3) + 3600;
1251
+ try {
1252
+ const connectionEntry = {
1253
+ connectionId,
1254
+ context: event.requestContext.authorizer,
1255
+ connectedAt: now,
1256
+ ttl
1257
+ };
1258
+ await docClient.send(
1259
+ new PutCommand({
1260
+ TableName: connectionsTable,
1261
+ Item: connectionEntry
1262
+ })
1263
+ );
1264
+ console.log("Connection established:", connectionId);
1265
+ return { statusCode: 200, body: "Connected" };
1266
+ } catch (error) {
1267
+ console.error("Error creating connection:", error);
1268
+ return { statusCode: 500, body: "Failed to connect" };
1269
+ }
1270
+ }
1271
+ async function disconnectHandler(event) {
1272
+ const connectionId = event.requestContext.connectionId;
1273
+ try {
1274
+ await docClient.send(
1275
+ new DeleteCommand({
1276
+ TableName: connectionsTable,
1277
+ Key: { connectionId }
1278
+ })
1279
+ );
1280
+ console.log("Connection removed:", connectionId);
1281
+ return { statusCode: 200, body: "Disconnected" };
1282
+ } catch (error) {
1283
+ console.error("Error removing connection:", error);
1284
+ return { statusCode: 500, body: "Failed to disconnect" };
1285
+ }
1286
+ }
1287
+ async function messageHandler(event) {
1288
+ const connectionId = event.requestContext.connectionId;
1289
+ try {
1290
+ const body = JSON.parse(event.body ?? "{}");
1291
+ const { type, subscriptionId } = body;
1292
+ let response;
1293
+ switch (type) {
1294
+ case "unsubscribe": {
1295
+ const subResponse = await docClient.send(
1296
+ new GetCommand({
1297
+ TableName: queriesTable,
1298
+ Key: { pk: connectionId, sk: subscriptionId }
1299
+ })
1300
+ );
1301
+ if (subResponse.Item) {
1302
+ const queryEntry = subResponse.Item;
1303
+ for (const key of queryEntry.dependencies ?? []) {
1304
+ await docClient.send(
1305
+ new DeleteCommand({
1306
+ TableName: dependenciesTable,
1307
+ Key: { pk: key, sk: `${connectionId}#${subscriptionId}` }
1308
+ })
1309
+ );
1310
+ }
1311
+ }
1312
+ await docClient.send(
1313
+ new DeleteCommand({
1314
+ TableName: queriesTable,
1315
+ Key: { pk: connectionId, sk: subscriptionId }
1316
+ })
1317
+ );
1318
+ response = { type: "result", data: { success: true } };
1319
+ break;
1320
+ }
1321
+ default:
1322
+ response = {
1323
+ type: "error",
1324
+ message: `Message type '${type}' should be handled by the app API route`
1325
+ };
1326
+ }
1327
+ const apiClient = getApiClient();
1328
+ await apiClient.send(
1329
+ new PostToConnectionCommand({
1330
+ ConnectionId: connectionId,
1331
+ Data: Buffer.from(JSON.stringify(response))
1332
+ })
1333
+ );
1334
+ return { statusCode: 200, body: "OK" };
1335
+ } catch (error) {
1336
+ console.error("Error handling message:", error);
1337
+ return { statusCode: 500, body: "Internal server error" };
1338
+ }
1339
+ }
1340
+ async function streamHandler(event) {
1341
+ const affectedSubscriptions = /* @__PURE__ */ new Map();
1342
+ for (const record of event.Records) {
1343
+ if (!record.dynamodb) continue;
1344
+ const tableName = extractTableName(record);
1345
+ if (!tableName) continue;
1346
+ const newImage = record.dynamodb.NewImage ? unmarshall(record.dynamodb.NewImage) : null;
1347
+ const oldImage = record.dynamodb.OldImage ? unmarshall(record.dynamodb.OldImage) : null;
1348
+ const affectedKeys = /* @__PURE__ */ new Set();
1349
+ if (newImage) {
1350
+ for (const key of extractAffectedKeys(tableName, newImage)) {
1351
+ affectedKeys.add(key);
1352
+ }
1353
+ }
1354
+ if (oldImage) {
1355
+ for (const key of extractAffectedKeys(tableName, oldImage)) {
1356
+ affectedKeys.add(key);
1357
+ }
1358
+ }
1359
+ for (const key of affectedKeys) {
1360
+ const subscriptions = await findAffectedSubscriptions(key);
1361
+ for (const sub of subscriptions) {
1362
+ const connId = sub.connectionId;
1363
+ const subId = sub.subscriptionId;
1364
+ if (!affectedSubscriptions.has(connId)) {
1365
+ affectedSubscriptions.set(connId, /* @__PURE__ */ new Map());
1366
+ }
1367
+ const connSubs = affectedSubscriptions.get(connId);
1368
+ if (!connSubs.has(subId)) {
1369
+ connSubs.set(subId, { oldImage, newImage });
1370
+ }
1371
+ }
1372
+ }
1373
+ }
1374
+ const sendPromises = [];
1375
+ for (const [connectionId, subscriptions] of affectedSubscriptions) {
1376
+ for (const [subscriptionId] of subscriptions) {
1377
+ sendPromises.push(processSubscription(connectionId, subscriptionId));
1378
+ }
1379
+ }
1380
+ await Promise.allSettled(sendPromises);
1381
+ }
1382
+ function extractTableName(record) {
1383
+ const arn = record.eventSourceARN;
1384
+ if (!arn) return null;
1385
+ const match = arn.match(/table\/([^/]+)/);
1386
+ return match ? match[1] : null;
1387
+ }
1388
+ async function findAffectedSubscriptions(dependencyKey) {
1389
+ try {
1390
+ const response = await docClient.send(
1391
+ new QueryCommand({
1392
+ TableName: dependenciesTable,
1393
+ KeyConditionExpression: "pk = :pk",
1394
+ ExpressionAttributeValues: {
1395
+ ":pk": dependencyKey
1396
+ }
1397
+ })
1398
+ );
1399
+ return (response.Items ?? []).map((item) => ({
1400
+ connectionId: item.connectionId,
1401
+ subscriptionId: item.subscriptionId
1402
+ }));
1403
+ } catch (error) {
1404
+ console.error("Error finding affected subscriptions:", error);
1405
+ return [];
1406
+ }
1407
+ }
1408
+ async function processSubscription(connectionId, subscriptionId) {
1409
+ try {
1410
+ const response = await docClient.send(
1411
+ new GetCommand({
1412
+ TableName: queriesTable,
1413
+ Key: { pk: connectionId, sk: subscriptionId }
1414
+ })
1415
+ );
1416
+ const queryState = response.Item;
1417
+ if (!queryState) {
1418
+ console.warn(
1419
+ `Subscription not found: ${connectionId}/${subscriptionId}`
1420
+ );
1421
+ return;
1422
+ }
1423
+ const newResult = await executeQueryFromMetadata(
1424
+ queryState.queryMetadata
1425
+ );
1426
+ if (!hasChanges(queryState.lastResult, newResult)) {
1427
+ return;
1428
+ }
1429
+ const patches = generatePatches(queryState.lastResult, newResult);
1430
+ await docClient.send(
1431
+ new PutCommand({
1432
+ TableName: queriesTable,
1433
+ Item: {
1434
+ ...queryState,
1435
+ lastResult: newResult,
1436
+ updatedAt: Date.now()
1437
+ }
1438
+ })
1439
+ );
1440
+ await sendPatch(connectionId, subscriptionId, patches);
1441
+ } catch (error) {
1442
+ if (error instanceof GoneException) {
1443
+ await cleanupConnection(connectionId);
1444
+ } else {
1445
+ console.error(
1446
+ `Error processing subscription ${connectionId}/${subscriptionId}:`,
1447
+ error
1448
+ );
1449
+ }
1450
+ }
1451
+ }
1452
+ async function executeQueryFromMetadata(metadata) {
1453
+ const { tableName, filterConditions, sortOrder, limit } = metadata;
1454
+ const whereClause = buildWhereClause(filterConditions);
1455
+ const orderClause = sortOrder === "desc" ? "ORDER BY SK DESC" : "";
1456
+ const limitClause = limit ? `LIMIT ${limit}` : "";
1457
+ const statement = `SELECT * FROM "${tableName}" ${whereClause} ${orderClause} ${limitClause}`.trim();
1458
+ try {
1459
+ const { ExecuteStatementCommand: ExecuteStatementCommand3 } = await import('@aws-sdk/client-dynamodb');
1460
+ const result = await ddbClient.send(
1461
+ new ExecuteStatementCommand3({
1462
+ Statement: statement
1463
+ })
1464
+ );
1465
+ return (result.Items ?? []).map(
1466
+ (item) => unmarshall(item)
1467
+ );
1468
+ } catch (error) {
1469
+ console.error("Error executing query from metadata:", error);
1470
+ console.error("Statement:", statement);
1471
+ return [];
1472
+ }
1473
+ }
1474
+ function buildWhereClause(conditions) {
1475
+ if (conditions.length === 0) return "";
1476
+ const clauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1477
+ if (clauses.length === 0) return "";
1478
+ return `WHERE ${clauses.join(" AND ")}`;
1479
+ }
1480
+ function buildConditionClause(condition) {
1481
+ const { type, operator, field, value, value2, conditions } = condition;
1482
+ if (type === "comparison" && field) {
1483
+ const escapedValue = escapeValue(value);
1484
+ switch (operator) {
1485
+ case "eq":
1486
+ return `"${field}" = ${escapedValue}`;
1487
+ case "ne":
1488
+ return `"${field}" <> ${escapedValue}`;
1489
+ case "gt":
1490
+ return `"${field}" > ${escapedValue}`;
1491
+ case "gte":
1492
+ return `"${field}" >= ${escapedValue}`;
1493
+ case "lt":
1494
+ return `"${field}" < ${escapedValue}`;
1495
+ case "lte":
1496
+ return `"${field}" <= ${escapedValue}`;
1497
+ case "between":
1498
+ return `"${field}" BETWEEN ${escapedValue} AND ${escapeValue(value2)}`;
1499
+ case void 0:
1500
+ default:
1501
+ return "";
1502
+ }
1503
+ }
1504
+ if (type === "function" && field) {
1505
+ const escapedValue = escapeValue(value);
1506
+ switch (operator) {
1507
+ case "beginsWith":
1508
+ return `begins_with("${field}", ${escapedValue})`;
1509
+ case "contains":
1510
+ return `contains("${field}", ${escapedValue})`;
1511
+ case void 0:
1512
+ default:
1513
+ return "";
1514
+ }
1515
+ }
1516
+ if (type === "logical" && conditions) {
1517
+ const subclauses = conditions.map((c) => buildConditionClause(c)).filter(Boolean);
1518
+ if (subclauses.length === 0) return "";
1519
+ switch (operator) {
1520
+ case "and":
1521
+ return `(${subclauses.join(" AND ")})`;
1522
+ case "or":
1523
+ return `(${subclauses.join(" OR ")})`;
1524
+ case "not":
1525
+ return subclauses.length > 0 ? `NOT (${subclauses[0]})` : "";
1526
+ case void 0:
1527
+ default:
1528
+ return "";
1529
+ }
1530
+ }
1531
+ return "";
1532
+ }
1533
+ function escapeValue(value) {
1534
+ if (value === null || value === void 0) return "NULL";
1535
+ if (typeof value === "string") return `'${value.replace(/'/g, "''")}'`;
1536
+ if (typeof value === "number") return String(value);
1537
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE";
1538
+ return `'${String(value).replace(/'/g, "''")}'`;
1539
+ }
1540
+ async function sendPatch(connectionId, subscriptionId, patches) {
1541
+ const message = JSON.stringify({
1542
+ type: "patch",
1543
+ subscriptionId,
1544
+ patches
1545
+ });
1546
+ try {
1547
+ const apiClient = getApiClient();
1548
+ await apiClient.send(
1549
+ new PostToConnectionCommand({
1550
+ ConnectionId: connectionId,
1551
+ Data: Buffer.from(message)
1552
+ })
1553
+ );
1554
+ } catch (error) {
1555
+ if (error instanceof GoneException) {
1556
+ throw error;
1557
+ }
1558
+ console.error(`Error sending patch to ${connectionId}:`, error);
1559
+ }
1560
+ }
1561
+ async function cleanupConnection(connectionId) {
1562
+ console.log("Cleaning up disconnected connection:", connectionId);
1563
+ try {
1564
+ await docClient.send(
1565
+ new DeleteCommand({
1566
+ TableName: connectionsTable,
1567
+ Key: { connectionId }
1568
+ })
1569
+ );
1570
+ } catch (error) {
1571
+ console.error("Error cleaning up connection:", error);
1572
+ }
1573
+ }
1574
+ return {
1575
+ connectHandler,
1576
+ disconnectHandler,
1577
+ messageHandler,
1578
+ streamHandler
1579
+ };
1580
+ }
1581
+
1582
+ // ../server/src/filter-evaluator.ts
1583
+ function evaluateFilter(filter, record) {
1584
+ switch (filter.type) {
1585
+ case "comparison":
1586
+ return evaluateComparison(filter, record);
1587
+ case "logical":
1588
+ return evaluateLogical(filter, record);
1589
+ case "function":
1590
+ return evaluateFunction(filter, record);
1591
+ default:
1592
+ console.warn(`Unknown filter type: ${filter.type}`);
1593
+ return false;
1594
+ }
1595
+ }
1596
+ function evaluateFilters(filters, record) {
1597
+ if (filters.length === 0) return true;
1598
+ return filters.every((filter) => evaluateFilter(filter, record));
1599
+ }
1600
+ function evaluateComparison(filter, record) {
1601
+ const { operator, field, value, value2 } = filter;
1602
+ if (!field || !operator) return false;
1603
+ const fieldValue = getFieldValue(record, field);
1604
+ switch (operator) {
1605
+ case "eq":
1606
+ return fieldValue === value;
1607
+ case "ne":
1608
+ return fieldValue !== value;
1609
+ case "gt":
1610
+ return compareValues(fieldValue, value) > 0;
1611
+ case "gte":
1612
+ return compareValues(fieldValue, value) >= 0;
1613
+ case "lt":
1614
+ return compareValues(fieldValue, value) < 0;
1615
+ case "lte":
1616
+ return compareValues(fieldValue, value) <= 0;
1617
+ case "between":
1618
+ return compareValues(fieldValue, value) >= 0 && compareValues(fieldValue, value2) <= 0;
1619
+ default:
1620
+ console.warn(`Unknown comparison operator: ${operator}`);
1621
+ return false;
1622
+ }
1623
+ }
1624
+ function evaluateLogical(filter, record) {
1625
+ const { operator, conditions } = filter;
1626
+ if (!operator || !conditions) return false;
1627
+ switch (operator) {
1628
+ case "and":
1629
+ return conditions.every((c) => evaluateFilter(c, record));
1630
+ case "or":
1631
+ return conditions.some((c) => evaluateFilter(c, record));
1632
+ case "not":
1633
+ return conditions.length > 0 && !evaluateFilter(conditions[0], record);
1634
+ default:
1635
+ console.warn(`Unknown logical operator: ${operator}`);
1636
+ return false;
1637
+ }
1638
+ }
1639
+ function evaluateFunction(filter, record) {
1640
+ const { operator, field, value } = filter;
1641
+ if (!field || !operator) return false;
1642
+ const fieldValue = getFieldValue(record, field);
1643
+ switch (operator) {
1644
+ case "beginsWith":
1645
+ return typeof fieldValue === "string" && typeof value === "string" && fieldValue.startsWith(value);
1646
+ case "contains":
1647
+ return typeof fieldValue === "string" && typeof value === "string" && fieldValue.includes(value);
1648
+ default:
1649
+ console.warn(`Unknown function operator: ${operator}`);
1650
+ return false;
1651
+ }
1652
+ }
1653
+ function getFieldValue(record, field) {
1654
+ const parts = field.split(".");
1655
+ let value = record;
1656
+ for (const part of parts) {
1657
+ if (value === null || value === void 0) return void 0;
1658
+ if (typeof value !== "object") return void 0;
1659
+ value = value[part];
1660
+ }
1661
+ return value;
1662
+ }
1663
+ function compareValues(a, b) {
1664
+ if (a === null || a === void 0) {
1665
+ return b === null || b === void 0 ? 0 : -1;
1666
+ }
1667
+ if (b === null || b === void 0) {
1668
+ return 1;
1669
+ }
1670
+ if (typeof a === "number" && typeof b === "number") {
1671
+ return a - b;
1672
+ }
1673
+ if (typeof a === "string" && typeof b === "string") {
1674
+ return a.localeCompare(b);
1675
+ }
1676
+ if (typeof a === "boolean" && typeof b === "boolean") {
1677
+ return a === b ? 0 : a ? 1 : -1;
1678
+ }
1679
+ return String(a).localeCompare(String(b));
1680
+ }
1681
+ function sortRecords(records, sortField, sortOrder = "asc") {
1682
+ if (!sortField) return records;
1683
+ return [...records].sort((a, b) => {
1684
+ const aValue = getFieldValue(a, sortField);
1685
+ const bValue = getFieldValue(b, sortField);
1686
+ const comparison = compareValues(aValue, bValue);
1687
+ return sortOrder === "desc" ? -comparison : comparison;
1688
+ });
1689
+ }
1690
+ function getRecordKey(record, pkField, skField) {
1691
+ const pk = record[pkField];
1692
+ const sk = skField ? record[skField] : void 0;
1693
+ return sk !== void 0 ? `${pk}#${sk}` : String(pk);
1694
+ }
1695
+
1696
+ 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 };
1697
+ //# sourceMappingURL=server.js.map
18
1698
  //# sourceMappingURL=server.js.map