@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.
- package/.turbo/turbo-build.log +33 -19
- package/CHANGELOG.md +7 -0
- package/dist/index.cjs +2 -81
- package/dist/index.d.cts +1 -145
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +1 -145
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -80
- package/dist/index.mjs.map +1 -1
- package/dist/index2.d.cts +148 -0
- package/dist/index2.d.cts.map +1 -0
- package/dist/index2.d.mts +148 -0
- package/dist/index2.d.mts.map +1 -0
- package/dist/stream-handler.cjs +91 -0
- package/dist/stream-handler.entry.cjs +8 -0
- package/dist/stream-handler.entry.d.cts +7 -0
- package/dist/stream-handler.entry.d.cts.map +1 -0
- package/dist/stream-handler.entry.d.mts +7 -0
- package/dist/stream-handler.entry.d.mts.map +1 -0
- package/dist/stream-handler.entry.mjs +10 -0
- package/dist/stream-handler.entry.mjs.map +1 -0
- package/dist/stream-handler.mjs +88 -0
- package/dist/stream-handler.mjs.map +1 -0
- package/package.json +7 -2
- package/src/stream-handler.entry.ts +6 -0
- package/src/stream-handler.test.ts +35 -0
- package/src/stream-handler.ts +6 -2
|
@@ -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
|
+
"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 .",
|
|
@@ -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);
|
package/src/stream-handler.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { AggregateHead, EventRecord } from './types';
|
|
|
11
11
|
const BATCH_SIZE = 10;
|
|
12
12
|
|
|
13
13
|
export interface CreateStreamHandlerConfig {
|
|
14
|
-
busName
|
|
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
|
-
|
|
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
|
}
|