@auriclabs/events 0.3.0 → 0.4.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.
@@ -1,39 +1,75 @@
1
1
 
2
- > @auriclabs/events@0.3.0 build /home/runner/work/packages/packages/packages/events
3
- > tsdown src/index.ts --format cjs,esm --dts --no-hash
2
+ > @auriclabs/events@0.4.1 build /home/runner/work/packages/packages/packages/events
3
+ > tsdown src/index.ts --format cjs,esm --dts --no-hash && tsdown src/stream-handler.entry.ts --format cjs,esm --dts --no-hash --no-clean
4
4
 
5
5
  [tsdown] Node.js v20.20.1 is deprecated. Support will be removed in the next minor release. Please upgrade to Node.js 22.18.0 or later.
6
6
  ℹ tsdown v0.21.4 powered by rolldown v1.0.0-rc.9
7
7
  ℹ entry: src/index.ts
8
8
  ℹ tsconfig: tsconfig.json
9
9
  ℹ Build start
10
- ℹ [CJS] dist/index.cjs 11.09 kB │ gzip: 3.32 kB
11
- ℹ [CJS] 1 files, total: 11.09 kB
10
+ ℹ [CJS] dist/index.cjs 11.16 kB │ gzip: 3.34 kB
11
+ ℹ [CJS] 1 files, total: 11.16 kB
12
12
  ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
13
13
  See more at https://tsdown.dev/options/dependencies#deps-onlybundle
14
14
  Detected dependencies in bundle:
15
15
  - @types/aws-lambda
16
+ ℹ [ESM] dist/index.mjs 10.40 kB │ gzip: 3.22 kB
17
+ ℹ [ESM] dist/index.mjs.map 23.95 kB │ gzip: 6.81 kB
18
+ ℹ [ESM] dist/index.d.mts.map  7.95 kB │ gzip: 2.65 kB
19
+ ℹ [ESM] dist/index.d.mts 15.81 kB │ gzip: 4.29 kB
20
+ ℹ [ESM] 4 files, total: 58.11 kB
21
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
22
+ - rolldown-plugin-dts:generate (68%)
23
+ - rolldown-plugin-dts:resolver (21%)
24
+ See https://rolldown.rs/options/checks#plugintimings for more details.
25
+
26
+ ✔ Build complete in 4949ms
16
27
  ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
17
28
  See more at https://tsdown.dev/options/dependencies#deps-onlybundle
18
29
  Detected dependencies in bundle:
19
30
  - @types/aws-lambda
20
31
  ℹ [CJS] dist/index.d.cts.map  7.95 kB │ gzip: 2.65 kB
21
- [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
22
32
  ℹ [CJS] dist/index.d.cts 15.81 kB │ gzip: 4.29 kB
23
33
  ℹ [CJS] 2 files, total: 23.76 kB
24
- - rolldown-plugin-dts:resolver (63%)
25
- - rolldown-plugin-dts:generate (24%)
34
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
35
+ - rolldown-plugin-dts:resolver (50%)
36
+ - rolldown-plugin-dts:generate (37%)
37
+ See https://rolldown.rs/options/checks#plugintimings for more details.
38
+
39
+ ✔ Build complete in 4978ms
40
+ [tsdown] Node.js v20.20.1 is deprecated. Support will be removed in the next minor release. Please upgrade to Node.js 22.18.0 or later.
41
+ ℹ tsdown v0.21.4 powered by rolldown v1.0.0-rc.9
42
+ ℹ entry: src/stream-handler.entry.ts
43
+ ℹ tsconfig: tsconfig.json
44
+ ℹ Build start
45
+ ℹ [CJS] dist/stream-handler.entry.cjs 3.28 kB │ gzip: 1.21 kB
46
+ ℹ [CJS] 1 files, total: 3.28 kB
47
+ ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
48
+ See more at https://tsdown.dev/options/dependencies#deps-onlybundle
49
+ Detected dependencies in bundle:
50
+ - @types/aws-lambda
51
+ ℹ Hint: consider adding deps.onlyBundle option to avoid unintended bundling of dependencies, or set deps.onlyBundle: false to disable this hint.
52
+ See more at https://tsdown.dev/options/dependencies#deps-onlybundle
53
+ Detected dependencies in bundle:
54
+ - @types/aws-lambda
55
+ ℹ [ESM] dist/stream-handler.entry.mjs 3.06 kB │ gzip: 1.14 kB
56
+ ℹ [ESM] dist/stream-handler.entry.mjs.map 5.85 kB │ gzip: 2.10 kB
57
+ ℹ [ESM] dist/stream-handler.entry.d.mts.map 3.62 kB │ gzip: 1.39 kB
58
+ ℹ [ESM] dist/stream-handler.entry.d.mts 8.19 kB │ gzip: 2.29 kB
59
+ ℹ [ESM] 4 files, total: 20.72 kB
60
+ ℹ [CJS] dist/stream-handler.entry.d.cts.map 3.62 kB │ gzip: 1.39 kB
61
+ ℹ [CJS] dist/stream-handler.entry.d.cts 8.19 kB │ gzip: 2.29 kB
62
+ ℹ [CJS] 2 files, total: 11.81 kB
63
+ [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
64
+ - rolldown-plugin-dts:resolver (43%)
65
+ - rolldown-plugin-dts:generate (40%)
66
+ - rolldown-plugin-dts:fake-js (17%)
26
67
  See https://rolldown.rs/options/checks#plugintimings for more details.
27
68
 
28
- ✔ Build complete in 5301ms
29
- ℹ [ESM] dist/index.mjs 10.34 kB │ gzip: 3.21 kB
30
- ℹ [ESM] dist/index.mjs.map 23.79 kB │ gzip: 6.76 kB
31
- ℹ [ESM] dist/index.d.mts.map  7.95 kB │ gzip: 2.65 kB
32
- ℹ [ESM] dist/index.d.mts 15.81 kB │ gzip: 4.29 kB
33
- ℹ [ESM] 4 files, total: 57.89 kB
34
69
  [PLUGIN_TIMINGS] Warning: Your build spent significant time in plugins. Here is a breakdown:
35
- - rolldown-plugin-dts:generate (57%)
36
- ✔ Build complete in 5304ms
37
- - rolldown-plugin-dts:resolver (34%)
70
+ - rolldown-plugin-dts:resolver (43%)
71
+ - rolldown-plugin-dts:generate (41%)
38
72
  See https://rolldown.rs/options/checks#plugintimings for more details.
39
73
 
74
+ ✔ Build complete in 3832ms
75
+ ✔ Build complete in 3833ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @auriclabs/events
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - adb821b: Fix broken type declarations by splitting tsdown into separate invocations to avoid DTS
8
+ code-splitting bug with multiple entry points.
9
+
10
+ ## 0.4.0
11
+
12
+ ### Minor Changes
13
+
14
+ - ddd017a: Add createEventQueue helper, make EventBridge bus optional, and bundle a pre-built stream
15
+ handler to eliminate consumer boilerplate.
16
+
3
17
  ## 0.3.0
4
18
 
5
19
  ### Minor Changes
package/dist/index.cjs CHANGED
@@ -297,7 +297,11 @@ function createStreamHandler(config) {
297
297
  return;
298
298
  }
299
299
  }).filter((eventRecord) => eventRecord?.itemType === "event");
300
- if (eventRecords.length > 0) await Promise.all([sendToBusBatch(eventRecords), sendToQueuesBatch(eventRecords)]);
300
+ if (eventRecords.length > 0) {
301
+ const tasks = [sendToQueuesBatch(eventRecords)];
302
+ if (config.busName) tasks.push(sendToBusBatch(eventRecords));
303
+ await Promise.all(tasks);
304
+ }
301
305
  };
302
306
  }
303
307
  //#endregion
