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 +21 -0
- package/README.md +138 -0
- package/dist/credentials/ClickHouseApi.credentials.d.ts +9 -0
- package/dist/credentials/ClickHouseApi.credentials.js +91 -0
- package/dist/nodes/ClickHouse/ClickHouse.helpers.d.ts +36 -0
- package/dist/nodes/ClickHouse/ClickHouse.helpers.js +185 -0
- package/dist/nodes/ClickHouse/ClickHouse.node.d.ts +5 -0
- package/dist/nodes/ClickHouse/ClickHouse.node.js +209 -0
- package/dist/nodes/ClickHouse/clickhouse.svg +5 -0
- package/dist/nodes/ClickHouse/descriptions/insert.description.d.ts +2 -0
- package/dist/nodes/ClickHouse/descriptions/insert.description.js +51 -0
- package/dist/nodes/ClickHouse/descriptions/query.description.d.ts +2 -0
- package/dist/nodes/ClickHouse/descriptions/query.description.js +95 -0
- package/dist/nodes/ClickHouse/descriptions/raw.description.d.ts +2 -0
- package/dist/nodes/ClickHouse/descriptions/raw.description.js +45 -0
- package/index.js +1 -0
- package/package.json +50 -0
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
|
+
[](https://www.npmjs.com/package/n8n-nodes-clickhouse)
|
|
4
|
+
[](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,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,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,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
|
+
}
|