@fjall/clickhouse 20.0.0 → 21.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.
@@ -9,22 +9,27 @@
9
9
  "package.json",
10
10
  "tsconfig.json"
11
11
  ],
12
- "hash": "fed8b5f23e405cd72b8ffac8e6bbb7366aeec50b549ff7ed01033e4ed1668691",
12
+ "hash": "a7b1cae6ae0602be76a77f22cd3aaeb4c2f54db3b7fa6075c6c71824d9248d27",
13
13
  "files": {
14
- "src/connectWithRetry.ts": "eb4d131ea8e81922",
15
- "src/constants.ts": "00b236fd35f098cb",
14
+ "src/awaitControlPlaneIdle.ts": "055a56eaeb34e080",
15
+ "src/classifyClickHouseError.ts": "aab06571cf2b7ae3",
16
+ "src/connectWithRetry.ts": "61d241f2e4114134",
17
+ "src/constants.ts": "8d7fc4f3f97d5777",
16
18
  "src/createClient.ts": "83f093bfd7765a9f",
17
- "src/index.ts": "2ab0816e1d411108",
19
+ "src/ensureUserWithPassword.ts": "ee9ad7e1a22a9c50",
20
+ "src/index.ts": "ec05b861e901baf8",
18
21
  "src/logger.ts": "659d04c534247903",
19
- "src/provisionUsers.ts": "3e88d53b5eb1ccf5",
20
- "src/runSqlMigrations.ts": "d187aa5de9e4316e",
21
- "src/schemas.ts": "f3be95e003bd7e6e",
22
- "src/sleepAbortable.ts": "0286a0719bef3a38",
23
- "src/verifyExpectedClickHouseSchemaVersion.ts": "84f5463cd47e4a30",
22
+ "src/maskSqlError.ts": "1747db71f13ea3ea",
23
+ "src/provisionFrameworkUsers.ts": "781feeb539c4f92d",
24
+ "src/provisionUsers.ts": "c627778679ebb2a2",
25
+ "src/runSqlMigrations.ts": "e71fcef12eefb3fc",
26
+ "src/schemas.ts": "0567f2d427d55707",
27
+ "src/sleepAbortable.ts": "ee2facc7b89ab9f5",
28
+ "src/verifyExpectedClickHouseSchemaVersion.ts": "d202d40c76ba8ad6",
24
29
  "../scripts/minify-dist.mjs": "6b2e4b0df8aec601",
25
- "package.json": "bb5effb75c412d2e",
30
+ "package.json": "e0f1cc4b1a798f0b",
26
31
  "tsconfig.json": "db759831ad95f396"
27
32
  },