package/dist/index.d.cts CHANGED
@@ -367,7 +367,7 @@ declare const createEventListener: (eventHandlers: EventHandlers, {
367
367
  //#endregion
368
368
  //#region src/stream-handler.d.ts
369
369
  interface CreateStreamHandlerConfig {
370
- busName: string;
370
+ busName?: string;
371
371
  queueUrls: string[];
372
372
  }
373
373
  /**
package/dist/index.d.mts CHANGED
@@ -367,7 +367,7 @@ declare const createEventListener: (eventHandlers: EventHandlers, {
367
367
  //#endregion
368
368
  //#region src/stream-handler.d.ts
369
369
  interface CreateStreamHandlerConfig {
370
- busName: string;
370
+ busName?: string;
371
371
  queueUrls: string[];
372
372
  }
373
373
  /**
package/dist/index.mjs CHANGED
@@ -296,7 +296,11 @@ function createStreamHandler(config) {
296
296
  return;
297
297
  }
298
298
  }).filter((eventRecord) => eventRecord?.itemType === "event");
299
- if (eventRecords.length > 0) await Promise.all([sendToBusBatch(eventRecords), sendToQueuesBatch(eventRecords)]);
299
+ if (eventRecords.length > 0) {
300
+ const tasks = [sendToQueuesBatch(eventRecords)];
301
+ if (config.busName) tasks.push(sendToBusBatch(eventRecords));
302
+ await Promise.all(tasks);
303
+ }
300
304
  };
301
305
  }
302
306
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/event-service.ts","../src/init.ts","../src/context.ts","../src/dispatch-event.ts","../src/dispatch-events.ts","../src/create-dispatch.ts","../src/create-event-listener.ts","../src/stream-handler.ts"],"sourcesContent":["import { normalizePaginationResponse, PaginationResponse } from '@auriclabs/pagination';\nimport { DynamoDBClient, ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n TransactWriteCommand,\n GetCommand,\n QueryCommand,\n} from '@aws-sdk/lib-dynamodb';\n\nimport type {\n EventRecord,\n AggregateHead,\n AggregatePK,\n EventId,\n AggregateId,\n AggregateType,\n EventSK,\n Source,\n} from './types';\n\nconst ddb = DynamoDBDocumentClient.from(new DynamoDBClient(), {\n marshallOptions: {\n removeUndefinedValues: true,\n },\n});\n\nconst pad = (n: number, w = 9): EventId => String(n).padStart(w, '0') as EventId;\nconst pkFor = (aggregateType: string, aggregateId: string): AggregatePK =>\n `AGG#${aggregateType}#${aggregateId}`;\n\nexport interface AppendArgs<P = unknown> {\n tenantId: string;\n aggregateType: string;\n aggregateId: string;\n source: string;\n /** Version you observed before appending (0 for brand new) */\n expectedVersion: number;\n /** Required for idempotent retries (e.g., the command id) */\n idempotencyKey: string;\n\n // Event properties (flattened)\n eventId: string; // ULID/UUID – must be stable across retries\n eventType: string;\n occurredAt?: string; // default: now ISO\n payload?: Readonly<P>;\n schemaVersion?: number; // optional but recommended\n\n // Optional metadata\n correlationId?: string;\n causationId?: string;\n actorId?: string;\n}\n\nexport interface AppendEventResult {\n pk: string;\n sk: string;\n version: number;\n}\n\nexport interface EventService {\n appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult>;\n getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined>;\n getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined>;\n listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>>;\n}\n\nexport function createEventService(tableName: string): EventService {\n const TABLE = tableName;\n\n return {\n async appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult> {\n const {\n tenantId,\n aggregateType,\n aggregateId,\n expectedVersion,\n idempotencyKey,\n eventId,\n eventType,\n occurredAt,\n source,\n payload,\n schemaVersion,\n correlationId,\n causationId,\n actorId,\n } = args;\n\n const pk = pkFor(aggregateType, aggregateId);\n const nextVersion = expectedVersion + 1;\n const sk = `EVT#${pad(nextVersion)}` as EventSK;\n const nowIso = new Date().toISOString();\n const eventOccurredAt = occurredAt ?? nowIso;\n\n try {\n await ddb.send(\n new TransactWriteCommand({\n TransactItems: [\n {\n Update: {\n TableName: TABLE,\n Key: { pk, sk: 'HEAD' },\n UpdateExpression:\n 'SET currentVersion = :next, lastEventId = :eid, lastIdemKey = :idem, updatedAt = :now, aggregateId = if_not_exists(aggregateId, :aid), aggregateType = if_not_exists(aggregateType, :atype)',\n ConditionExpression:\n '(attribute_not_exists(currentVersion) AND :expected = :zero) ' +\n 'OR currentVersion = :expected ' +\n 'OR lastIdemKey = :idem',\n ExpressionAttributeValues: {\n ':zero': 0,\n ':expected': expectedVersion,\n ':next': nextVersion,\n ':eid': eventId,\n ':idem': idempotencyKey,\n ':now': nowIso,\n ':aid': aggregateId,\n ':atype': aggregateType,\n },\n },\n },\n {\n Put: {\n TableName: TABLE,\n Item: {\n pk,\n sk,\n itemType: 'event',\n source: source as Source,\n aggregateId: aggregateId as AggregateId,\n aggregateType: aggregateType as AggregateType,\n version: nextVersion,\n\n tenantId,\n\n eventId: eventId as EventId,\n eventType: eventType,\n schemaVersion: schemaVersion ?? 1,\n occurredAt: eventOccurredAt,\n\n correlationId,\n causationId,\n actorId,\n\n payload: payload as Readonly<unknown>,\n } satisfies EventRecord,\n ConditionExpression: 'attribute_not_exists(pk) OR eventId = :eid',\n ExpressionAttributeValues: { ':eid': eventId },\n },\n },\n ],\n }),\n );\n } catch (err) {\n if (err instanceof ConditionalCheckFailedException) {\n throw new Error(\n `OCC failed for aggregate ${aggregateType}/${aggregateId}: expectedVersion=${expectedVersion}`,\n );\n }\n throw err;\n }\n\n return { pk, sk, version: nextVersion };\n },\n\n async getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk: 'HEAD' } }));\n return res.Item as AggregateHead | undefined;\n },\n\n async getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const sk = `EVT#${pad(version)}`;\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk } }));\n return res.Item as EventRecord | undefined;\n },\n\n async listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>> {\n const pk = pkFor(params.aggregateType, params.aggregateId);\n const fromSk =\n params.fromVersionExclusive != null\n ? `EVT#${pad(params.fromVersionExclusive + 1)}`\n : 'EVT#000000000';\n const toSk =\n params.toVersionInclusive != null\n ? `EVT#${pad(params.toVersionInclusive)}`\n : 'EVT#999999999';\n\n const res = await ddb.send(\n new QueryCommand({\n TableName: TABLE,\n KeyConditionExpression: 'pk = :pk AND sk BETWEEN :from AND :to',\n ExpressionAttributeValues: {\n ':pk': pk,\n ':from': fromSk,\n ':to': toSk,\n },\n ScanIndexForward: true,\n Limit: params.limit,\n }),\n );\n\n return normalizePaginationResponse({\n data: (res.Items ?? []) as EventRecord[],\n cursor: res.LastEvaluatedKey && (res.LastEvaluatedKey as { pk: string; sk: string }).sk,\n });\n },\n };\n}\n","import { createEventService, EventService } from './event-service';\n\nlet _eventService: EventService | undefined;\n\nexport function initEvents(config: { tableName: string }): void {\n _eventService = createEventService(config.tableName);\n}\n\nexport function getEventService(): EventService {\n if (!_eventService) {\n throw new Error('Call initEvents() before using events');\n }\n return _eventService;\n}\n","import { AppendArgs } from './event-service';\n\nexport type EventContext = Partial<AppendArgs>;\n\nlet context: EventContext = {};\n\nexport const setEventContext = (newContext: EventContext) => {\n context = { ...newContext };\n};\n\nexport const getEventContext = () => context;\n\nexport const resetEventContext = () => {\n context = {};\n};\n\nexport const appendEventContext = (event: EventContext) => {\n context = { ...context, ...event };\n};\n","import { retry } from '@auriclabs/api-core';\nimport { logger } from '@auriclabs/logger';\nimport { ulid } from 'ulid';\n\nimport { getEventContext } from './context';\nimport { AppendArgs, AppendEventResult } from './event-service';\nimport { getEventService } from './init';\n\nexport type DispatchEventArgs = Omit<\n AppendArgs,\n 'eventId' | 'expectedVersion' | 'schemaVersion' | 'occurredAt' | 'idempotencyKey'\n> &\n Partial<Pick<AppendArgs, 'idempotencyKey' | 'eventId'>>;\n\nexport const dispatchEvent = async (event: DispatchEventArgs): Promise<AppendEventResult> => {\n const eventService = getEventService();\n const eventId = event.eventId ?? `evt-${ulid()}`;\n const occurredAt = new Date().toISOString();\n const idempotencyKey = event.idempotencyKey ?? eventId;\n\n return retry(async () => {\n const head = await eventService.getHead(event.aggregateType, event.aggregateId);\n logger.debug({ event }, 'Dispatching event');\n return eventService.appendEvent({\n ...getEventContext(),\n ...event,\n eventId,\n expectedVersion: head?.currentVersion ?? 0,\n schemaVersion: 1,\n occurredAt,\n idempotencyKey,\n });\n });\n};\n","import { dispatchEvent, DispatchEventArgs } from './dispatch-event';\n\nexport interface DispatchEventsArgs {\n inOrder?: boolean;\n}\n\nexport const dispatchEvents = async (\n events: DispatchEventArgs[],\n { inOrder = false }: DispatchEventsArgs = {},\n) => {\n if (inOrder) {\n for (const event of events) {\n await dispatchEvent(event);\n }\n } else {\n await Promise.all(events.map((event) => dispatchEvent(event)));\n }\n};\n","import { ulid } from 'ulid';\n\nimport { EventContext, getEventContext } from './context';\nimport { dispatchEvent, DispatchEventArgs } from './dispatch-event';\nimport { AppendEventResult } from './event-service';\n\nexport type MakePartial<T, O> = Omit<T, keyof O> & Partial<O>;\n\nexport type DispatchRecord<\n Options extends Partial<DispatchEventArgs> = Partial<DispatchEventArgs>,\n> = Record<\n string,\n (\n ...args: any[]\n ) =>\n | ValueOrFactoryRecord<MakePartial<DispatchEventArgs, Options>>\n | DispatchEventArgsFactory<Options>\n>;\n\nexport type ValueOrFactory<T> = T | ((context: EventContext) => T);\nexport type ValueOrFactoryRecord<T> = {\n [K in keyof T]: ValueOrFactory<T[K]>;\n};\n\nexport type DispatchEventArgsFactory<Options extends Partial<DispatchEventArgs>> = (\n context: EventContext,\n) => MakePartial<DispatchEventArgs, Options>;\n\nexport type DispatchRecordResponse<\n R extends DispatchRecord<O>,\n O extends Partial<DispatchEventArgs>,\n> = {\n [K in keyof R]: (...args: Parameters<R[K]>) => Promise<AppendEventResult>;\n};\n\nexport function createDispatch<DR extends DispatchRecord<O>, O extends Partial<DispatchEventArgs>>(\n record: DR,\n optionsOrFactory?: ValueOrFactoryRecord<O> | ((context: EventContext) => O),\n): DispatchRecordResponse<DR, O> {\n return Object.fromEntries(\n Object.entries(record).map(([key, value]) => [\n key,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (...args: any[]) => {\n const eventId = `evt-${ulid()}`;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n const result = value(...args);\n const context: EventContext = { eventId, ...getEventContext() };\n const executeValueFn = (value: any) =>\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call\n typeof value === 'function' ? value(context) : value;\n const parseResponse = (result: any) =>\n Object.fromEntries(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n Object.entries(result).map(([key, value]) => [key, executeValueFn(value)]),\n );\n Object.assign(\n context,\n typeof result === 'function' ? result(context) : parseResponse(result),\n );\n Object.assign(\n context,\n typeof optionsOrFactory === 'function'\n ? optionsOrFactory(context)\n : parseResponse(optionsOrFactory),\n );\n return dispatchEvent(context as DispatchEventArgs);\n },\n ]),\n ) as DispatchRecordResponse<DR, O>;\n}\n","import { logger } from '@auriclabs/logger';\nimport { SQSBatchResponse, SQSEvent } from 'aws-lambda';\n\nimport { setEventContext } from './context';\nimport { EventHandlers, EventRecord } from './types';\n\nexport interface CreateEventListenerOptions {\n debug?: boolean;\n}\n\nexport const createEventListener =\n (eventHandlers: EventHandlers, { debug = false }: CreateEventListenerOptions = {}) =>\n async (sqsEvent: SQSEvent) => {\n const response: SQSBatchResponse = {\n batchItemFailures: [],\n };\n let hasFailed = false;\n for (const record of sqsEvent.Records) {\n // skip the job if it has failed\n if (hasFailed) {\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n continue;\n }\n\n let event: EventRecord | undefined;\n try {\n event = JSON.parse(record.body) as EventRecord;\n if (debug) {\n logger.debug({ event }, 'Processing event');\n }\n let handler = eventHandlers[event.eventType];\n while (typeof handler === 'string') {\n handler = eventHandlers[handler];\n }\n if (typeof handler === 'function') {\n setEventContext({\n causationId: event.eventId,\n correlationId: event.correlationId,\n actorId: event.actorId,\n });\n await handler(event);\n }\n } catch (error) {\n hasFailed = true;\n logger.error({ error, event, body: record.body }, 'Error processing event');\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n }\n }\n return response;\n };\n","import { logger } from '@auriclabs/logger';\nimport { AttributeValue } from '@aws-sdk/client-dynamodb';\nimport { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';\nimport { SendMessageBatchCommand, SQSClient } from '@aws-sdk/client-sqs';\nimport { unmarshall } from '@aws-sdk/util-dynamodb';\nimport { DynamoDBStreamEvent } from 'aws-lambda';\nimport { kebabCase } from 'lodash-es';\n\nimport { AggregateHead, EventRecord } from './types';\n\nconst BATCH_SIZE = 10;\n\nexport interface CreateStreamHandlerConfig {\n busName: string;\n queueUrls: string[];\n}\n\n/**\n * Creates a Lambda handler for DynamoDB stream events.\n * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.\n */\nexport function createStreamHandler(config: CreateStreamHandlerConfig) {\n const sqsClient = new SQSClient();\n const eventBridge = new EventBridgeClient({});\n\n function chunkArray<T>(array: T[], chunkSize: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n }\n\n async function sendToQueuesBatch(eventRecords: EventRecord[]) {\n await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));\n }\n\n async function sendToQueueBatch(eventRecords: EventRecord[], queue: string) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord, index) => ({\n Id: `${eventRecord.eventId}-${index}`,\n MessageBody: JSON.stringify(eventRecord),\n MessageGroupId: eventRecord.aggregateId,\n MessageDeduplicationId: eventRecord.eventId,\n }));\n\n await sqsClient.send(\n new SendMessageBatchCommand({\n QueueUrl: queue,\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch, queue }, 'Error sending batch to queue');\n throw error;\n }\n }\n }\n\n async function sendToBusBatch(eventRecords: EventRecord[]) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord) => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const source = eventRecord.source ?? kebabCase(eventRecord.aggregateType.split('.')[0]);\n return {\n Source: source,\n DetailType: eventRecord.eventType,\n Detail: JSON.stringify(eventRecord),\n EventBusName: config.busName,\n };\n });\n\n await eventBridge.send(\n new PutEventsCommand({\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch }, 'Error sending batch to bus');\n throw error;\n }\n }\n }\n\n return async (event: DynamoDBStreamEvent): Promise<void> => {\n const eventRecords = event.Records.filter((record) => record.eventName === 'INSERT')\n .map((record) => {\n try {\n const data = record.dynamodb?.NewImage;\n return unmarshall(data as Record<string, AttributeValue>) as EventRecord | AggregateHead;\n } catch (error) {\n logger.error({ error, record }, 'Error unmarshalling event record');\n return undefined;\n }\n })\n .filter((eventRecord): eventRecord is EventRecord => eventRecord?.itemType === 'event');\n\n if (eventRecords.length > 0) {\n await Promise.all([sendToBusBatch(eventRecords), sendToQueuesBatch(eventRecords)]);\n }\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/event-service.ts","../src/init.ts","../src/context.ts","../src/dispatch-event.ts","../src/dispatch-events.ts","../src/create-dispatch.ts","../src/create-event-listener.ts","../src/stream-handler.ts"],"sourcesContent":["import { normalizePaginationResponse, PaginationResponse } from '@auriclabs/pagination';\nimport { DynamoDBClient, ConditionalCheckFailedException } from '@aws-sdk/client-dynamodb';\nimport {\n DynamoDBDocumentClient,\n TransactWriteCommand,\n GetCommand,\n QueryCommand,\n} from '@aws-sdk/lib-dynamodb';\n\nimport type {\n EventRecord,\n AggregateHead,\n AggregatePK,\n EventId,\n AggregateId,\n AggregateType,\n EventSK,\n Source,\n} from './types';\n\nconst ddb = DynamoDBDocumentClient.from(new DynamoDBClient(), {\n marshallOptions: {\n removeUndefinedValues: true,\n },\n});\n\nconst pad = (n: number, w = 9): EventId => String(n).padStart(w, '0') as EventId;\nconst pkFor = (aggregateType: string, aggregateId: string): AggregatePK =>\n `AGG#${aggregateType}#${aggregateId}`;\n\nexport interface AppendArgs<P = unknown> {\n tenantId: string;\n aggregateType: string;\n aggregateId: string;\n source: string;\n /** Version you observed before appending (0 for brand new) */\n expectedVersion: number;\n /** Required for idempotent retries (e.g., the command id) */\n idempotencyKey: string;\n\n // Event properties (flattened)\n eventId: string; // ULID/UUID – must be stable across retries\n eventType: string;\n occurredAt?: string; // default: now ISO\n payload?: Readonly<P>;\n schemaVersion?: number; // optional but recommended\n\n // Optional metadata\n correlationId?: string;\n causationId?: string;\n actorId?: string;\n}\n\nexport interface AppendEventResult {\n pk: string;\n sk: string;\n version: number;\n}\n\nexport interface EventService {\n appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult>;\n getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined>;\n getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined>;\n listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>>;\n}\n\nexport function createEventService(tableName: string): EventService {\n const TABLE = tableName;\n\n return {\n async appendEvent<P = unknown>(args: AppendArgs<P>): Promise<AppendEventResult> {\n const {\n tenantId,\n aggregateType,\n aggregateId,\n expectedVersion,\n idempotencyKey,\n eventId,\n eventType,\n occurredAt,\n source,\n payload,\n schemaVersion,\n correlationId,\n causationId,\n actorId,\n } = args;\n\n const pk = pkFor(aggregateType, aggregateId);\n const nextVersion = expectedVersion + 1;\n const sk = `EVT#${pad(nextVersion)}` as EventSK;\n const nowIso = new Date().toISOString();\n const eventOccurredAt = occurredAt ?? nowIso;\n\n try {\n await ddb.send(\n new TransactWriteCommand({\n TransactItems: [\n {\n Update: {\n TableName: TABLE,\n Key: { pk, sk: 'HEAD' },\n UpdateExpression:\n 'SET currentVersion = :next, lastEventId = :eid, lastIdemKey = :idem, updatedAt = :now, aggregateId = if_not_exists(aggregateId, :aid), aggregateType = if_not_exists(aggregateType, :atype)',\n ConditionExpression:\n '(attribute_not_exists(currentVersion) AND :expected = :zero) ' +\n 'OR currentVersion = :expected ' +\n 'OR lastIdemKey = :idem',\n ExpressionAttributeValues: {\n ':zero': 0,\n ':expected': expectedVersion,\n ':next': nextVersion,\n ':eid': eventId,\n ':idem': idempotencyKey,\n ':now': nowIso,\n ':aid': aggregateId,\n ':atype': aggregateType,\n },\n },\n },\n {\n Put: {\n TableName: TABLE,\n Item: {\n pk,\n sk,\n itemType: 'event',\n source: source as Source,\n aggregateId: aggregateId as AggregateId,\n aggregateType: aggregateType as AggregateType,\n version: nextVersion,\n\n tenantId,\n\n eventId: eventId as EventId,\n eventType: eventType,\n schemaVersion: schemaVersion ?? 1,\n occurredAt: eventOccurredAt,\n\n correlationId,\n causationId,\n actorId,\n\n payload: payload as Readonly<unknown>,\n } satisfies EventRecord,\n ConditionExpression: 'attribute_not_exists(pk) OR eventId = :eid',\n ExpressionAttributeValues: { ':eid': eventId },\n },\n },\n ],\n }),\n );\n } catch (err) {\n if (err instanceof ConditionalCheckFailedException) {\n throw new Error(\n `OCC failed for aggregate ${aggregateType}/${aggregateId}: expectedVersion=${expectedVersion}`,\n );\n }\n throw err;\n }\n\n return { pk, sk, version: nextVersion };\n },\n\n async getHead(aggregateType: string, aggregateId: string): Promise<AggregateHead | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk: 'HEAD' } }));\n return res.Item as AggregateHead | undefined;\n },\n\n async getEvent(\n aggregateType: string,\n aggregateId: string,\n version: number,\n ): Promise<EventRecord | undefined> {\n const pk = pkFor(aggregateType, aggregateId);\n const sk = `EVT#${pad(version)}`;\n const res = await ddb.send(new GetCommand({ TableName: TABLE, Key: { pk, sk } }));\n return res.Item as EventRecord | undefined;\n },\n\n async listEvents(params: {\n aggregateType: string;\n aggregateId: string;\n fromVersionExclusive?: number;\n toVersionInclusive?: number;\n limit?: number;\n }): Promise<PaginationResponse<EventRecord>> {\n const pk = pkFor(params.aggregateType, params.aggregateId);\n const fromSk =\n params.fromVersionExclusive != null\n ? `EVT#${pad(params.fromVersionExclusive + 1)}`\n : 'EVT#000000000';\n const toSk =\n params.toVersionInclusive != null\n ? `EVT#${pad(params.toVersionInclusive)}`\n : 'EVT#999999999';\n\n const res = await ddb.send(\n new QueryCommand({\n TableName: TABLE,\n KeyConditionExpression: 'pk = :pk AND sk BETWEEN :from AND :to',\n ExpressionAttributeValues: {\n ':pk': pk,\n ':from': fromSk,\n ':to': toSk,\n },\n ScanIndexForward: true,\n Limit: params.limit,\n }),\n );\n\n return normalizePaginationResponse({\n data: (res.Items ?? []) as EventRecord[],\n cursor: res.LastEvaluatedKey && (res.LastEvaluatedKey as { pk: string; sk: string }).sk,\n });\n },\n };\n}\n","import { createEventService, EventService } from './event-service';\n\nlet _eventService: EventService | undefined;\n\nexport function initEvents(config: { tableName: string }): void {\n _eventService = createEventService(config.tableName);\n}\n\nexport function getEventService(): EventService {\n if (!_eventService) {\n throw new Error('Call initEvents() before using events');\n }\n return _eventService;\n}\n","import { AppendArgs } from './event-service';\n\nexport type EventContext = Partial<AppendArgs>;\n\nlet context: EventContext = {};\n\nexport const setEventContext = (newContext: EventContext) => {\n context = { ...newContext };\n};\n\nexport const getEventContext = () => context;\n\nexport const resetEventContext = () => {\n context = {};\n};\n\nexport const appendEventContext = (event: EventContext) => {\n context = { ...context, ...event };\n};\n","import { retry } from '@auriclabs/api-core';\nimport { logger } from '@auriclabs/logger';\nimport { ulid } from 'ulid';\n\nimport { getEventContext } from './context';\nimport { AppendArgs, AppendEventResult } from './event-service';\nimport { getEventService } from './init';\n\nexport type DispatchEventArgs = Omit<\n AppendArgs,\n 'eventId' | 'expectedVersion' | 'schemaVersion' | 'occurredAt' | 'idempotencyKey'\n> &\n Partial<Pick<AppendArgs, 'idempotencyKey' | 'eventId'>>;\n\nexport const dispatchEvent = async (event: DispatchEventArgs): Promise<AppendEventResult> => {\n const eventService = getEventService();\n const eventId = event.eventId ?? `evt-${ulid()}`;\n const occurredAt = new Date().toISOString();\n const idempotencyKey = event.idempotencyKey ?? eventId;\n\n return retry(async () => {\n const head = await eventService.getHead(event.aggregateType, event.aggregateId);\n logger.debug({ event }, 'Dispatching event');\n return eventService.appendEvent({\n ...getEventContext(),\n ...event,\n eventId,\n expectedVersion: head?.currentVersion ?? 0,\n schemaVersion: 1,\n occurredAt,\n idempotencyKey,\n });\n });\n};\n","import { dispatchEvent, DispatchEventArgs } from './dispatch-event';\n\nexport interface DispatchEventsArgs {\n inOrder?: boolean;\n}\n\nexport const dispatchEvents = async (\n events: DispatchEventArgs[],\n { inOrder = false }: DispatchEventsArgs = {},\n) => {\n if (inOrder) {\n for (const event of events) {\n await dispatchEvent(event);\n }\n } else {\n await Promise.all(events.map((event) => dispatchEvent(event)));\n }\n};\n","import { ulid } from 'ulid';\n\nimport { EventContext, getEventContext } from './context';\nimport { dispatchEvent, DispatchEventArgs } from './dispatch-event';\nimport { AppendEventResult } from './event-service';\n\nexport type MakePartial<T, O> = Omit<T, keyof O> & Partial<O>;\n\nexport type DispatchRecord<\n Options extends Partial<DispatchEventArgs> = Partial<DispatchEventArgs>,\n> = Record<\n string,\n (\n ...args: any[]\n ) =>\n | ValueOrFactoryRecord<MakePartial<DispatchEventArgs, Options>>\n | DispatchEventArgsFactory<Options>\n>;\n\nexport type ValueOrFactory<T> = T | ((context: EventContext) => T);\nexport type ValueOrFactoryRecord<T> = {\n [K in keyof T]: ValueOrFactory<T[K]>;\n};\n\nexport type DispatchEventArgsFactory<Options extends Partial<DispatchEventArgs>> = (\n context: EventContext,\n) => MakePartial<DispatchEventArgs, Options>;\n\nexport type DispatchRecordResponse<\n R extends DispatchRecord<O>,\n O extends Partial<DispatchEventArgs>,\n> = {\n [K in keyof R]: (...args: Parameters<R[K]>) => Promise<AppendEventResult>;\n};\n\nexport function createDispatch<DR extends DispatchRecord<O>, O extends Partial<DispatchEventArgs>>(\n record: DR,\n optionsOrFactory?: ValueOrFactoryRecord<O> | ((context: EventContext) => O),\n): DispatchRecordResponse<DR, O> {\n return Object.fromEntries(\n Object.entries(record).map(([key, value]) => [\n key,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (...args: any[]) => {\n const eventId = `evt-${ulid()}`;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n const result = value(...args);\n const context: EventContext = { eventId, ...getEventContext() };\n const executeValueFn = (value: any) =>\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call\n typeof value === 'function' ? value(context) : value;\n const parseResponse = (result: any) =>\n Object.fromEntries(\n // eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n Object.entries(result).map(([key, value]) => [key, executeValueFn(value)]),\n );\n Object.assign(\n context,\n typeof result === 'function' ? result(context) : parseResponse(result),\n );\n Object.assign(\n context,\n typeof optionsOrFactory === 'function'\n ? optionsOrFactory(context)\n : parseResponse(optionsOrFactory),\n );\n return dispatchEvent(context as DispatchEventArgs);\n },\n ]),\n ) as DispatchRecordResponse<DR, O>;\n}\n","import { logger } from '@auriclabs/logger';\nimport { SQSBatchResponse, SQSEvent } from 'aws-lambda';\n\nimport { setEventContext } from './context';\nimport { EventHandlers, EventRecord } from './types';\n\nexport interface CreateEventListenerOptions {\n debug?: boolean;\n}\n\nexport const createEventListener =\n (eventHandlers: EventHandlers, { debug = false }: CreateEventListenerOptions = {}) =>\n async (sqsEvent: SQSEvent) => {\n const response: SQSBatchResponse = {\n batchItemFailures: [],\n };\n let hasFailed = false;\n for (const record of sqsEvent.Records) {\n // skip the job if it has failed\n if (hasFailed) {\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n continue;\n }\n\n let event: EventRecord | undefined;\n try {\n event = JSON.parse(record.body) as EventRecord;\n if (debug) {\n logger.debug({ event }, 'Processing event');\n }\n let handler = eventHandlers[event.eventType];\n while (typeof handler === 'string') {\n handler = eventHandlers[handler];\n }\n if (typeof handler === 'function') {\n setEventContext({\n causationId: event.eventId,\n correlationId: event.correlationId,\n actorId: event.actorId,\n });\n await handler(event);\n }\n } catch (error) {\n hasFailed = true;\n logger.error({ error, event, body: record.body }, 'Error processing event');\n response.batchItemFailures.push({\n itemIdentifier: record.messageId,\n });\n }\n }\n return response;\n };\n","import { logger } from '@auriclabs/logger';\nimport { AttributeValue } from '@aws-sdk/client-dynamodb';\nimport { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';\nimport { SendMessageBatchCommand, SQSClient } from '@aws-sdk/client-sqs';\nimport { unmarshall } from '@aws-sdk/util-dynamodb';\nimport { DynamoDBStreamEvent } from 'aws-lambda';\nimport { kebabCase } from 'lodash-es';\n\nimport { AggregateHead, EventRecord } from './types';\n\nconst BATCH_SIZE = 10;\n\nexport interface CreateStreamHandlerConfig {\n busName?: string;\n queueUrls: string[];\n}\n\n/**\n * Creates a Lambda handler for DynamoDB stream events.\n * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.\n */\nexport function createStreamHandler(config: CreateStreamHandlerConfig) {\n const sqsClient = new SQSClient();\n const eventBridge = new EventBridgeClient({});\n\n function chunkArray<T>(array: T[], chunkSize: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n }\n\n async function sendToQueuesBatch(eventRecords: EventRecord[]) {\n await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));\n }\n\n async function sendToQueueBatch(eventRecords: EventRecord[], queue: string) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord, index) => ({\n Id: `${eventRecord.eventId}-${index}`,\n MessageBody: JSON.stringify(eventRecord),\n MessageGroupId: eventRecord.aggregateId,\n MessageDeduplicationId: eventRecord.eventId,\n }));\n\n await sqsClient.send(\n new SendMessageBatchCommand({\n QueueUrl: queue,\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch, queue }, 'Error sending batch to queue');\n throw error;\n }\n }\n }\n\n async function sendToBusBatch(eventRecords: EventRecord[]) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord) => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const source = eventRecord.source ?? kebabCase(eventRecord.aggregateType.split('.')[0]);\n return {\n Source: source,\n DetailType: eventRecord.eventType,\n Detail: JSON.stringify(eventRecord),\n EventBusName: config.busName,\n };\n });\n\n await eventBridge.send(\n new PutEventsCommand({\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch }, 'Error sending batch to bus');\n throw error;\n }\n }\n }\n\n return async (event: DynamoDBStreamEvent): Promise<void> => {\n const eventRecords = event.Records.filter((record) => record.eventName === 'INSERT')\n .map((record) => {\n try {\n const data = record.dynamodb?.NewImage;\n return unmarshall(data as Record<string, AttributeValue>) as EventRecord | AggregateHead;\n } catch (error) {\n logger.error({ error, record }, 'Error unmarshalling event record');\n return undefined;\n }\n })\n .filter((eventRecord): eventRecord is EventRecord => eventRecord?.itemType === 'event');\n\n if (eventRecords.length > 0) {\n const tasks: Promise<void>[] = [sendToQueuesBatch(eventRecords)];\n if (config.busName) {\n tasks.push(sendToBusBatch(eventRecords));\n }\n await Promise.all(tasks);\n }\n };\n}\n"],"mappings":";;;;;;;;;;;AAoBA,MAAM,MAAM,uBAAuB,KAAK,IAAI,gBAAgB,EAAE,EAC5D,iBAAiB,EACf,uBAAuB,MACxB,EACF,CAAC;AAEF,MAAM,OAAO,GAAW,IAAI,MAAe,OAAO,EAAE,CAAC,SAAS,GAAG,IAAI;AACrE,MAAM,SAAS,eAAuB,gBACpC,OAAO,cAAc,GAAG;AAgD1B,SAAgB,mBAAmB,WAAiC;CAClE,MAAM,QAAQ;AAEd,QAAO;EACL,MAAM,YAAyB,MAAiD;GAC9E,MAAM,EACJ,UACA,eACA,aACA,iBACA,gBACA,SACA,WACA,YACA,QACA,SACA,eACA,eACA,aACA,YACE;GAEJ,MAAM,KAAK,MAAM,eAAe,YAAY;GAC5C,MAAM,cAAc,kBAAkB;GACtC,MAAM,KAAK,OAAO,IAAI,YAAY;GAClC,MAAM,0BAAS,IAAI,MAAM,EAAC,aAAa;GACvC,MAAM,kBAAkB,cAAc;AAEtC,OAAI;AACF,UAAM,IAAI,KACR,IAAI,qBAAqB,EACvB,eAAe,CACb,EACE,QAAQ;KACN,WAAW;KACX,KAAK;MAAE;MAAI,IAAI;MAAQ;KACvB,kBACE;KACF,qBACE;KAGF,2BAA2B;MACzB,SAAS;MACT,aAAa;MACb,SAAS;MACT,QAAQ;MACR,SAAS;MACT,QAAQ;MACR,QAAQ;MACR,UAAU;MACX;KACF,EACF,EACD,EACE,KAAK;KACH,WAAW;KACX,MAAM;MACJ;MACA;MACA,UAAU;MACF;MACK;MACE;MACf,SAAS;MAET;MAES;MACE;MACX,eAAe,iBAAiB;MAChC,YAAY;MAEZ;MACA;MACA;MAES;MACV;KACD,qBAAqB;KACrB,2BAA2B,EAAE,QAAQ,SAAS;KAC/C,EACF,CACF,EACF,CAAC,CACH;YACM,KAAK;AACZ,QAAI,eAAe,gCACjB,OAAM,IAAI,MACR,4BAA4B,cAAc,GAAG,YAAY,oBAAoB,kBAC9E;AAEH,UAAM;;AAGR,UAAO;IAAE;IAAI;IAAI,SAAS;IAAa;;EAGzC,MAAM,QAAQ,eAAuB,aAAyD;GAC5F,MAAM,KAAK,MAAM,eAAe,YAAY;AAE5C,WADY,MAAM,IAAI,KAAK,IAAI,WAAW;IAAE,WAAW;IAAO,KAAK;KAAE;KAAI,IAAI;KAAQ;IAAE,CAAC,CAAC,EAC9E;;EAGb,MAAM,SACJ,eACA,aACA,SACkC;GAClC,MAAM,KAAK,MAAM,eAAe,YAAY;GAC5C,MAAM,KAAK,OAAO,IAAI,QAAQ;AAE9B,WADY,MAAM,IAAI,KAAK,IAAI,WAAW;IAAE,WAAW;IAAO,KAAK;KAAE;KAAI;KAAI;IAAE,CAAC,CAAC,EACtE;;EAGb,MAAM,WAAW,QAM4B;GAC3C,MAAM,KAAK,MAAM,OAAO,eAAe,OAAO,YAAY;GAC1D,MAAM,SACJ,OAAO,wBAAwB,OAC3B,OAAO,IAAI,OAAO,uBAAuB,EAAE,KAC3C;GACN,MAAM,OACJ,OAAO,sBAAsB,OACzB,OAAO,IAAI,OAAO,mBAAmB,KACrC;GAEN,MAAM,MAAM,MAAM,IAAI,KACpB,IAAI,aAAa;IACf,WAAW;IACX,wBAAwB;IACxB,2BAA2B;KACzB,OAAO;KACP,SAAS;KACT,OAAO;KACR;IACD,kBAAkB;IAClB,OAAO,OAAO;IACf,CAAC,CACH;AAED,UAAO,4BAA4B;IACjC,MAAO,IAAI,SAAS,EAAE;IACtB,QAAQ,IAAI,oBAAqB,IAAI,iBAAgD;IACtF,CAAC;;EAEL;;;;ACjOH,IAAI;AAEJ,SAAgB,WAAW,QAAqC;AAC9D,iBAAgB,mBAAmB,OAAO,UAAU;;AAGtD,SAAgB,kBAAgC;AAC9C,KAAI,CAAC,cACH,OAAM,IAAI,MAAM,wCAAwC;AAE1D,QAAO;;;;ACRT,IAAI,UAAwB,EAAE;AAE9B,MAAa,mBAAmB,eAA6B;AAC3D,WAAU,EAAE,GAAG,YAAY;;AAG7B,MAAa,wBAAwB;AAErC,MAAa,0BAA0B;AACrC,WAAU,EAAE;;AAGd,MAAa,sBAAsB,UAAwB;AACzD,WAAU;EAAE,GAAG;EAAS,GAAG;EAAO;;;;ACHpC,MAAa,gBAAgB,OAAO,UAAyD;CAC3F,MAAM,eAAe,iBAAiB;CACtC,MAAM,UAAU,MAAM,WAAW,OAAO,MAAM;CAC9C,MAAM,8BAAa,IAAI,MAAM,EAAC,aAAa;CAC3C,MAAM,iBAAiB,MAAM,kBAAkB;AAE/C,QAAO,MAAM,YAAY;EACvB,MAAM,OAAO,MAAM,aAAa,QAAQ,MAAM,eAAe,MAAM,YAAY;AAC/E,SAAO,MAAM,EAAE,OAAO,EAAE,oBAAoB;AAC5C,SAAO,aAAa,YAAY;GAC9B,GAAG,iBAAiB;GACpB,GAAG;GACH;GACA,iBAAiB,MAAM,kBAAkB;GACzC,eAAe;GACf;GACA;GACD,CAAC;GACF;;;;AC1BJ,MAAa,iBAAiB,OAC5B,QACA,EAAE,UAAU,UAA8B,EAAE,KACzC;AACH,KAAI,QACF,MAAK,MAAM,SAAS,OAClB,OAAM,cAAc,MAAM;KAG5B,OAAM,QAAQ,IAAI,OAAO,KAAK,UAAU,cAAc,MAAM,CAAC,CAAC;;;;ACoBlE,SAAgB,eACd,QACA,kBAC+B;AAC/B,QAAO,OAAO,YACZ,OAAO,QAAQ,OAAO,CAAC,KAAK,CAAC,KAAK,WAAW,CAC3C,MAEC,GAAG,SAAgB;EAClB,MAAM,UAAU,OAAO,MAAM;EAE7B,MAAM,SAAS,MAAM,GAAG,KAAK;EAC7B,MAAM,UAAwB;GAAE;GAAS,GAAG,iBAAiB;GAAE;EAC/D,MAAM,kBAAkB,UAEtB,OAAO,UAAU,aAAa,MAAM,QAAQ,GAAG;EACjD,MAAM,iBAAiB,WACrB,OAAO,YAEL,OAAO,QAAQ,OAAO,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,eAAe,MAAM,CAAC,CAAC,CAC3E;AACH,SAAO,OACL,SACA,OAAO,WAAW,aAAa,OAAO,QAAQ,GAAG,cAAc,OAAO,CACvE;AACD,SAAO,OACL,SACA,OAAO,qBAAqB,aACxB,iBAAiB,QAAQ,GACzB,cAAc,iBAAiB,CACpC;AACD,SAAO,cAAc,QAA6B;GAErD,CAAC,CACH;;;;AC3DH,MAAa,uBACV,eAA8B,EAAE,QAAQ,UAAsC,EAAE,KACjF,OAAO,aAAuB;CAC5B,MAAM,WAA6B,EACjC,mBAAmB,EAAE,EACtB;CACD,IAAI,YAAY;AAChB,MAAK,MAAM,UAAU,SAAS,SAAS;AAErC,MAAI,WAAW;AACb,YAAS,kBAAkB,KAAK,EAC9B,gBAAgB,OAAO,WACxB,CAAC;AACF;;EAGF,IAAI;AACJ,MAAI;AACF,WAAQ,KAAK,MAAM,OAAO,KAAK;AAC/B,OAAI,MACF,QAAO,MAAM,EAAE,OAAO,EAAE,mBAAmB;GAE7C,IAAI,UAAU,cAAc,MAAM;AAClC,UAAO,OAAO,YAAY,SACxB,WAAU,cAAc;AAE1B,OAAI,OAAO,YAAY,YAAY;AACjC,oBAAgB;KACd,aAAa,MAAM;KACnB,eAAe,MAAM;KACrB,SAAS,MAAM;KAChB,CAAC;AACF,UAAM,QAAQ,MAAM;;WAEf,OAAO;AACd,eAAY;AACZ,UAAO,MAAM;IAAE;IAAO;IAAO,MAAM,OAAO;IAAM,EAAE,yBAAyB;AAC3E,YAAS,kBAAkB,KAAK,EAC9B,gBAAgB,OAAO,WACxB,CAAC;;;AAGN,QAAO;;;;AC1CX,MAAM,aAAa;;;;;AAWnB,SAAgB,oBAAoB,QAAmC;CACrE,MAAM,YAAY,IAAI,WAAW;CACjC,MAAM,cAAc,IAAI,kBAAkB,EAAE,CAAC;CAE7C,SAAS,WAAc,OAAY,WAA0B;EAC3D,MAAM,SAAgB,EAAE;AACxB,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,UACrC,QAAO,KAAK,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC;AAE5C,SAAO;;CAGT,eAAe,kBAAkB,cAA6B;AAC5D,QAAM,QAAQ,IAAI,OAAO,UAAU,KAAK,UAAU,iBAAiB,cAAc,MAAM,CAAC,CAAC;;CAG3F,eAAe,iBAAiB,cAA6B,OAAe;EAC1E,MAAM,UAAU,WAAW,cAAc,WAAW;AAEpD,OAAK,MAAM,SAAS,QAClB,KAAI;GACF,MAAM,UAAU,MAAM,KAAK,aAAa,WAAW;IACjD,IAAI,GAAG,YAAY,QAAQ,GAAG;IAC9B,aAAa,KAAK,UAAU,YAAY;IACxC,gBAAgB,YAAY;IAC5B,wBAAwB,YAAY;IACrC,EAAE;AAEH,SAAM,UAAU,KACd,IAAI,wBAAwB;IAC1B,UAAU;IACV,SAAS;IACV,CAAC,CACH;WACM,OAAO;AACd,UAAO,MAAM;IAAE;IAAO;IAAO;IAAO,EAAE,+BAA+B;AACrE,SAAM;;;CAKZ,eAAe,eAAe,cAA6B;EACzD,MAAM,UAAU,WAAW,cAAc,WAAW;AAEpD,OAAK,MAAM,SAAS,QAClB,KAAI;GACF,MAAM,UAAU,MAAM,KAAK,gBAAgB;AAGzC,WAAO;KACL,QAFa,YAAY,UAAU,UAAU,YAAY,cAAc,MAAM,IAAI,CAAC,GAAG;KAGrF,YAAY,YAAY;KACxB,QAAQ,KAAK,UAAU,YAAY;KACnC,cAAc,OAAO;KACtB;KACD;AAEF,SAAM,YAAY,KAChB,IAAI,iBAAiB,EACnB,SAAS,SACV,CAAC,CACH;WACM,OAAO;AACd,UAAO,MAAM;IAAE;IAAO;IAAO,EAAE,6BAA6B;AAC5D,SAAM;;;AAKZ,QAAO,OAAO,UAA8C;EAC1D,MAAM,eAAe,MAAM,QAAQ,QAAQ,WAAW,OAAO,cAAc,SAAS,CACjF,KAAK,WAAW;AACf,OAAI;IACF,MAAM,OAAO,OAAO,UAAU;AAC9B,WAAO,WAAW,KAAuC;YAClD,OAAO;AACd,WAAO,MAAM;KAAE;KAAO;KAAQ,EAAE,mCAAmC;AACnE;;IAEF,CACD,QAAQ,gBAA4C,aAAa,aAAa,QAAQ;AAEzF,MAAI,aAAa,SAAS,GAAG;GAC3B,MAAM,QAAyB,CAAC,kBAAkB,aAAa,CAAC;AAChE,OAAI,OAAO,QACT,OAAM,KAAK,eAAe,aAAa,CAAC;AAE1C,SAAM,QAAQ,IAAI,MAAM"}
@@ -0,0 +1,93 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _auriclabs_logger = require("@auriclabs/logger");
3
+ let _aws_sdk_client_eventbridge = require("@aws-sdk/client-eventbridge");
4
+ let _aws_sdk_client_sqs = require("@aws-sdk/client-sqs");
5
+ let _aws_sdk_util_dynamodb = require("@aws-sdk/util-dynamodb");
6
+ let lodash_es = require("lodash-es");
7
+ //#region src/stream-handler.ts
8
+ const BATCH_SIZE = 10;
9
+ /**
10
+ * Creates a Lambda handler for DynamoDB stream events.
11
+ * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.
12
+ */
13
+ function createStreamHandler(config) {
14
+ const sqsClient = new _aws_sdk_client_sqs.SQSClient();
15
+ const eventBridge = new _aws_sdk_client_eventbridge.EventBridgeClient({});
16
+ function chunkArray(array, chunkSize) {
17
+ const chunks = [];
18
+ for (let i = 0; i < array.length; i += chunkSize) chunks.push(array.slice(i, i + chunkSize));
19
+ return chunks;
20
+ }
21
+ async function sendToQueuesBatch(eventRecords) {
22
+ await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));
23
+ }
24
+ async function sendToQueueBatch(eventRecords, queue) {
25
+ const batches = chunkArray(eventRecords, BATCH_SIZE);
26
+ for (const batch of batches) try {
27
+ const entries = batch.map((eventRecord, index) => ({
28
+ Id: `${eventRecord.eventId}-${index}`,
29
+ MessageBody: JSON.stringify(eventRecord),
30
+ MessageGroupId: eventRecord.aggregateId,
31
+ MessageDeduplicationId: eventRecord.eventId
32
+ }));
33
+ await sqsClient.send(new _aws_sdk_client_sqs.SendMessageBatchCommand({
34
+ QueueUrl: queue,
35
+ Entries: entries
36
+ }));
37
+ } catch (error) {
38
+ _auriclabs_logger.logger.error({
39
+ error,
40
+ batch,
41
+ queue
42
+ }, "Error sending batch to queue");
43
+ throw error;
44
+ }
45
+ }
46
+ async function sendToBusBatch(eventRecords) {
47
+ const batches = chunkArray(eventRecords, BATCH_SIZE);
48
+ for (const batch of batches) try {
49
+ const entries = batch.map((eventRecord) => {
50
+ return {
51
+ Source: eventRecord.source ?? (0, lodash_es.kebabCase)(eventRecord.aggregateType.split(".")[0]),
52
+ DetailType: eventRecord.eventType,
53
+ Detail: JSON.stringify(eventRecord),
54
+ EventBusName: config.busName
55
+ };
56
+ });
57
+ await eventBridge.send(new _aws_sdk_client_eventbridge.PutEventsCommand({ Entries: entries }));
58
+ } catch (error) {
59
+ _auriclabs_logger.logger.error({
60
+ error,
61
+ batch
62
+ }, "Error sending batch to bus");
63
+ throw error;
64
+ }
65
+ }
66
+ return async (event) => {
67
+ const eventRecords = event.Records.filter((record) => record.eventName === "INSERT").map((record) => {
68
+ try {
69
+ const data = record.dynamodb?.NewImage;
70
+ return (0, _aws_sdk_util_dynamodb.unmarshall)(data);
71
+ } catch (error) {
72
+ _auriclabs_logger.logger.error({
73
+ error,
74
+ record
75
+ }, "Error unmarshalling event record");
76
+ return;
77
+ }
78
+ }).filter((eventRecord) => eventRecord?.itemType === "event");
79
+ if (eventRecords.length > 0) {
80
+ const tasks = [sendToQueuesBatch(eventRecords)];
81
+ if (config.busName) tasks.push(sendToBusBatch(eventRecords));
82
+ await Promise.all(tasks);
83
+ }
84
+ };
85
+ }
86
+ //#endregion
87
+ //#region src/stream-handler.entry.ts
88
+ const handler = createStreamHandler({
89
+ busName: process.env.EVENT_BUS_NAME,
90
+ queueUrls: JSON.parse(process.env.QUEUE_URL_LIST ?? "[]")
91
+ });
92
+ //#endregion
93
+ exports.handler = handler;
@@ -0,0 +1,191 @@
1
+ import { Writable } from "node:stream";
2
+
3
+ //#region ../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/handler.d.ts
4
+ /**
5
+ * {@link Handler} context parameter.
6
+ * See {@link https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html AWS documentation}.
7
+ */
8
+ interface Context {
9
+ callbackWaitsForEmptyEventLoop: boolean;
10
+ functionName: string;
11
+ functionVersion: string;
12
+ invokedFunctionArn: string;
13
+ memoryLimitInMB: string;
14
+ awsRequestId: string;
15
+ logGroupName: string;
16
+ logStreamName: string;
17
+ identity?: CognitoIdentity | undefined;
18
+ clientContext?: ClientContext | undefined;
19
+ tenantId?: string | undefined;
20
+ getRemainingTimeInMillis(): number; // Functions for compatibility with earlier Node.js Runtime v0.10.42
21
+ // No longer documented, so they are deprecated, but they still work
22
+ // as of the 12.x runtime, so they are not removed from the types.
23
+ /** @deprecated Use handler callback or promise result */
24
+ done(error?: Error, result?: any): void;
25
+ /** @deprecated Use handler callback with first argument or reject a promise result */
26
+ fail(error: Error | string): void;
27
+ /** @deprecated Use handler callback with second argument or resolve a promise result */
28
+ succeed(messageOrObject: any): void; // Unclear what behavior this is supposed to have, I couldn't find any still extant reference,
29
+ // and it behaves like the above, ignoring the object parameter.
30
+ /** @deprecated Use handler callback or promise result */
31
+ succeed(message: string, object: any): void;
32
+ }
33
+ interface CognitoIdentity {
34
+ cognitoIdentityId: string;
35
+ cognitoIdentityPoolId: string;
36
+ }
37
+ interface ClientContext {
38
+ client: ClientContextClient;
39
+ Custom?: any;
40
+ env: ClientContextEnv;
41
+ }
42
+ interface ClientContextClient {
43
+ installationId: string;
44
+ appTitle: string;
45
+ appVersionName: string;
46
+ appVersionCode: string;
47
+ appPackageName: string;
48
+ }
49
+ interface ClientContextEnv {
50
+ platformVersion: string;
51
+ platform: string;
52
+ make: string;
53
+ model: string;
54
+ locale: string;
55
+ }
56
+ /**
57
+ * Interface for using response streaming from AWS Lambda.
58
+ * To indicate to the runtime that Lambda should stream your function’s responses, you must wrap your function handler with the `awslambda.streamifyResponse()` decorator.
59
+ *
60
+ * The `streamifyResponse` decorator accepts the following additional parameter, `responseStream`, besides the default node handler parameters, `event`, and `context`.
61
+ * The new `responseStream` object provides a stream object that your function can write data to. Data written to this stream is sent immediately to the client. You can optionally set the Content-Type header of the response to pass additional metadata to your client about the contents of the stream.
62
+ *
63
+ * {@link https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/ AWS blog post}
64
+ * {@link https://docs.aws.amazon.com/lambda/latest/dg/config-rs-write-functions.html AWS documentation}
65
+ *
66
+ * @example <caption>Writing to the response stream</caption>
67
+ * import 'aws-lambda';
68
+ *
69
+ * export const handler = awslambda.streamifyResponse(
70
+ * async (event, responseStream, context) => {
71
+ * responseStream.setContentType("text/plain");
72
+ * responseStream.write("Hello, world!");
73
+ * responseStream.end();
74
+ * }
75
+ * );
76
+ *
77
+ * @example <caption>Using pipeline</caption>
78
+ * import 'aws-lambda';
79
+ * import { Readable } from 'stream';
80
+ * import { pipeline } from 'stream/promises';
81
+ * import zlib from 'zlib';
82
+ *
83
+ * export const handler = awslambda.streamifyResponse(
84
+ * async (event, responseStream, context) => {
85
+ * // As an example, convert event to a readable stream.
86
+ * const requestStream = Readable.from(Buffer.from(JSON.stringify(event)));
87
+ *
88
+ * await pipeline(requestStream, zlib.createGzip(), responseStream);
89
+ * }
90
+ * );
91
+ */
92
+ type StreamifyHandler<TEvent = any, TResult = any> = (event: TEvent, responseStream: awslambda.HttpResponseStream, context: Context) => TResult | Promise<TResult>;
93
+ declare global {
94
+ namespace awslambda {
95
+ class HttpResponseStream extends Writable {
96
+ static from(writable: Writable, metadata: Record<string, unknown>): HttpResponseStream;
97
+ setContentType: (contentType: string) => void;
98
+ }
99
+ /**
100
+ * Decorator for using response streaming from AWS Lambda.
101
+ * To indicate to the runtime that Lambda should stream your function’s responses, you must wrap your function handler with the `awslambda.streamifyResponse()` decorator.
102
+ *
103
+ * The `streamifyResponse` decorator accepts the following additional parameter, `responseStream`, besides the default node handler parameters, `event`, and `context`.
104
+ * The new `responseStream` object provides a stream object that your function can write data to. Data written to this stream is sent immediately to the client. You can optionally set the Content-Type header of the response to pass additional metadata to your client about the contents of the stream.
105
+ *
106
+ * {@link https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/ AWS blog post}
107
+ * {@link https://docs.aws.amazon.com/lambda/latest/dg/config-rs-write-functions.html AWS documentation}
108
+ *
109
+ * @example <caption>Writing to the response stream</caption>
110
+ * import 'aws-lambda';
111
+ *
112
+ * export const handler = awslambda.streamifyResponse(
113
+ * async (event, responseStream, context) => {
114
+ * responseStream.setContentType("text/plain");
115
+ * responseStream.write("Hello, world!");
116
+ * responseStream.end();
117
+ * }
118
+ * );
119
+ *
120
+ * @example <caption>Using pipeline</caption>
121
+ * import 'aws-lambda';
122
+ * import { Readable } from 'stream';
123
+ * import { pipeline } from 'stream/promises';
124
+ * import zlib from 'zlib';
125
+ *
126
+ * export const handler = awslambda.streamifyResponse(
127
+ * async (event, responseStream, context) => {
128
+ * // As an example, convert event to a readable stream.
129
+ * const requestStream = Readable.from(Buffer.from(JSON.stringify(event)));
130
+ *
131
+ * await pipeline(requestStream, zlib.createGzip(), responseStream);
132
+ * }
133
+ * );
134
+ */
135
+ function streamifyResponse<TEvent = any, TResult = void>(handler: StreamifyHandler<TEvent, TResult>): StreamifyHandler<TEvent, TResult>;
136
+ }
137
+ }
138
+ //#endregion
139
+ //#region ../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/trigger/dynamodb-stream.d.ts
140
+ // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html
141
+ interface AttributeValue {
142
+ B?: string | undefined;
143
+ BS?: string[] | undefined;
144
+ BOOL?: boolean | undefined;
145
+ L?: AttributeValue[] | undefined;
146
+ M?: {
147
+ [id: string]: AttributeValue;
148
+ } | undefined;
149
+ N?: string | undefined;
150
+ NS?: string[] | undefined;
151
+ NULL?: boolean | undefined;
152
+ S?: string | undefined;
153
+ SS?: string[] | undefined;
154
+ }
155
+ // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_StreamRecord.html
156
+ interface StreamRecord {
157
+ ApproximateCreationDateTime?: number | undefined;
158
+ Keys?: {
159
+ [key: string]: AttributeValue;
160
+ } | undefined;
161
+ NewImage?: {
162
+ [key: string]: AttributeValue;
163
+ } | undefined;
164
+ OldImage?: {
165
+ [key: string]: AttributeValue;
166
+ } | undefined;
167
+ SequenceNumber?: string | undefined;
168
+ SizeBytes?: number | undefined;
169
+ StreamViewType?: "KEYS_ONLY" | "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | undefined;
170
+ }
171
+ // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_Record.html
172
+ interface DynamoDBRecord {
173
+ awsRegion?: string | undefined;
174
+ dynamodb?: StreamRecord | undefined;
175
+ eventID?: string | undefined;
176
+ eventName?: "INSERT" | "MODIFY" | "REMOVE" | undefined;
177
+ eventSource?: string | undefined;
178
+ eventSourceARN?: string | undefined;
179
+ eventVersion?: string | undefined;
180
+ userIdentity?: any;
181
+ }
182
+ // http://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-ddb-update
183
+ interface DynamoDBStreamEvent {
184
+ Records: DynamoDBRecord[];
185
+ }
186
+ //#endregion
187
+ //#region src/stream-handler.entry.d.ts
188
+ declare const handler: (event: DynamoDBStreamEvent) => Promise<void>;
189
+ //#endregion
190
+ export { handler };
191
+ //# sourceMappingURL=stream-handler.entry.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.entry.d.cts","names":["Writable","Handler","TEvent","TResult","Context","Callback","Promise","event","context","callback","CognitoIdentity","ClientContext","Error","callbackWaitsForEmptyEventLoop","functionName","functionVersion","invokedFunctionArn","memoryLimitInMB","awsRequestId","logGroupName","logStreamName","identity","clientContext","tenantId","getRemainingTimeInMillis","done","error","result","fail","succeed","messageOrObject","message","object","cognitoIdentityId","cognitoIdentityPoolId","ClientContextClient","ClientContextEnv","client","Custom","env","installationId","appTitle","appVersionName","appVersionCode","appPackageName","platformVersion","platform","make","model","locale","StreamifyHandler","awslambda","HttpResponseStream","responseStream","_0","Record","global","from","writable","metadata","setContentType","contentType","streamifyResponse","handler","sideEffect","Handler","DynamoDBStreamHandler","DynamoDBStreamEvent","DynamoDBBatchResponse","AttributeValue","B","BS","BOOL","L","M","id","N","NS","NULL","S","SS","StreamRecord","ApproximateCreationDateTime","Keys","key","NewImage","OldImage","SequenceNumber","SizeBytes","StreamViewType","DynamoDBRecord","awsRegion","dynamodb","eventID","eventName","eventSource","eventSourceARN","eventVersion","userIdentity","Records","DynamoDBBatchItemFailure","batchItemFailures","itemIdentifier"],"sources":["../../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/handler.d.ts","../../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/trigger/dynamodb-stream.d.ts","../src/stream-handler.entry.ts"],"x_google_ignoreList":[0,1],"mappings":";;;;;;;UA+FiBI,OAAAA;EACbS,8BAAAA;EACAC,YAAAA;EACAC,eAAAA;EACAC,kBAAAA;EACAC,eAAAA;EACAC,YAAAA;EACAC,YAAAA;EACAC,aAAAA;EACAC,QAAAA,GAAWX,eAAAA;EACXY,aAAAA,GAAgBX,aAAAA;EAChBY,QAAAA;EAEAC,wBAAAA;EAAAA;EAAAA;EA6JgCrB;EAtJhCsB,IAAAA,CAAKC,KAAAA,GAAQd,KAAAA,EAAOe,MAAAA;EAsJG;EApJvBC,IAAAA,CAAKF,KAAAA,EAAOd,KAAAA;EAoGR4C;EAlGJ3B,OAAAA,CAAQC,eAAAA;EAAAA;EAqGO2B;EAjGf5B,OAAAA,CAAQE,OAAAA,UAAiBC,MAAAA;AAAAA;AAAAA,UAGZtB,eAAAA;EACbuB,iBAAAA;EACAC,qBAAAA;AAAAA;AAAAA,UAGavB,aAAAA;EACb0B,MAAAA,EAAQF,mBAAAA;EACRG,MAAAA;EACAC,GAAAA,EAAKH,gBAAAA;AAAAA;AAAAA,UAGQD,mBAAAA;EACbK,cAAAA;EACAC,QAAAA;EACAC,cAAAA;EACAC,cAAAA;EACAC,cAAAA;AAAAA;AAAAA,UAGaR,gBAAAA;EACbS,eAAAA;EACAC,QAAAA;EACAC,IAAAA;EACAC,KAAAA;EACAC,MAAAA;AAAAA;;;;;;;;;;;;;;ACvHJ;;;;;;;;;;;;;;;AAYA;;;;;;;;KDwKYC,gBAAAA,iCACR3C,KAAAA,EAAOL,MAAAA,EACPmD,cAAAA,EAAgBF,SAAAA,CAAUC,kBAAAA,EAC1B5C,OAAAA,EAASJ,OAAAA,KACRD,OAAAA,GAAUG,OAAAA,CAAQH,OAAAA;AAAAA,QAEfqD,MAAAA;EAAAA,UACML,SAAAA;IAAAA,MACAC,kBAAAA,SAA2BpD,QAAAA;MAAAA,OACtByD,IAAAA,CACHC,QAAAA,EAAU1D,QAAAA,EACV2D,QAAAA,EAAUJ,MAAAA,oBACXH,kBAAAA;MACHQ,cAAAA,GAAiBC,WAAAA;IAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAuCZC,iBAAAA,8BAAAA,CACLC,OAAAA,EAASb,gBAAAA,CAAiBhD,MAAAA,EAAQC,OAAAA,IACnC+C,gBAAAA,CAAiBhD,MAAAA,EAAQC,OAAAA;EAAAA;AAAAA;;;;UCnQnBkE,cAAAA;EACbC,CAAAA;EACAC,EAAAA;EACAC,IAAAA;EACAC,CAAAA,GAAIJ,cAAAA;EACJK,CAAAA;IAAAA,CAAOC,EAAAA,WAAaN,cAAAA;EAAAA;EACpBO,CAAAA;EACAC,EAAAA;EACAC,IAAAA;EACAC,CAAAA;EACAC,EAAAA;AAAAA;AAAAA;AAAAA,UAIaC,YAAAA;EACbC,2BAAAA;EACAC,IAAAA;IAAAA,CAAUC,GAAAA,WAAcf,cAAAA;EAAAA;EACxBgB,QAAAA;IAAAA,CAAcD,GAAAA,WAAcf,cAAAA;EAAAA;EAC5BiB,QAAAA;IAAAA,CAAcF,GAAAA,WAAcf,cAAAA;EAAAA;EAC5BkB,cAAAA;EACAC,SAAAA;EACAC,cAAAA;AAAAA;AAAAA;AAAAA,UAIaC,cAAAA;EACbC,SAAAA;EACAC,QAAAA,GAAWX,YAAAA;EACXY,OAAAA;EACAC,SAAAA;EACAC,WAAAA;EACAC,cAAAA;EACAC,YAAAA;EACAC,YAAAA;AAAAA;AAAAA;AAAAA,UAIa/B,mBAAAA;EACbgC,OAAAA,EAAST,cAAAA;AAAAA;;;cC1CA,OAAA,GAAO,KAAA,EAGlB,mBAAA,KAHkB,OAAA"}
@@ -0,0 +1,191 @@
1
+ import { Writable } from "node:stream";
2
+
3
+ //#region ../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/handler.d.ts
4
+ /**
5
+ * {@link Handler} context parameter.
6
+ * See {@link https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-context.html AWS documentation}.
7
+ */
8
+ interface Context {
9
+ callbackWaitsForEmptyEventLoop: boolean;
10
+ functionName: string;
11
+ functionVersion: string;
12
+ invokedFunctionArn: string;
13
+ memoryLimitInMB: string;
14
+ awsRequestId: string;
15
+ logGroupName: string;
16
+ logStreamName: string;
17
+ identity?: CognitoIdentity | undefined;
18
+ clientContext?: ClientContext | undefined;
19
+ tenantId?: string | undefined;
20
+ getRemainingTimeInMillis(): number; // Functions for compatibility with earlier Node.js Runtime v0.10.42
21
+ // No longer documented, so they are deprecated, but they still work
22
+ // as of the 12.x runtime, so they are not removed from the types.
23
+ /** @deprecated Use handler callback or promise result */
24
+ done(error?: Error, result?: any): void;
25
+ /** @deprecated Use handler callback with first argument or reject a promise result */
26
+ fail(error: Error | string): void;
27
+ /** @deprecated Use handler callback with second argument or resolve a promise result */
28
+ succeed(messageOrObject: any): void; // Unclear what behavior this is supposed to have, I couldn't find any still extant reference,
29
+ // and it behaves like the above, ignoring the object parameter.
30
+ /** @deprecated Use handler callback or promise result */
31
+ succeed(message: string, object: any): void;
32
+ }
33
+ interface CognitoIdentity {
34
+ cognitoIdentityId: string;
35
+ cognitoIdentityPoolId: string;
36
+ }
37
+ interface ClientContext {
38
+ client: ClientContextClient;
39
+ Custom?: any;
40
+ env: ClientContextEnv;
41
+ }
42
+ interface ClientContextClient {
43
+ installationId: string;
44
+ appTitle: string;
45
+ appVersionName: string;
46
+ appVersionCode: string;
47
+ appPackageName: string;
48
+ }
49
+ interface ClientContextEnv {
50
+ platformVersion: string;
51
+ platform: string;
52
+ make: string;
53
+ model: string;
54
+ locale: string;
55
+ }
56
+ /**
57
+ * Interface for using response streaming from AWS Lambda.
58
+ * To indicate to the runtime that Lambda should stream your function’s responses, you must wrap your function handler with the `awslambda.streamifyResponse()` decorator.
59
+ *
60
+ * The `streamifyResponse` decorator accepts the following additional parameter, `responseStream`, besides the default node handler parameters, `event`, and `context`.
61
+ * The new `responseStream` object provides a stream object that your function can write data to. Data written to this stream is sent immediately to the client. You can optionally set the Content-Type header of the response to pass additional metadata to your client about the contents of the stream.
62
+ *
63
+ * {@link https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/ AWS blog post}
64
+ * {@link https://docs.aws.amazon.com/lambda/latest/dg/config-rs-write-functions.html AWS documentation}
65
+ *
66
+ * @example <caption>Writing to the response stream</caption>
67
+ * import 'aws-lambda';
68
+ *
69
+ * export const handler = awslambda.streamifyResponse(
70
+ * async (event, responseStream, context) => {
71
+ * responseStream.setContentType("text/plain");
72
+ * responseStream.write("Hello, world!");
73
+ * responseStream.end();
74
+ * }
75
+ * );
76
+ *
77
+ * @example <caption>Using pipeline</caption>
78
+ * import 'aws-lambda';
79
+ * import { Readable } from 'stream';
80
+ * import { pipeline } from 'stream/promises';
81
+ * import zlib from 'zlib';
82
+ *
83
+ * export const handler = awslambda.streamifyResponse(
84
+ * async (event, responseStream, context) => {
85
+ * // As an example, convert event to a readable stream.
86
+ * const requestStream = Readable.from(Buffer.from(JSON.stringify(event)));
87
+ *
88
+ * await pipeline(requestStream, zlib.createGzip(), responseStream);
89
+ * }
90
+ * );
91
+ */
92
+ type StreamifyHandler<TEvent = any, TResult = any> = (event: TEvent, responseStream: awslambda.HttpResponseStream, context: Context) => TResult | Promise<TResult>;
93
+ declare global {
94
+ namespace awslambda {
95
+ class HttpResponseStream extends Writable {
96
+ static from(writable: Writable, metadata: Record<string, unknown>): HttpResponseStream;
97
+ setContentType: (contentType: string) => void;
98
+ }
99
+ /**
100
+ * Decorator for using response streaming from AWS Lambda.
101
+ * To indicate to the runtime that Lambda should stream your function’s responses, you must wrap your function handler with the `awslambda.streamifyResponse()` decorator.
102
+ *
103
+ * The `streamifyResponse` decorator accepts the following additional parameter, `responseStream`, besides the default node handler parameters, `event`, and `context`.
104
+ * The new `responseStream` object provides a stream object that your function can write data to. Data written to this stream is sent immediately to the client. You can optionally set the Content-Type header of the response to pass additional metadata to your client about the contents of the stream.
105
+ *
106
+ * {@link https://aws.amazon.com/blogs/compute/introducing-aws-lambda-response-streaming/ AWS blog post}
107
+ * {@link https://docs.aws.amazon.com/lambda/latest/dg/config-rs-write-functions.html AWS documentation}
108
+ *
109
+ * @example <caption>Writing to the response stream</caption>
110
+ * import 'aws-lambda';
111
+ *
112
+ * export const handler = awslambda.streamifyResponse(
113
+ * async (event, responseStream, context) => {
114
+ * responseStream.setContentType("text/plain");
115
+ * responseStream.write("Hello, world!");
116
+ * responseStream.end();
117
+ * }
118
+ * );
119
+ *
120
+ * @example <caption>Using pipeline</caption>
121
+ * import 'aws-lambda';
122
+ * import { Readable } from 'stream';
123
+ * import { pipeline } from 'stream/promises';
124
+ * import zlib from 'zlib';
125
+ *
126
+ * export const handler = awslambda.streamifyResponse(
127
+ * async (event, responseStream, context) => {
128
+ * // As an example, convert event to a readable stream.
129
+ * const requestStream = Readable.from(Buffer.from(JSON.stringify(event)));
130
+ *
131
+ * await pipeline(requestStream, zlib.createGzip(), responseStream);
132
+ * }
133
+ * );
134
+ */
135
+ function streamifyResponse<TEvent = any, TResult = void>(handler: StreamifyHandler<TEvent, TResult>): StreamifyHandler<TEvent, TResult>;
136
+ }
137
+ }
138
+ //#endregion
139
+ //#region ../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/trigger/dynamodb-stream.d.ts
140
+ // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_AttributeValue.html
141
+ interface AttributeValue {
142
+ B?: string | undefined;
143
+ BS?: string[] | undefined;
144
+ BOOL?: boolean | undefined;
145
+ L?: AttributeValue[] | undefined;
146
+ M?: {
147
+ [id: string]: AttributeValue;
148
+ } | undefined;
149
+ N?: string | undefined;
150
+ NS?: string[] | undefined;
151
+ NULL?: boolean | undefined;
152
+ S?: string | undefined;
153
+ SS?: string[] | undefined;
154
+ }
155
+ // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_StreamRecord.html
156
+ interface StreamRecord {
157
+ ApproximateCreationDateTime?: number | undefined;
158
+ Keys?: {
159
+ [key: string]: AttributeValue;
160
+ } | undefined;
161
+ NewImage?: {
162
+ [key: string]: AttributeValue;
163
+ } | undefined;
164
+ OldImage?: {
165
+ [key: string]: AttributeValue;
166
+ } | undefined;
167
+ SequenceNumber?: string | undefined;
168
+ SizeBytes?: number | undefined;
169
+ StreamViewType?: "KEYS_ONLY" | "NEW_IMAGE" | "OLD_IMAGE" | "NEW_AND_OLD_IMAGES" | undefined;
170
+ }
171
+ // http://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_streams_Record.html
172
+ interface DynamoDBRecord {
173
+ awsRegion?: string | undefined;
174
+ dynamodb?: StreamRecord | undefined;
175
+ eventID?: string | undefined;
176
+ eventName?: "INSERT" | "MODIFY" | "REMOVE" | undefined;
177
+ eventSource?: string | undefined;
178
+ eventSourceARN?: string | undefined;
179
+ eventVersion?: string | undefined;
180
+ userIdentity?: any;
181
+ }
182
+ // http://docs.aws.amazon.com/lambda/latest/dg/eventsources.html#eventsources-ddb-update
183
+ interface DynamoDBStreamEvent {
184
+ Records: DynamoDBRecord[];
185
+ }
186
+ //#endregion
187
+ //#region src/stream-handler.entry.d.ts
188
+ declare const handler: (event: DynamoDBStreamEvent) => Promise<void>;
189
+ //#endregion
190
+ export { handler };
191
+ //# sourceMappingURL=stream-handler.entry.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.entry.d.mts","names":["Writable","Handler","TEvent","TResult","Context","Callback","Promise","event","context","callback","CognitoIdentity","ClientContext","Error","callbackWaitsForEmptyEventLoop","functionName","functionVersion","invokedFunctionArn","memoryLimitInMB","awsRequestId","logGroupName","logStreamName","identity","clientContext","tenantId","getRemainingTimeInMillis","done","error","result","fail","succeed","messageOrObject","message","object","cognitoIdentityId","cognitoIdentityPoolId","ClientContextClient","ClientContextEnv","client","Custom","env","installationId","appTitle","appVersionName","appVersionCode","appPackageName","platformVersion","platform","make","model","locale","StreamifyHandler","awslambda","HttpResponseStream","responseStream","_0","Record","global","from","writable","metadata","setContentType","contentType","streamifyResponse","handler","sideEffect","Handler","DynamoDBStreamHandler","DynamoDBStreamEvent","DynamoDBBatchResponse","AttributeValue","B","BS","BOOL","L","M","id","N","NS","NULL","S","SS","StreamRecord","ApproximateCreationDateTime","Keys","key","NewImage","OldImage","SequenceNumber","SizeBytes","StreamViewType","DynamoDBRecord","awsRegion","dynamodb","eventID","eventName","eventSource","eventSourceARN","eventVersion","userIdentity","Records","DynamoDBBatchItemFailure","batchItemFailures","itemIdentifier"],"sources":["../../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/handler.d.ts","../../../node_modules/.pnpm/@types+aws-lambda@8.10.152/node_modules/@types/aws-lambda/trigger/dynamodb-stream.d.ts","../src/stream-handler.entry.ts"],"x_google_ignoreList":[0,1],"mappings":";;;;;;;UA+FiBI,OAAAA;EACbS,8BAAAA;EACAC,YAAAA;EACAC,eAAAA;EACAC,kBAAAA;EACAC,eAAAA;EACAC,YAAAA;EACAC,YAAAA;EACAC,aAAAA;EACAC,QAAAA,GAAWX,eAAAA;EACXY,aAAAA,GAAgBX,aAAAA;EAChBY,QAAAA;EAEAC,wBAAAA;EAAAA;EAAAA;EA6JgCrB;EAtJhCsB,IAAAA,CAAKC,KAAAA,GAAQd,KAAAA,EAAOe,MAAAA;EAsJG;EApJvBC,IAAAA,CAAKF,KAAAA,EAAOd,KAAAA;EAoGR4C;EAlGJ3B,OAAAA,CAAQC,eAAAA;EAAAA;EAqGO2B;EAjGf5B,OAAAA,CAAQE,OAAAA,UAAiBC,MAAAA;AAAAA;AAAAA,UAGZtB,eAAAA;EACbuB,iBAAAA;EACAC,qBAAAA;AAAAA;AAAAA,UAGavB,aAAAA;EACb0B,MAAAA,EAAQF,mBAAAA;EACRG,MAAAA;EACAC,GAAAA,EAAKH,gBAAAA;AAAAA;AAAAA,UAGQD,mBAAAA;EACbK,cAAAA;EACAC,QAAAA;EACAC,cAAAA;EACAC,cAAAA;EACAC,cAAAA;AAAAA;AAAAA,UAGaR,gBAAAA;EACbS,eAAAA;EACAC,QAAAA;EACAC,IAAAA;EACAC,KAAAA;EACAC,MAAAA;AAAAA;;;;;;;;;;;;;;ACvHJ;;;;;;;;;;;;;;;AAYA;;;;;;;;KDwKYC,gBAAAA,iCACR3C,KAAAA,EAAOL,MAAAA,EACPmD,cAAAA,EAAgBF,SAAAA,CAAUC,kBAAAA,EAC1B5C,OAAAA,EAASJ,OAAAA,KACRD,OAAAA,GAAUG,OAAAA,CAAQH,OAAAA;AAAAA,QAEfqD,MAAAA;EAAAA,UACML,SAAAA;IAAAA,MACAC,kBAAAA,SAA2BpD,QAAAA;MAAAA,OACtByD,IAAAA,CACHC,QAAAA,EAAU1D,QAAAA,EACV2D,QAAAA,EAAUJ,MAAAA,oBACXH,kBAAAA;MACHQ,cAAAA,GAAiBC,WAAAA;IAAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;aAuCZC,iBAAAA,8BAAAA,CACLC,OAAAA,EAASb,gBAAAA,CAAiBhD,MAAAA,EAAQC,OAAAA,IACnC+C,gBAAAA,CAAiBhD,MAAAA,EAAQC,OAAAA;EAAAA;AAAAA;;;;UCnQnBkE,cAAAA;EACbC,CAAAA;EACAC,EAAAA;EACAC,IAAAA;EACAC,CAAAA,GAAIJ,cAAAA;EACJK,CAAAA;IAAAA,CAAOC,EAAAA,WAAaN,cAAAA;EAAAA;EACpBO,CAAAA;EACAC,EAAAA;EACAC,IAAAA;EACAC,CAAAA;EACAC,EAAAA;AAAAA;AAAAA;AAAAA,UAIaC,YAAAA;EACbC,2BAAAA;EACAC,IAAAA;IAAAA,CAAUC,GAAAA,WAAcf,cAAAA;EAAAA;EACxBgB,QAAAA;IAAAA,CAAcD,GAAAA,WAAcf,cAAAA;EAAAA;EAC5BiB,QAAAA;IAAAA,CAAcF,GAAAA,WAAcf,cAAAA;EAAAA;EAC5BkB,cAAAA;EACAC,SAAAA;EACAC,cAAAA;AAAAA;AAAAA;AAAAA,UAIaC,cAAAA;EACbC,SAAAA;EACAC,QAAAA,GAAWX,YAAAA;EACXY,OAAAA;EACAC,SAAAA;EACAC,WAAAA;EACAC,cAAAA;EACAC,YAAAA;EACAC,YAAAA;AAAAA;AAAAA;AAAAA,UAIa/B,mBAAAA;EACbgC,OAAAA,EAAST,cAAAA;AAAAA;;;cC1CA,OAAA,GAAO,KAAA,EAGlB,mBAAA,KAHkB,OAAA"}
@@ -0,0 +1,94 @@
1
+ import { logger } from "@auriclabs/logger";
2
+ import { EventBridgeClient, PutEventsCommand } from "@aws-sdk/client-eventbridge";
3
+ import { SQSClient, SendMessageBatchCommand } from "@aws-sdk/client-sqs";
4
+ import { unmarshall } from "@aws-sdk/util-dynamodb";
5
+ import { kebabCase } from "lodash-es";
6
+ //#region src/stream-handler.ts
7
+ const BATCH_SIZE = 10;
8
+ /**
9
+ * Creates a Lambda handler for DynamoDB stream events.
10
+ * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.
11
+ */
12
+ function createStreamHandler(config) {
13
+ const sqsClient = new SQSClient();
14
+ const eventBridge = new EventBridgeClient({});
15
+ function chunkArray(array, chunkSize) {
16
+ const chunks = [];
17
+ for (let i = 0; i < array.length; i += chunkSize) chunks.push(array.slice(i, i + chunkSize));
18
+ return chunks;
19
+ }
20
+ async function sendToQueuesBatch(eventRecords) {
21
+ await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));
22
+ }
23
+ async function sendToQueueBatch(eventRecords, queue) {
24
+ const batches = chunkArray(eventRecords, BATCH_SIZE);
25
+ for (const batch of batches) try {
26
+ const entries = batch.map((eventRecord, index) => ({
27
+ Id: `${eventRecord.eventId}-${index}`,
28
+ MessageBody: JSON.stringify(eventRecord),
29
+ MessageGroupId: eventRecord.aggregateId,
30
+ MessageDeduplicationId: eventRecord.eventId
31
+ }));
32
+ await sqsClient.send(new SendMessageBatchCommand({
33
+ QueueUrl: queue,
34
+ Entries: entries
35
+ }));
36
+ } catch (error) {
37
+ logger.error({
38
+ error,
39
+ batch,
40
+ queue
41
+ }, "Error sending batch to queue");
42
+ throw error;
43
+ }
44
+ }
45
+ async function sendToBusBatch(eventRecords) {
46
+ const batches = chunkArray(eventRecords, BATCH_SIZE);
47
+ for (const batch of batches) try {
48
+ const entries = batch.map((eventRecord) => {
49
+ return {
50
+ Source: eventRecord.source ?? kebabCase(eventRecord.aggregateType.split(".")[0]),
51
+ DetailType: eventRecord.eventType,
52
+ Detail: JSON.stringify(eventRecord),
53
+ EventBusName: config.busName
54
+ };
55
+ });
56
+ await eventBridge.send(new PutEventsCommand({ Entries: entries }));
57
+ } catch (error) {
58
+ logger.error({
59
+ error,
60
+ batch
61
+ }, "Error sending batch to bus");
62
+ throw error;
63
+ }
64
+ }
65
+ return async (event) => {
66
+ const eventRecords = event.Records.filter((record) => record.eventName === "INSERT").map((record) => {
67
+ try {
68
+ const data = record.dynamodb?.NewImage;
69
+ return unmarshall(data);
70
+ } catch (error) {
71
+ logger.error({
72
+ error,
73
+ record
74
+ }, "Error unmarshalling event record");
75
+ return;
76
+ }
77
+ }).filter((eventRecord) => eventRecord?.itemType === "event");
78
+ if (eventRecords.length > 0) {
79
+ const tasks = [sendToQueuesBatch(eventRecords)];
80
+ if (config.busName) tasks.push(sendToBusBatch(eventRecords));
81
+ await Promise.all(tasks);
82
+ }
83
+ };
84
+ }
85
+ //#endregion
86
+ //#region src/stream-handler.entry.ts
87
+ const handler = createStreamHandler({
88
+ busName: process.env.EVENT_BUS_NAME,
89
+ queueUrls: JSON.parse(process.env.QUEUE_URL_LIST ?? "[]")
90
+ });
91
+ //#endregion
92
+ export { handler };
93
+
94
+ //# sourceMappingURL=stream-handler.entry.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.entry.mjs","names":[],"sources":["../src/stream-handler.ts","../src/stream-handler.entry.ts"],"sourcesContent":["import { logger } from '@auriclabs/logger';\nimport { AttributeValue } from '@aws-sdk/client-dynamodb';\nimport { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';\nimport { SendMessageBatchCommand, SQSClient } from '@aws-sdk/client-sqs';\nimport { unmarshall } from '@aws-sdk/util-dynamodb';\nimport { DynamoDBStreamEvent } from 'aws-lambda';\nimport { kebabCase } from 'lodash-es';\n\nimport { AggregateHead, EventRecord } from './types';\n\nconst BATCH_SIZE = 10;\n\nexport interface CreateStreamHandlerConfig {\n busName?: string;\n queueUrls: string[];\n}\n\n/**\n * Creates a Lambda handler for DynamoDB stream events.\n * Processes INSERT events from the event store table and forwards them to SQS queues and EventBridge.\n */\nexport function createStreamHandler(config: CreateStreamHandlerConfig) {\n const sqsClient = new SQSClient();\n const eventBridge = new EventBridgeClient({});\n\n function chunkArray<T>(array: T[], chunkSize: number): T[][] {\n const chunks: T[][] = [];\n for (let i = 0; i < array.length; i += chunkSize) {\n chunks.push(array.slice(i, i + chunkSize));\n }\n return chunks;\n }\n\n async function sendToQueuesBatch(eventRecords: EventRecord[]) {\n await Promise.all(config.queueUrls.map((queue) => sendToQueueBatch(eventRecords, queue)));\n }\n\n async function sendToQueueBatch(eventRecords: EventRecord[], queue: string) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord, index) => ({\n Id: `${eventRecord.eventId}-${index}`,\n MessageBody: JSON.stringify(eventRecord),\n MessageGroupId: eventRecord.aggregateId,\n MessageDeduplicationId: eventRecord.eventId,\n }));\n\n await sqsClient.send(\n new SendMessageBatchCommand({\n QueueUrl: queue,\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch, queue }, 'Error sending batch to queue');\n throw error;\n }\n }\n }\n\n async function sendToBusBatch(eventRecords: EventRecord[]) {\n const batches = chunkArray(eventRecords, BATCH_SIZE);\n\n for (const batch of batches) {\n try {\n const entries = batch.map((eventRecord) => {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n const source = eventRecord.source ?? kebabCase(eventRecord.aggregateType.split('.')[0]);\n return {\n Source: source,\n DetailType: eventRecord.eventType,\n Detail: JSON.stringify(eventRecord),\n EventBusName: config.busName,\n };\n });\n\n await eventBridge.send(\n new PutEventsCommand({\n Entries: entries,\n }),\n );\n } catch (error) {\n logger.error({ error, batch }, 'Error sending batch to bus');\n throw error;\n }\n }\n }\n\n return async (event: DynamoDBStreamEvent): Promise<void> => {\n const eventRecords = event.Records.filter((record) => record.eventName === 'INSERT')\n .map((record) => {\n try {\n const data = record.dynamodb?.NewImage;\n return unmarshall(data as Record<string, AttributeValue>) as EventRecord | AggregateHead;\n } catch (error) {\n logger.error({ error, record }, 'Error unmarshalling event record');\n return undefined;\n }\n })\n .filter((eventRecord): eventRecord is EventRecord => eventRecord?.itemType === 'event');\n\n if (eventRecords.length > 0) {\n const tasks: Promise<void>[] = [sendToQueuesBatch(eventRecords)];\n if (config.busName) {\n tasks.push(sendToBusBatch(eventRecords));\n }\n await Promise.all(tasks);\n }\n };\n}\n","import { createStreamHandler } from './stream-handler';\n\nexport const handler = createStreamHandler({\n busName: process.env.EVENT_BUS_NAME,\n queueUrls: JSON.parse(process.env.QUEUE_URL_LIST ?? '[]') as string[],\n});\n"],"mappings":";;;;;;AAUA,MAAM,aAAa;;;;;AAWnB,SAAgB,oBAAoB,QAAmC;CACrE,MAAM,YAAY,IAAI,WAAW;CACjC,MAAM,cAAc,IAAI,kBAAkB,EAAE,CAAC;CAE7C,SAAS,WAAc,OAAY,WAA0B;EAC3D,MAAM,SAAgB,EAAE;AACxB,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,UACrC,QAAO,KAAK,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC;AAE5C,SAAO;;CAGT,eAAe,kBAAkB,cAA6B;AAC5D,QAAM,QAAQ,IAAI,OAAO,UAAU,KAAK,UAAU,iBAAiB,cAAc,MAAM,CAAC,CAAC;;CAG3F,eAAe,iBAAiB,cAA6B,OAAe;EAC1E,MAAM,UAAU,WAAW,cAAc,WAAW;AAEpD,OAAK,MAAM,SAAS,QAClB,KAAI;GACF,MAAM,UAAU,MAAM,KAAK,aAAa,WAAW;IACjD,IAAI,GAAG,YAAY,QAAQ,GAAG;IAC9B,aAAa,KAAK,UAAU,YAAY;IACxC,gBAAgB,YAAY;IAC5B,wBAAwB,YAAY;IACrC,EAAE;AAEH,SAAM,UAAU,KACd,IAAI,wBAAwB;IAC1B,UAAU;IACV,SAAS;IACV,CAAC,CACH;WACM,OAAO;AACd,UAAO,MAAM;IAAE;IAAO;IAAO;IAAO,EAAE,+BAA+B;AACrE,SAAM;;;CAKZ,eAAe,eAAe,cAA6B;EACzD,MAAM,UAAU,WAAW,cAAc,WAAW;AAEpD,OAAK,MAAM,SAAS,QAClB,KAAI;GACF,MAAM,UAAU,MAAM,KAAK,gBAAgB;AAGzC,WAAO;KACL,QAFa,YAAY,UAAU,UAAU,YAAY,cAAc,MAAM,IAAI,CAAC,GAAG;KAGrF,YAAY,YAAY;KACxB,QAAQ,KAAK,UAAU,YAAY;KACnC,cAAc,OAAO;KACtB;KACD;AAEF,SAAM,YAAY,KAChB,IAAI,iBAAiB,EACnB,SAAS,SACV,CAAC,CACH;WACM,OAAO;AACd,UAAO,MAAM;IAAE;IAAO;IAAO,EAAE,6BAA6B;AAC5D,SAAM;;;AAKZ,QAAO,OAAO,UAA8C;EAC1D,MAAM,eAAe,MAAM,QAAQ,QAAQ,WAAW,OAAO,cAAc,SAAS,CACjF,KAAK,WAAW;AACf,OAAI;IACF,MAAM,OAAO,OAAO,UAAU;AAC9B,WAAO,WAAW,KAAuC;YAClD,OAAO;AACd,WAAO,MAAM;KAAE;KAAO;KAAQ,EAAE,mCAAmC;AACnE;;IAEF,CACD,QAAQ,gBAA4C,aAAa,aAAa,QAAQ;AAEzF,MAAI,aAAa,SAAS,GAAG;GAC3B,MAAM,QAAyB,CAAC,kBAAkB,aAAa,CAAC;AAChE,OAAI,OAAO,QACT,OAAM,KAAK,eAAe,aAAa,CAAC;AAE1C,SAAM,QAAQ,IAAI,MAAM;;;;;;AC1G9B,MAAa,UAAU,oBAAoB;CACzC,SAAS,QAAQ,IAAI;CACrB,WAAW,KAAK,MAAM,QAAQ,IAAI,kBAAkB,KAAK;CAC1D,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auriclabs/events",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Event sourcing runtime utilities for DynamoDB-backed event stores",
5
5
  "prettier": "@auriclabs/prettier-config",
