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/chunk-HZ6JHAJJ.js +131 -0
- package/dist/chunk-HZ6JHAJJ.js.map +1 -0
- package/dist/chunk-KRZQWA2W.js +657 -0
- package/dist/chunk-KRZQWA2W.js.map +1 -0
- package/dist/client.d.ts +55 -2
- package/dist/client.js +2 -17
- package/dist/client.js.map +1 -1
- package/dist/core.d.ts +4 -2
- package/dist/core.js +2 -17
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +171 -2
- package/dist/index.js +2 -17
- package/dist/index.js.map +1 -1
- package/dist/infra.d.ts +189 -2
- package/dist/infra.js +358 -15
- package/dist/infra.js.map +1 -1
- package/dist/react-BMZQ8Mth.d.ts +371 -0
- package/dist/react.d.ts +3 -2
- package/dist/react.js +2 -17
- package/dist/react.js.map +1 -1
- package/dist/server.d.ts +631 -2
- package/dist/server.js +1687 -16
- package/dist/server.js.map +1 -1
- package/dist/table-CSJysZPQ.d.ts +85 -0
- package/dist/types-Ci7IieDA.d.ts +141 -0
- package/package.json +10 -6
- package/dist/client.d.ts.map +0 -1
- package/dist/core.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/infra.d.ts.map +0 -1
- package/dist/react.d.ts.map +0 -1
- package/dist/server.d.ts.map +0 -1
package/dist/server.js
CHANGED
|
@@ -1,18 +1,1689 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|