28
- "outputHash": "aca383c4bebdbc651b13cf6955b9b4c1943eeb59eecfd82e777d11aa3c143c93",
29
- "outputFileCount": 21
33
+ "outputHash": "b109dab9cb0f44e0e44420c4197505d972e8afb628941b5c54690e3a5dd29622",
34
+ "outputFileCount": 31
30
35
  }
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 10 files minified at 2026-08-24T18:04:51.057Z
1
+ 15 files minified at 2026-08-26T08:32:32.000Z
@@ -0,0 +1,42 @@
1
+ import type { ClickHouseClient } from "@clickhouse/client";
2
+ import type { MigrationLogger } from "./logger.js";
3
+ export interface AwaitControlPlaneIdleOpts {
4
+ client: ClickHouseClient;
5
+ /** The OTHER side of the mutex — never the caller's own identity. */
6
+ users: readonly string[];
7
+ /** Total wait budget. Exceeding it returns `timeout`; it never throws. */
8
+ timeoutMs: number;
9
+ /** Interval between probes. Defaults to 5000. */
10
+ pollMs?: number;
11
+ /** Overrides `CONTROL_PLANE_ACTIVITY_RECENCY_SECONDS`. */
12
+ recencySeconds?: number;
13
+ signal?: AbortSignal;
14
+ logger?: MigrationLogger;
15
+ }
16
+ export type ControlPlaneIdleResult = {
17
+ outcome: "idle";
18
+ } | {
19
+ outcome: "timeout";
20
+ waitedMs: number;
21
+ } | {
22
+ outcome: "aborted";
23
+ };
24
+ /**
25
+ * Waits for the other control-plane identities to go idle — the migration
26
+ * runner calls this as the schema admin with `users: [FJALL_MAINTENANCE_USER]`
27
+ * before its ClickHouse phase (the maintenance sidecars run the same SQL the
28
+ * other way round, in sh). Polls `buildControlPlaneActivityQuery` until it
29
+ * reports `active = 0`.
30
+ *
31
+ * Returns rather than throws on `timeout` and `aborted`: the runner owns the
32
+ * verdict, and it fails closed on `timeout` (a still-busy maintenance
33
+ * identity means an OPTIMIZE or BACKUP is holding the server). A probe that
34
+ * cannot be read is NOT proof of idleness — `busy` / `unreachable` /
35
+ * `indeterminate` probe errors are logged and polling continues inside the
36
+ * budget — including the `Code: 60` a brand-new server returns until its
37
+ * first log flush creates `system.query_log` (verified on 26.3; the probe's
38
+ * own queries create it inside one flush interval). A `denied` probe throws
39
+ * immediately: no amount of waiting grants `SELECT ON system.processes` /
40
+ * `system.query_log`.
41
+ */
42
+ export declare function awaitControlPlaneIdle(opts: AwaitControlPlaneIdleOpts): Promise<ControlPlaneIdleResult>;
@@ -0,0 +1 @@
1
+ var w=Object.defineProperty;var l=(e,s)=>w(e,"name",{value:s,configurable:!0});import{buildControlPlaneActivityQuery as y}from"@fjall/util/migration";import{classifyClickHouseError as m,describeClickHouseErrorClass as b}from"./classifyClickHouseError.js";import{sleepAbortable as p}from"./sleepAbortable.js";const v=5e3,S=new Set([1,"1",!0]),g=new Set([0,"0",!1]);async function E(e,s,r){try{const t=await(await e.query({query:s,format:"JSONEachRow",...r!==void 0&&{abort_signal:r}})).json(),o=t[0]?.active;return S.has(o)?{kind:"active",active:!0}:g.has(o)?{kind:"active",active:!1}:{kind:"error",err:new Error(`activity probe returned ${t.length} rows with active=${String(o)}; expected one row of 0/1`)}}catch(a){return{kind:"error",err:a}}}l(E,"probeActivity");async function L(e){const{client:s,users:r,timeoutMs:a,signal:t,logger:o}=e,d=e.pollMs??v,u=y({users:r,...e.recencySeconds!==void 0&&{recencySeconds:e.recencySeconds}}),f=Date.now();for(;;){if(t?.aborted)return{outcome:"aborted"};const c=await E(s,u,t),n=Date.now()-f;if(c.kind==="active"&&!c.active)return n>0&&o?.info("control plane idle",{users:[...r],waitedMs:n}),{outcome:"idle"};if(c.kind==="error"){if(t?.aborted)return{outcome:"aborted"};const i=m(c.err);if(i.class==="denied")throw new Error(`awaitControlPlaneIdle: activity probe denied (${b(i)}) \u2014 grant SELECT ON system.processes and system.query_log to the probing identity: ${i.message}`,{cause:c.err});o?.warn("control plane activity probe failed; retrying",{users:[...r],waitedMs:n,class:i.class,...i.code!==void 0&&{code:i.code},error:i.message})}else o?.info("control plane busy; waiting",{users:[...r],waitedMs:n});if(n>=a)return{outcome:"timeout",waitedMs:n};await p(Math.min(d,a-n),t)}}l(L,"awaitControlPlaneIdle");export{L as awaitControlPlaneIdle};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * ClickHouse server error codes this package branches on. `@clickhouse/client`
3
+ * carries them as STRING `code` values on `ClickHouseError`; the literals here
4
+ * are the single source for every class set below.
5
+ */
6
+ export declare const CLICKHOUSE_ERROR_CODES: {
7
+ readonly UNKNOWN_TABLE: "60";
8
+ readonly UNKNOWN_DATABASE: "81";
9
+ readonly READONLY: "164";
10
+ readonly UNKNOWN_USER: "192";
11
+ readonly IP_ADDRESS_NOT_ALLOWED: "195";
12
+ readonly QUOTA_EXCEEDED: "201";
13
+ readonly TOO_MANY_SIMULTANEOUS_QUERIES: "202";
14
+ readonly SETTING_CONSTRAINT_VIOLATION: "452";
15
+ readonly ACCESS_DENIED: "497";
16
+ readonly AUTHENTICATION_FAILED: "516";
17
+ };
18
+ export type ClickHouseErrorCode = (typeof CLICKHOUSE_ERROR_CODES)[keyof typeof CLICKHOUSE_ERROR_CODES];
19
+ export type ClickHouseErrorClass = "absent" | "busy" | "denied" | "unreachable" | "indeterminate";
20
+ export interface ClickHouseErrorClassification {
21
+ class: ClickHouseErrorClass;
22
+ /** Server code (`"202"`) or Node error code (`"ECONNREFUSED"`) when one was found. */
23
+ code?: string;
24
+ /** `busy` 202 only: the per-user cap (`user`) or the server-wide cap (`server`). */
25
+ scope?: "user" | "server";
26
+ /** Pre-masked via `maskSensitiveOutput` — safe for every sink without re-masking. */
27
+ message: string;
28
+ }
29
+ /**
30
+ * Classifies any error raised on the ClickHouse path into the verdict the
31
+ * caller needs — `absent` (table/database missing), `busy` (a concurrency or
32
+ * quota cap tripped), `denied` (authentication / authorisation / constraint),
33
+ * `unreachable` (the network or the client's own timeout/abort), or
34
+ * `indeterminate`.
35
+ *
36
+ * Server codes are read from the `code` property (string per
37
+ * `@clickhouse/client`; a numeric code is accepted defensively) or, when a
38
+ * wrapper has flattened the error, from the `Code: NNN.` marker in the
39
+ * message. A wrapper carrying no code of its own but a `cause` is classified
40
+ * by that cause; the wrapper's message is kept. Pre-masked producer:
41
+ * `message` has been through `maskSensitiveOutput` — sinks must not re-mask.
42
+ */
43
+ export declare function classifyClickHouseError(err: unknown): ClickHouseErrorClassification;
44
+ /** `denied 497` / `unreachable ECONNREFUSED` / `indeterminate` — for error messages and log context. */
45
+ export declare function describeClickHouseErrorClass(classification: ClickHouseErrorClassification): string;
@@ -0,0 +1 @@
1
+ var S=Object.defineProperty;var o=(e,n)=>S(e,"name",{value:n,configurable:!0});import{getErrorMessage as u,maskSensitiveOutput as A}from"@fjall/util";const t={UNKNOWN_TABLE:"60",UNKNOWN_DATABASE:"81",READONLY:"164",UNKNOWN_USER:"192",IP_ADDRESS_NOT_ALLOWED:"195",QUOTA_EXCEEDED:"201",TOO_MANY_SIMULTANEOUS_QUERIES:"202",SETTING_CONSTRAINT_VIOLATION:"452",ACCESS_DENIED:"497",AUTHENTICATION_FAILED:"516"},T=new Set([t.UNKNOWN_TABLE,t.UNKNOWN_DATABASE]),d=new Set([t.TOO_MANY_SIMULTANEOUS_QUERIES,t.QUOTA_EXCEEDED]),i=new Set([t.AUTHENTICATION_FAILED,t.ACCESS_DENIED,t.UNKNOWN_USER,t.IP_ADDRESS_NOT_ALLOWED,t.READONLY,t.SETTING_CONSTRAINT_VIOLATION]),a=new Set(["ECONNREFUSED","ECONNRESET","ETIMEDOUT","ENOTFOUND","EAI_AGAIN","EHOSTUNREACH","ECONNABORTED","EPROTO","ABORT_ERR"]),C=/^(?:ERR_(?:TLS|SSL|OSSL)_|CERT_|UNABLE_TO_|DEPTH_ZERO_SELF_SIGNED_CERT$|SELF_SIGNED_CERT_IN_CHAIN$|HOSTNAME_MISMATCH$)/,D=/^Timeout error\.?$|The user aborted a request|socket hang up/i,R=/^\d+$/,I=/\bCode: (\d+)\./;function f(e){if(typeof e!="object"||e===null||!("code"in e))return;const n=e.code;if(typeof n=="number"&&Number.isInteger(n))return String(n);if(typeof n=="string"&&n!=="")return n}o(f,"readCodeProperty");function U(e){return typeof e=="object"&&e!==null&&"name"in e&&e.name==="AbortError"}o(U,"isAbortError");const L=5;function l(e){return typeof e=="object"&&e!==null&&"cause"in e?e.cause:void 0}o(l,"readCause");function p(e){return c(e,0)}o(p,"classifyClickHouseError");function c(e,n){const E=m(e);if(E.class!=="indeterminate"||E.code!==void 0||n>=L)return E;const s=l(e);if(s===void 0)return E;const _=c(s,n+1);return{..._,message:E.message!==""?E.message:_.message}}o(c,"classifyWalkingCause");function m(e){const n=u(e),E=f(e),s=E!==void 0&&R.test(E)?E:I.exec(n)?.[1],_=A(n),r=_!==""?_:s??E??"";if(s!==void 0){if(T.has(s))return{class:"absent",code:s,message:r};if(d.has(s)){const O=s===t.TOO_MANY_SIMULTANEOUS_QUERIES;return{class:"busy",code:s,...O&&{scope:/for user/i.test(n)?"user":"server"},message:r}}return i.has(s)?{class:"denied",code:s,message:r}:{class:"indeterminate",code:s,message:r}}if(E!==void 0&&(a.has(E)||C.test(E)))return{class:"unreachable",code:E,message:r};const N=E!==void 0&&{code:E};return U(e)||D.test(n)?{class:"unreachable",...N,message:r}:{class:"indeterminate",...N,message:r}}o(m,"classifyOwn");function H(e){return e.code===void 0?e.class:`${e.class} ${e.code}`}o(H,"describeClickHouseErrorClass");export{t as CLICKHOUSE_ERROR_CODES,p as classifyClickHouseError,H as describeClickHouseErrorClass};
@@ -6,4 +6,14 @@ export interface ConnectWithRetryOpts {
6
6
  delays?: readonly number[];
7
7
  logger?: MigrationLogger;
8
8
  }
