dynamodb-reactive 0.1.22 → 0.2.2

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.
Files changed (46) hide show
  1. package/README.md +437 -153
  2. package/dist/client-r_tnzwqi.d.ts +224 -0
  3. package/dist/client.cjs +1023 -0
  4. package/dist/client.cjs.map +1 -0
  5. package/dist/client.d.cts +6 -0
  6. package/dist/client.d.ts +4 -70
  7. package/dist/client.js +1013 -1
  8. package/dist/client.js.map +1 -1
  9. package/dist/core.cjs +232 -0
  10. package/dist/core.cjs.map +1 -0
  11. package/dist/core.d.cts +4 -0
  12. package/dist/core.d.ts +3 -3
  13. package/dist/core.js +222 -1
  14. package/dist/core.js.map +1 -1
  15. package/dist/index.cjs +232 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +157 -0
  18. package/dist/index.d.ts +103 -56
  19. package/dist/index.js +222 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/infra.cjs +634 -0
  22. package/dist/infra.cjs.map +1 -0
  23. package/dist/infra.d.cts +126 -0
  24. package/dist/infra.d.ts +57 -102
  25. package/dist/infra.js +385 -147
  26. package/dist/infra.js.map +1 -1
  27. package/dist/react.cjs +1250 -0
  28. package/dist/react.cjs.map +1 -0
  29. package/dist/react.d.cts +68 -0
  30. package/dist/react.d.ts +68 -3
  31. package/dist/react.js +1237 -1
  32. package/dist/react.js.map +1 -1
  33. package/dist/server.cjs +2382 -0
  34. package/dist/server.cjs.map +1 -0
  35. package/dist/server.d.cts +364 -0
  36. package/dist/server.d.ts +58 -313
  37. package/dist/server.js +1882 -1204
  38. package/dist/server.js.map +1 -1
  39. package/dist/{table-tN3D4Tm-.d.ts → table-BijJ73pX.d.ts} +2 -34
  40. package/dist/{types-ISMJfyIv.d.ts → types-LMLk8JVC.d.ts} +56 -30
  41. package/package.json +79 -41
  42. package/dist/chunk-2KC44Q5I.js +0 -1103
  43. package/dist/chunk-2KC44Q5I.js.map +0 -1
  44. package/dist/chunk-ZAJA4KBE.js +0 -150
  45. package/dist/chunk-ZAJA4KBE.js.map +0 -1
  46. package/dist/react-D_NLHuuO.d.ts +0 -556
package/dist/infra.js CHANGED
@@ -1,15 +1,17 @@
1
- import { SystemTableNames } from './chunk-ZAJA4KBE.js';
2
1
  import * as fs from 'fs';
3
2
  import * as path from 'path';
4
3
  import * as cdk2 from 'aws-cdk-lib';
5
4
  import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2';
6
5
  import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations';
6
+ import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
7
7
  import * as iam from 'aws-cdk-lib/aws-iam';
8
8
  import * as lambda from 'aws-cdk-lib/aws-lambda';
9
9
  import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
10
10
  import * as logs from 'aws-cdk-lib/aws-logs';
11
11
  import { Construct } from 'constructs';
12
- import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
12
+ import { z } from 'zod';
13
+
14
+ // ../infra/src/engine.ts
13
15
 
14
16
  // ../infra/src/handlers/lambda-handlers.ts
