@auriclabs/events 0.2.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.
@@ -32,7 +32,7 @@ import { SendMessageBatchCommand } from '@aws-sdk/client-sqs';
32
32
 
33
33
  import { createStreamHandler } from './stream-handler';
34
34
 
35
- import type { DynamoDBStreamEvent } from 'aws-lambda';
35
+ import type { DynamoDBRecord, DynamoDBStreamEvent } from 'aws-lambda';
36
36
 
37
37
  const makeEventRecord = (overrides = {}) => ({
38
38
  pk: 'AGG#order#o-1',
@@ -42,6 +42,7 @@ const makeEventRecord = (overrides = {}) => ({
42
42
  aggregateId: 'o-1',
43
43
  aggregateType: 'order',
44
44
  version: 1,
45
+ tenantId: 'tenant-1',
45
46
  eventId: 'evt-1',
46
47
  eventType: 'OrderCreated',
47
48
  schemaVersion: 1,
@@ -50,11 +51,18 @@ const makeEventRecord = (overrides = {}) => ({
50
51
  ...overrides,
51
52
  });
52
53
 
53
- const makeStreamRecord = (eventName: string, newImage: object | undefined = {}) => ({
54
+ const makeStreamRecord = (
55
+ eventName: string,
56
+ newImage: object | undefined = {},
57
+ ): DynamoDBRecord => ({
54
58
  eventID: '1',
55
59
  eventVersion: '1.1',
56
60
  dynamodb: {
57
- NewImage: newImage,
61
+ NewImage: newImage as DynamoDBRecord['dynamodb'] extends infer D
62
+ ? D extends { NewImage?: infer N }
63
+ ? N
64
+ : never
65
+ : never,
58
66
  StreamViewType: 'NEW_IMAGE',
59
67
  },
60
68
  awsRegion: 'us-east-1',
@@ -63,6 +71,25 @@ const makeStreamRecord = (eventName: string, newImage: object | undefined = {})
63
71
  eventSource: 'aws:dynamodb',
64
72
  });
65
73
 
74
+ interface SqsBatchInput {
75
+ QueueUrl?: string;
76
+ Entries?: {
77
+ Id?: string;
78
+ MessageBody: string;
79
+ MessageGroupId?: string;
80
+ MessageDeduplicationId?: string;
81
+ }[];
82
+ }
83
+
84
+ interface EbPutEventsInput {
85
+ Entries?: {
86
+ EventBusName?: string;
87
+ DetailType?: string;
88
+ Source?: string;
89
+ Detail: string;
90
+ }[];
91
+ }
92
+
66
93
  describe('stream-handler', () => {
67
94
  const config = {
68
95
  busName: 'test-bus',
@@ -90,7 +117,7 @@ describe('stream-handler', () => {
90
117
  makeStreamRecord('INSERT', { data: { S: 'x' } }),
91
118
  makeStreamRecord('MODIFY', { data: { S: 'y' } }),
92
119
  makeStreamRecord('REMOVE', undefined),
93
- ] as any,
120
+ ],
94
121
  };
95
122
 
96
123
  await handler(event);
@@ -110,16 +137,16 @@ describe('stream-handler', () => {
110
137
  Records: [
111
138
  makeStreamRecord('INSERT', { a: { S: '1' } }),
112
139
  makeStreamRecord('INSERT', { b: { S: '2' } }),
113
- ] as any,
140
+ ],
114
141
  };
115
142
 
116
143
  await handler(event);
117
144
 
118
145
  // Only eventRecord (itemType='event') should be sent
119
146
  expect(SendMessageBatchCommand).toHaveBeenCalledTimes(1);
120
- const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0];
147
+ const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0] as SqsBatchInput;
121
148
  expect(sqsInput.Entries).toHaveLength(1);
122
- expect(JSON.parse(sqsInput.Entries![0].MessageBody)).toEqual(eventRecord);
149
+ expect(JSON.parse(sqsInput.Entries?.[0]?.MessageBody ?? '')).toEqual(eventRecord);
123
150
  });
124
151
 
125
152
  it('sends to all configured queues', async () => {
@@ -136,14 +163,14 @@ describe('stream-handler', () => {
136
163
 
137
164
  const handler = createStreamHandler(multiQueueConfig);
138
165
  const event: DynamoDBStreamEvent = {
139
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
166
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
140
167
  };
141
168
 
142
169
  await handler(event);
143
170
 
144
171
  expect(SendMessageBatchCommand).toHaveBeenCalledTimes(2);
145
- const call1 = vi.mocked(SendMessageBatchCommand).mock.calls[0][0];
146
- const call2 = vi.mocked(SendMessageBatchCommand).mock.calls[1][0];
172
+ const call1 = vi.mocked(SendMessageBatchCommand).mock.calls[0][0] as SqsBatchInput;
173
+ const call2 = vi.mocked(SendMessageBatchCommand).mock.calls[1][0] as SqsBatchInput;
147
174
  expect(call1.QueueUrl).toBe('https://sqs.us-east-1.amazonaws.com/123/queue-1');
148
175
  expect(call2.QueueUrl).toBe('https://sqs.us-east-1.amazonaws.com/123/queue-2');
149
176
  });
@@ -154,18 +181,18 @@ describe('stream-handler', () => {
154
181
 
155
182
  const handler = createStreamHandler(config);
156
183
  const event: DynamoDBStreamEvent = {
157
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
184
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
158
185
  };
159
186
 
160
187
  await handler(event);
161
188
 
162
189
  expect(PutEventsCommand).toHaveBeenCalledTimes(1);
163
- const ebInput = vi.mocked(PutEventsCommand).mock.calls[0][0];
190
+ const ebInput = vi.mocked(PutEventsCommand).mock.calls[0][0] as EbPutEventsInput;
164
191
  expect(ebInput.Entries).toHaveLength(1);
165
- expect(ebInput.Entries![0].EventBusName).toBe('test-bus');
166
- expect(ebInput.Entries![0].DetailType).toBe('CreditAdded');
167
- expect(ebInput.Entries![0].Source).toBe('billing');
168
- expect(JSON.parse(ebInput.Entries![0].Detail)).toEqual(eventRecord);
192
+ expect(ebInput.Entries?.[0]?.EventBusName).toBe('test-bus');
193
+ expect(ebInput.Entries?.[0]?.DetailType).toBe('CreditAdded');
194
+ expect(ebInput.Entries?.[0]?.Source).toBe('billing');
195
+ expect(JSON.parse(ebInput.Entries?.[0]?.Detail ?? '')).toEqual(eventRecord);
169
196
  });
170
197
 
171
198
  it('uses kebabCase of aggregateType as source fallback when source is undefined', async () => {
@@ -174,14 +201,14 @@ describe('stream-handler', () => {
174
201
 
175
202
  const handler = createStreamHandler(config);
176
203
  const event: DynamoDBStreamEvent = {
177
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
204
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
178
205
  };
179
206
 
180
207
  await handler(event);
181
208
 
182
- const ebInput = vi.mocked(PutEventsCommand).mock.calls[0][0];
209
+ const ebInput = vi.mocked(PutEventsCommand).mock.calls[0][0] as EbPutEventsInput;
183
210
  // kebabCase splits on '.', takes first part 'Order', which becomes 'order'
184
- expect(ebInput.Entries![0].Source).toBe('order');
211
+ expect(ebInput.Entries?.[0]?.Source).toBe('order');
185
212
  });
186
213
 
187
214
  it('uses aggregateId as MessageGroupId', async () => {
@@ -190,13 +217,13 @@ describe('stream-handler', () => {
190
217
 
191
218
  const handler = createStreamHandler(config);
192
219
  const event: DynamoDBStreamEvent = {
193
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
220
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
194
221
  };
195
222
 
196
223
  await handler(event);
197
224
 
198
- const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0];
199
- expect(sqsInput.Entries![0].MessageGroupId).toBe('agg-123');
225
+ const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0] as SqsBatchInput;
226
+ expect(sqsInput.Entries?.[0]?.MessageGroupId).toBe('agg-123');
200
227
  });
201
228
 
202
229
  it('uses eventId as MessageDeduplicationId', async () => {
@@ -205,37 +232,35 @@ describe('stream-handler', () => {
205
232
 
206
233
  const handler = createStreamHandler(config);
207
234
  const event: DynamoDBStreamEvent = {
208
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
235
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
209
236
  };
210
237
 
211
238
  await handler(event);
212
239
 
213
- const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0];
214
- expect(sqsInput.Entries![0].MessageDeduplicationId).toBe('evt-dedup-1');
240
+ const sqsInput = vi.mocked(SendMessageBatchCommand).mock.calls[0][0] as SqsBatchInput;
241
+ expect(sqsInput.Entries?.[0]?.MessageDeduplicationId).toBe('evt-dedup-1');
215
242
  });
216
243
 
217
244
  it('batches correctly (respects BATCH_SIZE of 10)', async () => {
218
245
  // Create 12 event records to trigger 2 batches
219
246
  const records = Array.from({ length: 12 }, (_, i) =>
220
- makeEventRecord({ eventId: `evt-${i}`, version: i + 1 }),
247
+ makeEventRecord({ eventId: `evt-${String(i)}`, version: i + 1 }),
221
248
  );
222
249
 
223
- mockUnmarshall.mockImplementation((_, i) => records[i]);
224
- // Reset to return each record in sequence
225
250
  mockUnmarshall.mockReset();
226
251
  records.forEach((r) => mockUnmarshall.mockReturnValueOnce(r));
227
252
 
228
253
  const handler = createStreamHandler(config);
229
254
  const event: DynamoDBStreamEvent = {
230
- Records: records.map((_, i) => makeStreamRecord('INSERT', { idx: { N: String(i) } })) as any,
255
+ Records: records.map((_r, i) => makeStreamRecord('INSERT', { idx: { N: String(i) } })),
231
256
  };
232
257
 
233
258
  await handler(event);
234
259
 
235
260
  // 2 batches for SQS (10 + 2), 1 queue = 2 calls
236
261
  expect(SendMessageBatchCommand).toHaveBeenCalledTimes(2);
237
- const firstBatch = vi.mocked(SendMessageBatchCommand).mock.calls[0][0];
238
- const secondBatch = vi.mocked(SendMessageBatchCommand).mock.calls[1][0];
262
+ const firstBatch = vi.mocked(SendMessageBatchCommand).mock.calls[0][0] as SqsBatchInput;
263
+ const secondBatch = vi.mocked(SendMessageBatchCommand).mock.calls[1][0] as SqsBatchInput;
239
264
  expect(firstBatch.Entries).toHaveLength(10);
240
265
  expect(secondBatch.Entries).toHaveLength(2);
241
266
 
@@ -250,13 +275,13 @@ describe('stream-handler', () => {
250
275
 
251
276
  const handler = createStreamHandler(config);
252
277
  const event: DynamoDBStreamEvent = {
253
- Records: [makeStreamRecord('INSERT', { bad: { S: 'data' } })] as any,
278
+ Records: [makeStreamRecord('INSERT', { bad: { S: 'data' } })],
254
279
  };
255
280
 
256
281
  await handler(event);
257
282
 
258
283
  expect(logger.error).toHaveBeenCalledWith(
259
- expect.objectContaining({ error: expect.any(Error) }),
284
+ expect.objectContaining({ error: expect.any(Error) as unknown }),
260
285
  'Error unmarshalling event record',
261
286
  );
262
287
  // Should not send to queues since no valid records
@@ -281,13 +306,48 @@ describe('stream-handler', () => {
281
306
 
282
307
  const handler = createStreamHandler(config);
283
308
  const event: DynamoDBStreamEvent = {
284
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
309
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
285
310
  };
286
311
 
287
312
  await expect(handler(event)).rejects.toThrow('SQS error');
288
313
  expect(logger.error).toHaveBeenCalled();
289
314
  });
290
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
+
291
351
  it('re-throws EventBridge send errors', async () => {
292
352
  const eventRecord = makeEventRecord();
293
353
  mockUnmarshall.mockReturnValue(eventRecord);
@@ -295,7 +355,7 @@ describe('stream-handler', () => {
295
355
 
296
356
  const handler = createStreamHandler(config);
297
357
  const event: DynamoDBStreamEvent = {
298
- Records: [makeStreamRecord('INSERT', { a: { S: '1' } })] as any,
358
+ Records: [makeStreamRecord('INSERT', { a: { S: '1' } })],
299
359
  };
300
360
 
301
361
  await expect(handler(event)).rejects.toThrow();
@@ -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
  }
package/src/types.ts CHANGED
@@ -23,6 +23,9 @@ export interface EventRecord<P = unknown> {
23
23
  aggregateType: AggregateType;
24
24
  version: number; // post-apply version
25
25
 
26
+ /** Tenant isolation */
27
+ tenantId: string;
28
+
26
29
  /** Event identity & semantics */
27
30
  eventId: EventId; // string (ULID/UUID), not number
28
31
  eventType: string;
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "extends": "./tsconfig.json",
3
3
  "compilerOptions": {
4
- "noEmit": true
4
+ "noEmit": true,
5
+ "types": ["vitest/globals"]
5
6
  },
6
7
  "include": ["src", "vitest.config.ts"],
7
8
  "exclude": ["dist", "node_modules"]