@voltro/plugin-clickhouse 0.1.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/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +4806 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +282 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { AggregateQuery } from '@voltro/runtime';
|
|
2
|
+
import { AnalyticsBucket } from '@voltro/runtime';
|
|
3
|
+
import { AnalyticsEvent } from '@voltro/runtime';
|
|
4
|
+
import { AnalyticsSinkSpec } from '@voltro/runtime';
|
|
5
|
+
import { AnalyticsTopEntry } from '@voltro/runtime';
|
|
6
|
+
import { TimeseriesQuery } from '@voltro/runtime';
|
|
7
|
+
import { TopNQuery } from '@voltro/runtime';
|
|
8
|
+
|
|
9
|
+
export { AggregateQuery }
|
|
10
|
+
|
|
11
|
+
export { AnalyticsBucket }
|
|
12
|
+
|
|
13
|
+
export { AnalyticsEvent }
|
|
14
|
+
|
|
15
|
+
export { AnalyticsTopEntry }
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Construct the spec. `initialize` opens the client + creates the
|
|
19
|
+
* events table on first boot.
|
|
20
|
+
*/
|
|
21
|
+
export declare const clickhouseAnalytics: (options: ClickhouseAnalyticsOptions) => AnalyticsSinkSpec;
|
|
22
|
+
|
|
23
|
+
export declare interface ClickhouseAnalyticsOptions {
|
|
24
|
+
/** HTTP URL of the ClickHouse server (incl. protocol + port). */
|
|
25
|
+
readonly url: string;
|
|
26
|
+
readonly username?: string;
|
|
27
|
+
readonly password?: string;
|
|
28
|
+
/** Database name. The plugin creates the `events` table inside it
|
|
29
|
+
* on first boot. Default: 'default'. */
|
|
30
|
+
readonly database?: string;
|
|
31
|
+
/** Override the events-table name. Default: 'events'. */
|
|
32
|
+
readonly table?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Reactive tables to CDC-mirror into ClickHouse. Each table gets a
|
|
35
|
+
* `voltro_mirror_<table>` ReplacingMergeTree (`{ id, data, version,
|
|
36
|
+
* is_deleted }`); the framework streams `DataStore` changes into it so
|
|
37
|
+
* analytical queries can JOIN events against live user data:
|
|
38
|
+
*
|
|
39
|
+
* SELECT JSONExtractString(m.data, 'tier'), count()
|
|
40
|
+
* FROM voltro_events e
|
|
41
|
+
* JOIN voltro_mirror_users FINAL m ON m.id = e.subject_id
|
|
42
|
+
* WHERE m.is_deleted = 0
|
|
43
|
+
* GROUP BY 1
|
|
44
|
+
*
|
|
45
|
+
* Empty / omitted → events-only.
|
|
46
|
+
*/
|
|
47
|
+
readonly mirrorTables?: ReadonlyArray<string>;
|
|
48
|
+
/** Primary-key column on the mirrored source rows. Default `'id'`. */
|
|
49
|
+
readonly mirrorPrimaryKey?: string;
|
|
50
|
+
/** Opt into client-side batching of `track()` inserts. Omit for immediate
|
|
51
|
+
* per-event inserts (a successful `track` = the row was accepted). See the
|
|
52
|
+
* batching note in the README for the delivery-semantics trade-off. */
|
|
53
|
+
readonly batch?: ClickhouseBatchOptions;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Opt-in client-side batching. Buffers `track()` rows and flushes them in
|
|
57
|
+
* ONE multi-row `client.insert`, cutting HTTP round-trips under load. */
|
|
58
|
+
export declare interface ClickhouseBatchOptions {
|
|
59
|
+
/** Flush once the buffer reaches this many events. Default 1000. */
|
|
60
|
+
readonly maxSize?: number;
|
|
61
|
+
/** Flush a non-empty buffer this many ms after the first event lands
|
|
62
|
+
* (so low-volume trickles don't sit until shutdown). Default 5000. */
|
|
63
|
+
readonly flushIntervalMs?: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export declare const EVENTS_TABLE_DDL: (database: string, table: string) => string;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* DDL for a mirror table. `ReplacingMergeTree(version)` keyed by `id`
|
|
70
|
+
* gives idempotent upserts — re-inserting the same row with a newer
|
|
71
|
+
* `version` collapses to the latest on merge. Deletes insert a tombstone
|
|
72
|
+
* (`is_deleted = 1`); queries filter `is_deleted = 0` (or use the
|
|
73
|
+
* `FINAL` modifier). `version` is a monotonic micro-timestamp so a later
|
|
74
|
+
* write always wins.
|
|
75
|
+
*/
|
|
76
|
+
export declare const MIRROR_TABLE_DDL: (database: string, sourceTable: string) => string;
|
|
77
|
+
|
|
78
|
+
export { TimeseriesQuery }
|
|
79
|
+
|
|
80
|
+
export { TopNQuery }
|
|
81
|
+
|
|
82
|
+
export { }
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { createClient as e } from "@clickhouse/client";
|
|
2
|
+
import { Effect as t, Option as n } from "effect";
|
|
3
|
+
import { createLogger as r } from "@voltro/logger";
|
|
4
|
+
import { AnalyticsError as i } from "@voltro/runtime";
|
|
5
|
+
//#region src/sink.ts
|
|
6
|
+
var a = r({ scope: "@voltro/plugin-clickhouse" }), o = "clickhouse", s = (e) => `voltro_mirror_${e}`, c = (e, t) => `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS ${e}.${s(t)} (
|
|
8
|
+
id String,
|
|
9
|
+
data String CODEC(ZSTD(1)),
|
|
10
|
+
version UInt64,
|
|
11
|
+
is_deleted UInt8 DEFAULT 0
|
|
12
|
+
) ENGINE = ReplacingMergeTree(version)
|
|
13
|
+
ORDER BY id
|
|
14
|
+
`.trim(), l = (e, t) => `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS ${e}.${t} (
|
|
16
|
+
id UUID DEFAULT generateUUIDv7(),
|
|
17
|
+
name LowCardinality(String),
|
|
18
|
+
subject_id Nullable(String),
|
|
19
|
+
tenant_id Nullable(String),
|
|
20
|
+
properties String CODEC(ZSTD(1)),
|
|
21
|
+
occurred_at DateTime64(3),
|
|
22
|
+
ingested_at DateTime64(3) DEFAULT now64()
|
|
23
|
+
) ENGINE = MergeTree
|
|
24
|
+
ORDER BY (name, occurred_at, id)
|
|
25
|
+
`.trim(), u = (e) => (t) => t instanceof i ? new i({
|
|
26
|
+
provider: o,
|
|
27
|
+
operation: e,
|
|
28
|
+
cause: t.cause
|
|
29
|
+
}) : new i({
|
|
30
|
+
provider: o,
|
|
31
|
+
operation: e,
|
|
32
|
+
cause: t instanceof Error ? `${t.name}: ${t.message}` : String(t)
|
|
33
|
+
}), d = (e) => (r) => r.pipe(t.catchSomeDefect((e) => e instanceof i ? n.some(t.fail(e)) : n.none()), t.mapError(u(e))), f = /^[A-Za-z_][A-Za-z0-9_]*$/, p = (e, t) => {
|
|
34
|
+
if (!f.test(e)) throw new i({
|
|
35
|
+
provider: o,
|
|
36
|
+
operation: "config",
|
|
37
|
+
cause: `invalid ${t} ${JSON.stringify(e)} — must match ${f.source} (letters, digits, underscore; no dots/quotes/spaces)`
|
|
38
|
+
});
|
|
39
|
+
return e;
|
|
40
|
+
}, m = /['\\]/, h = (e) => {
|
|
41
|
+
for (let t = 0; t < e.length; t++) if (e.charCodeAt(t) < 32) return !0;
|
|
42
|
+
return !1;
|
|
43
|
+
}, g = (e) => (t) => {
|
|
44
|
+
if (m.test(t) || h(t)) throw new i({
|
|
45
|
+
provider: o,
|
|
46
|
+
operation: e,
|
|
47
|
+
cause: `invalid property key ${JSON.stringify(t)} — must not contain quotes, backslashes, or control characters`
|
|
48
|
+
});
|
|
49
|
+
return t;
|
|
50
|
+
}, _ = (e) => e.client === null ? t.fail(new i({
|
|
51
|
+
provider: o,
|
|
52
|
+
operation: "ensureClient",
|
|
53
|
+
cause: "client not initialized — spec.initialize must run before queries"
|
|
54
|
+
})) : t.succeed(e.client), v = (e) => t.gen(function* () {
|
|
55
|
+
let n = e.client;
|
|
56
|
+
e.client = null, n !== null && (yield* t.tryPromise(() => n.close()).pipe(t.catchAll((e) => t.logWarning("clickhouse analytics sink teardown failed", {
|
|
57
|
+
provider: o,
|
|
58
|
+
err: e
|
|
59
|
+
}))));
|
|
60
|
+
}), y = (e) => ({
|
|
61
|
+
name: e.name,
|
|
62
|
+
subject_id: e.subjectId ?? null,
|
|
63
|
+
tenant_id: e.tenantId ?? null,
|
|
64
|
+
properties: JSON.stringify(e.properties),
|
|
65
|
+
occurred_at: (e.occurredAt ?? /* @__PURE__ */ new Date()).toISOString()
|
|
66
|
+
}), b = (e, t) => {
|
|
67
|
+
let n = [], r, i = () => {
|
|
68
|
+
r !== void 0 && (clearTimeout(r), r = void 0);
|
|
69
|
+
}, o = async () => {
|
|
70
|
+
if (i(), n.length === 0) return;
|
|
71
|
+
let t = n;
|
|
72
|
+
n = [];
|
|
73
|
+
let r = e.client;
|
|
74
|
+
if (r === null) {
|
|
75
|
+
a.warn("clickhouse batch flush skipped — client not initialized; events dropped", { dropped: t.length });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await r.insert({
|
|
80
|
+
table: `${e.database}.${e.table}`,
|
|
81
|
+
values: t,
|
|
82
|
+
format: "JSONEachRow"
|
|
83
|
+
});
|
|
84
|
+
} catch (e) {
|
|
85
|
+
a.warn("clickhouse batch flush failed — dropped events", { dropped: t.length }, e);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
return {
|
|
89
|
+
add: (e) => {
|
|
90
|
+
if (n.push(y(e)), n.length >= t.maxSize) {
|
|
91
|
+
o();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
r === void 0 && (r = setTimeout(() => {
|
|
95
|
+
o();
|
|
96
|
+
}, t.flushIntervalMs), r.unref?.());
|
|
97
|
+
},
|
|
98
|
+
flush: o
|
|
99
|
+
};
|
|
100
|
+
}, x = (e) => {
|
|
101
|
+
switch (e) {
|
|
102
|
+
case "minute": return "toStartOfMinute";
|
|
103
|
+
case "hour": return "toStartOfHour";
|
|
104
|
+
case "day": return "toStartOfDay";
|
|
105
|
+
case "week": return "toStartOfWeek";
|
|
106
|
+
case "month": return "toStartOfMonth";
|
|
107
|
+
}
|
|
108
|
+
}, S = (e, t) => `JSONExtractString(properties, '${g(e)(t)}')`, C = (e, t) => `JSONExtractFloat(properties, '${g(e)(t)}')`, w = (e, t) => t === "count" ? "count()" : t === "unique" ? "uniqExact(subject_id)" : "sum" in t ? `sum(${C(e, t.sum)})` : `avg(${C(e, t.avg)})`, T = (e, t) => t === "count" ? "count()" : `sum(${C(e, t.sum)})`, E = (e, t, n, r, i) => {
|
|
109
|
+
let a = {
|
|
110
|
+
event: t,
|
|
111
|
+
from: n.from.toISOString(),
|
|
112
|
+
to: (n.to ?? /* @__PURE__ */ new Date()).toISOString()
|
|
113
|
+
}, o = "name = {event:String} AND occurred_at >= {from:DateTime64(3)} AND occurred_at < {to:DateTime64(3)}";
|
|
114
|
+
if (i != null && (a.tenant = i, o += " AND tenant_id = {tenant:String}"), r !== void 0) {
|
|
115
|
+
let t = 0;
|
|
116
|
+
for (let [n, i] of Object.entries(r)) {
|
|
117
|
+
let r = `f${t++}`;
|
|
118
|
+
a[r] = String(i), o += ` AND ${S(e, n)} = {${r}:String}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
sql: o,
|
|
123
|
+
params: a
|
|
124
|
+
};
|
|
125
|
+
}, D = (e) => {
|
|
126
|
+
let n = (n, r) => t.gen(function* () {
|
|
127
|
+
p(n, "mirror table");
|
|
128
|
+
let i = yield* _(e);
|
|
129
|
+
yield* t.tryPromise({
|
|
130
|
+
try: () => i.insert({
|
|
131
|
+
table: `${e.database}.${s(n)}`,
|
|
132
|
+
values: [r],
|
|
133
|
+
format: "JSONEachRow"
|
|
134
|
+
}),
|
|
135
|
+
catch: (e) => e
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
tables: e.mirrorTables,
|
|
140
|
+
primaryKey: e.mirrorPrimaryKey,
|
|
141
|
+
upsert: (r, i) => t.gen(function* () {
|
|
142
|
+
let t = String(i[e.mirrorPrimaryKey]);
|
|
143
|
+
yield* n(r, {
|
|
144
|
+
id: t,
|
|
145
|
+
data: JSON.stringify(i),
|
|
146
|
+
version: Date.now() * 1e3,
|
|
147
|
+
is_deleted: 0
|
|
148
|
+
});
|
|
149
|
+
}).pipe(d("mirror.upsert")),
|
|
150
|
+
remove: (e, r) => t.gen(function* () {
|
|
151
|
+
yield* n(e, {
|
|
152
|
+
id: String(r),
|
|
153
|
+
data: "{}",
|
|
154
|
+
version: Date.now() * 1e3,
|
|
155
|
+
is_deleted: 1
|
|
156
|
+
});
|
|
157
|
+
}).pipe(d("mirror.remove"))
|
|
158
|
+
};
|
|
159
|
+
}, O = (e) => {
|
|
160
|
+
let n = e.mirrorTables.length > 0 ? D(e) : void 0;
|
|
161
|
+
return {
|
|
162
|
+
info: {
|
|
163
|
+
provider: o,
|
|
164
|
+
capabilities: {
|
|
165
|
+
track: !0,
|
|
166
|
+
aggregate: !0,
|
|
167
|
+
timeseries: !0,
|
|
168
|
+
topN: !0,
|
|
169
|
+
mirror: n !== void 0
|
|
170
|
+
},
|
|
171
|
+
detail: {
|
|
172
|
+
database: e.database,
|
|
173
|
+
table: e.table,
|
|
174
|
+
...e.mirrorTables.length > 0 ? { mirrorTables: e.mirrorTables } : {},
|
|
175
|
+
...e.batcher === void 0 ? {} : { batching: !0 }
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
track: (n) => e.batcher ? t.sync(() => e.batcher.add(n)) : t.gen(function* () {
|
|
179
|
+
let r = yield* _(e);
|
|
180
|
+
yield* t.tryPromise({
|
|
181
|
+
try: () => r.insert({
|
|
182
|
+
table: `${e.database}.${e.table}`,
|
|
183
|
+
values: [y(n)],
|
|
184
|
+
format: "JSONEachRow"
|
|
185
|
+
}),
|
|
186
|
+
catch: (e) => e
|
|
187
|
+
});
|
|
188
|
+
}).pipe(d("track")),
|
|
189
|
+
aggregate: (n) => t.gen(function* () {
|
|
190
|
+
let r = yield* _(e), i = E("aggregate", n.event, n.range, n.filter, n.tenantId), a = `SELECT ${w("aggregate", n.metric)} AS value FROM ${e.database}.${e.table} WHERE ${i.sql} FORMAT JSON`, o = yield* t.tryPromise({
|
|
191
|
+
try: () => r.query({
|
|
192
|
+
query: a,
|
|
193
|
+
query_params: i.params
|
|
194
|
+
}),
|
|
195
|
+
catch: (e) => e
|
|
196
|
+
}), { data: s } = yield* t.tryPromise({
|
|
197
|
+
try: () => o.json(),
|
|
198
|
+
catch: (e) => e
|
|
199
|
+
}), c = s[0]?.value ?? 0;
|
|
200
|
+
return typeof c == "number" ? c : Number(c ?? 0);
|
|
201
|
+
}).pipe(d("aggregate")),
|
|
202
|
+
timeseries: (n) => t.gen(function* () {
|
|
203
|
+
let r = yield* _(e), i = E("timeseries", n.event, n.range, n.filter, n.tenantId), a = `
|
|
204
|
+
SELECT ${`${x(n.bucket)}(occurred_at)`} AS bucket_start, ${w("timeseries", n.metric)} AS value
|
|
205
|
+
FROM ${e.database}.${e.table}
|
|
206
|
+
WHERE ${i.sql}
|
|
207
|
+
GROUP BY bucket_start
|
|
208
|
+
ORDER BY bucket_start ASC
|
|
209
|
+
FORMAT JSON
|
|
210
|
+
`, o = yield* t.tryPromise({
|
|
211
|
+
try: () => r.query({
|
|
212
|
+
query: a,
|
|
213
|
+
query_params: i.params
|
|
214
|
+
}),
|
|
215
|
+
catch: (e) => e
|
|
216
|
+
}), { data: s } = yield* t.tryPromise({
|
|
217
|
+
try: () => o.json(),
|
|
218
|
+
catch: (e) => e
|
|
219
|
+
});
|
|
220
|
+
return s.map((e) => ({
|
|
221
|
+
bucketStart: new Date(e.bucket_start),
|
|
222
|
+
value: typeof e.value == "number" ? e.value : Number(e.value ?? 0)
|
|
223
|
+
}));
|
|
224
|
+
}).pipe(d("timeseries")),
|
|
225
|
+
topN: (n) => t.gen(function* () {
|
|
226
|
+
let r = yield* _(e), i = E("topN", n.event, n.range, n.filter, n.tenantId), a = Math.max(1, Math.floor(n.n)), o = S("topN", n.groupBy), s = `
|
|
227
|
+
SELECT ${o} AS key, ${T("topN", n.metric)} AS value
|
|
228
|
+
FROM ${e.database}.${e.table}
|
|
229
|
+
WHERE ${i.sql}
|
|
230
|
+
GROUP BY ${o}
|
|
231
|
+
ORDER BY value DESC
|
|
232
|
+
LIMIT ${a}
|
|
233
|
+
FORMAT JSON
|
|
234
|
+
`, c = yield* t.tryPromise({
|
|
235
|
+
try: () => r.query({
|
|
236
|
+
query: s,
|
|
237
|
+
query_params: i.params
|
|
238
|
+
}),
|
|
239
|
+
catch: (e) => e
|
|
240
|
+
}), { data: l } = yield* t.tryPromise({
|
|
241
|
+
try: () => c.json(),
|
|
242
|
+
catch: (e) => e
|
|
243
|
+
});
|
|
244
|
+
return l.filter((e) => e.key !== null).map((e) => ({
|
|
245
|
+
key: e.key,
|
|
246
|
+
value: typeof e.value == "number" ? e.value : Number(e.value ?? 0)
|
|
247
|
+
}));
|
|
248
|
+
}).pipe(d("topN")),
|
|
249
|
+
...n === void 0 ? {} : { mirror: n }
|
|
250
|
+
};
|
|
251
|
+
}, k = 1e3, A = 5e3, j = (n) => {
|
|
252
|
+
let r = (n.mirrorTables ?? []).map((e) => p(e, "mirror table")), i = {
|
|
253
|
+
client: null,
|
|
254
|
+
database: p(n.database ?? "default", "database"),
|
|
255
|
+
table: p(n.table ?? "events", "table"),
|
|
256
|
+
mirrorTables: r,
|
|
257
|
+
mirrorPrimaryKey: n.mirrorPrimaryKey ?? "id"
|
|
258
|
+
};
|
|
259
|
+
return n.batch && (i.batcher = b(i, {
|
|
260
|
+
maxSize: n.batch.maxSize ?? k,
|
|
261
|
+
flushIntervalMs: n.batch.flushIntervalMs ?? A
|
|
262
|
+
})), {
|
|
263
|
+
kind: "clickhouse",
|
|
264
|
+
build: (e) => O(i),
|
|
265
|
+
initialize: () => t.gen(function* () {
|
|
266
|
+
let r = yield* t.promise(() => Promise.resolve(e({
|
|
267
|
+
url: n.url,
|
|
268
|
+
database: i.database,
|
|
269
|
+
...n.username === void 0 ? {} : { username: n.username },
|
|
270
|
+
...n.password === void 0 ? {} : { password: n.password }
|
|
271
|
+
})));
|
|
272
|
+
yield* t.promise(() => r.ping()), yield* t.promise(() => r.command({ query: l(i.database, i.table) }));
|
|
273
|
+
for (let e of i.mirrorTables) yield* t.promise(() => r.command({ query: c(i.database, e) }));
|
|
274
|
+
i.client = r;
|
|
275
|
+
}),
|
|
276
|
+
dispose: () => t.gen(function* () {
|
|
277
|
+
i.batcher && (yield* t.promise(() => i.batcher.flush())), yield* v(i);
|
|
278
|
+
})
|
|
279
|
+
};
|
|
280
|
+
};
|
|
281
|
+
//#endregion
|
|
282
|
+
export { l as EVENTS_TABLE_DDL, c as MIRROR_TABLE_DDL, j as clickhouseAnalytics };
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voltro/plugin-clickhouse",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ClickHouse AnalyticsSink. Native HTTP ingest into a ClickHouse cluster — self-hosted OR ClickHouse Cloud. Production-grade OLAP for high-cardinality events with millisecond aggregate queries over billions of rows.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"voltro",
|
|
7
|
+
"typescript",
|
|
8
|
+
"framework"
|
|
9
|
+
],
|
|
10
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
11
|
+
"homepage": "https://voltro.dev",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"email": "support@voltro.dev"
|
|
14
|
+
},
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Voltro UG",
|
|
17
|
+
"url": "https://voltro.dev"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=24.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@clickhouse/client": "^1.23.1",
|
|
36
|
+
"@voltro/logger": "0.1.0",
|
|
37
|
+
"@voltro/runtime": "0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"effect": "^3.21.4"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|