6
6
  "main": "dist/index.cjs",
@@ -11,6 +11,11 @@
11
11
  "types": "./dist/index.d.mts",
12
12
  "import": "./dist/index.mjs",
13
13
  "require": "./dist/index.cjs"
14
+ },
15
+ "./stream-handler": {
16
+ "types": "./dist/stream-handler.entry.d.mts",
17
+ "import": "./dist/stream-handler.entry.mjs",
18
+ "require": "./dist/stream-handler.entry.cjs"
14
19
  }
15
20
  },
16
21
  "keywords": [],
@@ -47,7 +52,7 @@
47
52
  "directory": "packages/events"
48
53
  },
49
54
  "scripts": {
50
- "build": "tsdown src/index.ts --format cjs,esm --dts --no-hash",
55
+ "build": "tsdown src/index.ts --format cjs,esm --dts --no-hash && tsdown src/stream-handler.entry.ts --format cjs,esm --dts --no-hash --no-clean",
51
56
  "dev": "concurrently \"pnpm build --watch\" \"pnpm:y:watch\"",
52
57
  "y:watch": "chokidar dist --initial --silent -c \"yalc publish --push\"",
53
58
  "lint": "eslint .",
@@ -0,0 +1,6 @@
1
+ import { createStreamHandler } from './stream-handler';
2
+
3
+ export const handler = createStreamHandler({
4
+ busName: process.env.EVENT_BUS_NAME,
5
+ queueUrls: JSON.parse(process.env.QUEUE_URL_LIST ?? '[]') as string[],
6
+ });
@@ -313,6 +313,41 @@ describe('stream-handler', () => {
313
313
  expect(logger.error).toHaveBeenCalled();
314
314
  });
315
315
 
316
+ it('skips EventBridge when busName is omitted', async () => {
317
+ const eventRecord = makeEventRecord();
318
+ mockUnmarshall.mockReturnValue(eventRecord);
319
+
320
+ const handler = createStreamHandler({
321
+ queueUrls: ['https://sqs.us-east-1.amazonaws.com/123/queue-1'],
322
+ });
323
+ const event: DynamoDBStreamEvent = {
324
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
325
+ };
326
+
327
+ await handler(event);
328
+
329
+ expect(SendMessageBatchCommand).toHaveBeenCalledTimes(1);
330
+ expect(PutEventsCommand).not.toHaveBeenCalled();
331
+ });
332
+
333
+ it('skips EventBridge when busName is empty string', async () => {
334
+ const eventRecord = makeEventRecord();
335
+ mockUnmarshall.mockReturnValue(eventRecord);
336
+
337
+ const handler = createStreamHandler({
338
+ busName: '',
339
+ queueUrls: ['https://sqs.us-east-1.amazonaws.com/123/queue-1'],
340
+ });
341
+ const event: DynamoDBStreamEvent = {
342
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
343
+ };
344
+
345
+ await handler(event);
346
+
347
+ expect(SendMessageBatchCommand).toHaveBeenCalledTimes(1);
348
+ expect(PutEventsCommand).not.toHaveBeenCalled();
349
+ });
350
+
316
351
  it('re-throws EventBridge send errors', async () => {
317
352
  const eventRecord = makeEventRecord();
318
353
  mockUnmarshall.mockReturnValue(eventRecord);
@@ -11,7 +11,7 @@ import { AggregateHead, EventRecord } from './types';
11
11
  const BATCH_SIZE = 10;
12
12
 
13
13
  export interface CreateStreamHandlerConfig {
14
- busName: string;
14
+ busName?: string;
15
15
  queueUrls: string[];
16
16
  }
17
17
 
@@ -102,7 +102,11 @@ export function createStreamHandler(config: CreateStreamHandlerConfig) {
102
102
  .filter((eventRecord): eventRecord is EventRecord => eventRecord?.itemType === 'event');
103
103
 
104
104
  if (eventRecords.length > 0) {
105
- await Promise.all([sendToBusBatch(eventRecords), sendToQueuesBatch(eventRecords)]);
105
+ const tasks: Promise<void>[] = [sendToQueuesBatch(eventRecords)];
106
+ if (config.busName) {
107
+ tasks.push(sendToBusBatch(eventRecords));
108
+ }
109
+ await Promise.all(tasks);
106
110
  }
107
111
  };
108
112
  }