9
+ /**
10
+ * Waits for the ClickHouse server to answer `/ping`, retrying `unreachable`,
11
+ * `indeterminate` and `busy` failures across `delays` and `denied` ONLY for
12
+ * the auth-propagation case (516, up to `AUTH_FAILED_MAX_RETRIES` attempts);
13
+ * every other `denied` fails closed at once with the classification in the
14
+ * message.
15
+ *
16
+ * `/ping` does NOT authenticate — a wrong password or a missing user
17
+ * surfaces at the first real query, not here.
18
+ */
9
19
  export declare function connectWithRetry(client: ClickHouseClient, opts: ConnectWithRetryOpts): Promise<void>;
@@ -1 +1 @@
1
- var y=Object.defineProperty;var f=(c,o)=>y(c,"name",{value:o,configurable:!0});import{getErrorMessage as h,maskSensitiveOutput as _}from"@fjall/util";import{sleepAbortable as p}from"./sleepAbortable.js";const m=[5e3,1e4,2e4,4e4,6e4],A=3,w=516,D=["ECONNREFUSED","ETIMEDOUT","ENOTFOUND"];async function O(c,o){const{label:n,signal:i,logger:s}=o,e=o.delays??m;let l;for(let t=0;t<e.length;t++){if(i?.aborted)throw new Error(`connectWithRetry: ${n} aborted by signal before attempt ${t+1}`);try{await c.ping(),t>0&&s?.info("connect succeeded after retry",{label:n,attempt:t+1});return}catch(r){l=r;const a=typeof r=="object"&&r!==null&&"code"in r?r.code:void 0,E=a===w,d=typeof a=="string"&&D.includes(a);if(!(d||E&&t<A))throw r;const g=E?"AUTH_FAILED":d?a:"UNKNOWN";s?.warn("connect attempt failed; retrying",{label:_(n),attempt:t+1,attempts:e.length,delayMs:e[t],code:g}),await p(e[t],i)}}if(i?.aborted)throw new Error(`connectWithRetry: ${n} aborted by signal after ${e.length} attempts`);const b=_(h(l));throw new Error(`connectWithRetry: ${n} failed after ${e.length} attempts: ${b}`)}f(O,"connectWithRetry");export{O as connectWithRetry};
1
+ var u=Object.defineProperty;var l=(s,r)=>u(s,"name",{value:r,configurable:!0});import{maskSensitiveOutput as y,normaliseError as h}from"@fjall/util";import{CLICKHOUSE_ERROR_CODES as g,classifyClickHouseError as p,describeClickHouseErrorClass as f}from"./classifyClickHouseError.js";import{sleepAbortable as b}from"./sleepAbortable.js";const _=[5e3,1e4,2e4,4e4,6e4],w=3,A=new Set(["unreachable","indeterminate","busy"]);async function R(s){try{return await s.ping()}catch(r){return{success:!1,error:h(r)}}}l(R,"pingSafely");async function L(s,r){const{label:n,signal:c,logger:d}=r,a=r.delays??_;let o;for(let t=0;t<a.length;t++){if(c?.aborted)throw new Error(`connectWithRetry: ${n} aborted by signal before attempt ${t+1}`);const i=await R(s);if(i.success){t>0&&d?.info("connect succeeded after retry",{label:n,attempt:t+1});return}const e=p(i.error);o=e;const E=e.class==="denied"&&e.code===g.AUTHENTICATION_FAILED&&t<w;if(!A.has(e.class)&&!E)throw new Error(`connectWithRetry: ${n} failed (${f(e)}): ${e.message}`,{cause:i.error});d?.warn("connect attempt failed; retrying",{label:y(n),attempt:t+1,attempts:a.length,delayMs:a[t],class:e.class,...e.code!==void 0&&{code:e.code},error:e.message}),await b(a[t],c)}if(c?.aborted)throw new Error(`connectWithRetry: ${n} aborted by signal after ${a.length} attempts`);const m=o===void 0?"no attempt made":`${f(o)}: ${o.message}`;throw new Error(`connectWithRetry: ${n} failed after ${a.length} attempts (${m})`)}l(L,"connectWithRetry");export{L as connectWithRetry};
@@ -1 +1,8 @@
1
- export { CLICKHOUSE_MANAGED_USERS_ENV, userPasswordEnvName, } from "@fjall/util/migration";
1
+ export { CLICKHOUSE_BACKUP_SCRATCH_DATABASE, CLICKHOUSE_FRAMEWORK_USERS_ENV, CLICKHOUSE_MAINTENANCE_PROFILE_ENV, CLICKHOUSE_MANAGED_USERS_ENV, FJALL_CLICKHOUSE_USERS, FJALL_MAINTENANCE_USER, FJALL_SCHEMA_GATE_USER, type FjallClickHouseUser, MANAGED_USER_NAME_PATTERN, SCHEMA_GATE_CH_DATABASE_ENV, userPasswordEnvName, } from "@fjall/util/migration";
2
+ /**
3
+ * The migration audit table. Coupled across the gate's read
4
+ * (`verifyExpectedClickHouseSchemaVersion`) and the gate identity's GRANT
5
+ * (`provisionFrameworkUsersFromEnv`): a drift grants SELECT on a table the
6
+ * gate never reads while the one it does read fails with ACCESS_DENIED.
7
+ */
8
+ export declare const SCHEMA_MIGRATIONS_TABLE: "_schema_migrations";
package/dist/constants.js CHANGED
@@ -1 +1 @@
1
- import{CLICKHOUSE_MANAGED_USERS_ENV as e,userPasswordEnvName as o}from"@fjall/util/migration";export{e as CLICKHOUSE_MANAGED_USERS_ENV,o as userPasswordEnvName};
1
+ import{CLICKHOUSE_BACKUP_SCRATCH_DATABASE as S,CLICKHOUSE_FRAMEWORK_USERS_ENV as C,CLICKHOUSE_MAINTENANCE_PROFILE_ENV as N,CLICKHOUSE_MANAGED_USERS_ENV as L,FJALL_CLICKHOUSE_USERS as R,FJALL_MAINTENANCE_USER as U,FJALL_SCHEMA_GATE_USER as T,MANAGED_USER_NAME_PATTERN as H,SCHEMA_GATE_CH_DATABASE_ENV as I,userPasswordEnvName as M}from"@fjall/util/migration";const E="_schema_migrations";export{S as CLICKHOUSE_BACKUP_SCRATCH_DATABASE,C as CLICKHOUSE_FRAMEWORK_USERS_ENV,N as CLICKHOUSE_MAINTENANCE_PROFILE_ENV,L as CLICKHOUSE_MANAGED_USERS_ENV,R as FJALL_CLICKHOUSE_USERS,U as FJALL_MAINTENANCE_USER,T as FJALL_SCHEMA_GATE_USER,H as MANAGED_USER_NAME_PATTERN,I as SCHEMA_GATE_CH_DATABASE_ENV,E as SCHEMA_MIGRATIONS_TABLE,M as userPasswordEnvName};
@@ -0,0 +1,24 @@
1
+ import type { ClickHouseClient } from "@clickhouse/client";
2
+ export interface LabelledStatement {
3
+ readonly label: string;
4
+ readonly query: string;
5
+ }
6
+ export declare function escapeSqlLiteral(value: string): string;
7
+ /**
8
+ * The idempotent create-or-rotate pair. `CREATE USER IF NOT EXISTS` is a
9
+ * silent no-op for an existing user — it converges NEITHER the password NOR
10
+ * any `SETTINGS` (ClickHouse 26.3, verified) — so the `ALTER USER` that
11
+ * follows is what actually rotates the password and applies the settings.
12
+ * Do not collapse to one statement.
13
+ *
14
+ * `settings`, when given, must be the COMPLETE settings list for the user:
15
+ * `ALTER USER … SETTINGS` replaces the whole list rather than merging.
16
+ */
17
+ export declare function buildEnsureUserStatements(name: string, password: string, settings?: string): readonly LabelledStatement[];
18
+ export interface EnsureUserWithPasswordOpts {
19
+ /** Complete settings list — see `buildEnsureUserStatements`. */
20
+ settings?: string;
21
+ /** Cancels the in-flight statement (worker shutdown), not just the next one. */
22
+ signal?: AbortSignal;
23
+ }
24
+ export declare function ensureUserWithPassword(client: ClickHouseClient, name: string, password: string, opts?: EnsureUserWithPasswordOpts): Promise<void>;
@@ -0,0 +1 @@
1
+ var i=Object.defineProperty;var s=(e,n)=>i(e,"name",{value:n,configurable:!0});function u(e){return e.replace(/\\/g,"\\\\").replace(/'/g,"''")}s(u,"escapeSqlLiteral");function c(e,n,t){const r=`IDENTIFIED WITH sha256_password BY '${u(n)}'`,a=t===void 0?"":` SETTINGS ${t}`;return[{label:"CREATE USER IF NOT EXISTS",query:`CREATE USER IF NOT EXISTS ${e} ${r}`},{label:"ALTER USER",query:`ALTER USER ${e} ${r}${a}`}]}s(c,"buildEnsureUserStatements");async function S(e,n,t,r={}){const{settings:a,signal:o}=r;for(const{query:E}of c(n,t,a))await e.command({query:E,...o!==void 0&&{abort_signal:o}})}s(S,"ensureUserWithPassword");export{c as buildEnsureUserStatements,S as ensureUserWithPassword,u as escapeSqlLiteral};
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
- export { CLICKHOUSE_MANAGED_USERS_ENV, userPasswordEnvName, } from "./constants.js";
2
- export { ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames, } from "./schemas.js";
1
+ export { CLICKHOUSE_BACKUP_SCRATCH_DATABASE, CLICKHOUSE_FRAMEWORK_USERS_ENV, CLICKHOUSE_MAINTENANCE_PROFILE_ENV, CLICKHOUSE_MANAGED_USERS_ENV, FJALL_CLICKHOUSE_USERS, FJALL_MAINTENANCE_USER, FJALL_SCHEMA_GATE_USER, type FjallClickHouseUser, SCHEMA_MIGRATIONS_TABLE, userPasswordEnvName, } from "./constants.js";
2
+ export { FrameworkUserNameSchema, FrameworkUserNamesSchema, type FrameworkUserName, type FrameworkUserNames, ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames, } from "./schemas.js";
3
3
  export { type MigrationLogger, createConsoleMigrationLogger, } from "./logger.js";
4
+ export { CLICKHOUSE_ERROR_CODES, type ClickHouseErrorClass, type ClickHouseErrorClassification, type ClickHouseErrorCode, classifyClickHouseError, describeClickHouseErrorClass, } from "./classifyClickHouseError.js";
4
5
  export { type ConnectWithRetryOpts, connectWithRetry, } from "./connectWithRetry.js";
5
6
  export { type ProvisionUsersFromEnvOpts, type ProvisionUsersFromEnvResult, provisionUsersFromEnv, } from "./provisionUsers.js";
7
+ export { type ProvisionFrameworkUsersFromEnvOpts, type ProvisionFrameworkUsersFromEnvResult, provisionFrameworkUsersFromEnv, } from "./provisionFrameworkUsers.js";
8
+ export { type AwaitControlPlaneIdleOpts, type ControlPlaneIdleResult, awaitControlPlaneIdle, } from "./awaitControlPlaneIdle.js";
6
9
  export { type RunSqlMigrationsOpts, type RunSqlMigrationsResult, runSqlMigrations, } from "./runSqlMigrations.js";
7
10
  export { type VerifyExpectedClickHouseSchemaVersionOpts, type VerifyExpectedClickHouseSchemaVersionResult, verifyExpectedClickHouseSchemaVersion, } from "./verifyExpectedClickHouseSchemaVersion.js";
8
11
  export { type CreateFjallClickHouseClientOptions, createFjallClickHouseClient, WIRE_FORMAT_CLICKHOUSE_SETTINGS, } from "./createClient.js";
12
+ export { sleepAbortable } from "./sleepAbortable.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{CLICKHOUSE_MANAGED_USERS_ENV as o,userPasswordEnvName as t}from"./constants.js";import{ManagedUserNameSchema as m,ManagedUserNamesSchema as s}from"./schemas.js";import{createConsoleMigrationLogger as i}from"./logger.js";import{connectWithRetry as p}from"./connectWithRetry.js";import{provisionUsersFromEnv as S}from"./provisionUsers.js";import{runSqlMigrations as x}from"./runSqlMigrations.js";import{verifyExpectedClickHouseSchemaVersion as l}from"./verifyExpectedClickHouseSchemaVersion.js";import{createFjallClickHouseClient as M,WIRE_FORMAT_CLICKHOUSE_SETTINGS as N}from"./createClient.js";export{o as CLICKHOUSE_MANAGED_USERS_ENV,m as ManagedUserNameSchema,s as ManagedUserNamesSchema,N as WIRE_FORMAT_CLICKHOUSE_SETTINGS,p as connectWithRetry,i as createConsoleMigrationLogger,M as createFjallClickHouseClient,S as provisionUsersFromEnv,x as runSqlMigrations,t as userPasswordEnvName,l as verifyExpectedClickHouseSchemaVersion};
1
+ import{CLICKHOUSE_BACKUP_SCRATCH_DATABASE as o,CLICKHOUSE_FRAMEWORK_USERS_ENV as E,CLICKHOUSE_MAINTENANCE_PROFILE_ENV as C,CLICKHOUSE_MANAGED_USERS_ENV as a,FJALL_CLICKHOUSE_USERS as S,FJALL_MAINTENANCE_USER as s,FJALL_SCHEMA_GATE_USER as m,SCHEMA_MIGRATIONS_TABLE as _,userPasswordEnvName as t}from"./constants.js";import{FrameworkUserNameSchema as i,FrameworkUserNamesSchema as U,ManagedUserNameSchema as n,ManagedUserNamesSchema as N}from"./schemas.js";import{createConsoleMigrationLogger as l}from"./logger.js";import{CLICKHOUSE_ERROR_CODES as L,classifyClickHouseError as R,describeClickHouseErrorClass as I}from"./classifyClickHouseError.js";import{connectWithRetry as H}from"./connectWithRetry.js";import{provisionUsersFromEnv as O}from"./provisionUsers.js";import{provisionFrameworkUsersFromEnv as M}from"./provisionFrameworkUsers.js";import{awaitControlPlaneIdle as K}from"./awaitControlPlaneIdle.js";import{runSqlMigrations as d}from"./runSqlMigrations.js";import{verifyExpectedClickHouseSchemaVersion as h}from"./verifyExpectedClickHouseSchemaVersion.js";import{createFjallClickHouseClient as v,WIRE_FORMAT_CLICKHOUSE_SETTINGS as w}from"./createClient.js";import{sleepAbortable as P}from"./sleepAbortable.js";export{o as CLICKHOUSE_BACKUP_SCRATCH_DATABASE,L as CLICKHOUSE_ERROR_CODES,E as CLICKHOUSE_FRAMEWORK_USERS_ENV,C as CLICKHOUSE_MAINTENANCE_PROFILE_ENV,a as CLICKHOUSE_MANAGED_USERS_ENV,S as FJALL_CLICKHOUSE_USERS,s as FJALL_MAINTENANCE_USER,m as FJALL_SCHEMA_GATE_USER,i as FrameworkUserNameSchema,U as FrameworkUserNamesSchema,n as ManagedUserNameSchema,N as ManagedUserNamesSchema,_ as SCHEMA_MIGRATIONS_TABLE,w as WIRE_FORMAT_CLICKHOUSE_SETTINGS,K as awaitControlPlaneIdle,R as classifyClickHouseError,H as connectWithRetry,l as createConsoleMigrationLogger,v as createFjallClickHouseClient,I as describeClickHouseErrorClass,M as provisionFrameworkUsersFromEnv,O as provisionUsersFromEnv,d as runSqlMigrations,P as sleepAbortable,t as userPasswordEnvName,h as verifyExpectedClickHouseSchemaVersion};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Masks a server error that may echo the failing statement. Every
3
+ * single-quoted literal is redacted first because an echoed
4
+ * `IDENTIFIED WITH sha256_password BY '<password>'` matches no credential
5
+ * regex; masking runs on the full string and truncation LAST (a truncated
6
+ * credential loses the anchors the mask needs).
7
+ */
8
+ export declare function maskSqlError(err: unknown): string;
@@ -0,0 +1 @@
1
+ var n=Object.defineProperty;var o=(t,e)=>n(t,"name",{value:e,configurable:!0});import{getErrorMessage as i,maskSensitiveOutput as E}from"@fjall/util";const s=400;function u(t){const e=i(t).replace(/'(?:[^']|'')*'/g,"'<REDACTED>'"),r=E(e);return r.length>s?r.slice(0,s)+"\u2026":r}o(u,"maskSqlError");export{u as maskSqlError};
@@ -0,0 +1,38 @@
1
+ import type { ClickHouseClient } from "@clickhouse/client";
2
+ import { type FjallClickHouseUser } from "./constants.js";
3
+ import type { MigrationLogger } from "./logger.js";
4
+ export interface ProvisionFrameworkUsersFromEnvOpts {
5
+ client: ClickHouseClient;
6
+ /**
7
+ * Database whose audit table the gate reads and whose tables the
8
+ * maintenance identity may OPTIMIZE / BACKUP. Defaults to env
9
+ * `CLICKHOUSE_DATABASE`; absent on both → fails closed (a GRANT on the
10
+ * wrong database is a silent misconfiguration).
11
+ */
12
+ database?: string;
13
+ signal?: AbortSignal;
14
+ logger?: MigrationLogger;
15
+ env?: NodeJS.ProcessEnv;
16
+ }
17
+ export interface ProvisionFrameworkUsersFromEnvResult {
18
+ provisioned: readonly FjallClickHouseUser[];
19
+ }
20
+ /**
21
+ * Provisions the framework-owned ClickHouse identities (`fjall_schema_gate`,
22
+ * `fjall_maintenance`) from the migration task, running as the XML schema
23
+ * admin. Sibling of `provisionUsersFromEnv` (customer users): reads the
24
+ * manifest from `CLICKHOUSE_FRAMEWORK_USERS`, each password from
25
+ * `USER_<NAME>_PASSWORD`, and the maintenance profile from
26
+ * `CLICKHOUSE_MAINTENANCE_PROFILE`.
27
+ *
28
+ * An absent manifest is NOT an error — constructs older than the framework
29
+ * identities emit no manifest, and a runner image ahead of its constructs
30
+ * must still migrate. Every other missing input fails closed BEFORE the
31
+ * first statement, naming the env (never the value).
32
+ *
33
+ * Idempotent: `CREATE USER IF NOT EXISTS` → `ALTER USER … SETTINGS <complete
34
+ * list>` → `GRANT …`, each safe to re-run. Failures carry the
35
+ * `classifyClickHouseError` verdict so the runner's forensic log reads
36
+ * `denied 497` rather than raw text.
37
+ */
38
+ export declare function provisionFrameworkUsersFromEnv(opts: ProvisionFrameworkUsersFromEnvOpts): Promise<ProvisionFrameworkUsersFromEnvResult>;
@@ -0,0 +1 @@
1
+ var v=Object.defineProperty;var o=(e,r)=>v(e,"name",{value:r,configurable:!0});import{getErrorMessage as h,maskSensitiveOutput as m}from"@fjall/util";import{classifyClickHouseError as b,describeClickHouseErrorClass as L}from"./classifyClickHouseError.js";import{CLICKHOUSE_BACKUP_SCRATCH_DATABASE as I,CLICKHOUSE_FRAMEWORK_USERS_ENV as u,CLICKHOUSE_MAINTENANCE_PROFILE_ENV as f,FJALL_CLICKHOUSE_USERS as U,FJALL_MAINTENANCE_USER as E,FJALL_SCHEMA_GATE_USER as p,MANAGED_USER_NAME_PATTERN as k,SCHEMA_GATE_CH_DATABASE_ENV as N,SCHEMA_MIGRATIONS_TABLE as F,userPasswordEnvName as M}from"./constants.js";import{buildEnsureUserStatements as P}from"./ensureUserWithPassword.js";import{maskSqlError as H}from"./maskSqlError.js";import{FrameworkUserNamesSchema as y}from"./schemas.js";const i="provisionFrameworkUsersFromEnv",q=/^[A-Za-z_][A-Za-z0-9_]*$/,B=["parts","disks","tables","processes","query_log"],G={[p]:{settings:o(()=>"readonly = 2 CONST, max_threads = 1 CONST, max_execution_time = 10 CONST","settings"),grants:o(({database:e})=>[`GRANT SELECT ON ${e}.${F} TO ${p}`],"grants")},[E]:{settings:o(({maintenanceProfile:e})=>{if(e===void 0)throw new Error(`${i}: ${E} requires env ${f} (the settings profile it binds to)`);return`PROFILE '${e}', max_concurrent_queries_for_user = 1 CONST`},"settings"),grants:o(({database:e})=>[`GRANT SELECT, SHOW, OPTIMIZE, BACKUP ON ${e}.* TO ${E}`,`GRANT SELECT, INSERT, CREATE DATABASE, CREATE TABLE, DROP DATABASE, DROP TABLE ON ${I}.* TO ${E}`,...B.map(r=>`GRANT SELECT ON system.${r} TO ${E}`)],"grants")}};function D(e){let r;try{r=JSON.parse(e)}catch(s){throw new Error(`${i}: malformed ${u} \u2014 JSON parse failed: ${m(h(s))}`)}const n=y.safeParse(r);if(!n.success)throw new Error(`${i}: malformed ${u} \u2014 ${n.error.message}`);return n.data}o(D,"parseManifest");function K(e,r){const n=r[N],s=e!==void 0&&e!==""?e:n!==void 0&&n!==""?n:void 0;if(s===void 0)throw new Error(`${i}: target database required (opts.database or env ${N})`);if(!q.test(s))throw new Error(`${i}: unsafe database identifier '${m(s)}'`);return s}o(K,"resolveDatabase");function J(e){const r=e[f];if(r===void 0||r==="")throw new Error(`${i}: required env ${f} is missing or empty (the settings profile ${E} binds to)`);if(!k.test(r))throw new Error(`${i}: ${f} must be lowercase snake_case, got '${m(r)}'`);return r}o(J,"requireMaintenanceProfile");function W(e,r){const n=M(r),s=e[n];if(s===void 0||s==="")throw new Error(`${i}: required env ${n} is missing or empty`);return s}o(W,"requirePassword");function x(e,r,n){const s=G[e];return[...P(e,r,s.settings(n)),...s.grants(n).map(a=>({label:a,query:a}))]}o(x,"buildFrameworkUserStatements");async function ee(e){const{client:r,signal:n,logger:s}=e,a=e.env??process.env,c=a[u];if(c===void 0||c==="")return s?.info("no framework users to provision (manifest env absent \u2014 constructs predate the framework identities)",{}),{provisioned:[]};const A=new Set(D(c)),S=U.filter(t=>A.has(t));if(S.length===0)return s?.info("no framework users to provision (manifest empty)",{}),{provisioned:[]};const _=K(e.database,a),w={database:_,...A.has(E)&&{maintenanceProfile:J(a)}},C=S.map(t=>({name:t,statements:x(t,W(a,t),w)})),T=[];for(const{name:t,statements:O}of C){for(const{label:d,query:R}of O){if(n?.aborted)throw new Error(`${i}: aborted by signal before '${t}' ${d}`);try{await r.command({query:R,...n!==void 0&&{abort_signal:n}})}catch($){const l=b($),g=l.class==="denied"?" \u2014 the provisioning identity needs access_management=1 and the granted privileges WITH GRANT OPTION":"";throw new Error(`${i}: ${t}: ${d} failed (${L(l)})${g}: ${H($)}`)}s?.info("framework user statement applied",{name:t,label:d})}s?.info("clickhouse framework user provisioned",{name:t,database:_}),T.push(t)}return{provisioned:T}}o(ee,"provisionFrameworkUsersFromEnv");export{ee as provisionFrameworkUsersFromEnv};
@@ -1 +1 @@
1
- var g=Object.defineProperty;var t=(r,s)=>g(r,"name",{value:s,configurable:!0});import{getErrorMessage as S,maskSensitiveOutput as h}from"@fjall/util";import{CLICKHOUSE_MANAGED_USERS_ENV as m,userPasswordEnvName as I}from"./constants.js";import{ManagedUserNamesSchema as N}from"./schemas.js";function U(r){return r.replace(/\\/g,"\\\\").replace(/'/g,"''")}t(U,"escapeSqlLiteral");async function R(r){const{client:s,signal:u,logger:o}=r,c=r.env??process.env,n=c[m];if(n===void 0||n==="")return o?.info("no managed users to provision (manifest env absent)",{}),{provisioned:[]};let p;try{p=JSON.parse(n)}catch(e){throw new Error(`provisionUsersFromEnv: malformed ${m} \u2014 JSON parse failed: ${h(S(e))}`)}const a=N.safeParse(p);if(!a.success)throw new Error(`provisionUsersFromEnv: malformed ${m} \u2014 ${a.error.message}`);const d=a.data;if(d.length===0)return o?.info("no managed users to provision (manifest empty)",{}),{provisioned:[]};const f=[];for(const e of d){if(u?.aborted)throw new Error(`provisionUsersFromEnv: aborted by signal before user '${e}'`);const E=I(e),i=c[E];if(i===void 0||i==="")throw new Error(`provisionUsersFromEnv: required env ${E} is missing or empty`);const v=U(i),l=`CREATE USER IF NOT EXISTS ${e} IDENTIFIED WITH sha256_password BY '${v}'`,w=`ALTER USER ${e} IDENTIFIED WITH sha256_password BY '${v}'`;await s.command({query:l}),await s.command({query:w}),o?.info("clickhouse user provisioned",{name:e}),f.push(e)}return{provisioned:f}}t(R,"provisionUsersFromEnv");export{R as provisionUsersFromEnv};
1
+ var E=Object.defineProperty;var u=(e,s)=>E(e,"name",{value:s,configurable:!0});import{getErrorMessage as w,maskSensitiveOutput as g}from"@fjall/util";import{CLICKHOUSE_MANAGED_USERS_ENV as m,userPasswordEnvName as l}from"./constants.js";import{ensureUserWithPassword as h}from"./ensureUserWithPassword.js";import{ManagedUserNamesSchema as U}from"./schemas.js";async function F(e){const{client:s,signal:o,logger:n}=e,f=e.env??process.env,i=f[m];if(i===void 0||i==="")return n?.info("no managed users to provision (manifest env absent)",{}),{provisioned:[]};let p;try{p=JSON.parse(i)}catch(r){throw new Error(`provisionUsersFromEnv: malformed ${m} \u2014 JSON parse failed: ${g(w(r))}`)}const t=U.safeParse(p);if(!t.success)throw new Error(`provisionUsersFromEnv: malformed ${m} \u2014 ${t.error.message}`);const v=t.data;if(v.length===0)return n?.info("no managed users to provision (manifest empty)",{}),{provisioned:[]};const d=[];for(const r of v){if(o?.aborted)throw new Error(`provisionUsersFromEnv: aborted by signal before user '${r}'`);const c=l(r),a=f[c];if(a===void 0||a==="")throw new Error(`provisionUsersFromEnv: required env ${c} is missing or empty`);await h(s,r,a,{...o!==void 0&&{signal:o}}),n?.info("clickhouse user provisioned",{name:r}),d.push(r)}return{provisioned:d}}u(F,"provisionUsersFromEnv");export{F as provisionUsersFromEnv};
@@ -1,3 +1,3 @@
1
- var S=Object.defineProperty;var l=(r,t)=>S(r,"name",{value:t,configurable:!0});import{readFileSync as y,readdirSync as k}from"node:fs";import{join as q}from"node:path";import{getErrorMessage as h,maskSensitiveOutput as u}from"@fjall/util";import{CLICKHOUSE_MIGRATION_SKIP_RE as R}from"@fjall/util/migration";const $=R,g=400,I=/^--\s*fjall:skip-if:(.*)$/,M=new Set([1,!0,"1"]),_=new Set([0,!1,"0"]);function A(r){return r.split(`
2
- `).some(t=>{const i=t.trim();return i.length>0&&!i.startsWith("--")})}l(A,"hasSqlContent");function b(r){return r.split(/;\s*\n/).map(t=>t.trim()).filter(t=>t.length>0&&A(t))}l(b,"splitSqlStatements");function w(r){const i=h(r).replace(/'(?:[^']|'')*'/g,"'<REDACTED>'"),e=u(i);return e.length>g?e.slice(0,g)+"\u2026":e}l(w,"maskAndTruncateError");function x(r){let t;for(const i of r.split(`
3
- `)){const e=i.trim();if(e.length>0&&!e.startsWith("--"))break;const s=I.exec(e);if(s===null)continue;const o=(s[1]??"").trim();if(o.length===0)throw new Error("fjall:skip-if directive has an empty expression");if(t!==void 0)throw new Error("duplicate fjall:skip-if directive in the leading comment block");t=o}return t}l(x,"parseSkipIfDirective");async function j(r,t,i){let e;try{e=await(await r.query({query:`SELECT (${i})`,format:"JSONEachRow"})).json()}catch(c){throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate failed: ${w(c)}`,{cause:c})}if(e.length!==1)throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate returned ${e.length} rows (expected exactly 1)`);const s=Object.values(e[0]??{});if(s.length!==1)throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate returned ${s.length} columns (expected exactly 1)`);const o=s[0];if(M.has(o))return!0;if(_.has(o))return!1;throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate returned a non-boolean value (${u(JSON.stringify(o))}); expected 0/1`)}l(j,"evaluateSkipIfPredicate");async function L(r){const{client:t,dir:i,signal:e,logger:s}=r,o=r.skipFilesMatching??$,c=k(i).filter(n=>n.endsWith(".sql")).filter(n=>!o.test(n)).sort(),p=[];for(const n of c){if(e?.aborted)throw new Error(`runSqlMigrations: aborted by signal before applying '${n}'`);const E=q(i,n),d=y(E,"utf8");let f;try{f=x(d)}catch(a){throw new Error(`runSqlMigrations: ${n}: ${u(h(a))}`,{cause:a})}if(f!==void 0&&await j(t,n,f)){s?.info("sql migration skipped by fjall:skip-if directive",{file:n,skipIf:f});continue}const m=b(d);try{for(const a of m){if(e?.aborted)throw new Error(`runSqlMigrations: aborted by signal while applying '${n}'`);await t.command({query:a})}}catch(a){throw new Error(`runSqlMigrations: ${n}: ${w(a)}`,{cause:a})}s?.info("sql migration applied",{file:n}),p.push(n)}return{applied:p}}l(L,"runSqlMigrations");export{A as hasSqlContent,x as parseSkipIfDirective,L as runSqlMigrations};
1
+ var g=Object.defineProperty;var l=(r,t)=>g(r,"name",{value:t,configurable:!0});import{readFileSync as w,readdirSync as E}from"node:fs";import{join as y}from"node:path";import{getErrorMessage as k,maskSensitiveOutput as d}from"@fjall/util";import{CLICKHOUSE_MIGRATION_SKIP_RE as q}from"@fjall/util/migration";import{maskSqlError as h}from"./maskSqlError.js";const $=q,I=/^--\s*fjall:skip-if:(.*)$/,b=new Set([1,!0,"1"]),x=new Set([0,!1,"0"]);function M(r){return r.split(`
2
+ `).some(t=>{const n=t.trim();return n.length>0&&!n.startsWith("--")})}l(M,"hasSqlContent");function j(r){return r.split(/;\s*\n/).map(t=>t.trim()).filter(t=>t.length>0&&M(t))}l(j,"splitSqlStatements");function _(r){let t;for(const n of r.split(`
3
+ `)){const i=n.trim();if(i.length>0&&!i.startsWith("--"))break;const o=I.exec(i);if(o===null)continue;const s=(o[1]??"").trim();if(s.length===0)throw new Error("fjall:skip-if directive has an empty expression");if(t!==void 0)throw new Error("duplicate fjall:skip-if directive in the leading comment block");t=s}return t}l(_,"parseSkipIfDirective");async function v(r,t,n){let i;try{i=await(await r.query({query:`SELECT (${n})`,format:"JSONEachRow"})).json()}catch(c){throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate failed: ${h(c)}`,{cause:c})}if(i.length!==1)throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate returned ${i.length} rows (expected exactly 1)`);const o=Object.values(i[0]??{});if(o.length!==1)throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate returned ${o.length} columns (expected exactly 1)`);const s=o[0];if(b.has(s))return!0;if(x.has(s))return!1;throw new Error(`runSqlMigrations: ${t}: fjall:skip-if predicate returned a non-boolean value (${d(JSON.stringify(s))}); expected 0/1`)}l(v,"evaluateSkipIfPredicate");async function O(r){const{client:t,dir:n,signal:i,logger:o}=r,s=r.skipFilesMatching??$,c=E(n).filter(e=>e.endsWith(".sql")).filter(e=>!s.test(e)).sort(),p=[];for(const e of c){if(i?.aborted)throw new Error(`runSqlMigrations: aborted by signal before applying '${e}'`);const m=y(n,e),u=w(m,"utf8");let f;try{f=_(u)}catch(a){throw new Error(`runSqlMigrations: ${e}: ${d(k(a))}`,{cause:a})}if(f!==void 0&&await v(t,e,f)){o?.info("sql migration skipped by fjall:skip-if directive",{file:e,skipIf:f});continue}const S=j(u);try{for(const a of S){if(i?.aborted)throw new Error(`runSqlMigrations: aborted by signal while applying '${e}'`);await t.command({query:a})}}catch(a){throw new Error(`runSqlMigrations: ${e}: ${h(a)}`,{cause:a})}o?.info("sql migration applied",{file:e}),p.push(e)}return{applied:p}}l(O,"runSqlMigrations");export{M as hasSqlContent,_ as parseSkipIfDirective,O as runSqlMigrations};
package/dist/schemas.d.ts CHANGED
@@ -1 +1 @@
1
- export { ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames, } from "@fjall/util/migration";
1
+ export { FrameworkUserNameSchema, FrameworkUserNamesSchema, type FrameworkUserName, type FrameworkUserNames, ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames, } from "@fjall/util/migration";
package/dist/schemas.js CHANGED
@@ -1 +1 @@
1
- import{ManagedUserNameSchema as m,ManagedUserNamesSchema as r}from"@fjall/util/migration";export{m as ManagedUserNameSchema,r as ManagedUserNamesSchema};
1
+ import{FrameworkUserNameSchema as m,FrameworkUserNamesSchema as r,ManagedUserNameSchema as s,ManagedUserNamesSchema as c}from"@fjall/util/migration";export{m as FrameworkUserNameSchema,r as FrameworkUserNamesSchema,s as ManagedUserNameSchema,c as ManagedUserNamesSchema};
@@ -1 +1 @@
1
- export declare function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void>;
1
+ export { sleepAbortable } from "@fjall/util/migration";
@@ -1 +1 @@
1
- var i=Object.defineProperty;var o=(t,e)=>i(t,"name",{value:e,configurable:!0});function s(t,e){return e?.aborted?Promise.resolve():new Promise(n=>{const r=o(()=>{clearTimeout(b),e?.removeEventListener("abort",r),n()},"onAbort"),b=setTimeout(()=>{e?.removeEventListener("abort",r),n()},t);e?.addEventListener("abort",r,{once:!0})})}o(s,"sleepAbortable");export{s as sleepAbortable};
1
+ import{sleepAbortable as r}from"@fjall/util/migration";export{r as sleepAbortable};
@@ -24,12 +24,26 @@
24
24
  * (`version` sha256 hash, `prisma_version` constant) fall back to strict
