@woovi/kafka 1.0.3 → 1.0.7

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 CHANGED
@@ -184,6 +184,40 @@ 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
+
187
221
  ## Health Check
188
222
 
189
223
  ```typescript
@@ -266,6 +300,29 @@ it('publishes a user event', async () => {
266
300
  });
267
301
  ```
268
302
 
303
+ ### Asserting Event Envelopes
304
+
305
+ Use `kafkaAssertEventEnvelope` to assert messages published with the event envelope pattern:
306
+
307
+ ```typescript
308
+ import { kafkaAssertEventEnvelope, clearAllMocks } from '@woovi/kafka/test-utils';
309
+
310
+ beforeEach(() => clearAllMocks());
311
+
312
+ it('publishes a compliance event', async () => {
313
+ await myService.approveCompliance({ movementId: '123' });
314
+
315
+ kafkaAssertEventEnvelope({
316
+ topic: 'pix-out.compliance.approved',
317
+ eventType: 'pix-out.compliance.approved',
318
+ source: 'woovi-compliance',
319
+ data: { movementId: '123', pixOutId: '456' },
320
+ });
321
+ });
322
+ ```
323
+
324
+ This validates the full envelope structure (metadata with `eventId`, `eventType`, `eventTime`, `source`) and checks that the data is a subset match.
325
+
269
326
  ## License
270
327
 
271
328
  ISC
package/dist/index.cjs CHANGED
@@ -39,10 +39,11 @@ __webpack_require__.d(__webpack_exports__, {
39
39
  createConsumer: ()=>createConsumer,
40
40
  getKafka: ()=>getKafka,
41
41
  resetMetrics: ()=>resetMetrics,
42
- disableTracing: ()=>disableTracing,
43
42
  getMetricsContentType: ()=>getMetricsContentType,
43
+ disableTracing: ()=>disableTracing,
44
44
  WooviProducer: ()=>WooviProducer,
45
45
  enableDefaultMetrics: ()=>enableDefaultMetrics,
46
+ createEventEnvelope: ()=>createEventEnvelope,
46
47
  messagesProducedTotal: ()=>messagesProducedTotal,
47
48
  messageProcessingDuration: ()=>messageProcessingDuration,
48
49
  createProducer: ()=>createProducer,
@@ -1085,6 +1086,18 @@ class WooviConsumer {
1085
1086
  function createConsumer(config) {
1086
1087
  return new WooviConsumer(config);
1087
1088
  }
1089
+ const external_node_crypto_namespaceObject = require("node:crypto");
1090
+ const createEventEnvelope = (args)=>({
1091
+ metadata: {
1092
+ eventId: (0, external_node_crypto_namespaceObject.randomUUID)(),
1093
+ eventType: args.eventType,
1094
+ eventTime: new Date().toISOString(),
1095
+ operationType: args.operationType,
1096
+ source: args.source,
1097
+ ...args.extraMetadata
1098
+ },
1099
+ data: args.data
1100
+ });
1088
1101
  exports.Kafka = __webpack_exports__.Kafka;
1089
1102
  exports.WooviConsumer = __webpack_exports__.WooviConsumer;
1090
1103
  exports.WooviProducer = __webpack_exports__.WooviProducer;
@@ -1092,6 +1105,7 @@ exports.batchProcessingDuration = __webpack_exports__.batchProcessingDuration;
1092
1105
  exports.batchSize = __webpack_exports__.batchSize;
1093
1106
  exports.consumerLag = __webpack_exports__.consumerLag;
1094
1107
  exports.createConsumer = __webpack_exports__.createConsumer;
1108
+ exports.createEventEnvelope = __webpack_exports__.createEventEnvelope;
1095
1109
  exports.createKafka = __webpack_exports__.createKafka;
1096
1110
  exports.createKafkaFromEnv = __webpack_exports__.createKafkaFromEnv;
1097
1111
  exports.createProducer = __webpack_exports__.createProducer;
@@ -1126,6 +1140,7 @@ for(var __webpack_i__ in __webpack_exports__)if (-1 === [
1126
1140
  "batchSize",
1127
1141
  "consumerLag",
1128
1142
  "createConsumer",
1143
+ "createEventEnvelope",
1129
1144
  "createKafka",
1130
1145
  "createKafkaFromEnv",
1131
1146
  "createProducer",
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.
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,15 @@ class WooviConsumer {
1025
1026
  function createConsumer(config) {
1026
1027
  return new WooviConsumer(config);
1027
1028
  }
1028
- export { Kafka, WooviConsumer, WooviProducer, batchProcessingDuration, batchSize, consumerLag, createConsumer, createKafka, createKafkaFromEnv, createProducer, disableTestMode, disableTracing, enableDefaultMetrics, enableTestMode, extractContextFromHeaders, getKafka, getMetrics, getMetricsContentType, healthCheck, initializeTracing, injectContextIntoHeaders, isTestMode, kafkaRegistry, lastMessageTimestamp, logLevel, messageProcessingDuration, messagesFailedTotal, messagesProcessedTotal, messagesProducedTotal, onShutdown, produceLatency, resetMetrics, setMockKafkaForTestMode };
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
+ 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, produceLatency, resetMetrics, setMockKafkaForTestMode };
@@ -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
- mockTransactionSend: ()=>mockTransactionSend,
40
+ mockTransaction: ()=>mockTransaction,
41
41
  KafkaJSNonRetriableError: ()=>KafkaJSNonRetriableError,
42
- setupKafkaTest: ()=>setupKafkaTest,
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",
@@ -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 = {
@@ -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,11 +1,11 @@
1
1
  {
2
2
  "name": "@woovi/kafka",
3
3
  "description": "Kafka setup and utilities for Woovi microservices",
4
- "version": "1.0.3",
4
+ "version": "1.0.7",
5
5
  "author": "",
6
6
  "type": "module",
7
7
  "dependencies": {
8
- "@woovi/logger": "^2.0.1",
8
+ "@woovi/logger": "^2.0.2",
9
9
  "kafkajs": "^2.2.4",
10
10
  "prom-client": "^15.1.3"
11
11
  },
@@ -47,10 +47,10 @@
47
47
  ],
48
48
  "license": "ISC",
49
49
  "main": "./dist/index.cjs",
50
- "packageManager": "pnpm@10.14.0",
51
50
  "publishConfig": {
52
51
  "access": "public"
53
52
  },
53
+ "types": "./dist/index.d.ts",
54
54
  "scripts": {
55
55
  "build": "rslib build",
56
56
  "test": "vitest",
@@ -63,6 +63,5 @@
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
- "types": "./dist/index.d.ts"
68
- }
66
+ }
67
+ }