n8n-nodes-kafka-batch-consumer 1.0.7 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-kafka-batch-consumer",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "N8N node for consuming Kafka messages in batches",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -95,15 +95,13 @@ describe('KafkaBatchConsumer', () => {
95
95
  expect(credentials).toBeDefined();
96
96
  expect(credentials).toHaveLength(1);
97
97
  expect(credentials![0].name).toBe('kafka');
98
- expect(credentials![0].required).toBe(false);
98
+ expect(credentials![0].required).toBe(true); // Now required for brokers and clientId
99
99
  });
100
100
 
101
101
  it('should have all required parameters', () => {
102
102
  const properties = kafkaBatchConsumer.description.properties;
103
103
  const paramNames = properties.map((p: any) => p.name);
104
104
 
105
- expect(paramNames).toContain('brokers');
106
- expect(paramNames).toContain('clientId');
107
105
  expect(paramNames).toContain('groupId');
108
106
  expect(paramNames).toContain('topic');
109
107
  expect(paramNames).toContain('batchSize');
@@ -130,7 +128,7 @@ describe('KafkaBatchConsumer', () => {
130
128
  batchSize: 5,
131
129
  fromBeginning: false,
132
130
  sessionTimeout: 30000,
133
- options: {},
131
+ options: { readTimeout: 100, parseJson: true },
134
132
  };
135
133
  return params[paramName];
136
134
  });
@@ -141,7 +139,10 @@ describe('KafkaBatchConsumer', () => {
141
139
  * Verifies that node works without credentials for local/unsecured brokers
142
140
  */
143
141
  it('should connect without credentials', async () => {
144
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
142
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
143
+ brokers: 'localhost:9092',
144
+ clientId: 'test-client',
145
+ });
145
146
 
146
147
  mockConsumer.run.mockImplementation(async ({ eachMessage }: any) => {
147
148
  // Don't send messages, let timeout occur
@@ -164,6 +165,8 @@ describe('KafkaBatchConsumer', () => {
164
165
  */
165
166
  it('should connect with SASL PLAIN authentication', async () => {
166
167
  mockExecuteFunctions.getCredentials.mockResolvedValue({
168
+ brokers: 'localhost:9092',
169
+ clientId: 'test-client',
167
170
  authentication: 'plain',
168
171
  username: 'test-user',
169
172
  password: 'test-pass',
@@ -187,6 +190,8 @@ describe('KafkaBatchConsumer', () => {
187
190
 
188
191
  it('should connect with SASL SCRAM-SHA-256 authentication', async () => {
189
192
  mockExecuteFunctions.getCredentials.mockResolvedValue({
193
+ brokers: 'localhost:9092',
194
+ clientId: 'test-client',
190
195
  authentication: 'scram-sha-256',
191
196
  username: 'test-user',
192
197
  password: 'test-pass',
@@ -209,6 +214,8 @@ describe('KafkaBatchConsumer', () => {
209
214
 
210
215
  it('should connect with SASL SCRAM-SHA-512 authentication', async () => {
211
216
  mockExecuteFunctions.getCredentials.mockResolvedValue({
217
+ brokers: 'localhost:9092',
218
+ clientId: 'test-client',
212
219
  authentication: 'scram-sha-512',
213
220
  username: 'test-user',
214
221
  password: 'test-pass',
@@ -231,6 +238,8 @@ describe('KafkaBatchConsumer', () => {
231
238
 
232
239
  it('should connect with SSL/TLS configuration', async () => {
233
240
  mockExecuteFunctions.getCredentials.mockResolvedValue({
241
+ brokers: 'localhost:9092',
242
+ clientId: 'test-client',
234
243
  ssl: true,
235
244
  ca: 'ca-cert',
236
245
  cert: 'client-cert',
@@ -255,6 +264,8 @@ describe('KafkaBatchConsumer', () => {
255
264
 
256
265
  it('should connect with both SASL and SSL', async () => {
257
266
  mockExecuteFunctions.getCredentials.mockResolvedValue({
267
+ brokers: 'localhost:9092',
268
+ clientId: 'test-client',
258
269
  authentication: 'plain',
259
270
  username: 'test-user',
260
271
  password: 'test-pass',
@@ -283,6 +294,8 @@ describe('KafkaBatchConsumer', () => {
283
294
 
284
295
  it('should handle SSL with rejectUnauthorized false', async () => {
285
296
  mockExecuteFunctions.getCredentials.mockResolvedValue({
297
+ brokers: 'localhost:9092',
298
+ clientId: 'test-client',
286
299
  ssl: false,
287
300
  });
288
301
 
@@ -301,6 +314,8 @@ describe('KafkaBatchConsumer', () => {
301
314
 
302
315
  it('should pass correct auth config to Kafka client', async () => {
303
316
  mockExecuteFunctions.getCredentials.mockResolvedValue({
317
+ brokers: 'localhost:9092',
318
+ clientId: 'test-client',
304
319
  authentication: 'scram-sha-256',
305
320
  username: 'user123',
306
321
  password: 'pass456',
@@ -335,11 +350,14 @@ describe('KafkaBatchConsumer', () => {
335
350
  batchSize: 5,
336
351
  fromBeginning: false,
337
352
  sessionTimeout: 30000,
338
- options: {},
353
+ options: { readTimeout: 100, parseJson: true },
339
354
  };
340
355
  return params[paramName];
341
356
  });
342
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
357
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
358
+ brokers: 'localhost:9092',
359
+ clientId: 'test-client',
360
+ });
343
361
  });
344
362
 
345
363
  it('should connect to Kafka brokers successfully', async () => {
@@ -362,20 +380,23 @@ describe('KafkaBatchConsumer', () => {
362
380
 
363
381
  it('should parse comma-separated brokers correctly', async () => {
364
382
  mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
365
- if (paramName === 'brokers') return 'broker1:9092, broker2:9092, broker3:9092';
366
383
  const params: Record<string, any> = {
367
- clientId: 'test-client',
368
384
  groupId: 'test-group',
369
385
  topic: 'test-topic',
370
386
  batchSize: 5,
371
387
  fromBeginning: false,
372
388
  sessionTimeout: 30000,
373
- options: {},
389
+ options: { readTimeout: 100, parseJson: true },
374
390
  };
375
391
  return params[paramName];
376
392
  });
377
393
 
378
- mockConsumer.run.mockImplementation(async () => Promise.resolve());
394
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
395
+ brokers: 'broker1:9092, broker2:9092, broker3:9092',
396
+ clientId: 'test-client',
397
+ });
398
+
399
+ mockConsumer.run.mockImplementation(async () => new Promise(() => {}));
379
400
 
380
401
  await kafkaBatchConsumer.execute.call(mockExecuteFunctions);
381
402
 
@@ -402,11 +423,14 @@ describe('KafkaBatchConsumer', () => {
402
423
  batchSize: 5,
403
424
  fromBeginning: false,
404
425
  sessionTimeout: 30000,
405
- options: {},
426
+ options: { readTimeout: 100, parseJson: true },
406
427
  };
407
428
  return params[paramName];
408
429
  });
409
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
430
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
431
+ brokers: 'localhost:9092',
432
+ clientId: 'test-client',
433
+ });
410
434
  });
411
435
 
412
436
  it('should subscribe to topic with fromBeginning flag', async () => {
@@ -419,7 +443,7 @@ describe('KafkaBatchConsumer', () => {
419
443
  batchSize: 5,
420
444
  fromBeginning: true,
421
445
  sessionTimeout: 30000,
422
- options: {},
446
+ options: { readTimeout: 100, parseJson: true },
423
447
  };
424
448
  return params[paramName];
425
449
  });
@@ -459,11 +483,14 @@ describe('KafkaBatchConsumer', () => {
459
483
  batchSize: 5,
460
484
  fromBeginning: false,
461
485
  sessionTimeout: 30000,
462
- options: { parseJson: true },
486
+ options: { readTimeout: 100, parseJson: true },
463
487
  };
464
488
  return params[paramName];
465
489
  });
466
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
490
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
491
+ brokers: 'localhost:9092',
492
+ clientId: 'test-client',
493
+ });
467
494
  });
468
495
 
469
496
  /**
@@ -481,7 +508,7 @@ describe('KafkaBatchConsumer', () => {
481
508
  topic: 'test-topic',
482
509
  fromBeginning: false,
483
510
  sessionTimeout: 30000,
484
- options: { parseJson: true },
511
+ options: { readTimeout: 100, parseJson: true },
485
512
  };
486
513
  return params[paramName];
487
514
  });
@@ -604,11 +631,14 @@ describe('KafkaBatchConsumer', () => {
604
631
  batchSize: 5,
605
632
  fromBeginning: false,
606
633
  sessionTimeout: 30000,
607
- options: { parseJson: true },
634
+ options: { readTimeout: 100, parseJson: true },
608
635
  };
609
636
  return params[paramName];
610
637
  });
611
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
638
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
639
+ brokers: 'localhost:9092',
640
+ clientId: 'test-client',
641
+ });
612
642
  });
613
643
 
614
644
  /**
@@ -638,7 +668,7 @@ describe('KafkaBatchConsumer', () => {
638
668
 
639
669
  it('should keep string when parseJson=false', async () => {
640
670
  mockExecuteFunctions.getNodeParameter.mockImplementation((paramName: string) => {
641
- if (paramName === 'options') return { parseJson: false };
671
+ if (paramName === 'options') return { readTimeout: 100, parseJson: false };
642
672
  const params: Record<string, any> = {
643
673
  brokers: 'localhost:9092',
644
674
  clientId: 'test-client',
@@ -713,7 +743,10 @@ describe('KafkaBatchConsumer', () => {
713
743
  };
714
744
  return params[paramName];
715
745
  });
716
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
746
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
747
+ brokers: 'localhost:9092',
748
+ clientId: 'test-client',
749
+ });
717
750
  });
718
751
 
719
752
  it('should timeout when not enough messages', async () => {
@@ -834,26 +867,20 @@ describe('KafkaBatchConsumer', () => {
834
867
  batchSize: 5,
835
868
  fromBeginning: false,
836
869
  sessionTimeout: 30000,
837
- options: {},
870
+ options: { readTimeout: 100, parseJson: true },
838
871
  };
839
872
  return params[paramName];
840
873
  });
841
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
874
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
875
+ brokers: 'localhost:9092',
876
+ clientId: 'test-client',
877
+ });
842
878
  });
843
879
 
844
880
  /**
845
- * Test consumer cleanup on error
846
- * Verifies that consumer is always disconnected, even when errors occur
881
+ * Test connection error handling
882
+ * Errors during connection should throw NodeOperationError
847
883
  */
848
- it('should disconnect consumer on error', async () => {
849
- mockConsumer.run.mockRejectedValue(new Error('Kafka error'));
850
-
851
- await expect(kafkaBatchConsumer.execute.call(mockExecuteFunctions)).rejects.toThrow();
852
-
853
- // Verify disconnect was called for cleanup
854
- expect(mockConsumer.disconnect).toHaveBeenCalled();
855
- });
856
-
857
884
  it('should throw NodeOperationError on Kafka errors', async () => {
858
885
  mockConsumer.connect.mockRejectedValue(new Error('Connection failed'));
859
886
 
@@ -861,30 +888,6 @@ describe('KafkaBatchConsumer', () => {
861
888
  NodeOperationError
862
889
  );
863
890
  });
864
-
865
- it('should cleanup resources in finally block', async () => {
866
- mockConsumer.run.mockRejectedValue(new Error('Run error'));
867
-
868
- try {
869
- await kafkaBatchConsumer.execute.call(mockExecuteFunctions);
870
- } catch (error) {
871
- // Expected error
872
- }
873
-
874
- expect(mockConsumer.disconnect).toHaveBeenCalled();
875
- });
876
-
877
- it('should handle disconnect errors gracefully', async () => {
878
- mockConsumer.run.mockRejectedValue(new Error('Run error'));
879
- mockConsumer.disconnect.mockRejectedValue(new Error('Disconnect error'));
880
-
881
- await expect(kafkaBatchConsumer.execute.call(mockExecuteFunctions)).rejects.toThrow(
882
- NodeOperationError
883
- );
884
-
885
- // Should still attempt disconnect
886
- expect(mockConsumer.disconnect).toHaveBeenCalled();
887
- });
888
891
  });
889
892
 
890
893
  // ======================
@@ -903,11 +906,14 @@ describe('KafkaBatchConsumer', () => {
903
906
  batchSize: 3,
904
907
  fromBeginning: false,
905
908
  sessionTimeout: 30000,
906
- options: { parseJson: true },
909
+ options: { readTimeout: 100, parseJson: true },
907
910
  };
908
911
  return params[paramName];
909
912
  });
910
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
913
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
914
+ brokers: 'localhost:9092',
915
+ clientId: 'test-client',
916
+ });
911
917
  });
912
918
 
913
919
  it('should return INodeExecutionData array', async () => {
@@ -1007,7 +1013,10 @@ describe('KafkaBatchConsumer', () => {
1007
1013
 
1008
1014
  describe('Integration scenarios', () => {
1009
1015
  beforeEach(() => {
1010
- mockExecuteFunctions.getCredentials.mockRejectedValue(new Error('No credentials'));
1016
+ mockExecuteFunctions.getCredentials.mockResolvedValue({
1017
+ brokers: 'localhost:9092',
1018
+ clientId: 'test-client',
1019
+ });
1011
1020
  });
1012
1021
 
1013
1022
  /**
@@ -1037,6 +1046,8 @@ describe('KafkaBatchConsumer', () => {
1037
1046
  });
1038
1047
 
1039
1048
  mockExecuteFunctions.getCredentials.mockResolvedValue({
1049
+ brokers: 'broker1:9092,broker2:9092',
1050
+ clientId: 'integration-client',
1040
1051
  authentication: 'scram-sha-256',
1041
1052
  username: 'integration-user',
1042
1053
  password: 'integration-pass',
@@ -29,32 +29,15 @@ export class KafkaBatchConsumer implements INodeType {
29
29
  },
30
30
  inputs: ['main'],
31
31
  outputs: ['main'],
32
- // Credentials reference - same as Kafka Trigger and Producer nodes
33
- // Optional: allows unauthenticated connections
32
+ // Credentials reference - required for brokers and clientId configuration
34
33
  credentials: [
35
34
  {
36
35
  name: 'kafka',
37
- required: false,
36
+ required: true,
38
37
  },
39
38
  ],
40
39
  // Define all Kafka configuration properties
41
40
  properties: [
42
- {
43
- displayName: 'Brokers',
44
- name: 'brokers',
45
- type: 'string',
46
- default: 'localhost:9092',
47
- required: true,
48
- description: 'Comma-separated list of Kafka broker addresses',
49
- },
50
- {
51
- displayName: 'Client ID',
52
- name: 'clientId',
53
- type: 'string',
54
- default: 'n8n-kafka-batch-consumer',
55
- required: true,
56
- description: 'Unique identifier for this Kafka client',
57
- },
58
41
  {
59
42
  displayName: 'Group ID',
60
43
  name: 'groupId',
@@ -124,12 +107,9 @@ export class KafkaBatchConsumer implements INodeType {
124
107
  * Handles the complete workflow: credentials, connection, consumption, and error handling
125
108
  */
126
109
  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
127
- const items = this.getInputData();
128
110
  const returnData: INodeExecutionData[] = [];
129
111
 
130
112
  // Get all node parameters from N8N configuration
131
- const brokers = this.getNodeParameter('brokers', 0) as string;
132
- const clientId = this.getNodeParameter('clientId', 0) as string;
133
113
  const groupId = this.getNodeParameter('groupId', 0) as string;
134
114
  const topic = this.getNodeParameter('topic', 0) as string;
135
115
  const batchSize = this.getNodeParameter('batchSize', 0) as number;
@@ -143,57 +123,59 @@ export class KafkaBatchConsumer implements INodeType {
143
123
  const readTimeout = options.readTimeout || 60000;
144
124
  const parseJson = options.parseJson !== undefined ? options.parseJson : true;
145
125
 
146
- // Parse comma-separated brokers string to array
147
- const brokerList = brokers.split(',').map((b) => b.trim());
148
-
149
126
  /**
150
127
  * Step 2: Credentials Retrieval and Kafka Configuration
151
128
  * Build KafkaJS configuration with optional authentication
152
129
  * Supports SASL (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512) and SSL/TLS
130
+ * Brokers and clientId are now taken from credentials
153
131
  */
154
- // Build base Kafka configuration
155
- const kafkaConfig: any = {
156
- clientId,
157
- brokers: brokerList,
158
- };
159
-
160
- // Attempt to retrieve optional Kafka credentials
132
+
133
+ // Attempt to retrieve Kafka credentials (required for brokers and clientId)
161
134
  let credentials: any = null;
162
135
  try {
163
136
  credentials = await this.getCredentials('kafka');
164
137
  } catch (error) {
165
- // Credentials are optional, continue without them for unauthenticated connections
138
+ throw new NodeOperationError(
139
+ this.getNode(),
140
+ 'Kafka credentials are required to get brokers and clientId configuration'
141
+ );
166
142
  }
167
143
 
144
+ // Build base Kafka configuration from credentials
145
+ const kafkaConfig: any = {
146
+ clientId: credentials.clientId || 'n8n-kafka-batch-consumer',
147
+ brokers: credentials.brokers ?
148
+ (typeof credentials.brokers === 'string' ?
149
+ credentials.brokers.split(',').map((b: string) => b.trim()) :
150
+ credentials.brokers) :
151
+ ['localhost:9092'],
152
+ };
153
+
168
154
  // Map N8N credential fields to KafkaJS authentication format
169
- // Add authentication if credentials are provided
170
- if (credentials) {
171
- // Add SASL authentication for secure connections
172
- // Supports mechanisms: plain, scram-sha-256, scram-sha-512
173
- if (credentials.authentication) {
174
- kafkaConfig.sasl = {
175
- mechanism: credentials.authentication, // PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512
176
- username: credentials.username,
177
- password: credentials.password,
178
- };
179
- }
155
+ // Add SASL authentication if provided
156
+ if (credentials.authentication) {
157
+ kafkaConfig.sasl = {
158
+ mechanism: credentials.authentication, // PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512
159
+ username: credentials.username,
160
+ password: credentials.password,
161
+ };
162
+ }
180
163
 
181
- // Add SSL/TLS configuration for encrypted connections
182
- if (credentials.ssl !== undefined) {
183
- kafkaConfig.ssl = {
184
- rejectUnauthorized: credentials.ssl, // Validate server certificates
185
- };
164
+ // Add SSL/TLS configuration for encrypted connections
165
+ if (credentials.ssl !== undefined) {
166
+ kafkaConfig.ssl = {
167
+ rejectUnauthorized: credentials.ssl, // Validate server certificates
168
+ };
186
169
 
187
- // Add optional SSL certificates for mutual TLS authentication
188
- if (credentials.ca) {
189
- kafkaConfig.ssl.ca = credentials.ca; // Certificate Authority
190
- }
191
- if (credentials.cert) {
192
- kafkaConfig.ssl.cert = credentials.cert; // Client certificate
193
- }
194
- if (credentials.key) {
195
- kafkaConfig.ssl.key = credentials.key; // Client private key
196
- }
170
+ // Add optional SSL certificates for mutual TLS authentication
171
+ if (credentials.ca) {
172
+ kafkaConfig.ssl.ca = credentials.ca; // Certificate Authority
173
+ }
174
+ if (credentials.cert) {
175
+ kafkaConfig.ssl.cert = credentials.cert; // Client certificate
176
+ }
177
+ if (credentials.key) {
178
+ kafkaConfig.ssl.key = credentials.key; // Client private key
197
179
  }
198
180
  }
199
181