15
17
  function generateEntryPointCode() {
@@ -25,6 +27,156 @@ export const messageHandler = handlers.messageHandler;
25
27
  export const streamHandler = handlers.streamHandler;
26
28
  `;
27
29
  }
30
+ var FilterConditionSchema = z.lazy(() => {
31
+ const comparison = z.object({
32
+ type: z.literal("comparison"),
33
+ operator: z.enum(["eq", "ne", "gt", "gte", "lt", "lte"]),
34
+ field: z.string().min(1),
35
+ value: z.unknown()
36
+ }).strict();
37
+ const queryFunction = z.union([
38
+ z.object({
39
+ type: z.literal("function"),
40
+ operator: z.literal("between"),
41
+ field: z.string().min(1),
42
+ value: z.unknown(),
43
+ value2: z.unknown()
44
+ }).strict(),
45
+ z.object({
46
+ type: z.literal("function"),
47
+ operator: z.enum(["beginsWith", "contains"]),
48
+ field: z.string().min(1),
49
+ value: z.unknown()
50
+ }).strict()
51
+ ]);
52
+ const logical = z.object({
53
+ type: z.literal("logical"),
54
+ operator: z.enum(["and", "or", "not"]),
55
+ conditions: z.array(FilterConditionSchema).min(1)
56
+ }).strict().superRefine((condition, context) => {
57
+ if (condition.operator === "not" && condition.conditions.length !== 1) {
58
+ context.addIssue({
59
+ code: "custom",
60
+ path: ["conditions"],
61
+ message: "not requires exactly one condition"
62
+ });
63
+ }
64
+ });
65
+ return z.union([comparison, queryFunction, logical]);
66
+ });
67
+ var QueryMetadataSchema = z.object({
68
+ planVersion: z.literal(1),
69
+ tableName: z.string().min(1),
70
+ indexName: z.string().min(1).optional(),
71
+ indexPkField: z.string().min(1).optional(),
72
+ indexSkField: z.string().min(1).optional(),
73
+ filterConditions: z.array(FilterConditionSchema),
74
+ sortField: z.string().min(1).optional(),
75
+ sortOrder: z.enum(["asc", "desc"]).optional(),
76
+ limit: z.number().int().positive().optional()
77
+ }).strict().superRefine((metadata, context) => {
78
+ if (metadata.indexName && !metadata.indexPkField) {
79
+ context.addIssue({
80
+ code: "custom",
81
+ path: ["indexPkField"],
82
+ message: "Index metadata requires its partition key field"
83
+ });
84
+ }
85
+ if (!metadata.indexName && (metadata.indexPkField || metadata.indexSkField)) {
86
+ context.addIssue({
87
+ code: "custom",
88
+ path: ["indexName"],
89
+ message: "Index key fields require an index name"
90
+ });
91
+ }
92
+ if (Boolean(metadata.sortField) !== Boolean(metadata.sortOrder)) {
93
+ context.addIssue({
94
+ code: "custom",
95
+ path: ["sortOrder"],
96
+ message: "Sort field and order must be specified together"
97
+ });
98
+ }
99
+ });
100
+ var DeliveryVersionSchema = z.object({
101
+ generation: z.number().int().nonnegative(),
102
+ fromVersion: z.number().int().nonnegative(),
103
+ toVersion: z.number().int().positive(),
104
+ messageId: z.string().min(1)
105
+ });
106
+ var PendingDeliverySchema = z.discriminatedUnion("type", [
107
+ DeliveryVersionSchema.extend({
108
+ type: z.literal("patch"),
109
+ patches: z.array(
110
+ z.object({
111
+ op: z.enum(["add", "remove", "replace", "move", "copy", "test"]),
112
+ path: z.string(),
113
+ value: z.unknown().optional(),
114
+ from: z.string().optional()
115
+ })
116
+ )
117
+ }),
118
+ DeliveryVersionSchema.extend({
119
+ type: z.literal("resync")
120
+ }).strict()
121
+ ]);
122
+ ({
123
+ /**
124
+ * ReactiveConnectionsV2 - Tracks active WebSocket connections
125
+ */
126
+ connections: z.object({
127
+ connectionId: z.string(),
128
+ status: z.enum(["open", "closed"]),
129
+ context: z.record(z.string(), z.unknown()).optional(),
130
+ authorizerContext: z.record(z.string(), z.unknown()).optional(),
131
+ requestIdentity: z.record(z.string(), z.unknown()).optional(),
132
+ connectedAt: z.number(),
133
+ ttl: z.number()
134
+ }),
135
+ /**
136
+ * ReactiveDependenciesV2 - The inverted index for O(1) lookups
137
+ */
138
+ dependencies: z.object({
139
+ pk: z.string(),
140
+ // Versioned encoded dependency key
141
+ sk: z.string(),
142
+ // ConnectionID#SubscriptionID
143
+ connectionId: z.string(),
144
+ subscriptionId: z.string(),
145
+ tableName: z.string(),
146
+ fieldName: z.string(),
147
+ fieldValue: z.string(),
148
+ generation: z.number().int().nonnegative(),
149
+ registrationId: z.string().min(1),
150
+ ttl: z.number()
151
+ }),
152
+ /**
153
+ * ReactiveConnectionQueriesV2 - Stores subscription state for diffing
154
+ */
155
+ queries: z.object({
156
+ pk: z.string(),
157
+ // ConnectionID
158
+ sk: z.string(),
159
+ // SubscriptionID
160
+ connectionId: z.string(),
161
+ subscriptionId: z.string(),
162
+ status: z.enum(["registering", "active", "cancelled", "faulted"]),
163
+ generation: z.number().int().nonnegative(),
164
+ registrationId: z.string().min(1),
165
+ queryMetadata: QueryMetadataSchema,
166
+ lastResult: z.array(z.unknown()),
167
+ dependencies: z.array(z.string()),
168
+ version: z.number().int().nonnegative(),
169
+ pendingDelivery: PendingDeliverySchema.optional(),
170
+ createdAt: z.number(),
171
+ updatedAt: z.number(),
172
+ ttl: z.number()
173
+ })
174
+ });
175
+ var SystemTableNames = {
176
+ connections: "ReactiveConnectionsV2",
177
+ dependencies: "ReactiveDependenciesV2",
178
+ queries: "ReactiveConnectionQueriesV2"
179
+ };
28
180
  var ReactiveSystemTables = class extends Construct {
29
181
  /**
30
182
  * ReactiveConnections table - tracks WebSocket connections
@@ -38,10 +190,16 @@ var ReactiveSystemTables = class extends Construct {
38
190
  * ReactiveConnectionQueries table - stores subscription state
39
191
  */
40
192
  queriesTable;
41
- constructor(scope, id, props = {}) {
193
+ constructor(scope, id, props) {
42
194
  super(scope, id);
43
- const prefix = props.tablePrefix ? `${props.tablePrefix}-` : "";
195
+ if (cdk2.Token.isUnresolved(props.tablePrefix) || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(props.tablePrefix) || props.tablePrefix.length > 229) {
196
+ throw new Error(
197
+ "ReactiveSystemTables tablePrefix must be a nonempty concrete value of at most 229 letters, numbers, hyphens, or underscores, starting with a letter or number."
198
+ );
199
+ }
200
+ const tableName = (name) => `${props.tablePrefix}-${name}`;
44
201
  const provisioningMode = props.provisioningMode ?? "ON_DEMAND";
202
+ const removalPolicy = props.removalPolicy ?? cdk2.RemovalPolicy.RETAIN;
45
203
  const isOnDemand = provisioningMode === "ON_DEMAND";
46
204
  const capacityProps = isOnDemand ? { billingMode: dynamodb.BillingMode.PAY_PER_REQUEST } : {
47
205
  billingMode: dynamodb.BillingMode.PROVISIONED,
@@ -49,17 +207,17 @@ var ReactiveSystemTables = class extends Construct {
49
207
  writeCapacity: provisioningMode.readWrite
50
208
  };
51
209
  this.connectionsTable = new dynamodb.Table(this, "ConnectionsTable", {
52
- tableName: `${prefix}${SystemTableNames.connections}`,
210
+ tableName: tableName(SystemTableNames.connections),
53
211
  partitionKey: {
54
212
  name: "connectionId",
55
213
  type: dynamodb.AttributeType.STRING
56
214
  },
57
215
  ...capacityProps,
58
- removalPolicy: cdk2.RemovalPolicy.DESTROY,
216
+ removalPolicy,
59
217
  timeToLiveAttribute: "ttl"
60
218
  });
61
219
  this.dependenciesTable = new dynamodb.Table(this, "DependenciesTable", {
62
- tableName: `${prefix}${SystemTableNames.dependencies}`,
220
+ tableName: tableName(SystemTableNames.dependencies),
63
221
  partitionKey: {
64
222
  name: "pk",
65
223
  // Format: "TableName#FieldName#FieldValue"
@@ -71,7 +229,7 @@ var ReactiveSystemTables = class extends Construct {
71
229
  type: dynamodb.AttributeType.STRING
72
230
  },
73
231
  ...capacityProps,
74
- removalPolicy: cdk2.RemovalPolicy.DESTROY,
232
+ removalPolicy,
75
233
  timeToLiveAttribute: "ttl"
76
234
  });
77
235
  const gsiProps = isOnDemand ? {} : {
@@ -88,7 +246,7 @@ var ReactiveSystemTables = class extends Construct {
88
246
  ...gsiProps
89
247
  });
90
248
  this.queriesTable = new dynamodb.Table(this, "QueriesTable", {
91
- tableName: `${prefix}${SystemTableNames.queries}`,
249
+ tableName: tableName(SystemTableNames.queries),
92
250
  partitionKey: {
93
251
  name: "pk",
94
252
  // ConnectionID
@@ -100,14 +258,22 @@ var ReactiveSystemTables = class extends Construct {
100
258
  type: dynamodb.AttributeType.STRING
101
259
  },
102
260
  ...capacityProps,
103
- removalPolicy: cdk2.RemovalPolicy.DESTROY,
261
+ removalPolicy,
104
262
  timeToLiveAttribute: "ttl"
105
263
  });
106
264
  }
107
265
  };
108
266
 
109
267
  // ../infra/src/engine.ts
268
+ var PARTIQL_ACTIONS = [
269
+ "dynamodb:PartiQLInsert",
270
+ "dynamodb:PartiQLUpdate",
271
+ "dynamodb:PartiQLDelete"
272
+ ];
273
+ var DEFAULT_STREAM_BATCH_SIZE = 100;
274
+ var MAX_STREAM_BATCH_SIZE = 1e3;
110
275
  var ReactiveEngine = class extends Construct {
276
+ tables;
111
277
  systemTables;
112
278
  webSocketApi;
113
279
  webSocketStage;
@@ -119,16 +285,45 @@ var ReactiveEngine = class extends Construct {
119
285
  callbackUrl;
120
286
  constructor(scope, id, props) {
121
287
  super(scope, id);
122
- const prefix = props.resourcePrefix ?? "";
288
+ if (cdk2.Token.isUnresolved(props.resourcePrefix) || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(props.resourcePrefix) || props.resourcePrefix.length > 45) {
289
+ throw new Error(
290
+ "ReactiveEngine resourcePrefix must be a nonempty concrete value of at most 45 letters, numbers, hyphens, or underscores, starting with a letter or number."
291
+ );
292
+ }
293
+ if (props.connectAuthorizer && props.allowUnauthenticatedConnections) {
294
+ throw new Error(
295
+ "Specify either connectAuthorizer or allowUnauthenticatedConnections, not both."
296
+ );
297
+ }
298
+ if (props.connectAuthorizer instanceof apigatewayv2.WebSocketNoneAuthorizer) {
299
+ throw new Error(
300
+ "WebSocketNoneAuthorizer does not authenticate connections. Set allowUnauthenticatedConnections to true instead."
301
+ );
302
+ }
303
+ if (!props.connectAuthorizer && !props.allowUnauthenticatedConnections) {
304
+ throw new Error(
305
+ "WebSocket authentication is required. Provide connectAuthorizer or explicitly set allowUnauthenticatedConnections to true."
306
+ );
307
+ }
308
+ if (props.streamBatchSize !== void 0 && (!Number.isInteger(props.streamBatchSize) || props.streamBatchSize < 1 || props.streamBatchSize > MAX_STREAM_BATCH_SIZE)) {
309
+ throw new Error(
310
+ `ReactiveEngine streamBatchSize must be an integer from 1 to ${MAX_STREAM_BATCH_SIZE}.`
311
+ );
312
+ }
313
+ for (const binding of props.tables) {
314
+ this.validateTableName(binding);
315
+ this.validateStream(binding);
316
+ }
317
+ this.tables = [...props.tables];
318
+ const resourceName = (name) => `${props.resourcePrefix}-${name}`;
123
319
  const memorySize = 128;
124
320
  const timeout = cdk2.Duration.seconds(30);
125
- const logRetention = logs.RetentionDays.ONE_DAY;
126
321
  this.systemTables = new ReactiveSystemTables(this, "SystemTables", {
127
- tablePrefix: prefix,
128
- ...props.systemTablesProps
322
+ ...props.systemTablesProps,
323
+ tablePrefix: props.resourcePrefix
129
324
  });
130
325
  this.webSocketApi = new apigatewayv2.WebSocketApi(this, "WebSocketApi", {
131
- apiName: `${prefix}ReactiveWebSocket`
326
+ apiName: resourceName("ReactiveWebSocket")
132
327
  });
133
328
  this.webSocketStage = new apigatewayv2.WebSocketStage(
134
329
  this,
@@ -141,88 +336,145 @@ var ReactiveEngine = class extends Construct {
141
336
  );
142
337
  this.webSocketUrl = this.webSocketStage.url;
143
338
  this.callbackUrl = this.webSocketStage.callbackUrl;
144
- const entryPointCode = generateEntryPointCode();
145
339
  const cdkOutDir = path.join(process.cwd(), "cdk.out", ".generated");
146
340
  fs.mkdirSync(cdkOutDir, { recursive: true });
147
- const entryPointPath = path.join(
148
- cdkOutDir,
149
- `reactive-entry-${id}-${Date.now()}.ts`
150
- );
151
- fs.writeFileSync(entryPointPath, entryPointCode);
341
+ const entryPointPath = path.join(cdkOutDir, "reactive-entry.ts");
342
+ fs.writeFileSync(entryPointPath, generateEntryPointCode());
152
343
  const environment = {
344
+ ...props.environment,
153
345
  CONNECTIONS_TABLE: this.systemTables.connectionsTable.tableName,
154
346
  DEPENDENCIES_TABLE: this.systemTables.dependenciesTable.tableName,
155
347
  QUERIES_TABLE: this.systemTables.queriesTable.tableName,
156
348
  WEBSOCKET_ENDPOINT: this.callbackUrl,
157
- ...props.environment,
158
349
  LOG_LEVEL: props.environment?.LOG_LEVEL ?? "INFO"
159
350
  };
351
+ const bundling = {
352
+ format: nodejs.OutputFormat.CJS,
353
+ minify: true,
354
+ sourceMap: false,
355
+ target: "node24"
356
+ };
160
357
  this.connectHandler = new nodejs.NodejsFunction(this, "ConnectHandler", {
161
- functionName: `${prefix}ReactiveConnect`,
162
- runtime: lambda.Runtime.NODEJS_LATEST,
358
+ functionName: resourceName("ReactiveConnect"),
359
+ runtime: lambda.Runtime.NODEJS_24_X,
163
360
  handler: "connectHandler",
164
361
  entry: entryPointPath,
362
+ bundling,
165
363
  environment,
166
364
  memorySize,
167
365
  timeout,
168
- logRetention
366
+ logGroup: this.createLogGroup("ConnectLogGroup")
169
367
  });
170
368
  this.disconnectHandler = new nodejs.NodejsFunction(
171
369
  this,
172
370
  "DisconnectHandler",
173
371
  {
174
- functionName: `${prefix}ReactiveDisconnect`,
175
- runtime: lambda.Runtime.NODEJS_LATEST,
372
+ functionName: resourceName("ReactiveDisconnect"),
373
+ runtime: lambda.Runtime.NODEJS_24_X,
176
374
  handler: "disconnectHandler",
177
375
  entry: entryPointPath,
376
+ bundling,
178
377
  environment,
179
378
  memorySize,
180
379
  timeout,
181
- logRetention
380
+ logGroup: this.createLogGroup("DisconnectLogGroup")
182
381
  }
183
382
  );
184
383
  this.messageHandler = new nodejs.NodejsFunction(this, "MessageHandler", {
185
- functionName: `${prefix}ReactiveMessage`,
186
- runtime: lambda.Runtime.NODEJS_LATEST,
384
+ functionName: resourceName("ReactiveMessage"),
385
+ runtime: lambda.Runtime.NODEJS_24_X,
187
386
  handler: "messageHandler",
188
387
  entry: entryPointPath,
388
+ bundling,
189
389
  environment,
190
390
  memorySize,
191
391
  timeout,
192
- logRetention
392
+ logGroup: this.createLogGroup("MessageLogGroup")
193
393
  });
194
394
  this.streamHandler = new nodejs.NodejsFunction(this, "StreamHandler", {
195
- functionName: `${prefix}ReactiveStream`,
196
- runtime: lambda.Runtime.NODEJS_LATEST,
395
+ functionName: resourceName("ReactiveStream"),
396
+ runtime: lambda.Runtime.NODEJS_24_X,
197
397
  handler: "streamHandler",
198
398
  entry: entryPointPath,
399
+ bundling,
199
400
  environment: {
200
401
  ...environment,
201
- USER_TABLES: props.tables.map((t) => t.tableName).join(",")
402
+ USER_TABLES: props.tables.map(({ definition }) => definition.tableName).join(",")
202
403
  },
203
- memorySize,
204
- timeout,
205
- logRetention
404
+ memorySize: props.streamHandlerProps?.memorySize ?? memorySize,
405
+ timeout: props.streamHandlerProps?.timeout ?? timeout,
406
+ logGroup: this.createLogGroup("StreamLogGroup")
407
+ });
408
+ iam.Grant.addToPrincipal({
409
+ grantee: this.connectHandler,
410
+ actions: ["dynamodb:PutItem"],
411
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
412
+ });
413
+ iam.Grant.addToPrincipal({
414
+ grantee: this.disconnectHandler,
415
+ actions: ["dynamodb:UpdateItem"],
416
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
417
+ });
418
+ iam.Grant.addToPrincipal({
419
+ grantee: this.disconnectHandler,
420
+ actions: ["dynamodb:Query", "dynamodb:UpdateItem"],
421
+ resourceArns: [this.systemTables.queriesTable.tableArn]
422
+ });
423
+ iam.Grant.addToPrincipal({
424
+ grantee: this.disconnectHandler,
425
+ actions: ["dynamodb:DeleteItem"],
426
+ resourceArns: [this.systemTables.dependenciesTable.tableArn]
427
+ });
428
+ iam.Grant.addToPrincipal({
429
+ grantee: this.messageHandler,
430
+ actions: ["dynamodb:UpdateItem"],
431
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
432
+ });
433
+ iam.Grant.addToPrincipal({
434
+ grantee: this.streamHandler,
435
+ actions: ["dynamodb:UpdateItem"],
436
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
437
+ });
438
+ iam.Grant.addToPrincipal({
439
+ grantee: this.streamHandler,
440
+ actions: ["dynamodb:Query", "dynamodb:DeleteItem"],
441
+ resourceArns: [this.systemTables.dependenciesTable.tableArn]
442
+ });
443
+ iam.Grant.addToPrincipal({
444
+ grantee: this.streamHandler,
445
+ actions: [
446
+ "dynamodb:GetItem",
447
+ "dynamodb:Query",
448
+ "dynamodb:UpdateItem"
449
+ ],
450
+ resourceArns: [this.systemTables.queriesTable.tableArn]
206
451
  });
207
- this.systemTables.connectionsTable.grantReadWriteData(this.connectHandler);
208
- this.systemTables.connectionsTable.grantReadWriteData(
209
- this.disconnectHandler
210
- );
211
- this.systemTables.connectionsTable.grantReadWriteData(this.messageHandler);
212
- this.systemTables.connectionsTable.grantReadWriteData(this.streamHandler);
213
- this.systemTables.dependenciesTable.grantReadWriteData(this.messageHandler);
214
- this.systemTables.dependenciesTable.grantReadWriteData(this.streamHandler);
215
- this.systemTables.queriesTable.grantReadWriteData(this.messageHandler);
216
- this.systemTables.queriesTable.grantReadWriteData(this.streamHandler);
217
452
  const managementPolicy = new iam.PolicyStatement({
218
453
  actions: ["execute-api:ManageConnections"],
219
454
  resources: [
220
- `arn:aws:execute-api:${cdk2.Stack.of(this).region}:${cdk2.Stack.of(this).account}:${this.webSocketApi.apiId}/${this.webSocketStage.stageName}/POST/@connections/*`
455
+ cdk2.Stack.of(this).formatArn({
456
+ service: "execute-api",
457
+ resource: this.webSocketApi.apiId,
458
+ resourceName: `${this.webSocketStage.stageName}/POST/@connections/*`,
459
+ arnFormat: cdk2.ArnFormat.SLASH_RESOURCE_NAME
460
+ })
221
461
  ]
222
462
  });
223
463
  this.messageHandler.addToRolePolicy(managementPolicy);
224
464
  this.streamHandler.addToRolePolicy(managementPolicy);
465
+ const connectAuthorizer = props.connectAuthorizer ? {
466
+ bind: (options) => {
467
+ const config = props.connectAuthorizer.bind(options);
468
+ if (config.authorizationType === "NONE") {
469
+ throw new Error(
470
+ "The connectAuthorizer bind result uses NONE authorization. Set allowUnauthenticatedConnections to true instead."
471
+ );
472
+ }
473
+ return config;
474
+ }
475
+ } : void 0;
225
476
  this.webSocketApi.addRoute("$connect", {
477
+ authorizer: connectAuthorizer,
226
478
  integration: new apigatewayv2Integrations.WebSocketLambdaIntegration(
227
479
  "ConnectIntegration",
228
480
  this.connectHandler
@@ -240,126 +492,112 @@ var ReactiveEngine = class extends Construct {
240
492
  this.messageHandler
241
493
  )
242
494
  });
243
- for (const table of props.tables) {
244
- this.setupStreamProcessing(table);
495
+ for (const [index, binding] of props.tables.entries()) {
496
+ this.setupStreamProcessing(
497
+ binding,
498
+ index,
499
+ props.streamBatchSize ?? DEFAULT_STREAM_BATCH_SIZE
500
+ );
245
501
  }
246
502
  new cdk2.CfnOutput(this, "WebSocketUrl", {
247
503
  value: this.webSocketUrl,
248
504
  description: "WebSocket URL for reactive connections"
249
505
  });
250
506
  }
251
- setupStreamProcessing(table) {
252
- const tableArn = `arn:aws:dynamodb:${cdk2.Stack.of(this).region}:${cdk2.Stack.of(this).account}:table/${table.tableName}`;
253
- const streamArn = `${tableArn}/stream/*`;
254
- this.streamHandler.addToRolePolicy(
255
- new iam.PolicyStatement({
256
- actions: [
257
- "dynamodb:GetRecords",
258
- "dynamodb:GetShardIterator",
259
- "dynamodb:DescribeStream",
260
- "dynamodb:ListStreams"
261
- ],
262
- resources: [streamArn]
263
- })
264
- );
265
- const tableReadPolicy = new iam.PolicyStatement({
266
- actions: [
267
- "dynamodb:Query",
268
- "dynamodb:Scan",
269
- "dynamodb:GetItem",
270
- "dynamodb:BatchGetItem",
271
- "dynamodb:PartiQLSelect"
272
- ],
273
- resources: [tableArn, `${tableArn}/index/*`]
507
+ /** Grant an external application handler access to reactive and user data. */
508
+ grantApplicationAccess(grantee) {
509
+ this.systemTables.connectionsTable.grantReadWriteData(grantee);
510
+ this.systemTables.dependenciesTable.grantReadWriteData(grantee);
511
+ this.systemTables.queriesTable.grantReadWriteData(grantee);
512
+ iam.Grant.addToPrincipal({
513
+ grantee,
514
+ actions: ["dynamodb:TransactWriteItems"],
515
+ resourceArns: [
516
+ this.systemTables.connectionsTable.tableArn,
517
+ this.systemTables.dependenciesTable.tableArn,
518
+ this.systemTables.queriesTable.tableArn
519
+ ]
274
520
  });
275
- this.streamHandler.addToRolePolicy(tableReadPolicy);
276
- this.messageHandler.addToRolePolicy(tableReadPolicy);
277
- this.messageHandler.addToRolePolicy(
278
- new iam.PolicyStatement({
279
- actions: [
280
- "dynamodb:PutItem",
281
- "dynamodb:UpdateItem",
282
- "dynamodb:DeleteItem",
283
- "dynamodb:BatchWriteItem",
284
- "dynamodb:PartiQLInsert",
285
- "dynamodb:PartiQLUpdate",
286
- "dynamodb:PartiQLDelete"
287
- ],
288
- resources: [tableArn, `${tableArn}/index/*`]
289
- })
290
- );
291
- }
292
- addTable(table) {
293
- if (!table.tableStreamArn) {
294
- throw new Error(`Table ${table.tableName} does not have streams enabled`);
521
+ for (const { table } of this.tables) {
522
+ table.grantReadWriteData(grantee);
523
+ iam.Grant.addToPrincipal({
524
+ grantee,
525
+ actions: ["dynamodb:PartiQLSelect"],
526
+ resourceArns: [table.tableArn, `${table.tableArn}/index/*`]
527
+ });
528
+ iam.Grant.addToPrincipal({
529
+ grantee,
530
+ actions: PARTIQL_ACTIONS,
531
+ resourceArns: [table.tableArn]
532
+ });
295
533
  }
534
+ }
535
+ setupStreamProcessing(binding, index, batchSize) {
536
+ const { table } = binding;
296
537
  table.grantStreamRead(this.streamHandler);
297
- table.grantReadData(this.streamHandler);
298
- new lambda.EventSourceMapping(this, `Stream-${table.tableName}`, {
538
+ iam.Grant.addToPrincipal({
539
+ grantee: this.streamHandler,
540
+ actions: ["dynamodb:PartiQLSelect"],
541
+ resourceArns: [table.tableArn, `${table.tableArn}/index/*`]
542
+ });
543
+ new lambda.EventSourceMapping(this, `UserTableStream${index}`, {
299
544
  target: this.streamHandler,
300
545
  eventSourceArn: table.tableStreamArn,
301
546
  startingPosition: lambda.StartingPosition.TRIM_HORIZON,
302
- batchSize: 100,
303
- maxBatchingWindow: cdk2.Duration.seconds(5)
547
+ batchSize,
548
+ maxBatchingWindow: cdk2.Duration.seconds(5),
549
+ bisectBatchOnError: true,
550
+ reportBatchItemFailures: true
304
551
  });
305
552
  }
306
- };
307
- var DynamoDBStreamSource = class extends Construct {
308
- eventSourceMapping;
309
- constructor(scope, id, props) {
310
- super(scope, id);
311
- const {
312
- table,
313
- target,
314
- batchSize = 100,
315
- maxBatchingWindow = cdk2.Duration.seconds(5),
316
- startingPosition = lambda.StartingPosition.TRIM_HORIZON,
317
- parallelizationFactor = 1,
318
- maxRecordAge = cdk2.Duration.days(1),
319
- retryAttempts = 3,
320
- filters
321
- } = props;
553
+ createLogGroup(id) {
554
+ return new logs.LogGroup(this, id, {
555
+ retention: logs.RetentionDays.ONE_DAY,
556
+ removalPolicy: cdk2.RemovalPolicy.DESTROY
557
+ });
558
+ }
559
+ validateStream(binding) {
560
+ const { definition, table } = binding;
322
561
  if (!table.tableStreamArn) {
323
562
  throw new Error(
324
- `Table ${table.tableName} does not have DynamoDB Streams enabled. Enable streams with streamSpecification when creating the table.`
563
+ `Table ${definition.tableName} does not have DynamoDB Streams enabled. NEW_AND_OLD_IMAGES is required.`
325
564
  );
326
565
  }
327
- table.grantStreamRead(target);
328
- this.eventSourceMapping = new lambda.EventSourceMapping(
329
- this,
330
- "EventSource",
331
- {
332
- target,
333
- eventSourceArn: table.tableStreamArn,
334
- startingPosition,
335
- batchSize,
336
- maxBatchingWindow,
337
- parallelizationFactor,
338
- maxRecordAge,
339
- retryAttempts,
340
- bisectBatchOnError: true,
341
- reportBatchItemFailures: true,
342
- filters
343
- }
566
+ const child = table.node.defaultChild;
567
+ if (!(child instanceof dynamodb.CfnTable) && !(child instanceof dynamodb.CfnGlobalTable)) {
568
+ return;
569
+ }
570
+ const specification = cdk2.Stack.of(this).resolve(
571
+ child.streamSpecification
344
572
  );
573
+ if (!specification || cdk2.Token.isUnresolved(specification) || typeof specification !== "object" || !("streamViewType" in specification)) {
574
+ return;
575
+ }
576
+ const streamViewType = specification.streamViewType;
577
+ if (!cdk2.Token.isUnresolved(streamViewType) && streamViewType !== dynamodb.StreamViewType.NEW_AND_OLD_IMAGES) {
578
+ throw new Error(
579
+ `Table ${definition.tableName} uses stream view ${streamViewType}; NEW_AND_OLD_IMAGES is required.`
580
+ );
581
+ }
345
582
  }
346
- };
347
- function createStreamFilter(options) {
348
- const filters = [];
349
- if (options.eventName) {
350
- filters.push({
351
- eventName: options.eventName
352
- });
353
- }
354
- if (options.patterns) {
355
- filters.push(...options.patterns);
356
- }
357
- if (filters.length === 0) {
358
- return [];
583
+ validateTableName(binding) {
584
+ const { definition, table } = binding;
585
+ const child = table.node.defaultChild;
586
+ const childTableName = child && typeof child === "object" && "tableName" in child ? child.tableName : void 0;
587
+ const physicalTableName = !cdk2.Token.isUnresolved(table.tableName) ? table.tableName : childTableName && !cdk2.Token.isUnresolved(childTableName) ? childTableName : table.tableName;
588
+ if (!physicalTableName || cdk2.Token.isUnresolved(physicalTableName)) {
589
+ throw new Error(
590
+ `Table ${definition.tableName} has an unresolved physical table name. Set an explicit tableName equal to definition.tableName; separately bundled application code cannot coordinate CloudFormation-generated names.`
591
+ );
592
+ }
593
+ if (physicalTableName !== definition.tableName) {
594
+ throw new Error(
595
+ `Table binding name mismatch: definition.tableName is "${definition.tableName}" but the physical table name is "${physicalTableName}". Set an explicit matching tableName.`
596
+ );
597
+ }
359
598
  }
360
- return [lambda.FilterCriteria.filter({ filters })];
361
- }
599
+ };
362
600
 
363
- export { DynamoDBStreamSource, ReactiveEngine, ReactiveSystemTables, createStreamFilter, generateEntryPointCode };
601
+ export { ReactiveEngine };
364
602
  //# sourceMappingURL=infra.js.map
365
603
  //# sourceMappingURL=infra.js.map