n8n-nodes-kafka-batch-consumer 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/.eslintrc.js +15 -0
- package/IMPLEMENTATION_VERIFICATION.md +417 -0
- package/PROJECT_STRUCTURE.md +268 -0
- package/QUICK_START.md +181 -0
- package/README.md +132 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.d.ts +16 -0
- package/dist/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.d.ts.map +1 -0
- package/dist/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.js +299 -0
- package/dist/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.js.map +1 -0
- package/jest.config.js +22 -0
- package/package.json +41 -0
- package/run-tests.sh +39 -0
- package/src/index.ts +1 -0
- package/src/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.test.ts +1100 -0
- package/src/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.ts +331 -0
- package/src/nodes/KafkaBatchConsumer/kafka.svg +3 -0
- package/tsconfig.json +20 -0
package/QUICK_START.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Quick Start Guide
|
|
2
|
+
|
|
3
|
+
## Installation & Testing
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# Install dependencies
|
|
7
|
+
npm install
|
|
8
|
+
|
|
9
|
+
# Run all tests
|
|
10
|
+
npm test
|
|
11
|
+
|
|
12
|
+
# Run tests with coverage
|
|
13
|
+
npm run test:coverage
|
|
14
|
+
|
|
15
|
+
# Build the project
|
|
16
|
+
npm run build
|
|
17
|
+
|
|
18
|
+
# Lint the code
|
|
19
|
+
npm run lint
|
|
20
|
+
|
|
21
|
+
# Or use the test runner script
|
|
22
|
+
./run-tests.sh
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Test Suite Summary
|
|
26
|
+
|
|
27
|
+
### 32 Comprehensive Tests
|
|
28
|
+
|
|
29
|
+
#### ✅ Node Description (3 tests)
|
|
30
|
+
- Node properties validation
|
|
31
|
+
- Credentials configuration
|
|
32
|
+
- Parameters validation
|
|
33
|
+
|
|
34
|
+
#### ✅ Credentials Handling (8 tests)
|
|
35
|
+
- Unauthenticated connections
|
|
36
|
+
- SASL PLAIN authentication
|
|
37
|
+
- SASL SCRAM-SHA-256 authentication
|
|
38
|
+
- SASL SCRAM-SHA-512 authentication
|
|
39
|
+
- SSL/TLS configuration
|
|
40
|
+
- Combined SASL + SSL
|
|
41
|
+
- SSL rejectUnauthorized handling
|
|
42
|
+
- Auth config validation
|
|
43
|
+
|
|
44
|
+
#### ✅ Connection Handling (3 tests)
|
|
45
|
+
- Successful broker connections
|
|
46
|
+
- Connection error handling
|
|
47
|
+
- Broker list parsing
|
|
48
|
+
|
|
49
|
+
#### ✅ Topic Subscription (2 tests)
|
|
50
|
+
- fromBeginning flag handling
|
|
51
|
+
- Subscription error handling
|
|
52
|
+
|
|
53
|
+
#### ✅ Message Collection (4 tests)
|
|
54
|
+
- Exact batch size collection
|
|
55
|
+
- Batch size limit enforcement
|
|
56
|
+
- Complete metadata handling
|
|
57
|
+
- Missing field handling
|
|
58
|
+
|
|
59
|
+
#### ✅ JSON Parsing (3 tests)
|
|
60
|
+
- Valid JSON parsing
|
|
61
|
+
- String preservation
|
|
62
|
+
- Invalid JSON handling
|
|
63
|
+
|
|
64
|
+
#### ✅ Timeout Handling (3 tests)
|
|
65
|
+
- Timeout with insufficient messages
|
|
66
|
+
- Partial batch collection
|
|
67
|
+
- Custom readTimeout
|
|
68
|
+
|
|
69
|
+
#### ✅ Error Handling (4 tests)
|
|
70
|
+
- Consumer disconnect on errors
|
|
71
|
+
- NodeOperationError wrapping
|
|
72
|
+
- Resource cleanup
|
|
73
|
+
- Disconnect error handling
|
|
74
|
+
|
|
75
|
+
#### ✅ Output Format (4 tests)
|
|
76
|
+
- INodeExecutionData format
|
|
77
|
+
- Complete field inclusion
|
|
78
|
+
- Null key handling
|
|
79
|
+
- Empty value handling
|
|
80
|
+
|
|
81
|
+
#### ✅ Integration (1 test)
|
|
82
|
+
- Complete workflow simulation
|
|
83
|
+
|
|
84
|
+
## Key Files
|
|
85
|
+
|
|
86
|
+
| File | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `src/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.ts` | Main node implementation |
|
|
89
|
+
| `src/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.test.ts` | Complete test suite |
|
|
90
|
+
| `package.json` | Dependencies and scripts |
|
|
91
|
+
| `jest.config.js` | Test configuration (80% coverage threshold) |
|
|
92
|
+
| `README.md` | Full documentation |
|
|
93
|
+
| `PROJECT_STRUCTURE.md` | Detailed project overview |
|
|
94
|
+
|
|
95
|
+
## Coverage Requirements
|
|
96
|
+
|
|
97
|
+
Minimum 80% for all metrics:
|
|
98
|
+
- ✅ Branches: 80%+
|
|
99
|
+
- ✅ Functions: 80%+
|
|
100
|
+
- ✅ Lines: 80%+
|
|
101
|
+
- ✅ Statements: 80%+
|
|
102
|
+
|
|
103
|
+
## Node Features
|
|
104
|
+
|
|
105
|
+
### Parameters
|
|
106
|
+
- **brokers**: Kafka broker addresses
|
|
107
|
+
- **clientId**: Client identifier
|
|
108
|
+
- **groupId**: Consumer group ID
|
|
109
|
+
- **topic**: Topic to consume from
|
|
110
|
+
- **batchSize**: Messages per batch
|
|
111
|
+
- **fromBeginning**: Read from start
|
|
112
|
+
- **sessionTimeout**: Session timeout (ms)
|
|
113
|
+
- **options.readTimeout**: Max wait time (ms)
|
|
114
|
+
- **options.parseJson**: Auto-parse JSON
|
|
115
|
+
|
|
116
|
+
### Credentials (Optional)
|
|
117
|
+
- **SASL**: plain, scram-sha-256, scram-sha-512
|
|
118
|
+
- **SSL**: TLS with certificates
|
|
119
|
+
- **Combined**: SASL + SSL
|
|
120
|
+
|
|
121
|
+
### Output Format
|
|
122
|
+
```typescript
|
|
123
|
+
{
|
|
124
|
+
json: {
|
|
125
|
+
topic: string,
|
|
126
|
+
partition: number,
|
|
127
|
+
offset: string,
|
|
128
|
+
key: string | null,
|
|
129
|
+
value: any,
|
|
130
|
+
timestamp: string,
|
|
131
|
+
headers: Record<string, any>
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Testing Tips
|
|
137
|
+
|
|
138
|
+
### Run specific test
|
|
139
|
+
```bash
|
|
140
|
+
npm test -- -t "should connect with SASL PLAIN"
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Watch mode
|
|
144
|
+
```bash
|
|
145
|
+
npm test -- --watch
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Debug mode
|
|
149
|
+
```bash
|
|
150
|
+
node --inspect-brk node_modules/.bin/jest --runInBand
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Update snapshots (if any)
|
|
154
|
+
```bash
|
|
155
|
+
npm test -- -u
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Common Issues
|
|
159
|
+
|
|
160
|
+
### TypeScript errors before npm install
|
|
161
|
+
**Expected** - Dependencies need to be installed first.
|
|
162
|
+
|
|
163
|
+
### Coverage below 80%
|
|
164
|
+
Review untested code paths and add missing test cases.
|
|
165
|
+
|
|
166
|
+
### Mock not working
|
|
167
|
+
Ensure `jest.clearAllMocks()` is called in `beforeEach()`.
|
|
168
|
+
|
|
169
|
+
## Next Steps
|
|
170
|
+
|
|
171
|
+
1. ✅ Project created with all files
|
|
172
|
+
2. ⏳ Run `npm install`
|
|
173
|
+
3. ⏳ Run `npm run test:coverage`
|
|
174
|
+
4. ⏳ Verify 80%+ coverage
|
|
175
|
+
5. ⏳ Build with `npm run build`
|
|
176
|
+
6. ⏳ Test in N8N environment
|
|
177
|
+
|
|
178
|
+
## Questions?
|
|
179
|
+
|
|
180
|
+
See [README.md](README.md) for full documentation.
|
|
181
|
+
See [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md) for detailed structure.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# N8N Kafka Batch Consumer Node
|
|
2
|
+
|
|
3
|
+
A custom N8N node for consuming Kafka messages in batches using KafkaJS.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Batch Message Consumption**: Collect a configurable number of messages before processing
|
|
8
|
+
- **Flexible Authentication**: Support for SASL (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512) and SSL/TLS
|
|
9
|
+
- **Comprehensive Error Handling**: Graceful error handling with proper resource cleanup
|
|
10
|
+
- **JSON Parsing**: Automatic JSON parsing with fallback to string
|
|
11
|
+
- **Timeout Management**: Configurable read timeout with partial batch support
|
|
12
|
+
- **N8N Integration**: Standard N8N node with credential support
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run build
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration Parameters
|
|
22
|
+
|
|
23
|
+
### Required Parameters
|
|
24
|
+
|
|
25
|
+
- **Brokers**: Comma-separated list of Kafka broker addresses (e.g., `localhost:9092`)
|
|
26
|
+
- **Client ID**: Unique identifier for this Kafka client
|
|
27
|
+
- **Group ID**: Consumer group identifier
|
|
28
|
+
- **Topic**: Kafka topic to consume from
|
|
29
|
+
- **Batch Size**: Number of messages to consume in a batch
|
|
30
|
+
|
|
31
|
+
### Optional Parameters
|
|
32
|
+
|
|
33
|
+
- **From Beginning**: Whether to read from the beginning of the topic (default: `false`)
|
|
34
|
+
- **Session Timeout**: Session timeout in milliseconds (default: `30000`)
|
|
35
|
+
|
|
36
|
+
### Options
|
|
37
|
+
|
|
38
|
+
- **Read Timeout**: Maximum time to wait for messages in milliseconds (default: `60000`)
|
|
39
|
+
- **Parse JSON**: Whether to parse message values as JSON (default: `true`)
|
|
40
|
+
|
|
41
|
+
## Credentials
|
|
42
|
+
|
|
43
|
+
The node supports optional Kafka credentials with the following features:
|
|
44
|
+
|
|
45
|
+
### SASL Authentication
|
|
46
|
+
|
|
47
|
+
- **PLAIN**: Simple username/password authentication
|
|
48
|
+
- **SCRAM-SHA-256**: Salted Challenge Response Authentication Mechanism with SHA-256
|
|
49
|
+
- **SCRAM-SHA-512**: Salted Challenge Response Authentication Mechanism with SHA-512
|
|
50
|
+
|
|
51
|
+
### SSL/TLS Configuration
|
|
52
|
+
|
|
53
|
+
- **Reject Unauthorized**: Whether to reject unauthorized SSL certificates
|
|
54
|
+
- **CA Certificate**: Certificate Authority certificate
|
|
55
|
+
- **Client Certificate**: Client certificate for mutual TLS
|
|
56
|
+
- **Client Key**: Client private key for mutual TLS
|
|
57
|
+
|
|
58
|
+
## Usage Example
|
|
59
|
+
|
|
60
|
+
1. Add the "Kafka Batch Consumer" node to your workflow
|
|
61
|
+
2. Configure the broker addresses and topic
|
|
62
|
+
3. Set the desired batch size
|
|
63
|
+
4. Optionally configure credentials for authentication
|
|
64
|
+
5. Run the workflow to consume messages
|
|
65
|
+
|
|
66
|
+
## Output Format
|
|
67
|
+
|
|
68
|
+
Each message is returned as an `INodeExecutionData` object with the following structure:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
{
|
|
72
|
+
json: {
|
|
73
|
+
topic: string,
|
|
74
|
+
partition: number,
|
|
75
|
+
offset: string,
|
|
76
|
+
key: string | null,
|
|
77
|
+
value: any,
|
|
78
|
+
timestamp: string,
|
|
79
|
+
headers: Record<string, any>
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Testing
|
|
85
|
+
|
|
86
|
+
The project includes comprehensive Jest tests covering:
|
|
87
|
+
|
|
88
|
+
- Credential handling (SASL, SSL, combinations)
|
|
89
|
+
- Connection management
|
|
90
|
+
- Message collection and batching
|
|
91
|
+
- JSON parsing
|
|
92
|
+
- Timeout handling
|
|
93
|
+
- Error handling
|
|
94
|
+
- Output format validation
|
|
95
|
+
|
|
96
|
+
Run tests:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm test
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Run tests with coverage:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
npm run test:coverage
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Coverage target: 80% minimum
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
### Build
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
npm run build
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Lint
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
npm run lint
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Test
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npm test
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oDAAoD,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./nodes/KafkaBatchConsumer/KafkaBatchConsumer.node"), exports);
|
|
18
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qFAAmE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Step 1: Node Interface
|
|
4
|
+
* Implements INodeType with complete node description
|
|
5
|
+
* Defines all Kafka configuration properties
|
|
6
|
+
* Includes optional credentials reference for authentication
|
|
7
|
+
*/
|
|
8
|
+
export declare class KafkaBatchConsumer implements INodeType {
|
|
9
|
+
description: INodeTypeDescription;
|
|
10
|
+
/**
|
|
11
|
+
* Main execution method
|
|
12
|
+
* Handles the complete workflow: credentials, connection, consumption, and error handling
|
|
13
|
+
*/
|
|
14
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=KafkaBatchConsumer.node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KafkaBatchConsumer.node.d.ts","sourceRoot":"","sources":["../../../src/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,SAAS,EACT,oBAAoB,EAErB,MAAM,cAAc,CAAC;AAItB;;;;;GAKG;AACH,qBAAa,kBAAmB,YAAW,SAAS;IAClD,WAAW,EAAE,oBAAoB,CAoG/B;IAEF;;;OAGG;IACG,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CA4MxE"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.KafkaBatchConsumer = void 0;
|
|
5
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
6
|
+
const kafkajs_1 = require("kafkajs");
|
|
7
|
+
/**
|
|
8
|
+
* Step 1: Node Interface
|
|
9
|
+
* Implements INodeType with complete node description
|
|
10
|
+
* Defines all Kafka configuration properties
|
|
11
|
+
* Includes optional credentials reference for authentication
|
|
12
|
+
*/
|
|
13
|
+
class KafkaBatchConsumer {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.description = {
|
|
16
|
+
displayName: 'Kafka Batch Consumer',
|
|
17
|
+
name: 'kafkaBatchConsumer',
|
|
18
|
+
icon: 'file:kafka.svg',
|
|
19
|
+
group: ['transform'],
|
|
20
|
+
version: 1,
|
|
21
|
+
description: 'Consume messages from Kafka in batches',
|
|
22
|
+
defaults: {
|
|
23
|
+
name: 'Kafka Batch Consumer',
|
|
24
|
+
},
|
|
25
|
+
inputs: ['main'],
|
|
26
|
+
outputs: ['main'],
|
|
27
|
+
// Credentials reference - same as Kafka Trigger and Producer nodes
|
|
28
|
+
// Optional: allows unauthenticated connections
|
|
29
|
+
credentials: [
|
|
30
|
+
{
|
|
31
|
+
name: 'kafka',
|
|
32
|
+
required: false,
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
// Define all Kafka configuration properties
|
|
36
|
+
properties: [
|
|
37
|
+
{
|
|
38
|
+
displayName: 'Brokers',
|
|
39
|
+
name: 'brokers',
|
|
40
|
+
type: 'string',
|
|
41
|
+
default: 'localhost:9092',
|
|
42
|
+
required: true,
|
|
43
|
+
description: 'Comma-separated list of Kafka broker addresses',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
displayName: 'Client ID',
|
|
47
|
+
name: 'clientId',
|
|
48
|
+
type: 'string',
|
|
49
|
+
default: 'n8n-kafka-batch-consumer',
|
|
50
|
+
required: true,
|
|
51
|
+
description: 'Unique identifier for this Kafka client',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
displayName: 'Group ID',
|
|
55
|
+
name: 'groupId',
|
|
56
|
+
type: 'string',
|
|
57
|
+
default: 'n8n-consumer-group',
|
|
58
|
+
required: true,
|
|
59
|
+
description: 'Consumer group identifier',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
displayName: 'Topic',
|
|
63
|
+
name: 'topic',
|
|
64
|
+
type: 'string',
|
|
65
|
+
default: '',
|
|
66
|
+
required: true,
|
|
67
|
+
description: 'Kafka topic to consume from',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
displayName: 'Batch Size',
|
|
71
|
+
name: 'batchSize',
|
|
72
|
+
type: 'number',
|
|
73
|
+
default: 10,
|
|
74
|
+
required: true,
|
|
75
|
+
description: 'Number of messages to consume in a batch',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
displayName: 'From Beginning',
|
|
79
|
+
name: 'fromBeginning',
|
|
80
|
+
type: 'boolean',
|
|
81
|
+
default: false,
|
|
82
|
+
description: 'Whether to read from the beginning of the topic',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
displayName: 'Session Timeout',
|
|
86
|
+
name: 'sessionTimeout',
|
|
87
|
+
type: 'number',
|
|
88
|
+
default: 30000,
|
|
89
|
+
description: 'Session timeout in milliseconds',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
displayName: 'Options',
|
|
93
|
+
name: 'options',
|
|
94
|
+
type: 'collection',
|
|
95
|
+
placeholder: 'Add Option',
|
|
96
|
+
default: {},
|
|
97
|
+
options: [
|
|
98
|
+
{
|
|
99
|
+
displayName: 'Read Timeout',
|
|
100
|
+
name: 'readTimeout',
|
|
101
|
+
type: 'number',
|
|
102
|
+
default: 60000,
|
|
103
|
+
description: 'Maximum time to wait for messages in milliseconds',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
displayName: 'Parse JSON',
|
|
107
|
+
name: 'parseJson',
|
|
108
|
+
type: 'boolean',
|
|
109
|
+
default: true,
|
|
110
|
+
description: 'Whether to parse message values as JSON',
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Main execution method
|
|
119
|
+
* Handles the complete workflow: credentials, connection, consumption, and error handling
|
|
120
|
+
*/
|
|
121
|
+
async execute() {
|
|
122
|
+
const items = this.getInputData();
|
|
123
|
+
const returnData = [];
|
|
124
|
+
// Get all node parameters from N8N configuration
|
|
125
|
+
const brokers = this.getNodeParameter('brokers', 0);
|
|
126
|
+
const clientId = this.getNodeParameter('clientId', 0);
|
|
127
|
+
const groupId = this.getNodeParameter('groupId', 0);
|
|
128
|
+
const topic = this.getNodeParameter('topic', 0);
|
|
129
|
+
const batchSize = this.getNodeParameter('batchSize', 0);
|
|
130
|
+
const fromBeginning = this.getNodeParameter('fromBeginning', 0);
|
|
131
|
+
const sessionTimeout = this.getNodeParameter('sessionTimeout', 0);
|
|
132
|
+
const options = this.getNodeParameter('options', 0);
|
|
133
|
+
const readTimeout = options.readTimeout || 60000;
|
|
134
|
+
const parseJson = options.parseJson !== undefined ? options.parseJson : true;
|
|
135
|
+
// Parse comma-separated brokers string to array
|
|
136
|
+
const brokerList = brokers.split(',').map((b) => b.trim());
|
|
137
|
+
/**
|
|
138
|
+
* Step 2: Credentials Retrieval and Kafka Configuration
|
|
139
|
+
* Build KafkaJS configuration with optional authentication
|
|
140
|
+
* Supports SASL (PLAIN, SCRAM-SHA-256, SCRAM-SHA-512) and SSL/TLS
|
|
141
|
+
*/
|
|
142
|
+
// Build base Kafka configuration
|
|
143
|
+
const kafkaConfig = {
|
|
144
|
+
clientId,
|
|
145
|
+
brokers: brokerList,
|
|
146
|
+
};
|
|
147
|
+
// Attempt to retrieve optional Kafka credentials
|
|
148
|
+
let credentials = null;
|
|
149
|
+
try {
|
|
150
|
+
credentials = await this.getCredentials('kafka');
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
// Credentials are optional, continue without them for unauthenticated connections
|
|
154
|
+
}
|
|
155
|
+
// Map N8N credential fields to KafkaJS authentication format
|
|
156
|
+
// Add authentication if credentials are provided
|
|
157
|
+
if (credentials) {
|
|
158
|
+
// Add SASL authentication for secure connections
|
|
159
|
+
// Supports mechanisms: plain, scram-sha-256, scram-sha-512
|
|
160
|
+
if (credentials.authentication) {
|
|
161
|
+
kafkaConfig.sasl = {
|
|
162
|
+
mechanism: credentials.authentication, // PLAIN, SCRAM-SHA-256, or SCRAM-SHA-512
|
|
163
|
+
username: credentials.username,
|
|
164
|
+
password: credentials.password,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Add SSL/TLS configuration for encrypted connections
|
|
168
|
+
if (credentials.ssl !== undefined) {
|
|
169
|
+
kafkaConfig.ssl = {
|
|
170
|
+
rejectUnauthorized: credentials.ssl, // Validate server certificates
|
|
171
|
+
};
|
|
172
|
+
// Add optional SSL certificates for mutual TLS authentication
|
|
173
|
+
if (credentials.ca) {
|
|
174
|
+
kafkaConfig.ssl.ca = credentials.ca; // Certificate Authority
|
|
175
|
+
}
|
|
176
|
+
if (credentials.cert) {
|
|
177
|
+
kafkaConfig.ssl.cert = credentials.cert; // Client certificate
|
|
178
|
+
}
|
|
179
|
+
if (credentials.key) {
|
|
180
|
+
kafkaConfig.ssl.key = credentials.key; // Client private key
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Step 3: Consumer Setup
|
|
186
|
+
* Initialize Kafka client and consumer with configuration
|
|
187
|
+
* Connect to brokers and subscribe to topic
|
|
188
|
+
*/
|
|
189
|
+
// Create Kafka instance with complete configuration
|
|
190
|
+
const kafka = new kafkajs_1.Kafka(kafkaConfig);
|
|
191
|
+
// Create consumer with group ID and session timeout
|
|
192
|
+
const consumer = kafka.consumer({
|
|
193
|
+
groupId, // Consumer group for load balancing and offset management
|
|
194
|
+
sessionTimeout, // Session timeout in milliseconds
|
|
195
|
+
});
|
|
196
|
+
// Track connection state for proper cleanup
|
|
197
|
+
let consumerConnected = false;
|
|
198
|
+
try {
|
|
199
|
+
// Establish connection to Kafka brokers
|
|
200
|
+
await consumer.connect();
|
|
201
|
+
consumerConnected = true;
|
|
202
|
+
// Subscribe to the specified topic
|
|
203
|
+
// fromBeginning: if true, read from start; if false, read from latest
|
|
204
|
+
await consumer.subscribe({ topic, fromBeginning });
|
|
205
|
+
/**
|
|
206
|
+
* Step 4: Message Collection
|
|
207
|
+
* Collect messages in batch with timeout support
|
|
208
|
+
* Stop when batch size reached or timeout occurs
|
|
209
|
+
*/
|
|
210
|
+
// Initialize message collection array
|
|
211
|
+
const messages = [];
|
|
212
|
+
let timeoutHandle = null;
|
|
213
|
+
let resolvePromise = null;
|
|
214
|
+
const collectionPromise = new Promise((resolve) => {
|
|
215
|
+
resolvePromise = resolve;
|
|
216
|
+
});
|
|
217
|
+
// Set maximum wait time for message collection
|
|
218
|
+
timeoutHandle = setTimeout(() => {
|
|
219
|
+
if (resolvePromise) {
|
|
220
|
+
resolvePromise(); // Resolve with partial batch on timeout
|
|
221
|
+
}
|
|
222
|
+
}, readTimeout);
|
|
223
|
+
/**
|
|
224
|
+
* Start message consumption
|
|
225
|
+
* eachMessage callback processes messages one by one
|
|
226
|
+
* Collects until batch size or timeout reached
|
|
227
|
+
*/
|
|
228
|
+
await consumer.run({
|
|
229
|
+
eachMessage: async ({ topic, partition, message }) => {
|
|
230
|
+
/**
|
|
231
|
+
* Step 6: Output Format
|
|
232
|
+
* Process each message and format for N8N output
|
|
233
|
+
* Parse JSON if configured, preserve metadata
|
|
234
|
+
*/
|
|
235
|
+
// Parse message value from Buffer to string from Buffer to string
|
|
236
|
+
let value = message.value?.toString() || '';
|
|
237
|
+
// Attempt JSON parsing if configured
|
|
238
|
+
if (parseJson && value) {
|
|
239
|
+
try {
|
|
240
|
+
value = JSON.parse(value); // Parse valid JSON to object
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
// Keep as string if JSON parsing fails (invalid JSON)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Build N8N execution data with complete Kafka message metadata
|
|
247
|
+
const messageData = {
|
|
248
|
+
json: {
|
|
249
|
+
topic,
|
|
250
|
+
partition,
|
|
251
|
+
offset: message.offset,
|
|
252
|
+
key: message.key?.toString() || null,
|
|
253
|
+
value,
|
|
254
|
+
timestamp: message.timestamp,
|
|
255
|
+
headers: message.headers || {},
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
messages.push(messageData);
|
|
259
|
+
// Check if batch size reached
|
|
260
|
+
if (messages.length >= batchSize) {
|
|
261
|
+
if (timeoutHandle) {
|
|
262
|
+
clearTimeout(timeoutHandle); // Cancel timeout
|
|
263
|
+
}
|
|
264
|
+
if (resolvePromise) {
|
|
265
|
+
resolvePromise(); // Complete batch collection
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
/**
|
|
271
|
+
* Wait for collection to complete
|
|
272
|
+
* Completes when: batch size reached OR timeout occurs
|
|
273
|
+
* Partial batches are valid on timeout
|
|
274
|
+
*/
|
|
275
|
+
await collectionPromise;
|
|
276
|
+
// Gracefully disconnect consumer and cleanup resources
|
|
277
|
+
await consumer.disconnect();
|
|
278
|
+
consumerConnected = false;
|
|
279
|
+
// Add collected messages to return data
|
|
280
|
+
returnData.push(...messages);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
// Ensure consumer is disconnected
|
|
284
|
+
if (consumerConnected) {
|
|
285
|
+
try {
|
|
286
|
+
await consumer.disconnect();
|
|
287
|
+
}
|
|
288
|
+
catch (disconnectError) {
|
|
289
|
+
// Ignore disconnect errors
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
293
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Kafka error: ${errorMessage}`, { description: errorMessage });
|
|
294
|
+
}
|
|
295
|
+
return [returnData];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
exports.KafkaBatchConsumer = KafkaBatchConsumer;
|
|
299
|
+
//# sourceMappingURL=KafkaBatchConsumer.node.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"KafkaBatchConsumer.node.js","sourceRoot":"","sources":["../../../src/nodes/KafkaBatchConsumer/KafkaBatchConsumer.node.ts"],"names":[],"mappings":"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"}
|