@entrolytics/clickhouse-client 3.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) 2025 Entrolytics <hey@entrolytics.click>
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.
@@ -0,0 +1,20 @@
1
+ import type { EventDBRow } from '@entrolytics/protocol';
2
+ interface Logger {
3
+ info: (msg: string, ...args: unknown[]) => void;
4
+ warn: (msg: string, ...args: unknown[]) => void;
5
+ error: (msg: string, ...args: unknown[]) => void;
6
+ }
7
+ export declare function setClickHouseLogger(customLogger: Logger): void;
8
+ export declare const clickhouse: import("@clickhouse/client").ClickHouseClient;
9
+ export declare function testClickHouseConnection(): Promise<boolean>;
10
+ /**
11
+ * Insert rows into a ClickHouse table with bounded retry and per-row error tolerance. Retries ONLY
12
+ * transient errors so a permanent bad-data error fails fast instead of being retried and amplifying
13
+ * duplicates (WP-6); per-row tolerance prevents one bad row from dropping the batch (WP-2). Used by
14
+ * every analytics ingest path (events, forms, vitals) so resilience is consistent.
15
+ */
16
+ export declare function insertWithRetry(table: string, values: readonly unknown[], retries?: number): Promise<void>;
17
+ /** Insert analytics events (events table) with retry + row tolerance. */
18
+ export declare function insertEvents(events: EventDBRow[], retries?: number): Promise<void>;
19
+ export {};
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAGvD,UAAU,MAAM;IACd,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;IAC/C,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;IAC/C,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CACjD;AAsBD,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,QAEvD;AA+CD,eAAO,MAAM,UAAU,+CAAiC,CAAA;AAGxD,wBAAsB,wBAAwB,qBAW7C;AA6CD;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,SAAS,OAAO,EAAE,EAC1B,OAAO,SAAI,GACV,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED,yEAAyE;AACzE,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,OAAO,SAAI,iBAEnE"}
package/dist/index.js ADDED
@@ -0,0 +1,153 @@
1
+ import { format } from 'node:util';
2
+ import { createClient } from '@clickhouse/client';
3
+ const defaultLogger = {
4
+ error: (msg, ...args) => {
5
+ process.stderr.write(`[clickhouse:error] ${msg}${args.length > 0 ? ` ${format(...args)}` : ''}\n`);
6
+ },
7
+ info: (msg, ...args) => {
8
+ process.stdout.write(`[clickhouse:info] ${msg}${args.length > 0 ? ` ${format(...args)}` : ''}\n`);
9
+ },
10
+ warn: (msg, ...args) => {
11
+ process.stdout.write(`[clickhouse:warn] ${msg}${args.length > 0 ? ` ${format(...args)}` : ''}\n`);
12
+ },
13
+ };
14
+ let logger = defaultLogger;
15
+ export function setClickHouseLogger(customLogger) {
16
+ logger = customLogger;
17
+ }
18
+ const clickhouseSettings = {
19
+ // 30 seconds max execution time for queries
20
+ max_execution_time: 30,
21
+ };
22
+ /**
23
+ * Resolve the ClickHouse HTTP endpoint, supporting TLS (CBI-011). Precedence:
24
+ *
25
+ * 1. CLICKHOUSE_URL — a full URL (e.g. https://host:8443), used verbatim.
26
+ * 2. CLICKHOUSE_HOST that already includes a scheme — used verbatim.
27
+ * 3. Host + port, with the scheme chosen by CLICKHOUSE_SECURE (https on 8443). This lets
28
+ * managed/TLS-only ClickHouse (e.g. ClickHouse Cloud) be configured purely through env instead
29
+ * of being hardcoded to http://.
30
+ */
31
+ function resolveClickHouseUrl() {
32
+ const fullUrl = process.env.CLICKHOUSE_URL;
33
+ if (fullUrl)
34
+ return fullUrl;
35
+ const host = process.env.CLICKHOUSE_HOST ?? 'localhost';
36
+ if (/^https?:\/\//i.test(host))
37
+ return host;
38
+ const secure = ['1', 'true', 'yes', 'on'].includes((process.env.CLICKHOUSE_SECURE ?? '').trim().toLowerCase());
39
+ const protocol = secure ? 'https' : 'http';
40
+ const port = process.env.CLICKHOUSE_PORT ?? (secure ? '8443' : '8123');
41
+ return `${protocol}://${host}:${port}`;
42
+ }
43
+ const clickhouseConfig = {
44
+ clickhouse_settings: clickhouseSettings,
45
+ compression: {
46
+ request: true,
47
+ response: true,
48
+ },
49
+ database: process.env.CLICKHOUSE_DATABASE ?? 'analytics',
50
+ keep_alive: {
51
+ enabled: true,
52
+ },
53
+ password: process.env.CLICKHOUSE_PASSWORD ?? '',
54
+ request_timeout: 30_000,
55
+ url: resolveClickHouseUrl(),
56
+ username: process.env.CLICKHOUSE_USER ?? 'default',
57
+ };
58
+ export const clickhouse = createClient(clickhouseConfig);
59
+ // Test connection
60
+ export async function testClickHouseConnection() {
61
+ try {
62
+ await clickhouse.query({
63
+ query: 'SELECT 1',
64
+ });
65
+ logger.info('✅ ClickHouse connection successful');
66
+ return true;
67
+ }
68
+ catch (err) {
69
+ logger.error('❌ ClickHouse connection failed:', err);
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Per-row error tolerance for analytics inserts. ClickHouse rejects a JSONEachRow batch atomically
75
+ * by default, so a single malformed row would drop up to ~100 good events. These settings let the
76
+ * server skip a small number of broken rows (and unknown fields) instead of failing the whole batch
77
+ * (WP-2). The ratio cap keeps a genuinely corrupt batch from silently disappearing.
78
+ */
79
+ const rowToleranceSettings = {
80
+ // Buffer small producer batches server-side while still acknowledging only after the flush
81
+ // succeeds. This reduces part creation without weakening delivery guarantees.
82
+ async_insert: 1,
83
+ // UInt64 settings are typed as strings, Float settings as numbers by the client.
84
+ input_format_allow_errors_num: '10',
85
+ input_format_allow_errors_ratio: 0.1,
86
+ input_format_skip_unknown_fields: 1,
87
+ wait_for_async_insert: 1,
88
+ };
89
+ /**
90
+ * Heuristic: is this ClickHouse error worth retrying? Connection/timeout/server (5xx-like) errors
91
+ * are transient; parse/type/syntax errors are permanent and retrying them only amplifies
92
+ * duplicate-insert risk (WP-6).
93
+ */
94
+ function isRetryableClickHouseError(err) {
95
+ const message = (err instanceof Error ? err.message : typeof err === 'string' ? err : '').toLowerCase();
96
+ if (!message)
97
+ return true;
98
+ const permanentSignals = [
99
+ 'cannot parse',
100
+ 'type mismatch',
101
+ 'unknown field',
102
+ 'syntax error',
103
+ 'cannot convert',
104
+ 'value is too',
105
+ 'illegal',
106
+ 'bad request',
107
+ ];
108
+ if (permanentSignals.some((signal) => message.includes(signal)))
109
+ return false;
110
+ // Explicit 4xx status in the message → client error, do not retry.
111
+ if (/\b4\d{2}\b/.test(message))
112
+ return false;
113
+ return true;
114
+ }
115
+ /**
116
+ * Insert rows into a ClickHouse table with bounded retry and per-row error tolerance. Retries ONLY
117
+ * transient errors so a permanent bad-data error fails fast instead of being retried and amplifying
118
+ * duplicates (WP-6); per-row tolerance prevents one bad row from dropping the batch (WP-2). Used by
119
+ * every analytics ingest path (events, forms, vitals) so resilience is consistent.
120
+ */
121
+ export async function insertWithRetry(table, values, retries = 3) {
122
+ if (values.length === 0)
123
+ return;
124
+ let lastError;
125
+ for (let i = 0; i < retries; i++) {
126
+ try {
127
+ await clickhouse.insert({
128
+ clickhouse_settings: rowToleranceSettings,
129
+ format: 'JSONEachRow',
130
+ table,
131
+ values: [...values],
132
+ });
133
+ return;
134
+ }
135
+ catch (err) {
136
+ lastError = err;
137
+ if (!isRetryableClickHouseError(err)) {
138
+ logger.error(`❌ ClickHouse insert into ${table} failed (non-retryable)`, err);
139
+ throw err;
140
+ }
141
+ logger.warn(`⚠️ ClickHouse insert into ${table} failed (attempt ${i + 1}/${retries}):`, err);
142
+ // Exponential backoff: 300ms, 600ms, 1200ms
143
+ await new Promise((resolve) => setTimeout(resolve, 300 * 2 ** i));
144
+ }
145
+ }
146
+ logger.error(`❌ ClickHouse insert into ${table} failed after retries`, lastError);
147
+ throw lastError;
148
+ }
149
+ /** Insert analytics events (events table) with retry + row tolerance. */
150
+ export async function insertEvents(events, retries = 3) {
151
+ await insertWithRetry('analytics.events', events, retries);
152
+ }
153
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@entrolytics/clickhouse-client",
3
+ "version": "3.0.0",
4
+ "files": [
5
+ "dist",
6
+ "!dist/*.tsbuildinfo",
7
+ "package.json"
8
+ ],
9
+ "type": "module",
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "dependencies": {
22
+ "@clickhouse/client": "^1.24.0-head.443b2f0.1",
23
+ "@entrolytics/protocol": "^3.0.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^26.5.0",
27
+ "typescript": "next"
28
+ },
29
+ "scripts": {
30
+ "build": "tsc",
31
+ "check-types": "tsc -p tsconfig.check.json"
32
+ }
33
+ }