@auriclabs/events 0.3.0 → 0.4.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.entry.mjs","names":[],"sources":["../src/stream-handler.entry.ts"],"sourcesContent":["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":";;AAEA,MAAa,UAAU,oBAAoB;CACzC,SAAS,QAAQ,IAAI;CACrB,WAAW,KAAK,MAAM,QAAQ,IAAI,kBAAkB,KAAK;CAC1D,CAAC"}
@@ -0,0 +1,88 @@
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
+ export { createStreamHandler as t };
87
+
88
+ //# sourceMappingURL=stream-handler.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stream-handler.mjs","names":[],"sources":["../src/stream-handler.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"],"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@auriclabs/events",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
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 src/stream-handler.entry.ts --format cjs,esm --dts --no-hash",
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
  }