25
25
  * identity.
26
26
  */
27
- import type { ClickHouseClient } from "@clickhouse/client";
28
27
  import type { MigrationLogger } from "./logger.js";
29
28
  declare const ALLOWED_FIELDS: readonly ["version", "ch_version", "prisma_version"];
30
29
  type AllowedField = (typeof ALLOWED_FIELDS)[number];
30
+ /**
31
+ * The slice of `ClickHouseClient` this check needs. Structural so a caller
32
+ * holding a narrower handle (the schema-gate container's injectable
33
+ * `SchemaGateChHandle`) passes it without a cast; `@clickhouse/client`
34
+ * satisfies it directly.
35
+ */
36
+ export interface SchemaVersionQueryClient {
37
+ query(params: {
38
+ query: string;
39
+ format: "JSONEachRow";
40
+ abort_signal?: AbortSignal;
41
+ }): Promise<{
42
+ json(): Promise<unknown>;
43
+ }>;
44
+ }
31
45
  export interface VerifyExpectedClickHouseSchemaVersionOpts {
32
- client: ClickHouseClient;
46
+ client: SchemaVersionQueryClient;
33
47
  /** Expected version (typically read from an env var baked into the image). */
34
48
  expected: string;
35
49
  /** Database holding the audit table. Defaults to `"analytics"`. */
@@ -1 +1 @@
1
- var u=Object.defineProperty;var s=(e,t)=>u(e,"name",{value:t,configurable:!0});import{isSchemaVersionSatisfied as E}from"@fjall/util/migration";const n=/^[A-Za-z_][A-Za-z0-9_]*$/,l=["version","ch_version","prisma_version"],f="analytics",d="_schema_migrations",h="ch_version";async function A(e){if(e.signal?.aborted)throw new Error("verifyExpectedClickHouseSchemaVersion: aborted by signal before query");const t=e.database??f,r=e.table??d,a=e.field??h;if(!n.test(t))throw new Error(`verifyExpectedClickHouseSchemaVersion: unsafe database identifier '${t}'`);if(!n.test(r))throw new Error(`verifyExpectedClickHouseSchemaVersion: unsafe table identifier '${r}'`);if(!l.includes(a))throw new Error(`verifyExpectedClickHouseSchemaVersion: field must be one of ${l.join("|")} (got '${a}')`);const o=(await(await e.client.query({query:`SELECT ${a} AS value FROM ${t}.${r} ORDER BY applied_at DESC LIMIT 1`,format:"JSONEachRow"})).json())[0]?.value,i=typeof o=="string"?o:null,c={matches:i!==null&&E(e.expected,i),expected:e.expected,actual:i};return e.logger?.info("ClickHouse schema-version check complete",{matches:c.matches,expected:c.expected,actual:c.actual,database:t,table:r,field:a}),c}s(A,"verifyExpectedClickHouseSchemaVersion");export{A as verifyExpectedClickHouseSchemaVersion};
1
+ var u=Object.defineProperty;var o=(e,t)=>u(e,"name",{value:t,configurable:!0});import{isSchemaVersionSatisfied as E}from"@fjall/util/migration";import{SCHEMA_MIGRATIONS_TABLE as f}from"./constants.js";const s=/^[A-Za-z_][A-Za-z0-9_]*$/,l=["version","ch_version","prisma_version"],d="analytics",h=f,m="ch_version";async function v(e){if(e.signal?.aborted)throw new Error("verifyExpectedClickHouseSchemaVersion: aborted by signal before query");const t=e.database??d,r=e.table??h,a=e.field??m;if(!s.test(t))throw new Error(`verifyExpectedClickHouseSchemaVersion: unsafe database identifier '${t}'`);if(!s.test(r))throw new Error(`verifyExpectedClickHouseSchemaVersion: unsafe table identifier '${r}'`);if(!l.includes(a))throw new Error(`verifyExpectedClickHouseSchemaVersion: field must be one of ${l.join("|")} (got '${a}')`);const n=(await(await e.client.query({query:`SELECT ${a} AS value FROM ${t}.${r} ORDER BY applied_at DESC LIMIT 1`,format:"JSONEachRow",...e.signal!==void 0&&{abort_signal:e.signal}})).json())[0]?.value,c=typeof n=="string"?n:null,i={matches:c!==null&&E(e.expected,c),expected:e.expected,actual:c};return e.logger?.info("ClickHouse schema-version check complete",{matches:i.matches,expected:i.expected,actual:i.actual,database:t,table:r,field:a}),i}o(v,"verifyExpectedClickHouseSchemaVersion");export{v as verifyExpectedClickHouseSchemaVersion};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/clickhouse",
3
- "version": "20.0.0",
3
+ "version": "21.0.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/fjall-tech/fjall.git",
@@ -50,7 +50,7 @@
50
50
  "@clickhouse/client": "^1.18.0"
51
51
  },
52
52
  "dependencies": {
53
- "@fjall/util": "^20.0.0",
53
+ "@fjall/util": "^21.0.0",
54
54
  "zod": "^4.4.3"
55
55
  },
56
56
  "engines": {