@squiz/dx-common-lib 1.72.1 → 1.72.4
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/CHANGELOG.md +18 -0
- package/lib/events/EventBusService.d.ts +107 -0
- package/lib/events/EventBusService.js +196 -0
- package/lib/events/EventBusService.js.map +1 -0
- package/lib/events/EventBusService.spec.d.ts +1 -0
- package/lib/events/EventBusService.spec.js +527 -0
- package/lib/events/EventBusService.spec.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/outbound-http/createServiceUserAgent.d.ts +6 -0
- package/lib/outbound-http/createServiceUserAgent.js +13 -0
- package/lib/outbound-http/createServiceUserAgent.js.map +1 -0
- package/lib/outbound-http/createServiceUserAgent.spec.d.ts +1 -0
- package/lib/outbound-http/createServiceUserAgent.spec.js +12 -0
- package/lib/outbound-http/createServiceUserAgent.spec.js.map +1 -0
- package/lib/outbound-http/index.d.ts +2 -0
- package/lib/outbound-http/index.js +8 -0
- package/lib/outbound-http/index.js.map +1 -0
- package/lib/outbound-http/outboundHttp.d.ts +2 -0
- package/lib/outbound-http/outboundHttp.js +23 -0
- package/lib/outbound-http/outboundHttp.js.map +1 -0
- package/lib/outbound-http/outboundHttp.spec.d.ts +1 -0
- package/lib/outbound-http/outboundHttp.spec.js +59 -0
- package/lib/outbound-http/outboundHttp.spec.js.map +1 -0
- package/lib/stream/EventStreamHandler.d.ts +38 -0
- package/lib/stream/EventStreamHandler.js +128 -0
- package/lib/stream/EventStreamHandler.js.map +1 -0
- package/lib/stream/EventStreamHandler.spec.d.ts +1 -0
- package/lib/stream/EventStreamHandler.spec.js +364 -0
- package/lib/stream/EventStreamHandler.spec.js.map +1 -0
- package/lib/stream/StreamUtils.d.ts +38 -0
- package/lib/stream/StreamUtils.js +59 -0
- package/lib/stream/StreamUtils.js.map +1 -0
- package/lib/stream/StreamUtils.spec.d.ts +1 -0
- package/lib/stream/StreamUtils.spec.js +92 -0
- package/lib/stream/StreamUtils.spec.js.map +1 -0
- package/package.json +3 -2
- package/src/events/EventBusService.spec.ts +707 -0
- package/src/events/EventBusService.ts +316 -0
- package/src/index.ts +4 -0
- package/src/outbound-http/createServiceUserAgent.spec.ts +11 -0
- package/src/outbound-http/createServiceUserAgent.ts +9 -0
- package/src/outbound-http/index.ts +2 -0
- package/src/outbound-http/outboundHttp.spec.ts +69 -0
- package/src/outbound-http/outboundHttp.ts +26 -0
- package/src/stream/EventStreamHandler.spec.ts +440 -0
- package/src/stream/EventStreamHandler.ts +192 -0
- package/src/stream/StreamUtils.spec.ts +113 -0
- package/src/stream/StreamUtils.ts +58 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outboundFetch = exports.createServiceUserAgent = void 0;
|
|
4
|
+
var createServiceUserAgent_1 = require("./createServiceUserAgent");
|
|
5
|
+
Object.defineProperty(exports, "createServiceUserAgent", { enumerable: true, get: function () { return createServiceUserAgent_1.createServiceUserAgent; } });
|
|
6
|
+
var outboundHttp_1 = require("./outboundHttp");
|
|
7
|
+
Object.defineProperty(exports, "outboundFetch", { enumerable: true, get: function () { return outboundHttp_1.outboundFetch; } });
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/outbound-http/index.ts"],"names":[],"mappings":";;;AAAA,mEAAkE;AAAzD,gIAAA,sBAAsB,OAAA;AAC/B,+CAAwE;AAA/D,6GAAA,aAAa,OAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outboundFetch = outboundFetch;
|
|
4
|
+
function buildOutboundHeaders(userAgent, input, init) {
|
|
5
|
+
const headers = new Headers();
|
|
6
|
+
if (input instanceof Request) {
|
|
7
|
+
input.headers.forEach((value, key) => {
|
|
8
|
+
headers.set(key, value);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
if (init === null || init === void 0 ? void 0 : init.headers) {
|
|
12
|
+
new Headers(init.headers).forEach((value, key) => {
|
|
13
|
+
headers.set(key, value);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
headers.set('User-Agent', userAgent);
|
|
17
|
+
return headers;
|
|
18
|
+
}
|
|
19
|
+
function outboundFetch(userAgent, input, init) {
|
|
20
|
+
const headers = buildOutboundHeaders(userAgent, input, init);
|
|
21
|
+
return fetch(input, { ...init, headers });
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=outboundHttp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outboundHttp.js","sourceRoot":"","sources":["../../src/outbound-http/outboundHttp.ts"],"names":[],"mappings":";;AAsBA,sCAGC;AAvBD,SAAS,oBAAoB,CAAC,SAAiB,EAAE,KAAyB,EAAE,IAAkB;IAC5F,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAE9B,IAAI,KAAK,YAAY,OAAO,EAAE,CAAC;QAC7B,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YACnC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,IAAI,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,OAAO,EAAE,CAAC;QAClB,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE;YAC/C,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;IAErC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAgB,aAAa,CAAC,SAAiB,EAAE,KAAyB,EAAE,IAAkB;IAC5F,MAAM,OAAO,GAAG,oBAAoB,CAAC,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC7D,OAAO,KAAK,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5C,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const outboundHttp_1 = require("./outboundHttp");
|
|
4
|
+
describe('outboundFetch', () => {
|
|
5
|
+
const userAgent = 'Squiz/test-service';
|
|
6
|
+
const originalFetch = global.fetch;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
global.fetch = jest.fn().mockResolvedValue(new Response());
|
|
9
|
+
});
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
global.fetch = originalFetch;
|
|
12
|
+
});
|
|
13
|
+
it('adds User-Agent to string URL requests with init headers', async () => {
|
|
14
|
+
await (0, outboundHttp_1.outboundFetch)(userAgent, 'https://example.com/resource', {
|
|
15
|
+
headers: { 'x-api-key': 'secret' },
|
|
16
|
+
});
|
|
17
|
+
const init = global.fetch.mock.calls[0][1];
|
|
18
|
+
const headers = init.headers;
|
|
19
|
+
expect(headers.get('x-api-key')).toBe('secret');
|
|
20
|
+
expect(headers.get('User-Agent')).toBe(userAgent);
|
|
21
|
+
});
|
|
22
|
+
it('preserves headers from a Request input and adds User-Agent', async () => {
|
|
23
|
+
const request = new Request('https://example.com/resource', {
|
|
24
|
+
method: 'GET',
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: 'Bearer token',
|
|
27
|
+
'x-dxp-tenant': 'tenant-1',
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
await (0, outboundHttp_1.outboundFetch)(userAgent, request);
|
|
31
|
+
const init = global.fetch.mock.calls[0][1];
|
|
32
|
+
const headers = init.headers;
|
|
33
|
+
expect(headers.get('Authorization')).toBe('Bearer token');
|
|
34
|
+
expect(headers.get('x-dxp-tenant')).toBe('tenant-1');
|
|
35
|
+
expect(headers.get('User-Agent')).toBe(userAgent);
|
|
36
|
+
});
|
|
37
|
+
it('merges init headers on top of Request headers without dropping existing values', async () => {
|
|
38
|
+
const request = new Request('https://example.com/resource', {
|
|
39
|
+
headers: { 'x-api-key': 'from-request' },
|
|
40
|
+
});
|
|
41
|
+
await (0, outboundHttp_1.outboundFetch)(userAgent, request, {
|
|
42
|
+
headers: { 'x-dxp-session': 'session-token' },
|
|
43
|
+
});
|
|
44
|
+
const init = global.fetch.mock.calls[0][1];
|
|
45
|
+
const headers = init.headers;
|
|
46
|
+
expect(headers.get('x-api-key')).toBe('from-request');
|
|
47
|
+
expect(headers.get('x-dxp-session')).toBe('session-token');
|
|
48
|
+
expect(headers.get('User-Agent')).toBe(userAgent);
|
|
49
|
+
});
|
|
50
|
+
it('Overrides an explicit User-Agent header', async () => {
|
|
51
|
+
await (0, outboundHttp_1.outboundFetch)(userAgent, 'https://example.com/resource', {
|
|
52
|
+
headers: { 'User-Agent': 'custom-agent' },
|
|
53
|
+
});
|
|
54
|
+
const init = global.fetch.mock.calls[0][1];
|
|
55
|
+
const headers = init.headers;
|
|
56
|
+
expect(headers.get('User-Agent')).toBe(userAgent);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=outboundHttp.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outboundHttp.spec.js","sourceRoot":"","sources":["../../src/outbound-http/outboundHttp.spec.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
4
|
+
*/
|
|
5
|
+
import { DynamoDBStreamEvent, DynamoDBRecord, Context, DynamoDBBatchResponse } from 'aws-lambda';
|
|
6
|
+
import { Logger } from '@squiz/dx-logger-lib';
|
|
7
|
+
import { EventBusService } from '../events/EventBusService';
|
|
8
|
+
export interface EventPayload {
|
|
9
|
+
detailType: string;
|
|
10
|
+
detail: object;
|
|
11
|
+
}
|
|
12
|
+
export type EventMapper = (record: DynamoDBRecord) => Promise<EventPayload[]>;
|
|
13
|
+
/** Resolves tenant id for publishing; use `record` when tenant is carried on the item rather than derivable from the table ARN. */
|
|
14
|
+
export type ExtractTenantId = (arn: string, tableServiceIdentifier: string, record: DynamoDBRecord) => string;
|
|
15
|
+
interface EventStreamHandlerConfig<T extends string> {
|
|
16
|
+
logger: Logger;
|
|
17
|
+
eventBusService: EventBusService;
|
|
18
|
+
/** Matches the table name segment in ARNs (e.g. `cmp` for `*.cmp-service.*`); also used as log field `service`. */
|
|
19
|
+
tableServiceIdentifier: string;
|
|
20
|
+
entityTypePrefixMap: Record<string, T>;
|
|
21
|
+
mappers: Partial<Record<T, EventMapper>>;
|
|
22
|
+
extractTenantId: ExtractTenantId;
|
|
23
|
+
batchSize?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* DynamoDB stream Lambda handler factory for mapping stream records to domain events and publishing via the event bus.
|
|
27
|
+
*
|
|
28
|
+
* Handles:
|
|
29
|
+
* - Record batching by configurable size
|
|
30
|
+
* - Tenant extraction from stream ARN and/or stream record (via `extractTenantId`)
|
|
31
|
+
* - Entity type detection via prefix matching
|
|
32
|
+
* - Mapper invocation for known entity types
|
|
33
|
+
* - Event grouping by tenant
|
|
34
|
+
* - Batch event publishing with retry tracking
|
|
35
|
+
* - Failure reporting for Lambda retry mechanism
|
|
36
|
+
*/
|
|
37
|
+
export declare function createEventStreamHandler<T extends string>(config: EventStreamHandlerConfig<T>): (event: DynamoDBStreamEvent, context: Context) => Promise<DynamoDBBatchResponse>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createEventStreamHandler = createEventStreamHandler;
|
|
4
|
+
/**
|
|
5
|
+
* DynamoDB stream Lambda handler factory for mapping stream records to domain events and publishing via the event bus.
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Record batching by configurable size
|
|
9
|
+
* - Tenant extraction from stream ARN and/or stream record (via `extractTenantId`)
|
|
10
|
+
* - Entity type detection via prefix matching
|
|
11
|
+
* - Mapper invocation for known entity types
|
|
12
|
+
* - Event grouping by tenant
|
|
13
|
+
* - Batch event publishing with retry tracking
|
|
14
|
+
* - Failure reporting for Lambda retry mechanism
|
|
15
|
+
*/
|
|
16
|
+
function createEventStreamHandler(config) {
|
|
17
|
+
const { logger, eventBusService, tableServiceIdentifier, entityTypePrefixMap, mappers, extractTenantId, batchSize = 10, } = config;
|
|
18
|
+
function extractEntityType(record) {
|
|
19
|
+
var _a, _b, _c;
|
|
20
|
+
const pk = (_c = (_b = (_a = record.dynamodb) === null || _a === void 0 ? void 0 : _a.Keys) === null || _b === void 0 ? void 0 : _b.pk) === null || _c === void 0 ? void 0 : _c.S;
|
|
21
|
+
if (!pk) {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
for (const [prefix, entityType] of Object.entries(entityTypePrefixMap)) {
|
|
25
|
+
if (pk.startsWith(prefix)) {
|
|
26
|
+
return entityType;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return async function handler(event, context) {
|
|
32
|
+
logger.info('Processing DynamoDB stream batch', {
|
|
33
|
+
recordCount: event.Records.length,
|
|
34
|
+
requestId: context.awsRequestId,
|
|
35
|
+
service: tableServiceIdentifier,
|
|
36
|
+
});
|
|
37
|
+
const mappedRecords = [];
|
|
38
|
+
// Process records in batches
|
|
39
|
+
for (let i = 0; i < event.Records.length; i += batchSize) {
|
|
40
|
+
const batch = event.Records.slice(i, i + batchSize);
|
|
41
|
+
const batchResults = await Promise.all(batch.map(async (record, batchIndex) => {
|
|
42
|
+
var _a, _b, _c, _d, _e, _f;
|
|
43
|
+
try {
|
|
44
|
+
if (!record.eventSourceARN) {
|
|
45
|
+
throw new Error('Missing eventSourceARN on stream record');
|
|
46
|
+
}
|
|
47
|
+
const tenantId = extractTenantId(record.eventSourceARN, tableServiceIdentifier, record);
|
|
48
|
+
const tableName = record.eventSourceARN.split('/')[1];
|
|
49
|
+
const entityType = extractEntityType(record);
|
|
50
|
+
logger.info('Processed stream record', {
|
|
51
|
+
eventName: record.eventName,
|
|
52
|
+
tenantId,
|
|
53
|
+
tableName,
|
|
54
|
+
entityType,
|
|
55
|
+
pk: (_c = (_b = (_a = record.dynamodb) === null || _a === void 0 ? void 0 : _a.Keys) === null || _b === void 0 ? void 0 : _b.pk) === null || _c === void 0 ? void 0 : _c.S,
|
|
56
|
+
sk: (_f = (_e = (_d = record.dynamodb) === null || _d === void 0 ? void 0 : _d.Keys) === null || _e === void 0 ? void 0 : _e.sk) === null || _f === void 0 ? void 0 : _f.S,
|
|
57
|
+
});
|
|
58
|
+
if (entityType !== undefined) {
|
|
59
|
+
const mapper = mappers[entityType];
|
|
60
|
+
if (mapper) {
|
|
61
|
+
// Note: For CMP, mappers return 0 or 1 event per record
|
|
62
|
+
const events = await mapper(record);
|
|
63
|
+
return { recordIndex: i + batchIndex, record, tenantId, events };
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
logger.debug(`No event mapper registered for entity type: ${entityType}`);
|
|
67
|
+
return { recordIndex: i + batchIndex, record, tenantId, events: [] };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { recordIndex: i + batchIndex, record, tenantId, events: [] };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
logger.error('Failed to process record', {
|
|
74
|
+
error: error instanceof Error ? error.message : String(error),
|
|
75
|
+
eventSourceARN: record.eventSourceARN,
|
|
76
|
+
});
|
|
77
|
+
return { recordIndex: i + batchIndex, record, tenantId: 'unknown', events: [] };
|
|
78
|
+
}
|
|
79
|
+
}));
|
|
80
|
+
mappedRecords.push(...batchResults);
|
|
81
|
+
}
|
|
82
|
+
// Group events by tenant and publish separately for each tenant
|
|
83
|
+
const eventsByTenant = new Map();
|
|
84
|
+
for (const { recordIndex, record, tenantId, events } of mappedRecords) {
|
|
85
|
+
if (events.length > 0) {
|
|
86
|
+
const existing = eventsByTenant.get(tenantId) || [];
|
|
87
|
+
existing.push({ recordIndex, record, events });
|
|
88
|
+
eventsByTenant.set(tenantId, existing);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Track failed records for retry
|
|
92
|
+
const failedRecordIndices = new Set();
|
|
93
|
+
// Publish batches for each tenant with their own context in parallel
|
|
94
|
+
await Promise.all(Array.from(eventsByTenant.entries()).map(async ([tenantId, recordsWithEvents]) => {
|
|
95
|
+
var _a;
|
|
96
|
+
const events = recordsWithEvents.flatMap((r) => r.events);
|
|
97
|
+
logger.info('Publishing batch of events for tenant', { tenantId, eventCount: events.length });
|
|
98
|
+
try {
|
|
99
|
+
eventBusService.setTenantId(tenantId);
|
|
100
|
+
await eventBusService.publishEvents(events);
|
|
101
|
+
logger.info('Successfully published all events for tenant', { tenantId });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
logger.error('Failed to publish events for tenant', {
|
|
105
|
+
tenantId,
|
|
106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
107
|
+
});
|
|
108
|
+
// Mark all records for this tenant as failed
|
|
109
|
+
for (const { record } of recordsWithEvents) {
|
|
110
|
+
const sequenceNumber = (_a = record.dynamodb) === null || _a === void 0 ? void 0 : _a.SequenceNumber;
|
|
111
|
+
if (sequenceNumber) {
|
|
112
|
+
failedRecordIndices.add(sequenceNumber);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}));
|
|
117
|
+
const batchItemFailures = Array.from(failedRecordIndices).map((sequenceNumber) => ({
|
|
118
|
+
itemIdentifier: sequenceNumber,
|
|
119
|
+
}));
|
|
120
|
+
logger.info('Batch processing complete', {
|
|
121
|
+
totalRecords: event.Records.length,
|
|
122
|
+
failedRecords: batchItemFailures.length,
|
|
123
|
+
service: tableServiceIdentifier,
|
|
124
|
+
});
|
|
125
|
+
return { batchItemFailures };
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=EventStreamHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventStreamHandler.js","sourceRoot":"","sources":["../../src/stream/EventStreamHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/*!
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright Squiz Australia Pty Ltd. All Rights Reserved.
|
|
6
|
+
*/
|
|
7
|
+
const EventStreamHandler_1 = require("./EventStreamHandler");
|
|
8
|
+
describe('EventStreamHandler', () => {
|
|
9
|
+
let mockLogger;
|
|
10
|
+
let mockEventBusService;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mockLogger = {
|
|
13
|
+
info: jest.fn(),
|
|
14
|
+
debug: jest.fn(),
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
};
|
|
17
|
+
mockEventBusService = {
|
|
18
|
+
setTenantId: jest.fn(),
|
|
19
|
+
publishEvents: jest.fn().mockResolvedValue(undefined),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
const createMockRecord = (overrides = {}) => {
|
|
23
|
+
return {
|
|
24
|
+
eventID: '1',
|
|
25
|
+
eventVersion: '1.0',
|
|
26
|
+
dynamodb: {
|
|
27
|
+
Keys: {
|
|
28
|
+
pk: { S: 'cv#component-123' },
|
|
29
|
+
sk: { S: 'v#1.0.0' },
|
|
30
|
+
},
|
|
31
|
+
SequenceNumber: '111',
|
|
32
|
+
SizeBytes: 26,
|
|
33
|
+
StreamViewType: 'NEW_AND_OLD_IMAGES',
|
|
34
|
+
},
|
|
35
|
+
awsRegion: 'us-east-1',
|
|
36
|
+
eventName: 'INSERT',
|
|
37
|
+
eventSource: 'aws:dynamodb',
|
|
38
|
+
eventSourceARN: 'arn:aws:dynamodb:us-east-1:123456789012:table/dev.cmp-service.tenant123/stream/2024-01-01T00:00:00.000',
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const createMockContext = () => ({
|
|
43
|
+
awsRequestId: 'test-request-id',
|
|
44
|
+
invokeid: 'test-invoke-id',
|
|
45
|
+
logGroupName: 'test-log-group',
|
|
46
|
+
logStreamName: 'test-log-stream',
|
|
47
|
+
functionName: 'test-function',
|
|
48
|
+
memoryLimitInMB: '128',
|
|
49
|
+
functionVersion: '1',
|
|
50
|
+
getRemainingTimeInMillis: jest.fn(() => 30000),
|
|
51
|
+
done: jest.fn(),
|
|
52
|
+
fail: jest.fn(),
|
|
53
|
+
succeed: jest.fn(),
|
|
54
|
+
});
|
|
55
|
+
describe('createEventStreamHandler', () => {
|
|
56
|
+
it('should successfully process records with mapped events', async () => {
|
|
57
|
+
const mockMapper = jest.fn(async () => [
|
|
58
|
+
{ detailType: 'component.change.create', detail: { name: 'test' } },
|
|
59
|
+
]);
|
|
60
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
61
|
+
logger: mockLogger,
|
|
62
|
+
eventBusService: mockEventBusService,
|
|
63
|
+
tableServiceIdentifier: 'cmp',
|
|
64
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
65
|
+
mappers: { componentVersion: mockMapper },
|
|
66
|
+
extractTenantId: (arn, _identifier) => {
|
|
67
|
+
const tableName = arn.split('/')[1];
|
|
68
|
+
const parts = tableName.split('.');
|
|
69
|
+
return parts[parts.length - 1];
|
|
70
|
+
},
|
|
71
|
+
batchSize: 10,
|
|
72
|
+
});
|
|
73
|
+
const event = {
|
|
74
|
+
Records: [createMockRecord()],
|
|
75
|
+
};
|
|
76
|
+
const response = await handle(event, createMockContext());
|
|
77
|
+
expect(mockLogger.info).toHaveBeenCalledWith('Processing DynamoDB stream batch', expect.any(Object));
|
|
78
|
+
expect(mockEventBusService.setTenantId).toHaveBeenCalledWith('tenant123');
|
|
79
|
+
expect(mockEventBusService.publishEvents).toHaveBeenCalledWith([
|
|
80
|
+
{ detailType: 'component.change.create', detail: { name: 'test' } },
|
|
81
|
+
]);
|
|
82
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
it('should pass the stream record as the third argument to extractTenantId', async () => {
|
|
85
|
+
const extractTenantId = jest.fn((arn, _identifier, _record) => {
|
|
86
|
+
const tableName = arn.split('/')[1];
|
|
87
|
+
const parts = tableName.split('.');
|
|
88
|
+
return parts[parts.length - 1];
|
|
89
|
+
});
|
|
90
|
+
const mockMapper = jest.fn(async () => [{ detailType: 'component.change.create', detail: {} }]);
|
|
91
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
92
|
+
logger: mockLogger,
|
|
93
|
+
eventBusService: mockEventBusService,
|
|
94
|
+
tableServiceIdentifier: 'cmp',
|
|
95
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
96
|
+
mappers: { componentVersion: mockMapper },
|
|
97
|
+
extractTenantId,
|
|
98
|
+
});
|
|
99
|
+
const streamRecord = createMockRecord();
|
|
100
|
+
await handle({ Records: [streamRecord] }, createMockContext());
|
|
101
|
+
expect(extractTenantId).toHaveBeenCalledWith(streamRecord.eventSourceARN, 'cmp', streamRecord);
|
|
102
|
+
});
|
|
103
|
+
it('should handle records with no mapper registered', async () => {
|
|
104
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
105
|
+
logger: mockLogger,
|
|
106
|
+
eventBusService: mockEventBusService,
|
|
107
|
+
tableServiceIdentifier: 'cmp',
|
|
108
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
109
|
+
mappers: {},
|
|
110
|
+
extractTenantId: (arn, _identifier) => {
|
|
111
|
+
const tableName = arn.split('/')[1];
|
|
112
|
+
const parts = tableName.split('.');
|
|
113
|
+
return parts[parts.length - 1];
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const event = {
|
|
117
|
+
Records: [createMockRecord()],
|
|
118
|
+
};
|
|
119
|
+
const response = await handle(event, createMockContext());
|
|
120
|
+
expect(mockLogger.debug).toHaveBeenCalledWith('No event mapper registered for entity type: componentVersion');
|
|
121
|
+
expect(mockEventBusService.publishEvents).not.toHaveBeenCalled();
|
|
122
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it('should handle records with unknown entity types', async () => {
|
|
125
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
126
|
+
logger: mockLogger,
|
|
127
|
+
eventBusService: mockEventBusService,
|
|
128
|
+
tableServiceIdentifier: 'cmp',
|
|
129
|
+
entityTypePrefixMap: { 'cs#': 'componentSet' },
|
|
130
|
+
mappers: {},
|
|
131
|
+
extractTenantId: (arn, _identifier) => {
|
|
132
|
+
const tableName = arn.split('/')[1];
|
|
133
|
+
const parts = tableName.split('.');
|
|
134
|
+
return parts[parts.length - 1];
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const event = {
|
|
138
|
+
Records: [createMockRecord()],
|
|
139
|
+
};
|
|
140
|
+
const response = await handle(event, createMockContext());
|
|
141
|
+
expect(mockEventBusService.publishEvents).not.toHaveBeenCalled();
|
|
142
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
143
|
+
});
|
|
144
|
+
it('should handle records without eventSourceARN', async () => {
|
|
145
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
146
|
+
logger: mockLogger,
|
|
147
|
+
eventBusService: mockEventBusService,
|
|
148
|
+
tableServiceIdentifier: 'cmp',
|
|
149
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
150
|
+
mappers: {},
|
|
151
|
+
extractTenantId: (arn, _identifier) => {
|
|
152
|
+
const tableName = arn.split('/')[1];
|
|
153
|
+
const parts = tableName.split('.');
|
|
154
|
+
return parts[parts.length - 1];
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const event = {
|
|
158
|
+
Records: [createMockRecord({ eventSourceARN: undefined })],
|
|
159
|
+
};
|
|
160
|
+
const response = await handle(event, createMockContext());
|
|
161
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Failed to process record', expect.any(Object));
|
|
162
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
163
|
+
});
|
|
164
|
+
it('should batch records according to batchSize', async () => {
|
|
165
|
+
const mockMapper = jest.fn(async () => [{ detailType: 'component.change.create', detail: {} }]);
|
|
166
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
167
|
+
logger: mockLogger,
|
|
168
|
+
eventBusService: mockEventBusService,
|
|
169
|
+
tableServiceIdentifier: 'cmp',
|
|
170
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
171
|
+
mappers: { componentVersion: mockMapper },
|
|
172
|
+
extractTenantId: (arn, _identifier) => {
|
|
173
|
+
const tableName = arn.split('/')[1];
|
|
174
|
+
const parts = tableName.split('.');
|
|
175
|
+
return parts[parts.length - 1];
|
|
176
|
+
},
|
|
177
|
+
batchSize: 2,
|
|
178
|
+
});
|
|
179
|
+
const records = [
|
|
180
|
+
createMockRecord({ eventID: '1' }),
|
|
181
|
+
createMockRecord({ eventID: '2' }),
|
|
182
|
+
createMockRecord({ eventID: '3' }),
|
|
183
|
+
];
|
|
184
|
+
const event = { Records: records };
|
|
185
|
+
await handle(event, createMockContext());
|
|
186
|
+
expect(mockMapper).toHaveBeenCalledTimes(3);
|
|
187
|
+
});
|
|
188
|
+
it('should mark records as failed when event publishing fails', async () => {
|
|
189
|
+
const mockMapper = jest.fn(async () => [
|
|
190
|
+
{ detailType: 'component.change.create', detail: { name: 'test' } },
|
|
191
|
+
]);
|
|
192
|
+
mockEventBusService.publishEvents.mockRejectedValue(new Error('Publishing failed'));
|
|
193
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
194
|
+
logger: mockLogger,
|
|
195
|
+
eventBusService: mockEventBusService,
|
|
196
|
+
tableServiceIdentifier: 'cmp',
|
|
197
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
198
|
+
mappers: { componentVersion: mockMapper },
|
|
199
|
+
extractTenantId: (arn, _identifier) => {
|
|
200
|
+
const tableName = arn.split('/')[1];
|
|
201
|
+
const parts = tableName.split('.');
|
|
202
|
+
return parts[parts.length - 1];
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const event = {
|
|
206
|
+
Records: [createMockRecord()],
|
|
207
|
+
};
|
|
208
|
+
const response = await handle(event, createMockContext());
|
|
209
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Failed to publish events for tenant', expect.any(Object));
|
|
210
|
+
expect(response.batchItemFailures).toHaveLength(1);
|
|
211
|
+
expect(response.batchItemFailures[0].itemIdentifier).toBe('111');
|
|
212
|
+
});
|
|
213
|
+
it('should handle mapper returning empty events', async () => {
|
|
214
|
+
const mockMapper = jest.fn(async () => []);
|
|
215
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
216
|
+
logger: mockLogger,
|
|
217
|
+
eventBusService: mockEventBusService,
|
|
218
|
+
tableServiceIdentifier: 'cmp',
|
|
219
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
220
|
+
mappers: { componentVersion: mockMapper },
|
|
221
|
+
extractTenantId: (arn, _identifier) => {
|
|
222
|
+
const tableName = arn.split('/')[1];
|
|
223
|
+
const parts = tableName.split('.');
|
|
224
|
+
return parts[parts.length - 1];
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
const event = {
|
|
228
|
+
Records: [createMockRecord()],
|
|
229
|
+
};
|
|
230
|
+
const response = await handle(event, createMockContext());
|
|
231
|
+
expect(mockEventBusService.publishEvents).not.toHaveBeenCalled();
|
|
232
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
233
|
+
});
|
|
234
|
+
it('should handle multiple mappers for different entity types', async () => {
|
|
235
|
+
const cvMapper = jest.fn(async () => [
|
|
236
|
+
{ detailType: 'component.change.create', detail: { type: 'cv' } },
|
|
237
|
+
]);
|
|
238
|
+
const csMapper = jest.fn(async () => [{ detailType: 'set.change.create', detail: { type: 'cs' } }]);
|
|
239
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
240
|
+
logger: mockLogger,
|
|
241
|
+
eventBusService: mockEventBusService,
|
|
242
|
+
tableServiceIdentifier: 'cmp',
|
|
243
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion', 'cs#': 'componentSet' },
|
|
244
|
+
mappers: { componentVersion: cvMapper, componentSet: csMapper },
|
|
245
|
+
extractTenantId: (arn, _identifier) => {
|
|
246
|
+
const tableName = arn.split('/')[1];
|
|
247
|
+
const parts = tableName.split('.');
|
|
248
|
+
return parts[parts.length - 1];
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
const event = {
|
|
252
|
+
Records: [
|
|
253
|
+
createMockRecord({ eventID: '1' }),
|
|
254
|
+
createMockRecord({ eventID: '2', dynamodb: { Keys: { pk: { S: 'cs#set-123' } } } }),
|
|
255
|
+
],
|
|
256
|
+
};
|
|
257
|
+
await handle(event, createMockContext());
|
|
258
|
+
expect(cvMapper).toHaveBeenCalledTimes(1);
|
|
259
|
+
expect(csMapper).toHaveBeenCalledTimes(1);
|
|
260
|
+
});
|
|
261
|
+
it('should handle mapper errors gracefully', async () => {
|
|
262
|
+
const mockMapper = jest.fn(async () => {
|
|
263
|
+
throw new Error('Mapper failed');
|
|
264
|
+
});
|
|
265
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
266
|
+
logger: mockLogger,
|
|
267
|
+
eventBusService: mockEventBusService,
|
|
268
|
+
tableServiceIdentifier: 'cmp',
|
|
269
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
270
|
+
mappers: { componentVersion: mockMapper },
|
|
271
|
+
extractTenantId: (arn, _identifier) => {
|
|
272
|
+
const tableName = arn.split('/')[1];
|
|
273
|
+
const parts = tableName.split('.');
|
|
274
|
+
return parts[parts.length - 1];
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
const event = {
|
|
278
|
+
Records: [createMockRecord()],
|
|
279
|
+
};
|
|
280
|
+
const response = await handle(event, createMockContext());
|
|
281
|
+
expect(mockLogger.error).toHaveBeenCalledWith('Failed to process record', expect.any(Object));
|
|
282
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
283
|
+
});
|
|
284
|
+
it('should group events by tenant and publish separately', async () => {
|
|
285
|
+
const mockMapper = jest.fn(async () => [{ detailType: 'component.change.create', detail: {} }]);
|
|
286
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
287
|
+
logger: mockLogger,
|
|
288
|
+
eventBusService: mockEventBusService,
|
|
289
|
+
tableServiceIdentifier: 'cmp',
|
|
290
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
291
|
+
mappers: { componentVersion: mockMapper },
|
|
292
|
+
extractTenantId: (arn) => {
|
|
293
|
+
if (arn.includes('tenant1'))
|
|
294
|
+
return 'tenant1';
|
|
295
|
+
if (arn.includes('tenant2'))
|
|
296
|
+
return 'tenant2';
|
|
297
|
+
return 'unknown';
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
const event = {
|
|
301
|
+
Records: [
|
|
302
|
+
createMockRecord({
|
|
303
|
+
eventID: '1',
|
|
304
|
+
eventSourceARN: 'arn:aws:dynamodb:us-east-1:123456789012:table/dev.cmp-service.tenant1/stream/ts',
|
|
305
|
+
}),
|
|
306
|
+
createMockRecord({
|
|
307
|
+
eventID: '2',
|
|
308
|
+
eventSourceARN: 'arn:aws:dynamodb:us-east-1:123456789012:table/dev.cmp-service.tenant2/stream/ts',
|
|
309
|
+
}),
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
await handle(event, createMockContext());
|
|
313
|
+
expect(mockEventBusService.setTenantId).toHaveBeenCalledWith('tenant1');
|
|
314
|
+
expect(mockEventBusService.setTenantId).toHaveBeenCalledWith('tenant2');
|
|
315
|
+
expect(mockEventBusService.publishEvents).toHaveBeenCalledTimes(2);
|
|
316
|
+
});
|
|
317
|
+
it('should handle records without primary key', async () => {
|
|
318
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
319
|
+
logger: mockLogger,
|
|
320
|
+
eventBusService: mockEventBusService,
|
|
321
|
+
tableServiceIdentifier: 'cmp',
|
|
322
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
323
|
+
mappers: {},
|
|
324
|
+
extractTenantId: (arn, _identifier) => {
|
|
325
|
+
const tableName = arn.split('/')[1];
|
|
326
|
+
const parts = tableName.split('.');
|
|
327
|
+
return parts[parts.length - 1];
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
const event = {
|
|
331
|
+
Records: [createMockRecord({ dynamodb: { SequenceNumber: '111', SizeBytes: 0 } })],
|
|
332
|
+
};
|
|
333
|
+
const response = await handle(event, createMockContext());
|
|
334
|
+
expect(response.batchItemFailures).toHaveLength(0);
|
|
335
|
+
});
|
|
336
|
+
it('should log batch processing summary', async () => {
|
|
337
|
+
const mockMapper = jest.fn(async () => [{ detailType: 'component.change.create', detail: {} }]);
|
|
338
|
+
const handle = (0, EventStreamHandler_1.createEventStreamHandler)({
|
|
339
|
+
logger: mockLogger,
|
|
340
|
+
eventBusService: mockEventBusService,
|
|
341
|
+
tableServiceIdentifier: 'cmp',
|
|
342
|
+
entityTypePrefixMap: { 'cv#': 'componentVersion' },
|
|
343
|
+
mappers: { componentVersion: mockMapper },
|
|
344
|
+
extractTenantId: (arn, _identifier) => {
|
|
345
|
+
const tableName = arn.split('/')[1];
|
|
346
|
+
const parts = tableName.split('.');
|
|
347
|
+
return parts[parts.length - 1];
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
const event = {
|
|
351
|
+
Records: [createMockRecord()],
|
|
352
|
+
};
|
|
353
|
+
await handle(event, createMockContext());
|
|
354
|
+
const finalLog = mockLogger.info.mock.calls.find((call) => call[0] === 'Batch processing complete');
|
|
355
|
+
expect(finalLog).toBeDefined();
|
|
356
|
+
expect(finalLog[1]).toEqual({
|
|
357
|
+
totalRecords: 1,
|
|
358
|
+
failedRecords: 0,
|
|
359
|
+
service: 'cmp',
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
//# sourceMappingURL=EventStreamHandler.spec.js.map
|