dynamodb-reactive 0.1.21 → 0.2.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.
Files changed (46) hide show
  1. package/README.md +378 -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 +623 -0
  22. package/dist/infra.cjs.map +1 -0
  23. package/dist/infra.d.cts +119 -0
  24. package/dist/infra.d.ts +50 -102
  25. package/dist/infra.js +371 -144
  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 +2351 -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 +1855 -1208
  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-2A7GJ2UO.js +0 -1088
  43. package/dist/chunk-2A7GJ2UO.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-B2b5RIz_.d.ts +0 -549
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,20 @@ 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
+ ];
110
273
  var ReactiveEngine = class extends Construct {
274
+ tables;
111
275
  systemTables;
112
276
  webSocketApi;
113
277
  webSocketStage;
@@ -119,16 +283,40 @@ var ReactiveEngine = class extends Construct {
119
283
  callbackUrl;
120
284
  constructor(scope, id, props) {
121
285
  super(scope, id);
122
- const prefix = props.resourcePrefix ?? "";
286
+ if (cdk2.Token.isUnresolved(props.resourcePrefix) || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(props.resourcePrefix) || props.resourcePrefix.length > 45) {
287
+ throw new Error(
288
+ "ReactiveEngine resourcePrefix must be a nonempty concrete value of at most 45 letters, numbers, hyphens, or underscores, starting with a letter or number."
289
+ );
290
+ }
291
+ if (props.connectAuthorizer && props.allowUnauthenticatedConnections) {
292
+ throw new Error(
293
+ "Specify either connectAuthorizer or allowUnauthenticatedConnections, not both."
294
+ );
295
+ }
296
+ if (props.connectAuthorizer instanceof apigatewayv2.WebSocketNoneAuthorizer) {
297
+ throw new Error(
298
+ "WebSocketNoneAuthorizer does not authenticate connections. Set allowUnauthenticatedConnections to true instead."
299
+ );
300
+ }
301
+ if (!props.connectAuthorizer && !props.allowUnauthenticatedConnections) {
302
+ throw new Error(
303
+ "WebSocket authentication is required. Provide connectAuthorizer or explicitly set allowUnauthenticatedConnections to true."
304
+ );
305
+ }
306
+ for (const binding of props.tables) {
307
+ this.validateTableName(binding);
308
+ this.validateStream(binding);
309
+ }
310
+ this.tables = [...props.tables];
311
+ const resourceName = (name) => `${props.resourcePrefix}-${name}`;
123
312
  const memorySize = 128;
124
313
  const timeout = cdk2.Duration.seconds(30);
125
- const logRetention = logs.RetentionDays.ONE_DAY;
126
314
  this.systemTables = new ReactiveSystemTables(this, "SystemTables", {
127
- tablePrefix: prefix,
128
- ...props.systemTablesProps
315
+ ...props.systemTablesProps,
316
+ tablePrefix: props.resourcePrefix
129
317
  });
130
318
  this.webSocketApi = new apigatewayv2.WebSocketApi(this, "WebSocketApi", {
131
- apiName: `${prefix}ReactiveWebSocket`
319
+ apiName: resourceName("ReactiveWebSocket")
132
320
  });
133
321
  this.webSocketStage = new apigatewayv2.WebSocketStage(
134
322
  this,
@@ -141,88 +329,145 @@ var ReactiveEngine = class extends Construct {
141
329
  );
142
330
  this.webSocketUrl = this.webSocketStage.url;
143
331
  this.callbackUrl = this.webSocketStage.callbackUrl;
144
- const entryPointCode = generateEntryPointCode();
145
332
  const cdkOutDir = path.join(process.cwd(), "cdk.out", ".generated");
146
333
  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);
334
+ const entryPointPath = path.join(cdkOutDir, "reactive-entry.ts");
335
+ fs.writeFileSync(entryPointPath, generateEntryPointCode());
152
336
  const environment = {
337
+ ...props.environment,
153
338
  CONNECTIONS_TABLE: this.systemTables.connectionsTable.tableName,
154
339
  DEPENDENCIES_TABLE: this.systemTables.dependenciesTable.tableName,
155
340
  QUERIES_TABLE: this.systemTables.queriesTable.tableName,
156
341
  WEBSOCKET_ENDPOINT: this.callbackUrl,
157
- ...props.environment,
158
342
  LOG_LEVEL: props.environment?.LOG_LEVEL ?? "INFO"
159
343
  };
344
+ const bundling = {
345
+ format: nodejs.OutputFormat.CJS,
346
+ minify: true,
347
+ sourceMap: false,
348
+ target: "node24"
349
+ };
160
350
  this.connectHandler = new nodejs.NodejsFunction(this, "ConnectHandler", {
161
- functionName: `${prefix}ReactiveConnect`,
162
- runtime: lambda.Runtime.NODEJS_LATEST,
351
+ functionName: resourceName("ReactiveConnect"),
352
+ runtime: lambda.Runtime.NODEJS_24_X,
163
353
  handler: "connectHandler",
164
354
  entry: entryPointPath,
355
+ bundling,
165
356
  environment,
166
357
  memorySize,
167
358
  timeout,
168
- logRetention
359
+ logGroup: this.createLogGroup("ConnectLogGroup")
169
360
  });
170
361
  this.disconnectHandler = new nodejs.NodejsFunction(
171
362
  this,
172
363
  "DisconnectHandler",
173
364
  {
174
- functionName: `${prefix}ReactiveDisconnect`,
175
- runtime: lambda.Runtime.NODEJS_LATEST,
365
+ functionName: resourceName("ReactiveDisconnect"),
366
+ runtime: lambda.Runtime.NODEJS_24_X,
176
367
  handler: "disconnectHandler",
177
368
  entry: entryPointPath,
369
+ bundling,
178
370
  environment,
179
371
  memorySize,
180
372
  timeout,
181
- logRetention
373
+ logGroup: this.createLogGroup("DisconnectLogGroup")
182
374
  }
183
375
  );
184
376
  this.messageHandler = new nodejs.NodejsFunction(this, "MessageHandler", {
185
- functionName: `${prefix}ReactiveMessage`,
186
- runtime: lambda.Runtime.NODEJS_LATEST,
377
+ functionName: resourceName("ReactiveMessage"),
378
+ runtime: lambda.Runtime.NODEJS_24_X,
187
379
  handler: "messageHandler",
188
380
  entry: entryPointPath,
381
+ bundling,
189
382
  environment,
190
383
  memorySize,
191
384
  timeout,
192
- logRetention
385
+ logGroup: this.createLogGroup("MessageLogGroup")
193
386
  });
194
387
  this.streamHandler = new nodejs.NodejsFunction(this, "StreamHandler", {
195
- functionName: `${prefix}ReactiveStream`,
196
- runtime: lambda.Runtime.NODEJS_LATEST,
388
+ functionName: resourceName("ReactiveStream"),
389
+ runtime: lambda.Runtime.NODEJS_24_X,
197
390
  handler: "streamHandler",
198
391
  entry: entryPointPath,
392
+ bundling,
199
393
  environment: {
200
394
  ...environment,
201
- USER_TABLES: props.tables.map((t) => t.tableName).join(",")
395
+ USER_TABLES: props.tables.map(({ definition }) => definition.tableName).join(",")
202
396
  },
203
397
  memorySize,
204
398
  timeout,
205
- logRetention
399
+ logGroup: this.createLogGroup("StreamLogGroup")
400
+ });
401
+ iam.Grant.addToPrincipal({
402
+ grantee: this.connectHandler,
403
+ actions: ["dynamodb:PutItem"],
404
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
405
+ });
406
+ iam.Grant.addToPrincipal({
407
+ grantee: this.disconnectHandler,
408
+ actions: ["dynamodb:UpdateItem"],
409
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
410
+ });
411
+ iam.Grant.addToPrincipal({
412
+ grantee: this.disconnectHandler,
413
+ actions: ["dynamodb:Query", "dynamodb:UpdateItem"],
414
+ resourceArns: [this.systemTables.queriesTable.tableArn]
415
+ });
416
+ iam.Grant.addToPrincipal({
417
+ grantee: this.disconnectHandler,
418
+ actions: ["dynamodb:DeleteItem"],
419
+ resourceArns: [this.systemTables.dependenciesTable.tableArn]
420
+ });
421
+ iam.Grant.addToPrincipal({
422
+ grantee: this.messageHandler,
423
+ actions: ["dynamodb:UpdateItem"],
424
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
425
+ });
426
+ iam.Grant.addToPrincipal({
427
+ grantee: this.streamHandler,
428
+ actions: ["dynamodb:UpdateItem"],
429
+ resourceArns: [this.systemTables.connectionsTable.tableArn]
430
+ });
431
+ iam.Grant.addToPrincipal({
432
+ grantee: this.streamHandler,
433
+ actions: ["dynamodb:Query", "dynamodb:DeleteItem"],
434
+ resourceArns: [this.systemTables.dependenciesTable.tableArn]
435
+ });
436
+ iam.Grant.addToPrincipal({
437
+ grantee: this.streamHandler,
438
+ actions: [
439
+ "dynamodb:GetItem",
440
+ "dynamodb:Query",
441
+ "dynamodb:UpdateItem"
442
+ ],
443
+ resourceArns: [this.systemTables.queriesTable.tableArn]
206
444
  });
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
445
  const managementPolicy = new iam.PolicyStatement({
218
446
  actions: ["execute-api:ManageConnections"],
219
447
  resources: [
220
- `arn:aws:execute-api:${cdk2.Stack.of(this).region}:${cdk2.Stack.of(this).account}:${this.webSocketApi.apiId}/${this.webSocketStage.stageName}/POST/@connections/*`
448
+ cdk2.Stack.of(this).formatArn({
449
+ service: "execute-api",
450
+ resource: this.webSocketApi.apiId,
451
+ resourceName: `${this.webSocketStage.stageName}/POST/@connections/*`,
452
+ arnFormat: cdk2.ArnFormat.SLASH_RESOURCE_NAME
453
+ })
221
454
  ]
222
455
  });
223
456
  this.messageHandler.addToRolePolicy(managementPolicy);
224
457
  this.streamHandler.addToRolePolicy(managementPolicy);
458
+ const connectAuthorizer = props.connectAuthorizer ? {
459
+ bind: (options) => {
460
+ const config = props.connectAuthorizer.bind(options);
461
+ if (config.authorizationType === "NONE") {
462
+ throw new Error(
463
+ "The connectAuthorizer bind result uses NONE authorization. Set allowUnauthenticatedConnections to true instead."
464
+ );
465
+ }
466
+ return config;
467
+ }
468
+ } : void 0;
225
469
  this.webSocketApi.addRoute("$connect", {
470
+ authorizer: connectAuthorizer,
226
471
  integration: new apigatewayv2Integrations.WebSocketLambdaIntegration(
227
472
  "ConnectIntegration",
228
473
  this.connectHandler
@@ -240,126 +485,108 @@ var ReactiveEngine = class extends Construct {
240
485
  this.messageHandler
241
486
  )
242
487
  });
243
- for (const table of props.tables) {
244
- this.setupStreamProcessing(table);
488
+ for (const [index, binding] of props.tables.entries()) {
489
+ this.setupStreamProcessing(binding, index);
245
490
  }
246
491
  new cdk2.CfnOutput(this, "WebSocketUrl", {
247
492
  value: this.webSocketUrl,
248
493
  description: "WebSocket URL for reactive connections"
249
494
  });
250
495
  }
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/*`]
496
+ /** Grant an external application handler access to reactive and user data. */
497
+ grantApplicationAccess(grantee) {
498
+ this.systemTables.connectionsTable.grantReadWriteData(grantee);
499
+ this.systemTables.dependenciesTable.grantReadWriteData(grantee);
500
+ this.systemTables.queriesTable.grantReadWriteData(grantee);
501
+ iam.Grant.addToPrincipal({
502
+ grantee,
503
+ actions: ["dynamodb:TransactWriteItems"],
504
+ resourceArns: [
505
+ this.systemTables.connectionsTable.tableArn,
506
+ this.systemTables.dependenciesTable.tableArn,
507
+ this.systemTables.queriesTable.tableArn
508
+ ]
274
509
  });
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`);
510
+ for (const { table } of this.tables) {
511
+ table.grantReadWriteData(grantee);
512
+ iam.Grant.addToPrincipal({
513
+ grantee,
514
+ actions: ["dynamodb:PartiQLSelect"],
515
+ resourceArns: [table.tableArn, `${table.tableArn}/index/*`]
516
+ });
517
+ iam.Grant.addToPrincipal({
518
+ grantee,
519
+ actions: PARTIQL_ACTIONS,
520
+ resourceArns: [table.tableArn]
521
+ });
295
522
  }
523
+ }
524
+ setupStreamProcessing(binding, index) {
525
+ const { table } = binding;
296
526
  table.grantStreamRead(this.streamHandler);
297
- table.grantReadData(this.streamHandler);
298
- new lambda.EventSourceMapping(this, `Stream-${table.tableName}`, {
527
+ iam.Grant.addToPrincipal({
528
+ grantee: this.streamHandler,
529
+ actions: ["dynamodb:PartiQLSelect"],
530
+ resourceArns: [table.tableArn, `${table.tableArn}/index/*`]
531
+ });
532
+ new lambda.EventSourceMapping(this, `UserTableStream${index}`, {
299
533
  target: this.streamHandler,
300
534
  eventSourceArn: table.tableStreamArn,
301
535
  startingPosition: lambda.StartingPosition.TRIM_HORIZON,
302
536
  batchSize: 100,
303
- maxBatchingWindow: cdk2.Duration.seconds(5)
537
+ maxBatchingWindow: cdk2.Duration.seconds(5),
538
+ bisectBatchOnError: true,
539
+ reportBatchItemFailures: true
304
540
  });
305
541
  }
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;
542
+ createLogGroup(id) {
543
+ return new logs.LogGroup(this, id, {
544
+ retention: logs.RetentionDays.ONE_DAY,
545
+ removalPolicy: cdk2.RemovalPolicy.DESTROY
546
+ });
547
+ }
548
+ validateStream(binding) {
549
+ const { definition, table } = binding;
322
550
  if (!table.tableStreamArn) {
323
551
  throw new Error(
324
- `Table ${table.tableName} does not have DynamoDB Streams enabled. Enable streams with streamSpecification when creating the table.`
552
+ `Table ${definition.tableName} does not have DynamoDB Streams enabled. NEW_AND_OLD_IMAGES is required.`
325
553
  );
326
554
  }
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
- }
555
+ const child = table.node.defaultChild;
556
+ if (!(child instanceof dynamodb.CfnTable) && !(child instanceof dynamodb.CfnGlobalTable)) {
557
+ return;
558
+ }
559
+ const specification = cdk2.Stack.of(this).resolve(
560
+ child.streamSpecification
344
561
  );
562
+ if (!specification || cdk2.Token.isUnresolved(specification) || typeof specification !== "object" || !("streamViewType" in specification)) {
563
+ return;
564
+ }
565
+ const streamViewType = specification.streamViewType;
566
+ if (!cdk2.Token.isUnresolved(streamViewType) && streamViewType !== dynamodb.StreamViewType.NEW_AND_OLD_IMAGES) {
567
+ throw new Error(
568
+ `Table ${definition.tableName} uses stream view ${streamViewType}; NEW_AND_OLD_IMAGES is required.`
569
+ );
570
+ }
345
571
  }
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 [];
572
+ validateTableName(binding) {
573
+ const { definition, table } = binding;
574
+ const child = table.node.defaultChild;
575
+ const childTableName = child && typeof child === "object" && "tableName" in child ? child.tableName : void 0;
576
+ const physicalTableName = !cdk2.Token.isUnresolved(table.tableName) ? table.tableName : childTableName && !cdk2.Token.isUnresolved(childTableName) ? childTableName : table.tableName;
577
+ if (!physicalTableName || cdk2.Token.isUnresolved(physicalTableName)) {
578
+ throw new Error(
579
+ `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.`
580
+ );
581
+ }
582
+ if (physicalTableName !== definition.tableName) {
583
+ throw new Error(
584
+ `Table binding name mismatch: definition.tableName is "${definition.tableName}" but the physical table name is "${physicalTableName}". Set an explicit matching tableName.`
585
+ );
586
+ }
359
587
  }
360
- return [lambda.FilterCriteria.filter({ filters })];
361
- }
588
+ };
362
589
 
363
- export { DynamoDBStreamSource, ReactiveEngine, ReactiveSystemTables, createStreamFilter, generateEntryPointCode };
590
+ export { ReactiveEngine };
364
591
  //# sourceMappingURL=infra.js.map
365
592
  //# sourceMappingURL=infra.js.map