@fjall/clickhouse-migrations 0.99.1
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 +69 -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/index.d.ts +6 -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 +13 -0
- package/dist/runSqlMigrations.js +1 -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/package.json +51 -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,69 @@
|
|
|
1
|
+
# @fjall/clickhouse-migrations
|
|
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-migrations @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_SQL_USERS_MANIFEST` | JSON `Array<{ name, profile }>` | `ClickHouseDatabase.getMigrationContributions()` |
|
|
20
|
+
| `USER_<NAME>_PASSWORD` | plaintext (one per manifest entry) | ECS executionRole + Secrets Manager (no runtime SDK call) |
|
|
21
|
+
|
|
22
|
+
The constant name + env-name helper live in `@fjall/util/migration`:
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import {
|
|
26
|
+
CLICKHOUSE_SQL_USERS_MANIFEST_ENV,
|
|
27
|
+
SqlUsersManifestSchema,
|
|
28
|
+
userPasswordEnvName,
|
|
29
|
+
} from "@fjall/util/migration";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
This package re-exports them for convenience so consumers don't need two imports.
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import {
|
|
38
|
+
connectWithRetry,
|
|
39
|
+
createConsoleMigrationLogger,
|
|
40
|
+
provisionUsersFromEnv,
|
|
41
|
+
runSqlMigrations,
|
|
42
|
+
} from "@fjall/clickhouse-migrations";
|
|
43
|
+
import { createClient } from "@clickhouse/client";
|
|
44
|
+
|
|
45
|
+
const logger = createConsoleMigrationLogger("migration-runner");
|
|
46
|
+
const client = createClient({ url, username, password, database });
|
|
47
|
+
|
|
48
|
+
await connectWithRetry(client, { label: "user-provision", logger });
|
|
49
|
+
await provisionUsersFromEnv({ client, logger });
|
|
50
|
+
await runSqlMigrations({ client, dir: "./clickhouse-init", logger });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`provisionUsersFromEnv` is idempotent — it issues `CREATE USER IF NOT EXISTS` then `ALTER USER` for every manifest entry, so repeated invocations are safe.
|
|
54
|
+
|
|
55
|
+
## Rolling your own consumer
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
If rolling your own, you'll want to replicate:
|
|
60
|
+
|
|
61
|
+
- single-quote-escape the password literal (`'` → `''`) before string-interpolating into `IDENTIFIED WITH … BY '…'`
|
|
62
|
+
- mask passwords in any log output (use `maskSensitiveOutput` from `@fjall/util`)
|
|
63
|
+
- order `CREATE USER IF NOT EXISTS` before `ALTER USER` so re-provisioning realigns drifted credentials
|
|
64
|
+
- place provisioning BEFORE any migration-hash idempotency gate
|
|
65
|
+
- regex-validate user names before string-interpolating into SQL (CH doesn't bind identifier parameters)
|
|
66
|
+
|
|
67
|
+
## Licence
|
|
68
|
+
|
|
69
|
+
Proprietary — see [LICENSE](./LICENSE).
|
package/dist/.minified
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
8 files minified at 2026-05-22T01:26:14.923Z
|
|
@@ -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,h=516,m=["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===h;if(!(typeof o=="string"&&m.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};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
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";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{CLICKHOUSE_MANAGED_USERS_ENV as o,userPasswordEnvName as a}from"./constants.js";import{ManagedUserNameSchema as t,ManagedUserNamesSchema as n}from"./schemas.js";import{createConsoleMigrationLogger as i}from"./logger.js";import{connectWithRetry as f}from"./connectWithRetry.js";import{provisionUsersFromEnv as x}from"./provisionUsers.js";import{runSqlMigrations as S}from"./runSqlMigrations.js";export{o as CLICKHOUSE_MANAGED_USERS_ENV,t as ManagedUserNameSchema,n as ManagedUserNamesSchema,f as connectWithRetry,i as createConsoleMigrationLogger,x as provisionUsersFromEnv,S as runSqlMigrations,a as userPasswordEnvName};
|
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 S}from"./constants.js";import{ManagedUserNamesSchema as g}from"./schemas.js";const v=/^[a-z][a-z0-9_]*$/;function h(r){return r.replace(/'/g,"''")}async function I(r){const{client:t,signal:u,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=g.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 E=[];for(const e of p){if(u?.aborted)throw new Error(`provisionUsersFromEnv: aborted by signal before user '${e}'`);if(!v.test(e))throw new Error(`provisionUsersFromEnv: user name '${e}' violates name pattern ${v.source}`);const d=S(e),a=m[d];if(a===void 0||a==="")throw new Error(`provisionUsersFromEnv: required env ${d} is missing or empty`);const f=h(a),l=`CREATE USER IF NOT EXISTS ${e} IDENTIFIED WITH sha256_password BY '${f}'`,w=`ALTER USER ${e} IDENTIFIED WITH sha256_password BY '${f}'`;await t.command({query:l}),await t.command({query:w}),s?.info("clickhouse user provisioned",{name:e}),E.push(e)}return{provisioned:E}}export{I as provisionUsersFromEnv};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClickHouseClient } from "@clickhouse/client";
|
|
2
|
+
import type { MigrationLogger } from "./logger.js";
|
|
3
|
+
export interface RunSqlMigrationsOpts {
|
|
4
|
+
client: ClickHouseClient;
|
|
5
|
+
dir: string;
|
|
6
|
+
skipFilesMatching?: RegExp;
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
logger?: MigrationLogger;
|
|
9
|
+
}
|
|
10
|
+
export interface RunSqlMigrationsResult {
|
|
11
|
+
applied: readonly string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function runSqlMigrations(opts: RunSqlMigrationsOpts): Promise<RunSqlMigrationsResult>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as h,readdirSync as E}from"node:fs";import{join as q}from"node:path";import{getErrorMessage as w,maskSensitiveOutput as M}from"@fjall/util";const S=/\.dev\.sql$/,n=400;async function A(e){const{client:a,dir:o,signal:l,logger:c}=e,g=e.skipFilesMatching??S,f=E(o).filter(t=>t.endsWith(".sql")).filter(t=>!g.test(t)).sort(),s=[];for(const t of f){if(l?.aborted)throw new Error(`runSqlMigrations: aborted by signal before applying '${t}'`);const d=q(o,t),p=h(d,"utf8");try{await a.command({query:p})}catch(i){const u=w(i).replace(/'(?:[^']|'')*'/g,"'<REDACTED>'"),r=M(u),m=r.length>n?r.slice(0,n)+"\u2026":r;throw new Error(`runSqlMigrations: ${t}: ${m}`,{cause:i})}c?.info("sql migration applied",{file:t}),s.push(t)}return{applied:s}}export{A 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};
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fjall/clickhouse-migrations",
|
|
3
|
+
"version": "0.99.1",
|
|
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
|
+
"scripts": {
|
|
18
|
+
"clean": "rm -rf ./dist ./sourcemaps",
|
|
19
|
+
"clean:node": "rm -rf ./node_modules",
|
|
20
|
+
"build": "npm run clean && npx tsc && node ../scripts/minify-dist.mjs dist",
|
|
21
|
+
"watch": "npm run build && npx tsc-watch",
|
|
22
|
+
"watch:only": "npx tsc-watch",
|
|
23
|
+
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
24
|
+
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json}\"",
|
|
25
|
+
"lint": "eslint src/",
|
|
26
|
+
"lint:fix": "eslint src/ --fix",
|
|
27
|
+
"test": "vitest run",
|
|
28
|
+
"typecheck": "npx tsc --noEmit"
|
|
29
|
+
},
|
|
30
|
+
"author": "",
|
|
31
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@clickhouse/client": "^1.18.0",
|
|
34
|
+
"@types/node": "^25.6.0",
|
|
35
|
+
"prettier": "^3.8.3",
|
|
36
|
+
"tsc-watch": "^7.2.0",
|
|
37
|
+
"typescript": "^6.0.3",
|
|
38
|
+
"vitest": "^4.1.5"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@clickhouse/client": "^1.18.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@fjall/util": "^0.99.1",
|
|
45
|
+
"zod": "^4.4.3"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=22.0.0"
|
|
49
|
+
},
|
|
50
|
+
"gitHead": "0b8cc9b7c5017ca126c884da4cb29793dd26c96a"
|
|
51
|
+
}
|