@fjall/clickhouse-migrations 0.99.1 → 0.99.3

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/README.md CHANGED
@@ -14,17 +14,19 @@ npm install @fjall/clickhouse-migrations @clickhouse/client
14
14
 
15
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
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) |
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
21
 
22
- The constant name + env-name helper live in `@fjall/util/migration`:
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`:
23
25
 
24
26
  ```typescript
25
27
  import {
26
- CLICKHOUSE_SQL_USERS_MANIFEST_ENV,
27
- SqlUsersManifestSchema,
28
+ CLICKHOUSE_MANAGED_USERS_ENV,
29
+ ManagedUserNamesSchema,
28
30
  userPasswordEnvName,
29
31
  } from "@fjall/util/migration";
30
32
  ```
package/dist/.minified CHANGED
@@ -1 +1 @@
1
- 8 files minified at 2026-05-22T01:26:14.923Z
1
+ 8 files minified at 2026-05-22T22:51:41.320Z
@@ -1 +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};
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};
@@ -1 +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};
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};
@@ -1,5 +1,6 @@
1
1
  import type { ClickHouseClient } from "@clickhouse/client";
2
2
  import type { MigrationLogger } from "./logger.js";
3
+ export declare function hasSqlContent(chunk: string): boolean;
3
4
  export interface RunSqlMigrationsOpts {
4
5
  client: ClickHouseClient;
5
6
  dir: string;
@@ -1 +1,2 @@
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};
1
+ import{readFileSync as S,readdirSync as q}from"node:fs";import{join as w}from"node:path";import{getErrorMessage as E,maskSensitiveOutput as y}from"@fjall/util";const M=/\.dev\.sql$/,l=400;function b(e){return e.split(`
2
+ `).some(r=>{const n=r.trim();return n.length>0&&!n.startsWith("--")})}function R(e){return e.split(/;\s*\n/).map(r=>r.trim()).filter(r=>r.length>0&&b(r))}async function _(e){const{client:r,dir:n,signal:o,logger:c}=e,f=e.skipFilesMatching??M,g=q(n).filter(t=>t.endsWith(".sql")).filter(t=>!f.test(t)).sort(),a=[];for(const t of g){if(o?.aborted)throw new Error(`runSqlMigrations: aborted by signal before applying '${t}'`);const m=w(n,t),p=S(m,"utf8"),u=R(p);try{for(const s of u){if(o?.aborted)throw new Error(`runSqlMigrations: aborted by signal while applying '${t}'`);await r.command({query:s})}}catch(s){const d=E(s).replace(/'(?:[^']|'')*'/g,"'<REDACTED>'"),i=y(d),h=i.length>l?i.slice(0,l)+"\u2026":i;throw new Error(`runSqlMigrations: ${t}: ${h}`,{cause:s})}c?.info("sql migration applied",{file:t}),a.push(t)}return{applied:a}}export{b as hasSqlContent,_ as runSqlMigrations};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fjall/clickhouse-migrations",
3
- "version": "0.99.1",
3
+ "version": "0.99.3",
4
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
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -41,11 +41,11 @@
41
41
  "@clickhouse/client": "^1.18.0"
42
42
  },
43
43
  "dependencies": {
44
- "@fjall/util": "^0.99.1",
44
+ "@fjall/util": "^0.99.3",
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "engines": {
48
48
  "node": ">=22.0.0"
49
49
  },
50
- "gitHead": "0b8cc9b7c5017ca126c884da4cb29793dd26c96a"
50
+ "gitHead": "e50d25185d5eab618e2a90622466296fa0cbffe8"
51
51
  }