@fjall/clickhouse 1.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/LICENSE +50 -0
- package/README.md +71 -0
- package/dist/.minified +1 -0
- package/dist/connectWithRetry.d.ts +9 -0
- package/dist/connectWithRetry.js +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/createClient.d.ts +44 -0
- package/dist/createClient.js +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -0
- package/dist/logger.d.ts +5 -0
- package/dist/logger.js +3 -0
- package/dist/provisionUsers.d.ts +12 -0
- package/dist/provisionUsers.js +1 -0
- package/dist/runSqlMigrations.d.ts +14 -0
- package/dist/runSqlMigrations.js +2 -0
- package/dist/schemas.d.ts +1 -0
- package/dist/schemas.js +1 -0
- package/dist/sleepAbortable.d.ts +1 -0
- package/dist/sleepAbortable.js +1 -0
- package/dist/verifyExpectedClickHouseSchemaVersion.d.ts +44 -0
- package/dist/verifyExpectedClickHouseSchemaVersion.js +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
Fjall Proprietary Software Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fjall. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software, including all source, object, bundled, and minified forms
|
|
6
|
+
("the Software"), is the proprietary and confidential property of Fjall.
|
|
7
|
+
|
|
8
|
+
1. Permitted Use. Subject to the terms of this Licence, Fjall grants you
|
|
9
|
+
a non-exclusive, non-transferable, revocable licence to install the
|
|
10
|
+
Software via the npm registry and to execute it solely for the purpose
|
|
11
|
+
of deploying, operating, and managing your own applications and
|
|
12
|
+
infrastructure on cloud providers.
|
|
13
|
+
|
|
14
|
+
2. Restrictions. You may NOT, and may not permit any third party to:
|
|
15
|
+
(a) copy, redistribute, sublicense, sell, rent, lease, or otherwise
|
|
16
|
+
transfer the Software;
|
|
17
|
+
(b) modify, adapt, translate, or create derivative works of the Software;
|
|
18
|
+
(c) reverse engineer, decompile, disassemble, deminify, or otherwise
|
|
19
|
+
attempt to derive the source code, structure, or organisation of
|
|
20
|
+
the Software, except to the minimum extent expressly permitted by
|
|
21
|
+
applicable mandatory law;
|
|
22
|
+
(d) use the Software, or any portion of it, to develop, train, or
|
|
23
|
+
improve any product or service that competes with Fjall;
|
|
24
|
+
(e) remove, alter, or obscure any proprietary notices contained in
|
|
25
|
+
the Software;
|
|
26
|
+
(f) publish, share, or otherwise disclose the Software or its contents
|
|
27
|
+
to any third party.
|
|
28
|
+
|
|
29
|
+
3. Ownership. All right, title, and interest in and to the Software,
|
|
30
|
+
including all intellectual property rights, remain with Fjall. No
|
|
31
|
+
rights are granted except as expressly set out in this Licence.
|
|
32
|
+
|
|
33
|
+
4. Termination. This Licence terminates automatically if you breach any
|
|
34
|
+
of its terms. Upon termination you must cease all use of the Software
|
|
35
|
+
and destroy all copies in your possession.
|
|
36
|
+
|
|
37
|
+
5. Disclaimer of Warranty. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT
|
|
38
|
+
WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
|
39
|
+
THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
|
40
|
+
AND NON-INFRINGEMENT.
|
|
41
|
+
|
|
42
|
+
6. Limitation of Liability. IN NO EVENT SHALL FJALL BE LIABLE FOR ANY
|
|
43
|
+
INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES
|
|
44
|
+
ARISING OUT OF OR RELATED TO THE SOFTWARE, EVEN IF ADVISED OF THE
|
|
45
|
+
POSSIBILITY OF SUCH DAMAGES.
|
|
46
|
+
|
|
47
|
+
7. Governing Law. This Licence is governed by the laws of England and
|
|
48
|
+
Wales, without regard to conflict of laws principles.
|
|
49
|
+
|
|
50
|
+
For commercial licensing enquiries, contact: contact@fjall.io
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @fjall/clickhouse
|
|
2
|
+
|
|
3
|
+
Runtime helpers for ClickHouse migration containers. Consumes the user manifest published by `@fjall/components-infrastructure` `ClickHouseDatabase` and provisions workload users in writable `users_local` storage. Also ships a generic SQL-file runner and a connection-retry helper.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fjall/clickhouse @clickhouse/client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`@clickhouse/client` is a peer dependency — the consumer supplies the runtime client.
|
|
12
|
+
|
|
13
|
+
## Env-var contract
|
|
14
|
+
|
|
15
|
+
The framework's commitment is the env-var shape, not this package's API. A customer rolling their own consumer reads the same env vars.
|
|
16
|
+
|
|
17
|
+
| Env var | Shape | Producer |
|
|
18
|
+
| -------------------------- | --------------------------------------- | --------------------------------------------------------- |
|
|
19
|
+
| `CLICKHOUSE_MANAGED_USERS` | JSON `Array<string>` of user names | `ClickHouseDatabase.getMigrationContributions()` |
|
|
20
|
+
| `USER_<NAME>_PASSWORD` | plaintext (one per managed-users entry) | ECS executionRole + Secrets Manager (no runtime SDK call) |
|
|
21
|
+
|
|
22
|
+
The manifest carries names only — the schema admin (XML-defined) is NOT included, and profile binding is the customer SQL's job (`ALTER USER <name> SETTINGS PROFILE '<profile>'`).
|
|
23
|
+
|
|
24
|
+
The constant name + env-name helper + schema live in `@fjall/util/migration`:
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import {
|
|
28
|
+
CLICKHOUSE_MANAGED_USERS_ENV,
|
|
29
|
+
ManagedUserNamesSchema,
|
|
30
|
+
userPasswordEnvName,
|
|
31
|
+
} from "@fjall/util/migration";
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This package re-exports them for convenience so consumers don't need two imports.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import {
|
|
40
|
+
connectWithRetry,
|
|
41
|
+
createConsoleMigrationLogger,
|
|
42
|
+
provisionUsersFromEnv,
|
|
43
|
+
runSqlMigrations,
|
|
44
|
+
} from "@fjall/clickhouse";
|
|
45
|
+
import { createClient } from "@clickhouse/client";
|
|
46
|
+
|
|
47
|
+
const logger = createConsoleMigrationLogger("migration-runner");
|
|
48
|
+
const client = createClient({ url, username, password, database });
|
|
49
|
+
|
|
50
|
+
await connectWithRetry(client, { label: "user-provision", logger });
|
|
51
|
+
await provisionUsersFromEnv({ client, logger });
|
|
52
|
+
await runSqlMigrations({ client, dir: "./clickhouse-init", logger });
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`provisionUsersFromEnv` is idempotent — it issues `CREATE USER IF NOT EXISTS` then `ALTER USER` for every manifest entry, so repeated invocations are safe.
|
|
56
|
+
|
|
57
|
+
## Rolling your own consumer
|
|
58
|
+
|
|
59
|
+
The env-var contract above is stable. A customer who prefers their own provisioner reads the same env vars; this package's helpers are convenience wrappers. The framework commits to the env shape, not to this package.
|
|
60
|
+
|
|
61
|
+
If rolling your own, you'll want to replicate:
|
|
62
|
+
|
|
63
|
+
- single-quote-escape the password literal (`'` → `''`) before string-interpolating into `IDENTIFIED WITH … BY '…'`
|
|
64
|
+
- mask passwords in any log output (use `maskSensitiveOutput` from `@fjall/util`)
|
|
65
|
+
- order `CREATE USER IF NOT EXISTS` before `ALTER USER` so re-provisioning realigns drifted credentials
|
|
66
|
+
- place provisioning BEFORE any migration-hash idempotency gate
|
|
67
|
+
- regex-validate user names before string-interpolating into SQL (CH doesn't bind identifier parameters)
|
|
68
|
+
|
|
69
|
+
## Licence
|
|
70
|
+
|
|
71
|
+
Proprietary — see [LICENSE](./LICENSE).
|
package/dist/.minified
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
10 files minified at 2026-05-23T21:29:47.546Z
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import type { MigrationLogger } from "./logger.js";
|
|
3
|
+
export interface ConnectWithRetryOpts {
|
|
4
|
+
label: string;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
delays?: readonly number[];
|
|
7
|
+
logger?: MigrationLogger;
|
|
8
|
+
}
|
|
9
|
+
export declare function connectWithRetry(client: ClickHouseClient, opts: ConnectWithRetryOpts): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getErrorMessage as f,maskSensitiveOutput as _}from"@fjall/util";import{sleepAbortable as g}from"./sleepAbortable.js";const p=[5e3,1e4,2e4,4e4,6e4],y=3,A=516,h=["ECONNREFUSED","ETIMEDOUT","ENOTFOUND"];async function u(E,a){const{label:n,signal:c,logger:i}=a,r=a.delays??p;let s;for(let t=0;t<r.length;t++){if(c?.aborted)throw new Error(`connectWithRetry: ${n} aborted by signal before attempt ${t+1}`);try{await E.ping(),t>0&&i?.info("connect succeeded after retry",{label:n,attempt:t+1});return}catch(e){s=e;const o=typeof e=="object"&&e!==null&&"code"in e?e.code:void 0,l=o===A;if(!(typeof o=="string"&&h.includes(o)||l&&t<y))throw e;i?.warn("connect attempt failed; retrying",{label:n,attempt:t+1,attempts:r.length,delayMs:r[t],code:l?"AUTH_FAILED":o??""}),await g(r[t],c)}}const d=_(f(s));throw new Error(`connectWithRetry: ${n} failed after ${r.length} attempts: ${d}`)}export{u as connectWithRetry};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { CLICKHOUSE_MANAGED_USERS_ENV, userPasswordEnvName, } from "@fjall/util/migration";
|
|
@@ -0,0 +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};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type ClickHouseClient, type ClickHouseClientConfigOptions } from "@clickhouse/client";
|
|
2
|
+
/**
|
|
3
|
+
* Construction options for {@link createFjallClickHouseClient}.
|
|
4
|
+
*
|
|
5
|
+
* Every field is optional. `url` falls back to `process.env.CLICKHOUSE_URL`;
|
|
6
|
+
* `caCert` falls back to `process.env.CLICKHOUSE_CA_CERT`. Empty strings
|
|
7
|
+
* are treated identically to `undefined` on both — dev compose without a
|
|
8
|
+
* CA cert injected boots transparently in plaintext, while prod ECS
|
|
9
|
+
* containers receive the CA PEM via the secret-block on `secretsImport`
|
|
10
|
+
* and run on `https://`.
|
|
11
|
+
*/
|
|
12
|
+
export interface CreateFjallClickHouseClientOptions {
|
|
13
|
+
/** Defaults to env CLICKHOUSE_URL. Empty/undefined throws. */
|
|
14
|
+
readonly url?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Defaults to env CLICKHOUSE_CA_CERT (PEM string injected by the ECS
|
|
17
|
+
* `secrets:` block from `ClickHouseDatabase.tlsCaSecret`). Absent/empty
|
|
18
|
+
* disables the `tls:` config (plaintext fallback for dev compose).
|
|
19
|
+
*/
|
|
20
|
+
readonly caCert?: string;
|
|
21
|
+
readonly username?: string;
|
|
22
|
+
readonly password?: string;
|
|
23
|
+
/** Optional database name override. */
|
|
24
|
+
readonly database?: string;
|
|
25
|
+
/** Pass-through to `@clickhouse/client`. */
|
|
26
|
+
readonly clickhouse_settings?: ClickHouseClientConfigOptions["clickhouse_settings"];
|
|
27
|
+
readonly compression?: ClickHouseClientConfigOptions["compression"];
|
|
28
|
+
readonly request_timeout?: number;
|
|
29
|
+
readonly max_open_connections?: number;
|
|
30
|
+
readonly keep_alive?: ClickHouseClientConfigOptions["keep_alive"];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Sync factory wrapping `@clickhouse/client`'s `createClient`. No AWS SDK
|
|
34
|
+
* call, no `await`, no module-level cache — the CA PEM is already on
|
|
35
|
+
* `process.env` when the container's user code runs because ECS resolves
|
|
36
|
+
* the `secrets:` block during container bring-up.
|
|
37
|
+
*
|
|
38
|
+
* `tls.ca_cert` MUST be a `Buffer` per the `@clickhouse/client` contract.
|
|
39
|
+
* Passing a raw string type-checks (the field accepts `Buffer | string`
|
|
40
|
+
* structurally) but the underlying TLS stack silently skips the
|
|
41
|
+
* trust-store add and the handshake fails downstream. Wrap explicitly
|
|
42
|
+
* with `Buffer.from(pem, "utf8")`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createFjallClickHouseClient(opts?: CreateFjallClickHouseClientOptions): ClickHouseClient;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createClient as d}from"@clickhouse/client";function c(e={}){const n=process.env.CLICKHOUSE_URL,i=e.url!==void 0&&e.url!==""?e.url:n!==void 0&&n!==""?n:void 0;if(i===void 0)throw new Error("createFjallClickHouseClient: CLICKHOUSE_URL must be set (env or opts.url)");const r=process.env.CLICKHOUSE_CA_CERT,u=e.caCert!==void 0&&e.caCert!==""?e.caCert:r!==void 0&&r!==""?r:void 0;return d({url:i,...e.username!==void 0&&{username:e.username},...e.password!==void 0&&{password:e.password},...e.database!==void 0&&{database:e.database},...e.clickhouse_settings!==void 0&&{clickhouse_settings:e.clickhouse_settings},...e.compression!==void 0&&{compression:e.compression},...e.request_timeout!==void 0&&{request_timeout:e.request_timeout},...e.max_open_connections!==void 0&&{max_open_connections:e.max_open_connections},...e.keep_alive!==void 0&&{keep_alive:e.keep_alive},...u!==void 0&&{tls:{ca_cert:Buffer.from(u,"utf8")}}})}export{c as createFjallClickHouseClient};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CLICKHOUSE_MANAGED_USERS_ENV, userPasswordEnvName, } from "./constants.js";
|
|
2
|
+
export { ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames, } from "./schemas.js";
|
|
3
|
+
export { type MigrationLogger, createConsoleMigrationLogger, } from "./logger.js";
|
|
4
|
+
export { type ConnectWithRetryOpts, connectWithRetry, } from "./connectWithRetry.js";
|
|
5
|
+
export { type ProvisionUsersFromEnvOpts, type ProvisionUsersFromEnvResult, provisionUsersFromEnv, } from "./provisionUsers.js";
|
|
6
|
+
export { type RunSqlMigrationsOpts, type RunSqlMigrationsResult, runSqlMigrations, } from "./runSqlMigrations.js";
|
|
7
|
+
export { type VerifyExpectedClickHouseSchemaVersionOpts, type VerifyExpectedClickHouseSchemaVersionResult, verifyExpectedClickHouseSchemaVersion, } from "./verifyExpectedClickHouseSchemaVersion.js";
|
|
8
|
+
export { type CreateFjallClickHouseClientOptions, createFjallClickHouseClient, } from "./createClient.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +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 x}from"./provisionUsers.js";import{runSqlMigrations as E}from"./runSqlMigrations.js";import{verifyExpectedClickHouseSchemaVersion as g}from"./verifyExpectedClickHouseSchemaVersion.js";import{createFjallClickHouseClient as M}from"./createClient.js";export{o as CLICKHOUSE_MANAGED_USERS_ENV,m as ManagedUserNameSchema,s as ManagedUserNamesSchema,p as connectWithRetry,i as createConsoleMigrationLogger,M as createFjallClickHouseClient,x as provisionUsersFromEnv,E as runSqlMigrations,t as userPasswordEnvName,g as verifyExpectedClickHouseSchemaVersion};
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
function r(n){return{info(t,e){process.stdout.write(JSON.stringify({level:"info",namespace:n,msg:t,ctx:e??{},ts:new Date().toISOString()})+`
|
|
2
|
+
`)},warn(t,e){process.stdout.write(JSON.stringify({level:"warn",namespace:n,msg:t,ctx:e??{},ts:new Date().toISOString()})+`
|
|
3
|
+
`)}}}export{r as createConsoleMigrationLogger};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import type { MigrationLogger } from "./logger.js";
|
|
3
|
+
export interface ProvisionUsersFromEnvOpts {
|
|
4
|
+
client: ClickHouseClient;
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
logger?: MigrationLogger;
|
|
7
|
+
env?: NodeJS.ProcessEnv;
|
|
8
|
+
}
|
|
9
|
+
export interface ProvisionUsersFromEnvResult {
|
|
10
|
+
provisioned: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function provisionUsersFromEnv(opts: ProvisionUsersFromEnvOpts): Promise<ProvisionUsersFromEnvResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CLICKHOUSE_MANAGED_USERS_ENV as i,userPasswordEnvName as w}from"./constants.js";import{ManagedUserNamesSchema as S}from"./schemas.js";function g(r){return r.replace(/\\/g,"\\\\").replace(/'/g,"''")}async function N(r){const{client:t,signal:v,logger:s}=r,m=r.env??process.env,o=m[i];if(o===void 0||o==="")return s?.info("no managed users to provision (manifest env absent)",{}),{provisioned:[]};let c;try{c=JSON.parse(o)}catch(e){throw new Error(`provisionUsersFromEnv: malformed ${i} \u2014 JSON parse failed`,{cause:e})}const n=S.safeParse(c);if(!n.success)throw new Error(`provisionUsersFromEnv: malformed ${i} \u2014 ${n.error.message}`);const p=n.data;if(p.length===0)return s?.info("no managed users to provision (manifest empty)",{}),{provisioned:[]};const d=[];for(const e of p){if(v?.aborted)throw new Error(`provisionUsersFromEnv: aborted by signal before user '${e}'`);const f=w(e),a=m[f];if(a===void 0||a==="")throw new Error(`provisionUsersFromEnv: required env ${f} is missing or empty`);const E=g(a),u=`CREATE USER IF NOT EXISTS ${e} IDENTIFIED WITH sha256_password BY '${E}'`,l=`ALTER USER ${e} IDENTIFIED WITH sha256_password BY '${E}'`;await t.command({query:u}),await t.command({query:l}),s?.info("clickhouse user provisioned",{name:e}),d.push(e)}return{provisioned:d}}export{N as provisionUsersFromEnv};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import type { MigrationLogger } from "./logger.js";
|
|
3
|
+
export declare function hasSqlContent(chunk: string): boolean;
|
|
4
|
+
export interface RunSqlMigrationsOpts {
|
|
5
|
+
client: ClickHouseClient;
|
|
6
|
+
dir: string;
|
|
7
|
+
skipFilesMatching?: RegExp;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
logger?: MigrationLogger;
|
|
10
|
+
}
|
|
11
|
+
export interface RunSqlMigrationsResult {
|
|
12
|
+
applied: readonly string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function runSqlMigrations(opts: RunSqlMigrationsOpts): Promise<RunSqlMigrationsResult>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{readFileSync as S,readdirSync as E}from"node:fs";import{join as w}from"node:path";import{getErrorMessage as q,maskSensitiveOutput as M}from"@fjall/util";import{CLICKHOUSE_MIGRATION_SKIP_RE as y}from"@fjall/util/migration";const R=y,l=400;function b(e){return e.split(`
|
|
2
|
+
`).some(r=>{const n=r.trim();return n.length>0&&!n.startsWith("--")})}function _(e){return e.split(/;\s*\n/).map(r=>r.trim()).filter(r=>r.length>0&&b(r))}async function P(e){const{client:r,dir:n,signal:s,logger:c}=e,m=e.skipFilesMatching??R,f=E(n).filter(t=>t.endsWith(".sql")).filter(t=>!m.test(t)).sort(),a=[];for(const t of f){if(s?.aborted)throw new Error(`runSqlMigrations: aborted by signal before applying '${t}'`);const p=w(n,t),g=S(p,"utf8"),u=_(g);try{for(const o of u){if(s?.aborted)throw new Error(`runSqlMigrations: aborted by signal while applying '${t}'`);await r.command({query:o})}}catch(o){const d=q(o).replace(/'(?:[^']|'')*'/g,"'<REDACTED>'"),i=M(d),h=i.length>l?i.slice(0,l)+"\u2026":i;throw new Error(`runSqlMigrations: ${t}: ${h}`,{cause:o})}c?.info("sql migration applied",{file:t}),a.push(t)}return{applied:a}}export{b as hasSqlContent,P as runSqlMigrations};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ManagedUserNameSchema, ManagedUserNamesSchema, type ManagedUserName, type ManagedUserNames, } from "@fjall/util/migration";
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{ManagedUserNameSchema as m,ManagedUserNamesSchema as r}from"@fjall/util/migration";export{m as ManagedUserNameSchema,r as ManagedUserNamesSchema};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sleepAbortable(ms: number, signal?: AbortSignal): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function b(o,e){return e?.aborted?Promise.resolve():new Promise(r=>{const t=()=>{clearTimeout(n),e?.removeEventListener("abort",t),r()},n=setTimeout(()=>{e?.removeEventListener("abort",t),r()},o);e?.addEventListener("abort",t,{once:!0})})}export{b as sleepAbortable};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Boot-time gate: verify the ClickHouse `_schema_migrations` audit table's
|
|
3
|
+
* most recently applied row matches the expected version baked into the
|
|
4
|
+
* deployed image. Companion to the SQL-side helper
|
|
5
|
+
* `verifyExpectedSchemaVersion` from `@fjall/util/migration` — together they
|
|
6
|
+
* close the asymmetry where a webapp's boot gate checks Postgres but blindly
|
|
7
|
+
* trusts ClickHouse.
|
|
8
|
+
*
|
|
9
|
+
* The audit table is owned by the migration runner: see the canonical DDL
|
|
10
|
+
* recorded by `ensureSchemaMigrationsTable` in the webapp's migration-runner
|
|
11
|
+
* (columns `version`, `applied_at`, `snapshot_arn`, `prisma_version`,
|
|
12
|
+
* `ch_version`). This helper reads a single configurable column (default
|
|
13
|
+
* `ch_version`) so callers can pin to whichever the image's expected env
|
|
14
|
+
* carries (overall migration hash via `version`, Prisma-side via
|
|
15
|
+
* `prisma_version`, or ClickHouse-side via `ch_version`).
|
|
16
|
+
*
|
|
17
|
+
* Returns `{ matches, expected, actual }` rather than throwing on mismatch:
|
|
18
|
+
* the consumer's boot script owns the exit-code / log shape.
|
|
19
|
+
*/
|
|
20
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
21
|
+
import type { MigrationLogger } from "./logger.js";
|
|
22
|
+
declare const ALLOWED_FIELDS: readonly ["version", "ch_version", "prisma_version"];
|
|
23
|
+
type AllowedField = (typeof ALLOWED_FIELDS)[number];
|
|
24
|
+
export interface VerifyExpectedClickHouseSchemaVersionOpts {
|
|
25
|
+
client: ClickHouseClient;
|
|
26
|
+
/** Expected version (typically read from an env var baked into the image). */
|
|
27
|
+
expected: string;
|
|
28
|
+
/** Database holding the audit table. Defaults to `"analytics"`. */
|
|
29
|
+
database?: string;
|
|
30
|
+
/** Audit-table name. Defaults to `"_schema_migrations"`. */
|
|
31
|
+
table?: string;
|
|
32
|
+
/** Column whose latest value to compare. Defaults to `"ch_version"`. */
|
|
33
|
+
field?: AllowedField;
|
|
34
|
+
signal?: AbortSignal;
|
|
35
|
+
logger?: MigrationLogger;
|
|
36
|
+
}
|
|
37
|
+
export interface VerifyExpectedClickHouseSchemaVersionResult {
|
|
38
|
+
matches: boolean;
|
|
39
|
+
expected: string;
|
|
40
|
+
/** `null` when the audit table is empty (no migrations applied yet). */
|
|
41
|
+
actual: string | null;
|
|
42
|
+
}
|
|
43
|
+
export declare function verifyExpectedClickHouseSchemaVersion(opts: VerifyExpectedClickHouseSchemaVersionOpts): Promise<VerifyExpectedClickHouseSchemaVersionResult>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const s=/^[A-Za-z_][A-Za-z0-9_]*$/,n=["version","ch_version","prisma_version"],l="analytics",u="_schema_migrations",E="ch_version";async function h(e){if(e.signal?.aborted)throw new Error("verifyExpectedClickHouseSchemaVersion: aborted by signal before query");const t=e.database??l,c=e.table??u,a=e.field??E;if(!s.test(t))throw new Error(`verifyExpectedClickHouseSchemaVersion: unsafe database identifier '${t}'`);if(!s.test(c))throw new Error(`verifyExpectedClickHouseSchemaVersion: unsafe table identifier '${c}'`);if(!n.includes(a))throw new Error(`verifyExpectedClickHouseSchemaVersion: field must be one of ${n.join("|")} (got '${a}')`);const i=(await(await e.client.query({query:`SELECT ${a} AS value FROM ${t}.${c} ORDER BY applied_at DESC LIMIT 1`,format:"JSONEachRow"})).json())[0]?.value,o=typeof i=="string"?i:null,r={matches:o===e.expected,expected:e.expected,actual:o};return e.logger?.info("ClickHouse schema-version check complete",{matches:r.matches,expected:r.expected,actual:r.actual,database:t,table:c,field:a}),r}export{h as verifyExpectedClickHouseSchemaVersion};
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fjall/clickhouse",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Runtime helpers for ClickHouse migration containers — user provisioning, SQL file application, connection retry. Consumes the manifest contract published by @fjall/components-infrastructure ClickHouseDatabase.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist/"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"clean": "rm -rf ./dist ./sourcemaps",
|
|
22
|
+
"clean:node": "rm -rf ./node_modules",
|
|
23
|
+
"build": "npm run clean && npx tsc && node ../scripts/minify-dist.mjs dist",
|
|
24
|
+
"watch": "npm run build && npx tsc-watch",
|
|
25
|
+
"watch:only": "npx tsc-watch",
|
|
26
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
27
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
28
|
+
"lint": "eslint src/",
|
|
29
|
+
"lint:fix": "eslint src/ --fix",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"typecheck": "npx tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"author": "",
|
|
34
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@clickhouse/client": "^1.18.0",
|
|
37
|
+
"@types/node": "^25.6.0",
|
|
38
|
+
"prettier": "^3.8.3",
|
|
39
|
+
"tsc-watch": "^7.2.0",
|
|
40
|
+
"typescript": "^6.0.3",
|
|
41
|
+
"vitest": "^4.1.5"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@clickhouse/client": "^1.18.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@fjall/util": "^1.1.0",
|
|
48
|
+
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=22.0.0"
|
|
52
|
+
},
|
|
53
|
+
"gitHead": "437ec726425bd7610b83a57c12c1a4e1980bbb01"
|
|
54
|
+
}
|