n8n-nodes-clickhouse-db 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 [Your Name or Org]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,138 @@
1
+ # n8n-nodes-clickhouse
2
+
3
+ [![npm version](https://img.shields.io/npm/v/n8n-nodes-clickhouse.svg)](https://www.npmjs.com/package/n8n-nodes-clickhouse)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ n8n community node for [ClickHouse](https://clickhouse.com/) — query, insert, and manage your ClickHouse databases directly from n8n workflows and AI agents.
7
+
8
+ ## Why This Node
9
+
10
+ While n8n's built-in HTTP Request node can communicate with ClickHouse's HTTP interface, it requires manual URL construction, auth headers, response parsing, and error handling for every request. This node wraps all of that into a clean, purpose-built interface with parameterized queries, batch inserts, and full ClickHouse Cloud support — so you can focus on your data, not boilerplate.
11
+
12
+ ## Installation
13
+
14
+ ### Self-hosted n8n
15
+
16
+ Go to **Settings → Community Nodes → Install** and enter:
17
+
18
+ ```
19
+ n8n-nodes-clickhouse
20
+ ```
21
+
22
+ ### n8n Cloud
23
+
24
+ Search for **ClickHouse** in the nodes panel. As a verified node, no manual installation is needed.
25
+
26
+ ### Manual
27
+
28
+ ```bash
29
+ npm install n8n-nodes-clickhouse
30
+ ```
31
+
32
+ ## Operations
33
+
34
+ | Operation | Description |
35
+ |-----------|-------------|
36
+ | **Execute Query** | Run a SELECT query with optional parameterized values and return rows as n8n items |
37
+ | **Insert** | Insert n8n input items into a ClickHouse table using JSONEachRow format with configurable batch size |
38
+ | **Execute Raw** | Execute DDL/DML statements — CREATE TABLE, ALTER, DROP, TRUNCATE, OPTIMIZE, etc. |
39
+
40
+ ## Credentials Setup
41
+
42
+ Create a new **ClickHouse API** credential with the following fields:
43
+
44
+ | Field | Default | Description |
45
+ |-------|---------|-------------|
46
+ | Host | `localhost` | ClickHouse server hostname (no protocol or port) |
47
+ | Port | `8123` | HTTP port (`8123` for HTTP, `8443` for HTTPS / ClickHouse Cloud) |
48
+ | Database | `default` | Default database |
49
+ | Username | `default` | ClickHouse username |
50
+ | Password | *(empty)* | ClickHouse password |
51
+ | Protocol | `http` | `http` or `https` (use `https` for ClickHouse Cloud) |
52
+
53
+ The credential includes a built-in connectivity test that runs `SELECT 1` to verify the connection.
54
+
55
+ ## Usage Examples
56
+
57
+ ### Parameterized SELECT
58
+
59
+ Use `{name:Type}` placeholders in your SQL for safe, parameterized queries:
60
+
61
+ **Query:**
62
+ ```sql
63
+ SELECT * FROM events WHERE user_id = {user_id:UInt64} AND event_date >= {start_date:Date}
64
+ ```
65
+
66
+ **Query Parameters:**
67
+ - `user_id` → `12345`
68
+ - `start_date` → `2026-01-01`
69
+
70
+ Parameters are passed as URL query params (`param_user_id=12345`), which ClickHouse handles natively — no string concatenation, no SQL injection risk.
71
+
72
+ ### Insert from Webhook
73
+
74
+ 1. Add a **Webhook** trigger node
75
+ 2. Connect it to the **ClickHouse** node
76
+ 3. Set operation to **Insert**
77
+ 4. Set the table name (e.g., `webhook_events`)
78
+
79
+ All fields from the incoming webhook JSON body are inserted as columns. The node sends data in batches (default: 1000 rows per request).
80
+
81
+ ### CREATE TABLE with Execute Raw
82
+
83
+ Set operation to **Execute Raw** and enter:
84
+
85
+ ```sql
86
+ CREATE TABLE IF NOT EXISTS events (
87
+ event_id UInt64,
88
+ user_id UInt64,
89
+ event_type String,
90
+ event_date Date,
91
+ payload String
92
+ ) ENGINE = MergeTree()
93
+ ORDER BY (event_date, event_id)
94
+ ```
95
+
96
+ ## AI Agent Tool Usage
97
+
98
+ This node has `usableAsTool: true`, which means it can be wired as a tool in n8n's **AI Agent** node. This allows LLMs to query ClickHouse dynamically:
99
+
100
+ 1. Add an **AI Agent** node to your workflow
101
+ 2. In the Agent's **Tools** section, add the **ClickHouse** node
102
+ 3. Configure the ClickHouse credentials
103
+ 4. The AI agent can now generate and execute ClickHouse queries based on natural language prompts
104
+
105
+ This is useful for building conversational analytics interfaces where users can ask questions about their data in plain English.
106
+
107
+ ## ClickHouse Cloud
108
+
109
+ For ClickHouse Cloud instances:
110
+
111
+ - Set **Protocol** to `https`
112
+ - Set **Port** to `8443`
113
+ - Set **Host** to your Cloud hostname (from the ClickHouse Cloud console)
114
+ - Compatible with ClickHouse 26.x and the Cloud service
115
+
116
+ ## ClickHouse Settings
117
+
118
+ You can pass [ClickHouse settings](https://clickhouse.com/docs/en/operations/settings/settings) as a JSON string in the **Query Settings** option field:
119
+
120
+ ```json
121
+ {"max_execution_time": 30, "max_rows_to_read": 1000000}
122
+ ```
123
+
124
+ These are appended as URL query parameters to the ClickHouse HTTP request.
125
+
126
+ ## Compatibility
127
+
128
+ - **n8n**: >= 1.94.0 (verified node support)
129
+ - **Node.js**: >= 20
130
+ - **ClickHouse**: >= 22.x (tested up to 26.1)
131
+
132
+ ## Contributing
133
+
134
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for local development setup, PR guidelines, and the zero-runtime-dependencies requirement.
135
+
136
+ ## License
137
+
138
+ [MIT](LICENSE)
@@ -0,0 +1,9 @@
1
+ import type { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
2
+ export declare class ClickHouseApi implements ICredentialType {
3
+ name: string;
4
+ displayName: string;
5
+ documentationUrl: string;
6
+ properties: INodeProperties[];
7
+ authenticate: IAuthenticateGeneric;
8
+ test: ICredentialTestRequest;
9
+ }
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClickHouseApi = void 0;
4
+ class ClickHouseApi {
5
+ constructor() {
6
+ this.name = 'clickHouseApi';
7
+ this.displayName = 'ClickHouse API';
8
+ this.documentationUrl = 'https://clickhouse.com/docs/en/interfaces/http';
9
+ this.properties = [
10
+ {
11
+ displayName: 'Host',
12
+ name: 'host',
13
+ type: 'string',
14
+ default: 'localhost',
15
+ placeholder: 'localhost',
16
+ description: 'Hostname of the ClickHouse server (no protocol or port)',
17
+ },
18
+ {
19
+ displayName: 'Port',
20
+ name: 'port',
21
+ type: 'number',
22
+ default: 8123,
23
+ description: 'HTTP port of the ClickHouse server (8123 for HTTP, 8443 for HTTPS)',
24
+ },
25
+ {
26
+ displayName: 'Database',
27
+ name: 'database',
28
+ type: 'string',
29
+ default: 'default',
30
+ description: 'Default database to use',
31
+ },
32
+ {
33
+ displayName: 'Username',
34
+ name: 'username',
35
+ type: 'string',
36
+ default: 'default',
37
+ },
38
+ {
39
+ displayName: 'Password',
40
+ name: 'password',
41
+ type: 'string',
42
+ typeOptions: {
43
+ password: true,
44
+ },
45
+ default: '',
46
+ },
47
+ {
48
+ displayName: 'Protocol',
49
+ name: 'protocol',
50
+ type: 'options',
51
+ options: [
52
+ {
53
+ name: 'HTTP',
54
+ value: 'http',
55
+ description: 'Unencrypted — credentials are sent in plaintext. Use only for local/trusted networks.',
56
+ },
57
+ {
58
+ name: 'HTTPS (Recommended)',
59
+ value: 'https',
60
+ description: 'Encrypted connection. Required for ClickHouse Cloud (port 8443).',
61
+ },
62
+ ],
63
+ default: 'http',
64
+ description: 'HTTPS is strongly recommended. HTTP transmits credentials in plaintext.',
65
+ },
66
+ ];
67
+ this.authenticate = {
68
+ type: 'generic',
69
+ properties: {
70
+ headers: {
71
+ Authorization: '=Basic {{Buffer.from($credentials.username + ":" + $credentials.password).toString("base64")}}',
72
+ },
73
+ },
74
+ };
75
+ this.test = {
76
+ request: {
77
+ baseURL: '={{$credentials.protocol}}://{{$credentials.host}}:{{$credentials.port}}',
78
+ url: '/',
79
+ method: 'POST',
80
+ qs: {
81
+ database: '={{$credentials.database}}',
82
+ },
83
+ body: 'SELECT 1',
84
+ headers: {
85
+ 'Content-Type': 'text/plain',
86
+ },
87
+ },
88
+ };
89
+ }
90
+ }
91
+ exports.ClickHouseApi = ClickHouseApi;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Pure utility functions for ClickHouse HTTP interface communication.
3
+ * Uses ONLY Node.js built-ins — no npm imports.
4
+ */
5
+ export interface ClickHouseCredentials {
6
+ host: string;
7
+ port: number;
8
+ database: string;
9
+ username: string;
10
+ password: string;
11
+ protocol: string;
12
+ }
13
+ export interface QueryParam {
14
+ name: string;
15
+ value: string;
16
+ }
17
+ /**
18
+ * Validates a ClickHouse identifier (database name, table name).
19
+ * Throws if the identifier contains invalid characters that could enable injection.
20
+ */
21
+ export declare function validateIdentifier(value: string, label: string): void;
22
+ /**
23
+ * Validates that a port number is within the valid TCP range.
24
+ */
25
+ export declare function validatePort(port: number): void;
26
+ export declare function buildBaseUrl(credentials: ClickHouseCredentials): string;
27
+ export declare function buildAuthHeader(credentials: ClickHouseCredentials): string;
28
+ export declare function buildUrlParams(database: string, settings?: string, queryParams?: QueryParam[]): string;
29
+ export declare function chunkArray<T>(arr: T[], size: number): T[][];
30
+ export declare function parseClickHouseResponse(body: string): Record<string, unknown>[];
31
+ /**
32
+ * Extracts a user-safe error message from a ClickHouse HTTP error response.
33
+ * Strips potential credential/connection details and keeps only the
34
+ * ClickHouse error code and message.
35
+ */
36
+ export declare function extractClickHouseError(error: unknown): string;
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ /**
3
+ * Pure utility functions for ClickHouse HTTP interface communication.
4
+ * Uses ONLY Node.js built-ins — no npm imports.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.validateIdentifier = validateIdentifier;
8
+ exports.validatePort = validatePort;
9
+ exports.buildBaseUrl = buildBaseUrl;
10
+ exports.buildAuthHeader = buildAuthHeader;
11
+ exports.buildUrlParams = buildUrlParams;
12
+ exports.chunkArray = chunkArray;
13
+ exports.parseClickHouseResponse = parseClickHouseResponse;
14
+ exports.extractClickHouseError = extractClickHouseError;
15
+ /** Pattern for valid ClickHouse identifiers (database names, table names). */
16
+ const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
17
+ /** Pattern for valid query parameter names. */
18
+ const VALID_PARAM_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
19
+ /** Allowlist of ClickHouse settings that can be passed as URL parameters. */
20
+ const ALLOWED_SETTINGS = new Set([
21
+ 'max_execution_time',
22
+ 'max_rows_to_read',
23
+ 'max_result_rows',
24
+ 'max_result_bytes',
25
+ 'max_memory_usage',
26
+ 'max_bytes_before_external_sort',
27
+ 'max_bytes_before_external_group_by',
28
+ 'max_threads',
29
+ 'max_block_size',
30
+ 'max_insert_block_size',
31
+ 'read_overflow_mode',
32
+ 'result_overflow_mode',
33
+ 'timeout_overflow_mode',
34
+ 'connect_timeout',
35
+ 'receive_timeout',
36
+ 'send_timeout',
37
+ 'output_format_json_quote_64bit_integers',
38
+ 'output_format_json_quote_denormals',
39
+ 'enable_http_compression',
40
+ 'use_client_time_zone',
41
+ 'session_id',
42
+ 'session_timeout',
43
+ 'session_check',
44
+ 'extremes',
45
+ 'replace_running_query',
46
+ 'insert_quorum',
47
+ 'insert_quorum_timeout',
48
+ 'select_sequential_consistency',
49
+ 'date_time_input_format',
50
+ 'date_time_output_format',
51
+ 'input_format_allow_errors_num',
52
+ 'input_format_allow_errors_ratio',
53
+ 'join_use_nulls',
54
+ 'any_join_distinct_right_table_keys',
55
+ 'max_partitions_per_insert_block',
56
+ 'allow_experimental_lightweight_delete',
57
+ 'mutations_sync',
58
+ 'async_insert',
59
+ 'wait_for_async_insert',
60
+ ]);
61
+ /**
62
+ * Validates a ClickHouse identifier (database name, table name).
63
+ * Throws if the identifier contains invalid characters that could enable injection.
64
+ */
65
+ function validateIdentifier(value, label) {
66
+ if (!value || !VALID_IDENTIFIER.test(value)) {
67
+ throw new Error(`Invalid ${label}: "${value}". Only letters, digits, underscores, and dots are allowed.`);
68
+ }
69
+ }
70
+ /**
71
+ * Validates a query parameter name.
72
+ * Throws if the name contains invalid characters.
73
+ */
74
+ function validateParamName(name) {
75
+ if (!name || !VALID_PARAM_NAME.test(name)) {
76
+ throw new Error(`Invalid query parameter name: "${name}". Only letters, digits, and underscores are allowed.`);
77
+ }
78
+ }
79
+ /**
80
+ * Validates that a port number is within the valid TCP range.
81
+ */
82
+ function validatePort(port) {
83
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
84
+ throw new Error(`Invalid port: ${port}. Must be an integer between 1 and 65535.`);
85
+ }
86
+ }
87
+ function buildBaseUrl(credentials) {
88
+ validatePort(credentials.port);
89
+ const protocol = credentials.protocol === 'https' ? 'https' : 'http';
90
+ return `${protocol}://${credentials.host}:${credentials.port}`;
91
+ }
92
+ function buildAuthHeader(credentials) {
93
+ const token = Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
94
+ return `Basic ${token}`;
95
+ }
96
+ function buildUrlParams(database, settings, queryParams) {
97
+ validateIdentifier(database, 'database name');
98
+ const params = [`database=${encodeURIComponent(database)}`];
99
+ if (queryParams && queryParams.length > 0) {
100
+ for (const param of queryParams) {
101
+ validateParamName(param.name);
102
+ params.push(`param_${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`);
103
+ }
104
+ }
105
+ if (settings) {
106
+ try {
107
+ const parsed = JSON.parse(settings);
108
+ for (const [key, value] of Object.entries(parsed)) {
109
+ if (!ALLOWED_SETTINGS.has(key)) {
110
+ throw new Error(`ClickHouse setting "${key}" is not in the allowlist. ` +
111
+ 'See the node documentation for supported settings.');
112
+ }
113
+ params.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
114
+ }
115
+ }
116
+ catch (error) {
117
+ if (error instanceof SyntaxError) {
118
+ throw new Error('Query settings must be a valid JSON object (e.g. {"max_execution_time": 30}).');
119
+ }
120
+ throw error;
121
+ }
122
+ }
123
+ return params.join('&');
124
+ }
125
+ function chunkArray(arr, size) {
126
+ const safeSize = Math.max(1, Math.min(Math.floor(size), 100_000));
127
+ const chunks = [];
128
+ for (let i = 0; i < arr.length; i += safeSize) {
129
+ chunks.push(arr.slice(i, i + safeSize));
130
+ }
131
+ return chunks;
132
+ }
133
+ function parseClickHouseResponse(body) {
134
+ if (!body || !body.trim()) {
135
+ return [];
136
+ }
137
+ return body
138
+ .split('\n')
139
+ .filter(Boolean)
140
+ .map((line) => JSON.parse(line));
141
+ }
142
+ /**
143
+ * Extracts a user-safe error message from a ClickHouse HTTP error response.
144
+ * Strips potential credential/connection details and keeps only the
145
+ * ClickHouse error code and message.
146
+ */
147
+ function extractClickHouseError(error) {
148
+ let raw = '';
149
+ if (error && typeof error === 'object') {
150
+ const err = error;
151
+ if (err.response && typeof err.response === 'object') {
152
+ const response = err.response;
153
+ if (typeof response.body === 'string') {
154
+ raw = response.body;
155
+ }
156
+ }
157
+ if (!raw && err.cause && typeof err.cause === 'object') {
158
+ const cause = err.cause;
159
+ if (typeof cause.body === 'string') {
160
+ raw = cause.body;
161
+ }
162
+ }
163
+ if (!raw && typeof err.message === 'string') {
164
+ raw = err.message;
165
+ }
166
+ }
167
+ if (!raw) {
168
+ raw = String(error);
169
+ }
170
+ return sanitizeErrorMessage(raw);
171
+ }
172
+ /**
173
+ * Removes potentially sensitive data (hostnames, IPs, ports, file paths)
174
+ * from ClickHouse error messages while preserving the error code and
175
+ * the human-readable portion.
176
+ */
177
+ function sanitizeErrorMessage(message) {
178
+ // Keep the first 2000 characters to prevent huge error payloads
179
+ let sanitized = message.slice(0, 2000);
180
+ // Strip stack traces (lines starting with "at ")
181
+ sanitized = sanitized.replace(/\n\s+at .+/g, '');
182
+ // Strip absolute file paths
183
+ sanitized = sanitized.replace(/\/[^\s:]+\.(cpp|h|cc|c):\d+/g, '[internal]');
184
+ return sanitized.trim();
185
+ }
@@ -0,0 +1,5 @@
1
+ import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class ClickHouse implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClickHouse = void 0;
4
+ const n8n_workflow_1 = require("n8n-workflow");
5
+ const query_description_1 = require("./descriptions/query.description");
6
+ const insert_description_1 = require("./descriptions/insert.description");
7
+ const raw_description_1 = require("./descriptions/raw.description");
8
+ const ClickHouse_helpers_1 = require("./ClickHouse.helpers");
9
+ class ClickHouse {
10
+ constructor() {
11
+ this.description = {
12
+ displayName: 'ClickHouse',
13
+ name: 'clickHouse',
14
+ icon: 'file:clickhouse.svg',
15
+ group: ['transform'],
16
+ version: 1,
17
+ subtitle: '={{ $parameter["operation"] }}',
18
+ description: 'Query and insert data using ClickHouse',
19
+ defaults: {
20
+ name: 'ClickHouse',
21
+ },
22
+ inputs: ['main'],
23
+ outputs: ['main'],
24
+ credentials: [
25
+ {
26
+ name: 'clickHouseApi',
27
+ required: true,
28
+ },
29
+ ],
30
+ usableAsTool: true,
31
+ properties: [
32
+ {
33
+ displayName: 'Operation',
34
+ name: 'operation',
35
+ type: 'options',
36
+ noDataExpression: true,
37
+ options: [
38
+ {
39
+ name: 'Execute Query',
40
+ value: 'executeQuery',
41
+ description: 'Run a SELECT query and return the results',
42
+ action: 'Execute a query',
43
+ },
44
+ {
45
+ name: 'Insert',
46
+ value: 'insert',
47
+ description: 'Insert input items into a ClickHouse table',
48
+ action: 'Insert rows into a table',
49
+ },
50
+ {
51
+ name: 'Execute Raw',
52
+ value: 'executeRaw',
53
+ description: 'Execute DDL or DML statements (CREATE, ALTER, DROP, TRUNCATE, OPTIMIZE)',
54
+ action: 'Execute a raw DDL or DML statement',
55
+ },
56
+ ],
57
+ default: 'executeQuery',
58
+ },
59
+ ...query_description_1.queryFields,
60
+ ...insert_description_1.insertFields,
61
+ ...raw_description_1.rawFields,
62
+ ],
63
+ };
64
+ }
65
+ async execute() {
66
+ const items = this.getInputData();
67
+ const returnData = [];
68
+ const operation = this.getNodeParameter('operation', 0);
69
+ const credentials = (await this.getCredentials('clickHouseApi'));
70
+ const baseUrl = (0, ClickHouse_helpers_1.buildBaseUrl)(credentials);
71
+ const authHeader = (0, ClickHouse_helpers_1.buildAuthHeader)(credentials);
72
+ if (operation === 'executeQuery') {
73
+ for (let i = 0; i < items.length; i++) {
74
+ try {
75
+ let query = this.getNodeParameter('query', i);
76
+ const options = this.getNodeParameter('options', i, {});
77
+ // Build query parameters
78
+ const queryParametersData = this.getNodeParameter('queryParameters', i, {});
79
+ const queryParams = queryParametersData.params || [];
80
+ // Append LIMIT if not already present — use safe integer coercion
81
+ const limit = Math.max(0, Math.floor(options.limit ?? 100));
82
+ if (limit > 0 && !/\blimit\b/i.test(query)) {
83
+ query = `${query.replace(/;\s*$/, '')} LIMIT ${limit}`;
84
+ }
85
+ // Add FORMAT JSONEachRow
86
+ query = `${query.replace(/;\s*$/, '')} FORMAT JSONEachRow`;
87
+ const urlParams = (0, ClickHouse_helpers_1.buildUrlParams)(credentials.database, options.querySettings, queryParams);
88
+ const response = (await this.helpers.httpRequest({
89
+ method: 'POST',
90
+ url: `${baseUrl}/?${urlParams}`,
91
+ headers: {
92
+ Authorization: authHeader,
93
+ 'Content-Type': 'text/plain',
94
+ },
95
+ body: query,
96
+ returnFullResponse: false,
97
+ }));
98
+ const rows = (0, ClickHouse_helpers_1.parseClickHouseResponse)(response);
99
+ for (const row of rows) {
100
+ returnData.push({
101
+ json: row,
102
+ pairedItem: { item: i },
103
+ });
104
+ }
105
+ }
106
+ catch (error) {
107
+ const chError = (0, ClickHouse_helpers_1.extractClickHouseError)(error);
108
+ if (this.continueOnFail()) {
109
+ returnData.push({
110
+ json: { error: chError },
111
+ pairedItem: { item: i },
112
+ });
113
+ continue;
114
+ }
115
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), chError, { itemIndex: i });
116
+ }
117
+ }
118
+ }
119
+ else if (operation === 'insert') {
120
+ const table = this.getNodeParameter('table', 0);
121
+ const options = this.getNodeParameter('options', 0, {});
122
+ // Validate table name to prevent SQL injection
123
+ (0, ClickHouse_helpers_1.validateIdentifier)(table, 'table name');
124
+ const chunkSize = Math.max(1, Math.min(Math.floor(options.chunkSize ?? 1000), 100_000));
125
+ const database = options.database || credentials.database;
126
+ // Collect all items' JSON data
127
+ const allRows = items.map((item) => item.json);
128
+ const chunks = (0, ClickHouse_helpers_1.chunkArray)(allRows, chunkSize);
129
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
130
+ try {
131
+ const chunk = chunks[chunkIndex];
132
+ const ndjson = chunk.map((row) => JSON.stringify(row)).join('\n') + '\n';
133
+ const urlParams = (0, ClickHouse_helpers_1.buildUrlParams)(database);
134
+ const insertQuery = `INSERT INTO ${table} FORMAT JSONEachRow`;
135
+ await this.helpers.httpRequest({
136
+ method: 'POST',
137
+ url: `${baseUrl}/?${urlParams}&query=${encodeURIComponent(insertQuery)}`,
138
+ headers: {
139
+ Authorization: authHeader,
140
+ 'Content-Type': 'application/x-ndjson',
141
+ },
142
+ body: ndjson,
143
+ returnFullResponse: false,
144
+ });
145
+ }
146
+ catch (error) {
147
+ const chError = (0, ClickHouse_helpers_1.extractClickHouseError)(error);
148
+ if (this.continueOnFail()) {
149
+ returnData.push({
150
+ json: { error: chError, chunk: chunkIndex },
151
+ pairedItem: { item: 0 },
152
+ });
153
+ continue;
154
+ }
155
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), chError, { itemIndex: 0 });
156
+ }
157
+ }
158
+ // Return summary on success
159
+ if (returnData.length === 0) {
160
+ returnData.push({
161
+ json: {
162
+ success: true,
163
+ insertedRows: allRows.length,
164
+ },
165
+ pairedItem: { item: 0 },
166
+ });
167
+ }
168
+ }
169
+ else if (operation === 'executeRaw') {
170
+ for (let i = 0; i < items.length; i++) {
171
+ try {
172
+ const query = this.getNodeParameter('query', i);
173
+ const options = this.getNodeParameter('options', i, {});
174
+ const urlParams = (0, ClickHouse_helpers_1.buildUrlParams)(credentials.database, options.querySettings);
175
+ const response = (await this.helpers.httpRequest({
176
+ method: 'POST',
177
+ url: `${baseUrl}/?${urlParams}`,
178
+ headers: {
179
+ Authorization: authHeader,
180
+ 'Content-Type': 'text/plain',
181
+ },
182
+ body: query,
183
+ returnFullResponse: false,
184
+ }));
185
+ returnData.push({
186
+ json: {
187
+ success: true,
188
+ response: response || 'OK',
189
+ },
190
+ pairedItem: { item: i },
191
+ });
192
+ }
193
+ catch (error) {
194
+ const chError = (0, ClickHouse_helpers_1.extractClickHouseError)(error);
195
+ if (this.continueOnFail()) {
196
+ returnData.push({
197
+ json: { error: chError },
198
+ pairedItem: { item: i },
199
+ });
200
+ continue;
201
+ }
202
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), chError, { itemIndex: i });
203
+ }
204
+ }
205
+ }
206
+ return [returnData];
207
+ }
208
+ }
209
+ exports.ClickHouse = ClickHouse;
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
2
+ <rect x="6" y="8" width="10" height="44" rx="2" fill="#FBCD2C"/>
3
+ <rect x="20" y="16" width="10" height="36" rx="2" fill="#F5D76E"/>
4
+ <rect x="34" y="26" width="10" height="26" rx="2" fill="#FBE89B"/>
5
+ </svg>
@@ -0,0 +1,2 @@
1
+ import type { INodeProperties } from 'n8n-workflow';
2
+ export declare const insertFields: INodeProperties[];
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.insertFields = void 0;
4
+ exports.insertFields = [
5
+ {
6
+ displayName: 'Table',
7
+ name: 'table',
8
+ type: 'string',
9
+ required: true,
10
+ default: '',
11
+ placeholder: 'my_table',
12
+ description: 'The name of the table to insert data into',
13
+ displayOptions: {
14
+ show: {
15
+ operation: ['insert'],
16
+ },
17
+ },
18
+ },
19
+ {
20
+ displayName: 'Options',
21
+ name: 'options',
22
+ type: 'collection',
23
+ placeholder: 'Add Option',
24
+ default: {},
25
+ displayOptions: {
26
+ show: {
27
+ operation: ['insert'],
28
+ },
29
+ },
30
+ options: [
31
+ {
32
+ displayName: 'Chunk Size',
33
+ name: 'chunkSize',
34
+ type: 'number',
35
+ default: 1000,
36
+ description: 'Number of rows to insert per HTTP request',
37
+ typeOptions: {
38
+ minValue: 1,
39
+ },
40
+ },
41
+ {
42
+ displayName: 'Database',
43
+ name: 'database',
44
+ type: 'string',
45
+ default: '',
46
+ placeholder: 'my_database',
47
+ description: 'Override the database from credentials for this insert',
48
+ },
49
+ ],
50
+ },
51
+ ];
@@ -0,0 +1,2 @@
1
+ import type { INodeProperties } from 'n8n-workflow';
2
+ export declare const queryFields: INodeProperties[];
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.queryFields = void 0;
4
+ exports.queryFields = [
5
+ {
6
+ displayName: 'Query',
7
+ name: 'query',
8
+ type: 'string',
9
+ required: true,
10
+ default: '',
11
+ placeholder: 'SELECT * FROM my_table WHERE id = {id:UInt64}',
12
+ description: 'The SELECT SQL query to execute. Use {name:Type} syntax for parameterized queries.',
13
+ typeOptions: {
14
+ rows: 5,
15
+ alwaysOpenEditWindow: true,
16
+ },
17
+ displayOptions: {
18
+ show: {
19
+ operation: ['executeQuery'],
20
+ },
21
+ },
22
+ },
23
+ {
24
+ displayName: 'Query Parameters',
25
+ name: 'queryParameters',
26
+ type: 'fixedCollection',
27
+ default: {},
28
+ placeholder: 'Add Parameter',
29
+ description: 'Parameters for the query. Use {name:Type} placeholders in SQL and define values here.',
30
+ typeOptions: {
31
+ multipleValues: true,
32
+ },
33
+ displayOptions: {
34
+ show: {
35
+ operation: ['executeQuery'],
36
+ },
37
+ },
38
+ options: [
39
+ {
40
+ name: 'params',
41
+ displayName: 'Parameter',
42
+ values: [
43
+ {
44
+ displayName: 'Name',
45
+ name: 'name',
46
+ type: 'string',
47
+ default: '',
48
+ placeholder: 'id',
49
+ description: 'Parameter name (must match the {name:Type} placeholder in the query)',
50
+ },
51
+ {
52
+ displayName: 'Value',
53
+ name: 'value',
54
+ type: 'string',
55
+ default: '',
56
+ placeholder: '42',
57
+ description: 'Parameter value',
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ displayName: 'Options',
65
+ name: 'options',
66
+ type: 'collection',
67
+ placeholder: 'Add Option',
68
+ default: {},
69
+ displayOptions: {
70
+ show: {
71
+ operation: ['executeQuery'],
72
+ },
73
+ },
74
+ options: [
75
+ {
76
+ displayName: 'Limit',
77
+ name: 'limit',
78
+ type: 'number',
79
+ default: 100,
80
+ description: 'Maximum number of rows to return. Appended as LIMIT unless the query already contains one.',
81
+ typeOptions: {
82
+ minValue: 0,
83
+ },
84
+ },
85
+ {
86
+ displayName: 'Query Settings',
87
+ name: 'querySettings',
88
+ type: 'string',
89
+ default: '',
90
+ placeholder: '{"max_execution_time": 30}',
91
+ description: 'JSON string of ClickHouse settings to pass as URL parameters (e.g. max_execution_time, max_rows_to_read)',
92
+ },
93
+ ],
94
+ },
95
+ ];
@@ -0,0 +1,2 @@
1
+ import type { INodeProperties } from 'n8n-workflow';
2
+ export declare const rawFields: INodeProperties[];
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rawFields = void 0;
4
+ exports.rawFields = [
5
+ {
6
+ displayName: 'Query',
7
+ name: 'query',
8
+ type: 'string',
9
+ required: true,
10
+ default: '',
11
+ placeholder: 'CREATE TABLE IF NOT EXISTS my_table (id UInt64, name String) ENGINE = MergeTree() ORDER BY id',
12
+ description: 'The DDL or DML statement to execute (CREATE, ALTER, DROP, TRUNCATE, OPTIMIZE, etc.)',
13
+ typeOptions: {
14
+ rows: 5,
15
+ alwaysOpenEditWindow: true,
16
+ },
17
+ displayOptions: {
18
+ show: {
19
+ operation: ['executeRaw'],
20
+ },
21
+ },
22
+ },
23
+ {
24
+ displayName: 'Options',
25
+ name: 'options',
26
+ type: 'collection',
27
+ placeholder: 'Add Option',
28
+ default: {},
29
+ displayOptions: {
30
+ show: {
31
+ operation: ['executeRaw'],
32
+ },
33
+ },
34
+ options: [
35
+ {
36
+ displayName: 'Query Settings',
37
+ name: 'querySettings',
38
+ type: 'string',
39
+ default: '',
40
+ placeholder: '{"max_execution_time": 60}',
41
+ description: 'JSON string of ClickHouse settings to pass as URL parameters',
42
+ },
43
+ ],
44
+ },
45
+ ];
package/index.js ADDED
@@ -0,0 +1 @@
1
+ // n8n entry point — nothing to export; n8n loads nodes and credentials via package.json "n8n" key
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "n8n-nodes-clickhouse-db",
3
+ "version": "1.0.0",
4
+ "description": "n8n community node for ClickHouse — query, insert, and manage ClickHouse from n8n workflows and AI agents",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "clickhouse",
8
+ "database",
9
+ "analytics",
10
+ "olap",
11
+ "columnar"
12
+ ],
13
+ "license": "MIT",
14
+ "main": "index.js",
15
+ "scripts": {
16
+ "build": "tsc && gulp build:icons",
17
+ "dev": "tsc --watch",
18
+ "format": "prettier nodes credentials --write",
19
+ "lint": "eslint nodes credentials --ext .ts",
20
+ "lintfix": "eslint nodes credentials --ext .ts --fix",
21
+ "prepublishOnly": "npm run build && npm run lint"
22
+ },
23
+ "n8n": {
24
+ "n8nNodesApiVersion": 1,
25
+ "credentials": [
26
+ "dist/credentials/ClickHouseApi.credentials.js"
27
+ ],
28
+ "nodes": [
29
+ "dist/nodes/ClickHouse/ClickHouse.node.js"
30
+ ]
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "peerDependencies": {
38
+ "n8n-workflow": "*"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "@typescript-eslint/parser": "^8.57.0",
43
+ "eslint": "^8.50.0",
44
+ "eslint-plugin-n8n-nodes-base": "^1.16.0",
45
+ "gulp": "^5.0.1",
46
+ "n8n-workflow": "*",
47
+ "prettier": "^3.0.0",
48
+ "typescript": "^5.1.3"
49
+ }
50
+ }