@woovi/kafka 1.0.4 → 1.0.8
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/README.md +78 -0
- package/dist/index.cjs +34 -1
- package/dist/index.d.ts +91 -0
- package/dist/index.js +28 -1
- package/dist/test-utils.cjs +39 -3
- package/dist/test-utils.d.ts +23 -0
- package/dist/test-utils.js +34 -1
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -184,6 +184,61 @@ await consumer.pause(['events']); // Pause specific topics
|
|
|
184
184
|
await consumer.resume(); // Resume all topics
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
+
## Event Envelope
|
|
188
|
+
|
|
189
|
+
Wrap event data with standardized metadata using the event envelope pattern:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
import { createEventEnvelope } from '@woovi/kafka';
|
|
193
|
+
|
|
194
|
+
const envelope = createEventEnvelope({
|
|
195
|
+
eventType: 'pix-out.compliance.approved',
|
|
196
|
+
operationType: 'update',
|
|
197
|
+
source: 'woovi-compliance',
|
|
198
|
+
data: { movementId: '123', pixOutId: '456' },
|
|
199
|
+
extraMetadata: { correlationId: 'abc-123', traceId: 'xyz-789' },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await producer.publish({ data: envelope });
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The envelope has the shape:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
{
|
|
209
|
+
metadata: {
|
|
210
|
+
eventId: string; // auto-generated UUID
|
|
211
|
+
eventType: string; // e.g. "pix-out.compliance.approved"
|
|
212
|
+
eventTime: string; // auto-generated ISO 8601 timestamp
|
|
213
|
+
operationType?: string; // e.g. "insert", "update", "delete"
|
|
214
|
+
source: string; // e.g. "woovi-compliance"
|
|
215
|
+
// ...any extra metadata fields
|
|
216
|
+
},
|
|
217
|
+
data: T;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Parsing Event Envelopes (Consumer Side)
|
|
222
|
+
|
|
223
|
+
Use `parseEventEnvelope` to validate and unwrap envelope-wrapped messages in your consumer handlers:
|
|
224
|
+
|
|
225
|
+
```typescript
|
|
226
|
+
import type { ConsumerMessage, EventEnvelope } from '@woovi/kafka';
|
|
227
|
+
import { parseEventEnvelope } from '@woovi/kafka';
|
|
228
|
+
|
|
229
|
+
const handler = async (message: ConsumerMessage<EventEnvelope<MyEvent>>) => {
|
|
230
|
+
const { metadata, data } = parseEventEnvelope(message);
|
|
231
|
+
|
|
232
|
+
console.log(metadata.eventId); // UUID
|
|
233
|
+
console.log(metadata.eventType); // e.g. "pix-out.compliance.approved"
|
|
234
|
+
console.log(metadata.eventTime); // ISO 8601 timestamp
|
|
235
|
+
console.log(metadata.source); // e.g. "woovi-compliance"
|
|
236
|
+
console.log(data); // typed as MyEvent
|
|
237
|
+
};
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Throws with a descriptive error if the message does not follow the envelope pattern (missing metadata, missing data, or malformed metadata fields).
|
|
241
|
+
|
|
187
242
|
## Health Check
|
|
188
243
|
|
|
189
244
|
```typescript
|
|
@@ -266,6 +321,29 @@ it('publishes a user event', async () => {
|
|
|
266
321
|
});
|
|
267
322
|
```
|
|
268
323
|
|
|
324
|
+
### Asserting Event Envelopes
|
|
325
|
+
|
|
326
|
+
Use `kafkaAssertEventEnvelope` to assert messages published with the event envelope pattern:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
import { kafkaAssertEventEnvelope, clearAllMocks } from '@woovi/kafka/test-utils';
|
|
330
|
+
|
|
331
|
+
beforeEach(() => clearAllMocks());
|
|
332
|
+
|
|
333
|
+
it('publishes a compliance event', async () => {
|
|
334
|
+
await myService.approveCompliance({ movementId: '123' });
|
|
335
|
+
|
|
336
|
+
kafkaAssertEventEnvelope({
|
|
337
|
+
topic: 'pix-out.compliance.approved',
|
|
338
|
+
eventType: 'pix-out.compliance.approved',
|
|
339
|
+
source: 'woovi-compliance',
|
|
340
|
+
data: { movementId: '123', pixOutId: '456' },
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
This validates the full envelope structure (metadata with `eventId`, `eventType`, `eventTime`, `source`) and checks that the data is a subset match.
|
|
346
|
+
|
|
269
347
|
## License
|
|
270
348
|
|
|
271
349
|
ISC
|
package/dist/index.cjs
CHANGED
|
@@ -38,11 +38,13 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
38
38
|
produceLatency: ()=>produceLatency,
|
|
39
39
|
createConsumer: ()=>createConsumer,
|
|
40
40
|
getKafka: ()=>getKafka,
|
|
41
|
+
parseEventEnvelope: ()=>parseEventEnvelope,
|
|
41
42
|
resetMetrics: ()=>resetMetrics,
|
|
42
|
-
disableTracing: ()=>disableTracing,
|
|
43
43
|
getMetricsContentType: ()=>getMetricsContentType,
|
|
44
|
+
disableTracing: ()=>disableTracing,
|
|
44
45
|
WooviProducer: ()=>WooviProducer,
|
|
45
46
|
enableDefaultMetrics: ()=>enableDefaultMetrics,
|
|
47
|
+
createEventEnvelope: ()=>createEventEnvelope,
|
|
46
48
|
messagesProducedTotal: ()=>messagesProducedTotal,
|
|
47
49
|
messageProcessingDuration: ()=>messageProcessingDuration,
|
|
48
50
|
createProducer: ()=>createProducer,
|
|
@@ -1085,6 +1087,33 @@ class WooviConsumer {
|
|
|
1085
1087
|
function createConsumer(config) {
|
|
1086
1088
|
return new WooviConsumer(config);
|
|
1087
1089
|
}
|
|
1090
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
1091
|
+
const createEventEnvelope = (args)=>({
|
|
1092
|
+
metadata: {
|
|
1093
|
+
eventId: (0, external_node_crypto_namespaceObject.randomUUID)(),
|
|
1094
|
+
eventType: args.eventType,
|
|
1095
|
+
eventTime: new Date().toISOString(),
|
|
1096
|
+
operationType: args.operationType,
|
|
1097
|
+
source: args.source,
|
|
1098
|
+
...args.extraMetadata
|
|
1099
|
+
},
|
|
1100
|
+
data: args.data
|
|
1101
|
+
});
|
|
1102
|
+
const isValidMetadata = (metadata)=>{
|
|
1103
|
+
if ('object' != typeof metadata || null === metadata) return false;
|
|
1104
|
+
const m = metadata;
|
|
1105
|
+
return 'string' == typeof m.eventId && 'string' == typeof m.eventType && 'string' == typeof m.eventTime && 'string' == typeof m.source;
|
|
1106
|
+
};
|
|
1107
|
+
const parseEventEnvelope = (message)=>{
|
|
1108
|
+
const envelope = message.data;
|
|
1109
|
+
if (!envelope || 'object' != typeof envelope) throw new Error(`Invalid event envelope: message data is not an object. Topic: ${message.topic}, offset: ${message.offset}`);
|
|
1110
|
+
if (!isValidMetadata(envelope.metadata)) throw new Error(`Invalid event envelope: missing or malformed metadata. Expected eventId, eventType, eventTime, and source. Topic: ${message.topic}, offset: ${message.offset}`);
|
|
1111
|
+
if (!('data' in envelope) || void 0 === envelope.data) throw new Error(`Invalid event envelope: missing data field. Topic: ${message.topic}, offset: ${message.offset}`);
|
|
1112
|
+
return {
|
|
1113
|
+
metadata: envelope.metadata,
|
|
1114
|
+
data: envelope.data
|
|
1115
|
+
};
|
|
1116
|
+
};
|
|
1088
1117
|
exports.Kafka = __webpack_exports__.Kafka;
|
|
1089
1118
|
exports.WooviConsumer = __webpack_exports__.WooviConsumer;
|
|
1090
1119
|
exports.WooviProducer = __webpack_exports__.WooviProducer;
|
|
@@ -1092,6 +1121,7 @@ exports.batchProcessingDuration = __webpack_exports__.batchProcessingDuration;
|
|
|
1092
1121
|
exports.batchSize = __webpack_exports__.batchSize;
|
|
1093
1122
|
exports.consumerLag = __webpack_exports__.consumerLag;
|
|
1094
1123
|
exports.createConsumer = __webpack_exports__.createConsumer;
|
|
1124
|
+
exports.createEventEnvelope = __webpack_exports__.createEventEnvelope;
|
|
1095
1125
|
exports.createKafka = __webpack_exports__.createKafka;
|
|
1096
1126
|
exports.createKafkaFromEnv = __webpack_exports__.createKafkaFromEnv;
|
|
1097
1127
|
exports.createProducer = __webpack_exports__.createProducer;
|
|
@@ -1115,6 +1145,7 @@ exports.messagesFailedTotal = __webpack_exports__.messagesFailedTotal;
|
|
|
1115
1145
|
exports.messagesProcessedTotal = __webpack_exports__.messagesProcessedTotal;
|
|
1116
1146
|
exports.messagesProducedTotal = __webpack_exports__.messagesProducedTotal;
|
|
1117
1147
|
exports.onShutdown = __webpack_exports__.onShutdown;
|
|
1148
|
+
exports.parseEventEnvelope = __webpack_exports__.parseEventEnvelope;
|
|
1118
1149
|
exports.produceLatency = __webpack_exports__.produceLatency;
|
|
1119
1150
|
exports.resetMetrics = __webpack_exports__.resetMetrics;
|
|
1120
1151
|
exports.setMockKafkaForTestMode = __webpack_exports__.setMockKafkaForTestMode;
|
|
@@ -1126,6 +1157,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
|
1126
1157
|
"batchSize",
|
|
1127
1158
|
"consumerLag",
|
|
1128
1159
|
"createConsumer",
|
|
1160
|
+
"createEventEnvelope",
|
|
1129
1161
|
"createKafka",
|
|
1130
1162
|
"createKafkaFromEnv",
|
|
1131
1163
|
"createProducer",
|
|
@@ -1149,6 +1181,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
|
1149
1181
|
"messagesProcessedTotal",
|
|
1150
1182
|
"messagesProducedTotal",
|
|
1151
1183
|
"onShutdown",
|
|
1184
|
+
"parseEventEnvelope",
|
|
1152
1185
|
"produceLatency",
|
|
1153
1186
|
"resetMetrics",
|
|
1154
1187
|
"setMockKafkaForTestMode"
|
package/dist/index.d.ts
CHANGED
|
@@ -85,6 +85,36 @@ declare interface Context {
|
|
|
85
85
|
*/
|
|
86
86
|
export declare function createConsumer(config: WooviConsumerConfig): WooviConsumer;
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Creates an event envelope wrapping data with standardized metadata.
|
|
90
|
+
* Extra metadata fields can be passed to extend the envelope dynamically.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* const envelope = createEventEnvelope({
|
|
95
|
+
* eventType: 'pix-out.compliance.approved',
|
|
96
|
+
* operationType: 'update',
|
|
97
|
+
* source: 'woovi-compliance',
|
|
98
|
+
* data: { movementId: '123', pixOutId: '456' },
|
|
99
|
+
* extraMetadata: { correlationId: 'abc-123', traceId: 'xyz-789' },
|
|
100
|
+
* });
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare const createEventEnvelope: <T>(args: CreateEventEnvelopeArgs<T>) => EventEnvelope<T>;
|
|
104
|
+
|
|
105
|
+
declare interface CreateEventEnvelopeArgs<T> {
|
|
106
|
+
/** Type/name of the event (e.g. "pix-out.compliance.approved") */
|
|
107
|
+
eventType: string;
|
|
108
|
+
/** Type of operation that triggered the event (e.g. "insert", "update", "delete") */
|
|
109
|
+
operationType?: string;
|
|
110
|
+
/** Source system that produced the event (e.g. "woovi-compliance") */
|
|
111
|
+
source: string;
|
|
112
|
+
/** The event payload data */
|
|
113
|
+
data: T;
|
|
114
|
+
/** Additional custom metadata fields to include in the envelope */
|
|
115
|
+
extraMetadata?: Record<string, unknown>;
|
|
116
|
+
}
|
|
117
|
+
|
|
88
118
|
export declare function createKafka(config: KafkaConfig): Kafka;
|
|
89
119
|
|
|
90
120
|
export declare function createKafkaFromEnv(clientId: string): Kafka;
|
|
@@ -130,6 +160,47 @@ export declare interface ErrorHandler<T = unknown> {
|
|
|
130
160
|
(error: Error, message: ConsumerMessage<T>): Promise<void>;
|
|
131
161
|
}
|
|
132
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Generic event envelope that wraps event data with standardized metadata.
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* ```typescript
|
|
168
|
+
* const envelope: EventEnvelope<UserCreatedData> = {
|
|
169
|
+
* metadata: {
|
|
170
|
+
* eventId: "9e28d9f6-693e-4aff-92f5-0d85faa06442",
|
|
171
|
+
* eventType: "user.created",
|
|
172
|
+
* eventTime: "2026-02-08T12:33:02.470Z",
|
|
173
|
+
* operationType: "insert",
|
|
174
|
+
* source: "user-service",
|
|
175
|
+
* },
|
|
176
|
+
* data: { userId: "123", name: "John" },
|
|
177
|
+
* };
|
|
178
|
+
* ```
|
|
179
|
+
*/
|
|
180
|
+
export declare interface EventEnvelope<T> {
|
|
181
|
+
metadata: EventMetadata;
|
|
182
|
+
data: T;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Standard event metadata following the event envelope pattern.
|
|
187
|
+
* Can be extended with additional fields via index signature.
|
|
188
|
+
*/
|
|
189
|
+
export declare interface EventMetadata {
|
|
190
|
+
/** Unique identifier for the event (UUID) */
|
|
191
|
+
eventId: string;
|
|
192
|
+
/** Type/name of the event (e.g. "pix-out.compliance.approved") */
|
|
193
|
+
eventType: string;
|
|
194
|
+
/** ISO 8601 timestamp of when the event was created */
|
|
195
|
+
eventTime: string;
|
|
196
|
+
/** Type of operation that triggered the event (e.g. "insert", "update", "delete") */
|
|
197
|
+
operationType?: string;
|
|
198
|
+
/** Source system that produced the event (e.g. "woovi-compliance") */
|
|
199
|
+
source: string;
|
|
200
|
+
/** Additional custom metadata fields */
|
|
201
|
+
[key: string]: unknown;
|
|
202
|
+
}
|
|
203
|
+
|
|
133
204
|
/**
|
|
134
205
|
* Extract trace context from Kafka message headers.
|
|
135
206
|
* Returns a Context that can be used to create child spans.
|
|
@@ -216,6 +287,26 @@ export declare const messagesProducedTotal: Counter<"topic" | "status">;
|
|
|
216
287
|
|
|
217
288
|
export declare function onShutdown(callback: () => Promise<void>): void;
|
|
218
289
|
|
|
290
|
+
declare interface ParsedEventEnvelope<T> {
|
|
291
|
+
metadata: EventMetadata;
|
|
292
|
+
data: T;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Validates and unwraps an event envelope from a consumed message.
|
|
297
|
+
* Throws if the message does not follow the event envelope pattern.
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* ```typescript
|
|
301
|
+
* const handler = async (message: ConsumerMessage<EventEnvelope<MyEvent>>) => {
|
|
302
|
+
* const { metadata, data } = parseEventEnvelope<MyEvent>(message);
|
|
303
|
+
* console.log(metadata.eventType, metadata.source);
|
|
304
|
+
* console.log(data); // typed as MyEvent
|
|
305
|
+
* };
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
export declare const parseEventEnvelope: <T>(message: ConsumerMessage<EventEnvelope<T>>) => ParsedEventEnvelope<T>;
|
|
309
|
+
|
|
219
310
|
export declare const produceLatency: Histogram<"topic">;
|
|
220
311
|
|
|
221
312
|
export declare interface PublishArgs<T = unknown> {
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Kafka, logLevel } from "kafkajs";
|
|
2
2
|
import { logger } from "@woovi/logger";
|
|
3
3
|
import { Counter, Gauge, Histogram, Registry, collectDefaultMetrics } from "prom-client";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
let kafkaInstance = null;
|
|
5
6
|
let testMode = false;
|
|
6
7
|
const shutdownCallbacks = [];
|
|
@@ -1025,4 +1026,30 @@ class WooviConsumer {
|
|
|
1025
1026
|
function createConsumer(config) {
|
|
1026
1027
|
return new WooviConsumer(config);
|
|
1027
1028
|
}
|
|
1028
|
-
|
|
1029
|
+
const createEventEnvelope = (args)=>({
|
|
1030
|
+
metadata: {
|
|
1031
|
+
eventId: randomUUID(),
|
|
1032
|
+
eventType: args.eventType,
|
|
1033
|
+
eventTime: new Date().toISOString(),
|
|
1034
|
+
operationType: args.operationType,
|
|
1035
|
+
source: args.source,
|
|
1036
|
+
...args.extraMetadata
|
|
1037
|
+
},
|
|
1038
|
+
data: args.data
|
|
1039
|
+
});
|
|
1040
|
+
const isValidMetadata = (metadata)=>{
|
|
1041
|
+
if ('object' != typeof metadata || null === metadata) return false;
|
|
1042
|
+
const m = metadata;
|
|
1043
|
+
return 'string' == typeof m.eventId && 'string' == typeof m.eventType && 'string' == typeof m.eventTime && 'string' == typeof m.source;
|
|
1044
|
+
};
|
|
1045
|
+
const parseEventEnvelope = (message)=>{
|
|
1046
|
+
const envelope = message.data;
|
|
1047
|
+
if (!envelope || 'object' != typeof envelope) throw new Error(`Invalid event envelope: message data is not an object. Topic: ${message.topic}, offset: ${message.offset}`);
|
|
1048
|
+
if (!isValidMetadata(envelope.metadata)) throw new Error(`Invalid event envelope: missing or malformed metadata. Expected eventId, eventType, eventTime, and source. Topic: ${message.topic}, offset: ${message.offset}`);
|
|
1049
|
+
if (!('data' in envelope) || void 0 === envelope.data) throw new Error(`Invalid event envelope: missing data field. Topic: ${message.topic}, offset: ${message.offset}`);
|
|
1050
|
+
return {
|
|
1051
|
+
metadata: envelope.metadata,
|
|
1052
|
+
data: envelope.data
|
|
1053
|
+
};
|
|
1054
|
+
};
|
|
1055
|
+
export { Kafka, WooviConsumer, WooviProducer, batchProcessingDuration, batchSize, consumerLag, createConsumer, createEventEnvelope, createKafka, createKafkaFromEnv, createProducer, disableTestMode, disableTracing, enableDefaultMetrics, enableTestMode, extractContextFromHeaders, getKafka, getMetrics, getMetricsContentType, healthCheck, initializeTracing, injectContextIntoHeaders, isTestMode, kafkaRegistry, lastMessageTimestamp, logLevel, messageProcessingDuration, messagesFailedTotal, messagesProcessedTotal, messagesProducedTotal, onShutdown, parseEventEnvelope, produceLatency, resetMetrics, setMockKafkaForTestMode };
|
package/dist/test-utils.cjs
CHANGED
|
@@ -33,14 +33,15 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
33
33
|
mockConsumer: ()=>mockConsumer,
|
|
34
34
|
kafkaAssert: ()=>kafkaAssert,
|
|
35
35
|
Kafka: ()=>Kafka,
|
|
36
|
+
kafkaAssertEventEnvelope: ()=>kafkaAssertEventEnvelope,
|
|
36
37
|
kafkaAssertLength: ()=>kafkaAssertLength,
|
|
37
38
|
mockProducerSend: ()=>mockProducerSend,
|
|
38
|
-
mockTransaction: ()=>mockTransaction,
|
|
39
39
|
mockProducerSendBatch: ()=>mockProducerSendBatch,
|
|
40
|
-
|
|
40
|
+
mockTransaction: ()=>mockTransaction,
|
|
41
41
|
KafkaJSNonRetriableError: ()=>KafkaJSNonRetriableError,
|
|
42
|
-
|
|
42
|
+
mockTransactionSend: ()=>mockTransactionSend,
|
|
43
43
|
CompressionTypes: ()=>CompressionTypes,
|
|
44
|
+
setupKafkaTest: ()=>setupKafkaTest,
|
|
44
45
|
mockProducer: ()=>mockProducer
|
|
45
46
|
});
|
|
46
47
|
const getMockFn = ()=>{
|
|
@@ -221,6 +222,39 @@ const kafkaAssert = (args)=>{
|
|
|
221
222
|
throw error;
|
|
222
223
|
}
|
|
223
224
|
};
|
|
225
|
+
const kafkaAssertEventEnvelope_isSubset = (subset, superset)=>{
|
|
226
|
+
for (const key of Object.keys(subset)){
|
|
227
|
+
const subValue = subset[key];
|
|
228
|
+
const superValue = superset[key];
|
|
229
|
+
if ('object' == typeof subValue && null !== subValue && 'object' == typeof superValue && null !== superValue) {
|
|
230
|
+
if (!kafkaAssertEventEnvelope_isSubset(subValue, superValue)) return false;
|
|
231
|
+
} else if (subValue !== superValue) return false;
|
|
232
|
+
}
|
|
233
|
+
return true;
|
|
234
|
+
};
|
|
235
|
+
const kafkaAssertEventEnvelope = (args)=>{
|
|
236
|
+
const { topic, eventType, source, data } = args;
|
|
237
|
+
const kafkaMessages = getKafkaMessages();
|
|
238
|
+
const topicMessages = kafkaMessages.filter((m)=>m.topic === topic);
|
|
239
|
+
if (0 === topicMessages.length) {
|
|
240
|
+
const error = new Error(`No messages found for topic: ${topic}`);
|
|
241
|
+
Error.captureStackTrace(error, kafkaAssertEventEnvelope);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
const parsedMessages = topicMessages.flatMap((m)=>m.messages.map((msg)=>JSON.parse(msg.value)));
|
|
245
|
+
const found = parsedMessages.some((envelope)=>{
|
|
246
|
+
if (!envelope.metadata || !envelope.data) return false;
|
|
247
|
+
if (envelope.metadata.eventType !== eventType) return false;
|
|
248
|
+
if (envelope.metadata.source !== source) return false;
|
|
249
|
+
if (!envelope.metadata.eventId || !envelope.metadata.eventTime) return false;
|
|
250
|
+
return kafkaAssertEventEnvelope_isSubset(data, envelope.data);
|
|
251
|
+
});
|
|
252
|
+
if (!found) {
|
|
253
|
+
const error = new Error(`Event envelope not found in topic "${topic}".\nExpected eventType: ${eventType}\nExpected source: ${source}\nExpected data: ${JSON.stringify(data, null, 2)}\nReceived: ${JSON.stringify(parsedMessages, null, 2)}`);
|
|
254
|
+
Error.captureStackTrace(error, kafkaAssertEventEnvelope);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
224
258
|
const kafkaAssertLength = (args)=>{
|
|
225
259
|
const { topic, length } = args;
|
|
226
260
|
const kafkaMessages = getKafkaMessages();
|
|
@@ -256,6 +290,7 @@ exports.clearAllMocks = __webpack_exports__.clearAllMocks;
|
|
|
256
290
|
exports.createMockKafka = __webpack_exports__.createMockKafka;
|
|
257
291
|
exports.getKafkaMessages = __webpack_exports__.getKafkaMessages;
|
|
258
292
|
exports.kafkaAssert = __webpack_exports__.kafkaAssert;
|
|
293
|
+
exports.kafkaAssertEventEnvelope = __webpack_exports__.kafkaAssertEventEnvelope;
|
|
259
294
|
exports.kafkaAssertLength = __webpack_exports__.kafkaAssertLength;
|
|
260
295
|
exports.logLevel = __webpack_exports__.logLevel;
|
|
261
296
|
exports.mockAdmin = __webpack_exports__.mockAdmin;
|
|
@@ -275,6 +310,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
|
|
|
275
310
|
"createMockKafka",
|
|
276
311
|
"getKafkaMessages",
|
|
277
312
|
"kafkaAssert",
|
|
313
|
+
"kafkaAssertEventEnvelope",
|
|
278
314
|
"kafkaAssertLength",
|
|
279
315
|
"logLevel",
|
|
280
316
|
"mockAdmin",
|
package/dist/test-utils.d.ts
CHANGED
|
@@ -33,6 +33,29 @@ declare type KafkaAssertArgs = {
|
|
|
33
33
|
message: Record<string, unknown>;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Asserts that a message with the event envelope pattern was published to a topic.
|
|
38
|
+
* Validates the envelope structure (metadata + data) and checks the data content.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* kafkaAssertEventEnvelope({
|
|
43
|
+
* topic: 'pix-out.compliance.approved',
|
|
44
|
+
* eventType: 'pix-out.compliance.approved',
|
|
45
|
+
* source: 'woovi-compliance',
|
|
46
|
+
* data: { movementId: '123', pixOutId: '456' },
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare const kafkaAssertEventEnvelope: (args: KafkaAssertEventEnvelopeArgs) => void;
|
|
51
|
+
|
|
52
|
+
declare type KafkaAssertEventEnvelopeArgs = {
|
|
53
|
+
topic: string;
|
|
54
|
+
eventType: string;
|
|
55
|
+
source: string;
|
|
56
|
+
data: Record<string, unknown>;
|
|
57
|
+
};
|
|
58
|
+
|
|
36
59
|
export declare const kafkaAssertLength: (args: KafkaAssertLengthArgs) => void;
|
|
37
60
|
|
|
38
61
|
declare type KafkaAssertLengthArgs = {
|
package/dist/test-utils.js
CHANGED
|
@@ -176,6 +176,39 @@ const kafkaAssert = (args)=>{
|
|
|
176
176
|
throw error;
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
|
+
const kafkaAssertEventEnvelope_isSubset = (subset, superset)=>{
|
|
180
|
+
for (const key of Object.keys(subset)){
|
|
181
|
+
const subValue = subset[key];
|
|
182
|
+
const superValue = superset[key];
|
|
183
|
+
if ('object' == typeof subValue && null !== subValue && 'object' == typeof superValue && null !== superValue) {
|
|
184
|
+
if (!kafkaAssertEventEnvelope_isSubset(subValue, superValue)) return false;
|
|
185
|
+
} else if (subValue !== superValue) return false;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
};
|
|
189
|
+
const kafkaAssertEventEnvelope = (args)=>{
|
|
190
|
+
const { topic, eventType, source, data } = args;
|
|
191
|
+
const kafkaMessages = getKafkaMessages();
|
|
192
|
+
const topicMessages = kafkaMessages.filter((m)=>m.topic === topic);
|
|
193
|
+
if (0 === topicMessages.length) {
|
|
194
|
+
const error = new Error(`No messages found for topic: ${topic}`);
|
|
195
|
+
Error.captureStackTrace(error, kafkaAssertEventEnvelope);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
const parsedMessages = topicMessages.flatMap((m)=>m.messages.map((msg)=>JSON.parse(msg.value)));
|
|
199
|
+
const found = parsedMessages.some((envelope)=>{
|
|
200
|
+
if (!envelope.metadata || !envelope.data) return false;
|
|
201
|
+
if (envelope.metadata.eventType !== eventType) return false;
|
|
202
|
+
if (envelope.metadata.source !== source) return false;
|
|
203
|
+
if (!envelope.metadata.eventId || !envelope.metadata.eventTime) return false;
|
|
204
|
+
return kafkaAssertEventEnvelope_isSubset(data, envelope.data);
|
|
205
|
+
});
|
|
206
|
+
if (!found) {
|
|
207
|
+
const error = new Error(`Event envelope not found in topic "${topic}".\nExpected eventType: ${eventType}\nExpected source: ${source}\nExpected data: ${JSON.stringify(data, null, 2)}\nReceived: ${JSON.stringify(parsedMessages, null, 2)}`);
|
|
208
|
+
Error.captureStackTrace(error, kafkaAssertEventEnvelope);
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
179
212
|
const kafkaAssertLength = (args)=>{
|
|
180
213
|
const { topic, length } = args;
|
|
181
214
|
const kafkaMessages = getKafkaMessages();
|
|
@@ -203,4 +236,4 @@ function setupKafkaTest() {
|
|
|
203
236
|
testMode: true
|
|
204
237
|
};
|
|
205
238
|
}
|
|
206
|
-
export { CompressionTypes, Kafka, KafkaJSNonRetriableError, KafkaJSProtocolError, clearAllMocks, createMockKafka, getKafkaMessages, kafkaAssert, kafkaAssertLength, logLevel, mockAdmin, mockConsumer, mockProducer, mockProducerSend, mockProducerSendBatch, mockTransaction, mockTransactionSend, setupKafkaTest };
|
|
239
|
+
export { CompressionTypes, Kafka, KafkaJSNonRetriableError, KafkaJSProtocolError, clearAllMocks, createMockKafka, getKafkaMessages, kafkaAssert, kafkaAssertEventEnvelope, kafkaAssertLength, logLevel, mockAdmin, mockConsumer, mockProducer, mockProducerSend, mockProducerSendBatch, mockTransaction, mockTransactionSend, setupKafkaTest };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@woovi/kafka",
|
|
3
3
|
"description": "Kafka setup and utilities for Woovi microservices",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.8",
|
|
5
5
|
"author": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"dependencies": {
|
|
@@ -47,10 +47,10 @@
|
|
|
47
47
|
],
|
|
48
48
|
"license": "ISC",
|
|
49
49
|
"main": "./dist/index.cjs",
|
|
50
|
+
"packageManager": "pnpm@10.14.0",
|
|
50
51
|
"publishConfig": {
|
|
51
52
|
"access": "public"
|
|
52
53
|
},
|
|
53
|
-
"types": "./dist/index.d.ts",
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "rslib build",
|
|
56
56
|
"test": "vitest",
|
|
@@ -63,5 +63,6 @@
|
|
|
63
63
|
"release:major": "npm version major && git push --follow-tags",
|
|
64
64
|
"release:minor": "npm version minor && git push --follow-tags",
|
|
65
65
|
"release:patch": "npm version patch && git push --follow-tags"
|
|
66
|
-
}
|
|
67
|
-
|
|
66
|
+
},
|
|
67
|
+
"types": "./dist/index.d.ts"
|
|
68
|
+
}
|