drizzle-multitenant 1.3.1 → 1.3.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/dist/cli/index.js +121 -121
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {Command}from'commander';import
|
|
2
|
+
import {Command}from'commander';import M from'chalk';import {checkbox,confirm,input,select,Separator}from'@inquirer/prompts';import {mkdir,readdir,writeFile,readFile,access}from'fs/promises';import {resolve,join,basename,relative,extname,dirname}from'path';import {createHash}from'crypto';import {existsSync,mkdirSync,writeFileSync,readdirSync,statSync}from'fs';import {Pool}from'pg';import {drizzle}from'drizzle-orm/node-postgres';import st from'ora';import lr from'cli-table3';import {pathToFileURL}from'url';import Sr from'cli-progress';import {createInterface}from'readline';import {LRUCache}from'lru-cache';async function Me(n,e,t){if(!(await n.query(`SELECT EXISTS (
|
|
3
3
|
SELECT 1 FROM information_schema.tables
|
|
4
4
|
WHERE table_schema = $1 AND table_name = $2
|
|
5
5
|
) as exists`,[e,t])).rows[0]?.exists)return null;let a=await n.query(`SELECT column_name, data_type
|
|
@@ -56,13 +56,13 @@ import {Command}from'commander';import C from'chalk';import {checkbox,confirm,in
|
|
|
56
56
|
ON tc.constraint_name = cc.constraint_name
|
|
57
57
|
AND tc.constraint_type = 'CHECK'
|
|
58
58
|
WHERE tc.table_schema = $1 AND tc.table_name = $2
|
|
59
|
-
ORDER BY tc.constraint_name, kcu.ordinal_position`,[e,t]),a=new Map;for(let o of r.rows){let s=a.get(o.constraint_name);if(s)o.column_name&&!s.columns.includes(o.column_name)&&s.columns.push(o.column_name),o.foreign_column_name&&s.foreignColumns&&!s.foreignColumns.includes(o.foreign_column_name)&&s.foreignColumns.push(o.foreign_column_name);else {let i={name:o.constraint_name,type:o.constraint_type,columns:o.column_name?[o.column_name]:[]};o.foreign_table_name&&(i.foreignTable=o.foreign_table_name),o.foreign_column_name&&(i.foreignColumns=[o.foreign_column_name]),o.check_clause&&(i.checkExpression=o.check_clause),a.set(o.constraint_name,i);}}return Array.from(a.values())}function Xn(n,e){let t=[],r=new Map(n.map(o=>[o.name,o])),a=new Map(e.map(o=>[o.name,o]));for(let o of n){let s=a.get(o.name);if(!s){t.push({constraint:o.name,type:"missing",expected:`${o.type} on (${o.columns.join(", ")})`,description:`Constraint "${o.name}" (${o.type}) is missing`});continue}let i=o.columns.sort().join(","),c=s.columns.sort().join(",");(o.type!==s.type||i!==c)&&t.push({constraint:o.name,type:"definition_mismatch",expected:`${o.type} on (${o.columns.join(", ")})`,actual:`${s.type} on (${s.columns.join(", ")})`,description:`Constraint "${o.name}" definition differs`});}for(let o of e)r.has(o.name)||t.push({constraint:o.name,type:"extra",actual:`${o.type} on (${o.columns.join(", ")})`,description:`Extra constraint "${o.name}" (${o.type}) not in reference`});return t}var ya="__drizzle_migrations",Ge=class{constructor(e,t,r){this.tenantConfig=e;this.schemaManager=t;this.driftConfig=r;this.migrationsTable=r.migrationsTable??ya;}migrationsTable;getSchemaName(e){return this.tenantConfig.isolation.schemaNameTemplate(e)}async createPool(e){return this.schemaManager.createPool(e)}async detectDrift(e={}){let t=Date.now(),{concurrency:r=10,includeIndexes:a=true,includeConstraints:o=true,excludeTables:s=[this.migrationsTable],onProgress:i}=e,c=e.tenantIds??await this.driftConfig.tenantDiscovery();if(c.length===0)return {referenceTenant:"",total:0,noDrift:0,withDrift:0,error:0,details:[],timestamp:new Date().toISOString(),durationMs:Date.now()-t};let l=e.referenceTenant??c[0];i?.(l,"starting"),i?.(l,"introspecting");let m=await this.introspectSchema(l,{includeIndexes:a,includeConstraints:o,excludeTables:s});if(!m)return {referenceTenant:l,total:c.length,noDrift:0,withDrift:0,error:c.length,details:c.map(h=>({tenantId:h,schemaName:this.getSchemaName(h),hasDrift:false,tables:[],issueCount:0,error:h===l?"Failed to introspect reference tenant":"Reference tenant introspection failed"})),timestamp:new Date().toISOString(),durationMs:Date.now()-t};i?.(l,"completed");let u=c.filter(h=>h!==l),p=[];p.push({tenantId:l,schemaName:m.schemaName,hasDrift:false,tables:[],issueCount:0});for(let h=0;h<u.length;h+=r){let g=u.slice(h,h+r)
|
|
59
|
+
ORDER BY tc.constraint_name, kcu.ordinal_position`,[e,t]),a=new Map;for(let o of r.rows){let s=a.get(o.constraint_name);if(s)o.column_name&&!s.columns.includes(o.column_name)&&s.columns.push(o.column_name),o.foreign_column_name&&s.foreignColumns&&!s.foreignColumns.includes(o.foreign_column_name)&&s.foreignColumns.push(o.foreign_column_name);else {let i={name:o.constraint_name,type:o.constraint_type,columns:o.column_name?[o.column_name]:[]};o.foreign_table_name&&(i.foreignTable=o.foreign_table_name),o.foreign_column_name&&(i.foreignColumns=[o.foreign_column_name]),o.check_clause&&(i.checkExpression=o.check_clause),a.set(o.constraint_name,i);}}return Array.from(a.values())}function Xn(n,e){let t=[],r=new Map(n.map(o=>[o.name,o])),a=new Map(e.map(o=>[o.name,o]));for(let o of n){let s=a.get(o.name);if(!s){t.push({constraint:o.name,type:"missing",expected:`${o.type} on (${o.columns.join(", ")})`,description:`Constraint "${o.name}" (${o.type}) is missing`});continue}let i=o.columns.sort().join(","),c=s.columns.sort().join(",");(o.type!==s.type||i!==c)&&t.push({constraint:o.name,type:"definition_mismatch",expected:`${o.type} on (${o.columns.join(", ")})`,actual:`${s.type} on (${s.columns.join(", ")})`,description:`Constraint "${o.name}" definition differs`});}for(let o of e)r.has(o.name)||t.push({constraint:o.name,type:"extra",actual:`${o.type} on (${o.columns.join(", ")})`,description:`Extra constraint "${o.name}" (${o.type}) not in reference`});return t}var ya="__drizzle_migrations",Ge=class{constructor(e,t,r){this.tenantConfig=e;this.schemaManager=t;this.driftConfig=r;this.migrationsTable=r.migrationsTable??ya;}migrationsTable;getSchemaName(e){return this.tenantConfig.isolation.schemaNameTemplate(e)}async createPool(e){return this.schemaManager.createPool(e)}async detectDrift(e={}){let t=Date.now(),{concurrency:r=10,includeIndexes:a=true,includeConstraints:o=true,excludeTables:s=[this.migrationsTable],onProgress:i}=e,c=e.tenantIds??await this.driftConfig.tenantDiscovery();if(c.length===0)return {referenceTenant:"",total:0,noDrift:0,withDrift:0,error:0,details:[],timestamp:new Date().toISOString(),durationMs:Date.now()-t};let l=e.referenceTenant??c[0];i?.(l,"starting"),i?.(l,"introspecting");let m=await this.introspectSchema(l,{includeIndexes:a,includeConstraints:o,excludeTables:s});if(!m)return {referenceTenant:l,total:c.length,noDrift:0,withDrift:0,error:c.length,details:c.map(h=>({tenantId:h,schemaName:this.getSchemaName(h),hasDrift:false,tables:[],issueCount:0,error:h===l?"Failed to introspect reference tenant":"Reference tenant introspection failed"})),timestamp:new Date().toISOString(),durationMs:Date.now()-t};i?.(l,"completed");let u=c.filter(h=>h!==l),p=[];p.push({tenantId:l,schemaName:m.schemaName,hasDrift:false,tables:[],issueCount:0});for(let h=0;h<u.length;h+=r){let g=u.slice(h,h+r),$=await Promise.all(g.map(async S=>{try{i?.(S,"starting"),i?.(S,"introspecting");let b=await this.introspectSchema(S,{includeIndexes:a,includeConstraints:o,excludeTables:s});if(!b)return i?.(S,"failed"),{tenantId:S,schemaName:this.getSchemaName(S),hasDrift:!1,tables:[],issueCount:0,error:"Failed to introspect schema"};i?.(S,"comparing");let x=this.compareSchemas(m,b,{includeIndexes:a,includeConstraints:o});return i?.(S,"completed"),x}catch(b){return i?.(S,"failed"),{tenantId:S,schemaName:this.getSchemaName(S),hasDrift:false,tables:[],issueCount:0,error:b.message}}}));p.push(...$);}return {referenceTenant:l,total:p.length,noDrift:p.filter(h=>!h.hasDrift&&!h.error).length,withDrift:p.filter(h=>h.hasDrift&&!h.error).length,error:p.filter(h=>!!h.error).length,details:p,timestamp:new Date().toISOString(),durationMs:Date.now()-t}}async compareTenant(e,t,r={}){let{includeIndexes:a=true,includeConstraints:o=true,excludeTables:s=[this.migrationsTable]}=r,i=await this.introspectSchema(t,{includeIndexes:a,includeConstraints:o,excludeTables:s});if(!i)return {tenantId:e,schemaName:this.getSchemaName(e),hasDrift:false,tables:[],issueCount:0,error:"Failed to introspect reference tenant"};let c=await this.introspectSchema(e,{includeIndexes:a,includeConstraints:o,excludeTables:s});return c?this.compareSchemas(i,c,{includeIndexes:a,includeConstraints:o}):{tenantId:e,schemaName:this.getSchemaName(e),hasDrift:false,tables:[],issueCount:0,error:"Failed to introspect tenant schema"}}async introspectSchema(e,t={}){let r=this.getSchemaName(e),a=await this.createPool(r);try{let o=await this.introspectTables(a,r,t);return {tenantId:e,schemaName:r,tables:o,introspectedAt:new Date}}catch{return null}finally{await a.end();}}compareSchemas(e,t,r={}){let{includeIndexes:a=true,includeConstraints:o=true}=r,s=[],i=0,c=new Map(e.tables.map(m=>[m.name,m])),l=new Map(t.tables.map(m=>[m.name,m]));for(let m of e.tables){let u=l.get(m.name);if(!u){s.push({table:m.name,status:"missing",columns:m.columns.map(S=>({column:S.name,type:"missing",expected:S.dataType,description:`Column "${S.name}" (${S.dataType}) is missing`})),indexes:[],constraints:[]}),i+=m.columns.length;continue}let p=Vn(m.columns,u.columns),h=a?Qn(m.indexes,u.indexes):[],g=o?Xn(m.constraints,u.constraints):[],$=p.length+h.length+g.length;i+=$,$>0&&s.push({table:m.name,status:"drifted",columns:p,indexes:h,constraints:g});}for(let m of t.tables)c.has(m.name)||(s.push({table:m.name,status:"extra",columns:m.columns.map(u=>({column:u.name,type:"extra",actual:u.dataType,description:`Extra column "${u.name}" (${u.dataType}) not in reference`})),indexes:[],constraints:[]}),i+=m.columns.length);return {tenantId:t.tenantId,schemaName:t.schemaName,hasDrift:i>0,tables:s,issueCount:i}}async introspectTables(e,t,r){let{includeIndexes:a=true,includeConstraints:o=true,excludeTables:s=[]}=r,i=await e.query(`SELECT table_name
|
|
60
60
|
FROM information_schema.tables
|
|
61
61
|
WHERE table_schema = $1
|
|
62
62
|
AND table_type = 'BASE TABLE'
|
|
63
63
|
ORDER BY table_name`,[t]),c=[];for(let l of i.rows){if(s.includes(l.table_name))continue;let m=await Kn(e,t,l.table_name),u=a?await Yn(e,t,l.table_name):[],p=o?await Zn(e,t,l.table_name):[];c.push({name:l.table_name,columns:m,indexes:u,constraints:p});}return c}};var Je=class{constructor(e,t){this.config=e;this.deps=t;}async seedTenant(e,t){let r=Date.now(),a=this.deps.schemaNameTemplate(e),o=await this.deps.createPool(a);try{let s=drizzle(o,{schema:this.deps.tenantSchema});return await t(s,e),{tenantId:e,schemaName:a,success:!0,durationMs:Date.now()-r}}catch(s){return {tenantId:e,schemaName:a,success:false,error:s.message,durationMs:Date.now()-r}}finally{await o.end();}}async seedAll(e,t={}){let{concurrency:r=10,onProgress:a,onError:o}=t,s=await this.config.tenantDiscovery(),i=[],c=false;for(let l=0;l<s.length&&!c;l+=r){let m=s.slice(l,l+r),u=await Promise.all(m.map(async p=>{if(c)return this.createSkippedResult(p);try{a?.(p,"starting"),a?.(p,"seeding");let h=await this.seedTenant(p,e);return a?.(p,h.success?"completed":"failed"),h}catch(h){return a?.(p,"failed"),o?.(p,h)==="abort"&&(c=true),this.createErrorResult(p,h)}}));i.push(...u);}if(c){let l=s.slice(i.length);for(let m of l)i.push(this.createSkippedResult(m));}return this.aggregateResults(i)}async seedTenants(e,t,r={}){let{concurrency:a=10,onProgress:o,onError:s}=r,i=[];for(let c=0;c<e.length;c+=a){let l=e.slice(c,c+a),m=await Promise.all(l.map(async u=>{try{o?.(u,"starting"),o?.(u,"seeding");let p=await this.seedTenant(u,t);return o?.(u,p.success?"completed":"failed"),p}catch(p){return o?.(u,"failed"),s?.(u,p),this.createErrorResult(u,p)}}));i.push(...m);}return this.aggregateResults(i)}createSkippedResult(e){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,error:"Skipped due to abort",durationMs:0}}createErrorResult(e,t){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,error:t.message,durationMs:0}}aggregateResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success&&t.error!=="Skipped due to abort").length,skipped:e.filter(t=>t.error==="Skipped due to abort").length,details:e}}};var Ta="public",Ke=class{constructor(e,t){this.config=e;this.deps=t;this.schemaName=e.schemaName??Ta;}schemaName;async seed(e){let t=Date.now(),r=await this.deps.createPool();try{this.config.hooks?.onStart?.();let a=drizzle(r,{schema:this.deps.sharedSchema});return await e(a),this.config.hooks?.onComplete?.(),{schemaName:this.schemaName,success:!0,durationMs:Date.now()-t}}catch(a){return this.config.hooks?.onError?.(a),{schemaName:this.schemaName,success:false,error:a.message,durationMs:Date.now()-t}}finally{await r.end();}}};var Ve=class{constructor(e,t){this.config=e;this.deps=t;}async getSyncStatus(){let e=await this.config.tenantDiscovery(),t=await this.deps.loadMigrations(),r=[];for(let a of e)r.push(await this.getTenantSyncStatus(a,t));return {total:r.length,inSync:r.filter(a=>a.inSync&&!a.error).length,outOfSync:r.filter(a=>!a.inSync&&!a.error).length,error:r.filter(a=>!!a.error).length,details:r}}async getTenantSyncStatus(e,t){let r=this.deps.schemaNameTemplate(e),a=await this.deps.createPool(r);try{let o=t??await this.deps.loadMigrations(),s=new Set(o.map(g=>g.name)),i=new Set(o.map(g=>g.hash));if(!await this.deps.migrationsTableExists(a,r))return {tenantId:e,schemaName:r,missing:o.map(g=>g.name),orphans:[],inSync:o.length===0,format:null};let l=await this.deps.getOrDetectFormat(a,r),m=await this.getAppliedMigrations(a,r,l),u=new Set(m.map(g=>g.identifier)),p=o.filter(g=>!this.isMigrationApplied(g,u,l)).map(g=>g.name),h=m.filter(g=>(l.columns.identifier==="name"||!i.has(g.identifier))&&!s.has(g.identifier)).map(g=>g.identifier);return {tenantId:e,schemaName:r,missing:p,orphans:h,inSync:p.length===0&&h.length===0,format:l.format}}catch(o){return {tenantId:e,schemaName:r,missing:[],orphans:[],inSync:false,format:null,error:o.message}}finally{await a.end();}}async markMissing(e){let t=Date.now(),r=this.deps.schemaNameTemplate(e),a=[],o=await this.deps.createPool(r);try{let s=await this.getTenantSyncStatus(e);if(s.error)return {tenantId:e,schemaName:r,success:!1,markedMigrations:[],removedOrphans:[],error:s.error,durationMs:Date.now()-t};if(s.missing.length===0)return {tenantId:e,schemaName:r,success:!0,markedMigrations:[],removedOrphans:[],durationMs:Date.now()-t};let i=await this.deps.getOrDetectFormat(o,r);await this.deps.ensureMigrationsTable(o,r,i);let c=await this.deps.loadMigrations(),l=new Set(s.missing);for(let m of c)l.has(m.name)&&(await this.recordMigration(o,r,m,i),a.push(m.name));return {tenantId:e,schemaName:r,success:!0,markedMigrations:a,removedOrphans:[],durationMs:Date.now()-t}}catch(s){return {tenantId:e,schemaName:r,success:false,markedMigrations:a,removedOrphans:[],error:s.message,durationMs:Date.now()-t}}finally{await o.end();}}async markAllMissing(e={}){let{concurrency:t=10,onProgress:r,onError:a}=e,o=await this.config.tenantDiscovery(),s=[],i=false;for(let c=0;c<o.length&&!i;c+=t){let l=o.slice(c,c+t),m=await Promise.all(l.map(async u=>{if(i)return this.createSkippedSyncResult(u);try{r?.(u,"starting");let p=await this.markMissing(u);return r?.(u,p.success?"completed":"failed"),p}catch(p){return r?.(u,"failed"),a?.(u,p)==="abort"&&(i=true),this.createErrorSyncResult(u,p)}}));s.push(...m);}return this.aggregateSyncResults(s)}async cleanOrphans(e){let t=Date.now(),r=this.deps.schemaNameTemplate(e),a=[],o=await this.deps.createPool(r);try{let s=await this.getTenantSyncStatus(e);if(s.error)return {tenantId:e,schemaName:r,success:!1,markedMigrations:[],removedOrphans:[],error:s.error,durationMs:Date.now()-t};if(s.orphans.length===0)return {tenantId:e,schemaName:r,success:!0,markedMigrations:[],removedOrphans:[],durationMs:Date.now()-t};let i=await this.deps.getOrDetectFormat(o,r),c=i.columns.identifier;for(let l of s.orphans)await o.query(`DELETE FROM "${r}"."${i.tableName}" WHERE "${c}" = $1`,[l]),a.push(l);return {tenantId:e,schemaName:r,success:!0,markedMigrations:[],removedOrphans:a,durationMs:Date.now()-t}}catch(s){return {tenantId:e,schemaName:r,success:false,markedMigrations:[],removedOrphans:a,error:s.message,durationMs:Date.now()-t}}finally{await o.end();}}async cleanAllOrphans(e={}){let{concurrency:t=10,onProgress:r,onError:a}=e,o=await this.config.tenantDiscovery(),s=[],i=false;for(let c=0;c<o.length&&!i;c+=t){let l=o.slice(c,c+t),m=await Promise.all(l.map(async u=>{if(i)return this.createSkippedSyncResult(u);try{r?.(u,"starting");let p=await this.cleanOrphans(u);return r?.(u,p.success?"completed":"failed"),p}catch(p){return r?.(u,"failed"),a?.(u,p)==="abort"&&(i=true),this.createErrorSyncResult(u,p)}}));s.push(...m);}return this.aggregateSyncResults(s)}async getAppliedMigrations(e,t,r){let a=r.columns.identifier,o=r.columns.timestamp;return (await e.query(`SELECT id, "${a}" as identifier, "${o}" as applied_at
|
|
64
64
|
FROM "${t}"."${r.tableName}"
|
|
65
|
-
ORDER BY id`)).rows.map(i=>{let c=r.columns.timestampType==="bigint"?new Date(Number(i.applied_at)):new Date(i.applied_at);return {identifier:i.identifier,appliedAt:c}})}isMigrationApplied(e,t,r){return r.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async recordMigration(e,t,r,a){let{identifier:o,timestamp:s,timestampType:i}=a.columns,c=o==="name"?r.name:r.hash,l=i==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${t}"."${a.tableName}" ("${o}", "${s}") VALUES ($1, $2)`,[c,l]);}createSkippedSyncResult(e){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,markedMigrations:[],removedOrphans:[],error:"Skipped due to abort",durationMs:0}}createErrorSyncResult(e,t){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,markedMigrations:[],removedOrphans:[],error:t.message,durationMs:0}}aggregateSyncResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success).length,details:e}}};var Ye=class{constructor(e,t){this.config=e;this.deps=t;}async migrateTenant(e,t,r={}){let a=Date.now(),o=this.deps.schemaNameTemplate(e),s=[],i=await this.deps.createPool(o);try{await this.config.hooks?.beforeTenant?.(e);let c=await this.deps.getOrDetectFormat(i,o);await this.deps.ensureMigrationsTable(i,o,c);let l=t??await this.deps.loadMigrations(),m=await this.getAppliedMigrations(i,o,c),u=new Set(m.map(g=>g.identifier)),p=l.filter(g=>!this.isMigrationApplied(g,u,c));if(r.dryRun)return {tenantId:e,schemaName:o,success:!0,appliedMigrations:p.map(g=>g.name),durationMs:Date.now()-a,format:c.format};for(let g of p){let
|
|
65
|
+
ORDER BY id`)).rows.map(i=>{let c=r.columns.timestampType==="bigint"?new Date(Number(i.applied_at)):new Date(i.applied_at);return {identifier:i.identifier,appliedAt:c}})}isMigrationApplied(e,t,r){return r.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async recordMigration(e,t,r,a){let{identifier:o,timestamp:s,timestampType:i}=a.columns,c=o==="name"?r.name:r.hash,l=i==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${t}"."${a.tableName}" ("${o}", "${s}") VALUES ($1, $2)`,[c,l]);}createSkippedSyncResult(e){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,markedMigrations:[],removedOrphans:[],error:"Skipped due to abort",durationMs:0}}createErrorSyncResult(e,t){return {tenantId:e,schemaName:this.deps.schemaNameTemplate(e),success:false,markedMigrations:[],removedOrphans:[],error:t.message,durationMs:0}}aggregateSyncResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success).length,details:e}}};var Ye=class{constructor(e,t){this.config=e;this.deps=t;}async migrateTenant(e,t,r={}){let a=Date.now(),o=this.deps.schemaNameTemplate(e),s=[],i=await this.deps.createPool(o);try{await this.config.hooks?.beforeTenant?.(e);let c=await this.deps.getOrDetectFormat(i,o);await this.deps.ensureMigrationsTable(i,o,c);let l=t??await this.deps.loadMigrations(),m=await this.getAppliedMigrations(i,o,c),u=new Set(m.map(g=>g.identifier)),p=l.filter(g=>!this.isMigrationApplied(g,u,c));if(r.dryRun)return {tenantId:e,schemaName:o,success:!0,appliedMigrations:p.map(g=>g.name),durationMs:Date.now()-a,format:c.format};for(let g of p){let $=Date.now();r.onProgress?.(e,"migrating",g.name),await this.config.hooks?.beforeMigration?.(e,g.name),await this.applyMigration(i,o,g,c),await this.config.hooks?.afterMigration?.(e,g.name,Date.now()-$),s.push(g.name);}let h={tenantId:e,schemaName:o,success:!0,appliedMigrations:s,durationMs:Date.now()-a,format:c.format};return await this.config.hooks?.afterTenant?.(e,h),h}catch(c){let l={tenantId:e,schemaName:o,success:false,appliedMigrations:s,error:c.message,durationMs:Date.now()-a};return await this.config.hooks?.afterTenant?.(e,l),l}finally{await i.end();}}async markAsApplied(e,t={}){let r=Date.now(),a=this.deps.schemaNameTemplate(e),o=[],s=await this.deps.createPool(a);try{await this.config.hooks?.beforeTenant?.(e);let i=await this.deps.getOrDetectFormat(s,a);await this.deps.ensureMigrationsTable(s,a,i);let c=await this.deps.loadMigrations(),l=await this.getAppliedMigrations(s,a,i),m=new Set(l.map(h=>h.identifier)),u=c.filter(h=>!this.isMigrationApplied(h,m,i));for(let h of u){let g=Date.now();t.onProgress?.(e,"migrating",h.name),await this.config.hooks?.beforeMigration?.(e,h.name),await this.recordMigration(s,a,h,i),await this.config.hooks?.afterMigration?.(e,h.name,Date.now()-g),o.push(h.name);}let p={tenantId:e,schemaName:a,success:!0,appliedMigrations:o,durationMs:Date.now()-r,format:i.format};return await this.config.hooks?.afterTenant?.(e,p),p}catch(i){let c={tenantId:e,schemaName:a,success:false,appliedMigrations:o,error:i.message,durationMs:Date.now()-r};return await this.config.hooks?.afterTenant?.(e,c),c}finally{await s.end();}}async getTenantStatus(e,t){let r=this.deps.schemaNameTemplate(e),a=await this.deps.createPool(r);try{let o=t??await this.deps.loadMigrations();if(!await this.deps.migrationsTableExists(a,r))return {tenantId:e,schemaName:r,appliedCount:0,pendingCount:o.length,pendingMigrations:o.map(u=>u.name),status:o.length>0?"behind":"ok",format:null};let i=await this.deps.getOrDetectFormat(a,r),c=await this.getAppliedMigrations(a,r,i),l=new Set(c.map(u=>u.identifier)),m=o.filter(u=>!this.isMigrationApplied(u,l,i));return {tenantId:e,schemaName:r,appliedCount:c.length,pendingCount:m.length,pendingMigrations:m.map(u=>u.name),status:m.length>0?"behind":"ok",format:i.format}}catch(o){return {tenantId:e,schemaName:r,appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:o.message,format:null}}finally{await a.end();}}async executeMigration(e,t,r,a,o){o?.markOnly?(o.onProgress?.("recording"),await this.recordMigration(e,t,r,a)):(o?.onProgress?.("applying"),await this.applyMigration(e,t,r,a));}async executeMigrations(e,t,r,a,o){let s=[];for(let i of r)await this.executeMigration(e,t,i,a,o),s.push(i.name);return s}async recordMigration(e,t,r,a){let{identifier:o,timestamp:s,timestampType:i}=a.columns,c=o==="name"?r.name:r.hash,l=i==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${t}"."${a.tableName}" ("${o}", "${s}") VALUES ($1, $2)`,[c,l]);}async getAppliedMigrations(e,t,r){let a=r.columns.identifier,o=r.columns.timestamp;return (await e.query(`SELECT id, "${a}" as identifier, "${o}" as applied_at
|
|
66
66
|
FROM "${t}"."${r.tableName}"
|
|
67
67
|
ORDER BY id`)).rows.map(i=>{let c=r.columns.timestampType==="bigint"?new Date(Number(i.applied_at)):new Date(i.applied_at);return {identifier:i.identifier,...r.columns.identifier==="name"?{name:i.identifier}:{hash:i.identifier},appliedAt:c}})}async getPendingMigrations(e,t,r,a){let o=await this.getAppliedMigrations(e,t,a),s=new Set(o.map(i=>i.identifier));return r.filter(i=>!this.isMigrationApplied(i,s,a))}isMigrationApplied(e,t,r){return r.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async applyMigration(e,t,r,a){let o=await e.connect();try{await o.query("BEGIN"),await o.query(r.sql);let{identifier:s,timestamp:i,timestampType:c}=a.columns,l=s==="name"?r.name:r.hash,m=c==="bigint"?Date.now():new Date;await o.query(`INSERT INTO "${t}"."${a.tableName}" ("${s}", "${i}") VALUES ($1, $2)`,[l,m]),await o.query("COMMIT");}catch(s){throw await o.query("ROLLBACK"),s}finally{o.release();}}};var Qe=class{constructor(e,t,r){this.config=e;this.executor=t;this.loadMigrations=r;}async migrateAll(e={}){let{concurrency:t=10,onProgress:r,onError:a,dryRun:o=false}=e,s=await this.config.tenantDiscovery(),i=await this.loadMigrations(),c=[],l=false;for(let m=0;m<s.length&&!l;m+=t){let u=s.slice(m,m+t),p=await Promise.all(u.map(async h=>{if(l)return this.createSkippedResult(h);try{r?.(h,"starting");let g=await this.executor.migrateTenant(h,i,{dryRun:o,onProgress:r});return r?.(h,g.success?"completed":"failed"),g}catch(g){return r?.(h,"failed"),a?.(h,g)==="abort"&&(l=true),this.createErrorResult(h,g)}}));c.push(...p);}if(l){let m=s.slice(c.length);for(let u of m)c.push(this.createSkippedResult(u));}return this.aggregateResults(c)}async migrateTenants(e,t={}){let r=await this.loadMigrations(),a=[],{concurrency:o=10,onProgress:s,onError:i,dryRun:c=false}=t;for(let l=0;l<e.length;l+=o){let m=e.slice(l,l+o),u=await Promise.all(m.map(async p=>{try{s?.(p,"starting");let h=await this.executor.migrateTenant(p,r,{dryRun:c,onProgress:s});return s?.(p,h.success?"completed":"failed"),h}catch(h){return s?.(p,"failed"),i?.(p,h),this.createErrorResult(p,h)}}));a.push(...u);}return this.aggregateResults(a)}async markAllAsApplied(e={}){let{concurrency:t=10,onProgress:r,onError:a}=e,o=await this.config.tenantDiscovery(),s=[],i=false;for(let c=0;c<o.length&&!i;c+=t){let l=o.slice(c,c+t),m=await Promise.all(l.map(async u=>{if(i)return this.createSkippedResult(u);try{r?.(u,"starting");let p=await this.executor.markAsApplied(u,{onProgress:r});return r?.(u,p.success?"completed":"failed"),p}catch(p){return r?.(u,"failed"),a?.(u,p)==="abort"&&(i=true),this.createErrorResult(u,p)}}));s.push(...m);}if(i){let c=o.slice(s.length);for(let l of c)s.push(this.createSkippedResult(l));}return this.aggregateResults(s)}async getStatus(){let e=await this.config.tenantDiscovery(),t=await this.loadMigrations(),r=[];for(let a of e)r.push(await this.executor.getTenantStatus(a,t));return r}createSkippedResult(e){return {tenantId:e,schemaName:"",success:false,appliedMigrations:[],error:"Skipped due to abort",durationMs:0}}createErrorResult(e,t){return {tenantId:e,schemaName:"",success:false,appliedMigrations:[],error:t.message,durationMs:0}}aggregateResults(e){return {total:e.length,succeeded:e.filter(t=>t.success).length,failed:e.filter(t=>!t.success&&t.error!=="Skipped due to abort").length,skipped:e.filter(t=>t.error==="Skipped due to abort").length,details:e}}};async function er(n,e,t=[]){let r=t.length>0?t.map((o,s)=>`$${s+2}`).join(", "):"''::text";return (await n.query(`SELECT table_name
|
|
68
68
|
FROM information_schema.tables
|
|
@@ -150,13 +150,13 @@ import {Command}from'commander';import C from'chalk';import {checkbox,confirm,in
|
|
|
150
150
|
AND tc.table_schema = ccu.table_schema
|
|
151
151
|
WHERE tc.table_schema = $1
|
|
152
152
|
AND tc.constraint_type = 'FOREIGN KEY'
|
|
153
|
-
AND tc.table_name != ccu.table_name`,[e]),a=new Map,o=new Set(t);for(let m of t)a.set(m,new Set);for(let m of r.rows)o.has(m.table_name)&&o.has(m.foreign_table_name)&&a.get(m.table_name).add(m.foreign_table_name);let s=[],i=new Map,c=[];for(let m of t)i.set(m,0);for(let[m,u]of a)for(let p of u)i.set(p,(i.get(p)??0)+1);for(let[m,u]of i)u===0&&c.push(m);for(;c.length>0;){let m=c.shift();s.push(m);for(let[u,p]of a)if(p.has(m)){p.delete(m);let h=(i.get(u)??0)-1;i.set(u,h),h===0&&c.push(u);}}let l=t.filter(m=>!s.includes(m));return [...s,...l]}async function nr(n,e,t,r,a,o){let s=0,i=await za(n,e,r);await n.query("SET session_replication_role = replica");try{for(let c=0;c<i.length;c++){let l=i[c];o?.("copying_data",{table:l,progress:c+1,total:i.length});let m=await Pa(n,e,t,l,a);s+=m;}}finally{await n.query("SET session_replication_role = DEFAULT");}return s}var ka="__drizzle_migrations",Ze=class{constructor(e,t){this.deps=t;this.migrationsTable=e.migrationsTable??ka;}migrationsTable;async cloneTenant(e,t,r={}){let a=Date.now(),{includeData:o=false,anonymize:s,excludeTables:i=[],onProgress:c}=r,l=this.deps.schemaNameTemplate(e),m=this.deps.schemaNameTemplate(t),u=[this.migrationsTable,...i],p=null,h=null;try{if(c?.("starting"),!await this.deps.schemaExists(e))return this.createErrorResult(e,t,m,`Source tenant "${e}" does not exist`,a);if(await this.deps.schemaExists(t))return this.createErrorResult(e,t,m,`Target tenant "${t}" already exists`,a);c?.("introspecting"),p=await this.deps.createPool(l);let
|
|
153
|
+
AND tc.table_name != ccu.table_name`,[e]),a=new Map,o=new Set(t);for(let m of t)a.set(m,new Set);for(let m of r.rows)o.has(m.table_name)&&o.has(m.foreign_table_name)&&a.get(m.table_name).add(m.foreign_table_name);let s=[],i=new Map,c=[];for(let m of t)i.set(m,0);for(let[m,u]of a)for(let p of u)i.set(p,(i.get(p)??0)+1);for(let[m,u]of i)u===0&&c.push(m);for(;c.length>0;){let m=c.shift();s.push(m);for(let[u,p]of a)if(p.has(m)){p.delete(m);let h=(i.get(u)??0)-1;i.set(u,h),h===0&&c.push(u);}}let l=t.filter(m=>!s.includes(m));return [...s,...l]}async function nr(n,e,t,r,a,o){let s=0,i=await za(n,e,r);await n.query("SET session_replication_role = replica");try{for(let c=0;c<i.length;c++){let l=i[c];o?.("copying_data",{table:l,progress:c+1,total:i.length});let m=await Pa(n,e,t,l,a);s+=m;}}finally{await n.query("SET session_replication_role = DEFAULT");}return s}var ka="__drizzle_migrations",Ze=class{constructor(e,t){this.deps=t;this.migrationsTable=e.migrationsTable??ka;}migrationsTable;async cloneTenant(e,t,r={}){let a=Date.now(),{includeData:o=false,anonymize:s,excludeTables:i=[],onProgress:c}=r,l=this.deps.schemaNameTemplate(e),m=this.deps.schemaNameTemplate(t),u=[this.migrationsTable,...i],p=null,h=null;try{if(c?.("starting"),!await this.deps.schemaExists(e))return this.createErrorResult(e,t,m,`Source tenant "${e}" does not exist`,a);if(await this.deps.schemaExists(t))return this.createErrorResult(e,t,m,`Target tenant "${t}" already exists`,a);c?.("introspecting"),p=await this.deps.createPool(l);let S=await er(p,l,u);if(S.length===0)return c?.("creating_schema"),await this.deps.createSchema(t),c?.("completed"),{sourceTenant:e,targetTenant:t,targetSchema:m,success:!0,tables:[],durationMs:Date.now()-a};let b=await Promise.all(S.map(w=>tr(p,l,m,w)));await p.end(),p=null,c?.("creating_schema"),await this.deps.createSchema(t),h=await this.deps.createRootPool(),c?.("creating_tables");for(let w of b)await h.query(`SET search_path TO "${m}"; ${w.createDdl}`);c?.("creating_constraints");for(let w of b)for(let D of w.constraintDdls.filter(W=>!W.includes("FOREIGN KEY")))try{await h.query(`SET search_path TO "${m}"; ${D}`);}catch{}c?.("creating_indexes");for(let w of b)for(let D of w.indexDdls)try{await h.query(D);}catch{}let x=0;o&&(c?.("copying_data"),x=await nr(h,l,m,S,s?.enabled?s.rules:void 0,c));for(let w of b)for(let D of w.constraintDdls.filter(W=>W.includes("FOREIGN KEY")))try{await h.query(D);}catch{}c?.("completed");let y={sourceTenant:e,targetTenant:t,targetSchema:m,success:!0,tables:S,durationMs:Date.now()-a};return o&&(y.rowsCopied=x),y}catch(g){return r.onError?.(g),c?.("failed"),this.createErrorResult(e,t,m,g.message,a)}finally{p&&await p.end().catch(()=>{}),h&&await h.end().catch(()=>{});}}createErrorResult(e,t,r,a,o){return {sourceTenant:e,targetTenant:t,targetSchema:r,success:false,error:a,tables:[],durationMs:Date.now()-o}}};var Na="public",Xe=class{constructor(e,t){this.config=e;this.deps=t;this.schemaName=e.schemaName??Na;}schemaName;async migrate(e={}){let t=Date.now(),r=[],a=await this.deps.createPool();try{e.onProgress?.("starting"),await this.config.hooks?.beforeMigration?.();let o=await this.deps.getOrDetectFormat(a,this.schemaName);await this.deps.ensureMigrationsTable(a,this.schemaName,o);let s=await this.deps.loadMigrations(),i=await this.getAppliedMigrations(a,o),c=new Set(i.map(m=>m.identifier)),l=s.filter(m=>!this.isMigrationApplied(m,c,o));if(e.dryRun)return {schemaName:this.schemaName,success:!0,appliedMigrations:l.map(m=>m.name),durationMs:Date.now()-t,format:o.format};for(let m of l){let u=Date.now();e.onProgress?.("migrating",m.name),await this.applyMigration(a,m,o),await this.config.hooks?.afterMigration?.(m.name,Date.now()-u),r.push(m.name);}return e.onProgress?.("completed"),{schemaName:this.schemaName,success:!0,appliedMigrations:r,durationMs:Date.now()-t,format:o.format}}catch(o){return e.onProgress?.("failed"),{schemaName:this.schemaName,success:false,appliedMigrations:r,error:o.message,durationMs:Date.now()-t}}finally{await a.end();}}async markAsApplied(e={}){let t=Date.now(),r=[],a=await this.deps.createPool();try{e.onProgress?.("starting");let o=await this.deps.getOrDetectFormat(a,this.schemaName);await this.deps.ensureMigrationsTable(a,this.schemaName,o);let s=await this.deps.loadMigrations(),i=await this.getAppliedMigrations(a,o),c=new Set(i.map(m=>m.identifier)),l=s.filter(m=>!this.isMigrationApplied(m,c,o));for(let m of l)e.onProgress?.("migrating",m.name),await this.recordMigration(a,m,o),r.push(m.name);return e.onProgress?.("completed"),{schemaName:this.schemaName,success:!0,appliedMigrations:r,durationMs:Date.now()-t,format:o.format}}catch(o){return e.onProgress?.("failed"),{schemaName:this.schemaName,success:false,appliedMigrations:r,error:o.message,durationMs:Date.now()-t}}finally{await a.end();}}async getStatus(){let e=await this.deps.createPool();try{let t=await this.deps.loadMigrations();if(!await this.deps.migrationsTableExists(e,this.schemaName))return {schemaName:this.schemaName,appliedCount:0,pendingCount:t.length,pendingMigrations:t.map(c=>c.name),status:t.length>0?"behind":"ok",format:null};let a=await this.deps.getOrDetectFormat(e,this.schemaName),o=await this.getAppliedMigrations(e,a),s=new Set(o.map(c=>c.identifier)),i=t.filter(c=>!this.isMigrationApplied(c,s,a));return {schemaName:this.schemaName,appliedCount:o.length,pendingCount:i.length,pendingMigrations:i.map(c=>c.name),status:i.length>0?"behind":"ok",format:a.format}}catch(t){return {schemaName:this.schemaName,appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:t.message,format:null}}finally{await e.end();}}async getAppliedMigrations(e,t){let r=t.columns.identifier,a=t.columns.timestamp;return (await e.query(`SELECT id, "${r}" as identifier, "${a}" as applied_at
|
|
154
154
|
FROM "${this.schemaName}"."${t.tableName}"
|
|
155
|
-
ORDER BY id`)).rows.map(s=>{let i=t.columns.timestampType==="bigint"?new Date(Number(s.applied_at)):new Date(s.applied_at);return {identifier:s.identifier,...t.columns.identifier==="name"?{name:s.identifier}:{hash:s.identifier},appliedAt:i}})}isMigrationApplied(e,t,r){return r.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async applyMigration(e,t,r){let a=await e.connect();try{await a.query("BEGIN"),await a.query(t.sql);let{identifier:o,timestamp:s,timestampType:i}=r.columns,c=o==="name"?t.name:t.hash,l=i==="bigint"?Date.now():new Date;await a.query(`INSERT INTO "${this.schemaName}"."${r.tableName}" ("${o}", "${s}") VALUES ($1, $2)`,[c,l]),await a.query("COMMIT");}catch(o){throw await a.query("ROLLBACK"),o}finally{a.release();}}async recordMigration(e,t,r){let{identifier:a,timestamp:o,timestampType:s}=r.columns,i=a==="name"?t.name:t.hash,c=s==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${this.schemaName}"."${r.tableName}" ("${a}", "${o}") VALUES ($1, $2)`,[i,c]);}};var Aa="__drizzle_migrations",cr="__drizzle_shared_migrations",Rt=class{constructor(e,t){this.migratorConfig=t;if(this.migrationsTable=t.migrationsTable??Aa,this.schemaManager=new Be(e,this.migrationsTable),this.driftDetector=new Ge(e,this.schemaManager,{migrationsTable:this.migrationsTable,tenantDiscovery:t.tenantDiscovery}),this.seeder=new Je({tenantDiscovery:t.tenantDiscovery},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,tenantSchema:e.schemas.tenant}),this.syncManager=new Ve({tenantDiscovery:t.tenantDiscovery,migrationsFolder:t.migrationsFolder,migrationsTable:this.migrationsTable},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectFormat.bind(this),loadMigrations:this.loadMigrations.bind(this)}),this.migrationExecutor=new Ye({hooks:t.hooks},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectFormat.bind(this),loadMigrations:this.loadMigrations.bind(this)}),this.batchExecutor=new Qe({tenantDiscovery:t.tenantDiscovery},this.migrationExecutor,this.loadMigrations.bind(this)),this.cloner=new Ze({migrationsTable:this.migrationsTable},{createPool:this.schemaManager.createPool.bind(this.schemaManager),createRootPool:this.schemaManager.createRootPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,schemaExists:this.schemaManager.schemaExists.bind(this.schemaManager),createSchema:this.schemaManager.createSchema.bind(this.schemaManager)}),t.sharedMigrationsFolder&&existsSync(t.sharedMigrationsFolder)){let r=t.sharedMigrationsTable??cr,a=t.sharedHooks,o={schemaName:"public",migrationsTable:r};(a?.beforeMigration||a?.afterApply)&&(o.hooks={},a.beforeMigration&&(o.hooks.beforeMigration=a.beforeMigration),a.afterApply&&(o.hooks.afterMigration=a.afterApply)),this.sharedMigrationExecutor=new Xe(o,{createPool:this.schemaManager.createRootPool.bind(this.schemaManager),migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectSharedFormat.bind(this),loadMigrations:this.loadSharedMigrations.bind(this)});}else this.sharedMigrationExecutor=null;e.schemas.shared?this.sharedSeeder=new Ke({schemaName:"public"},{createPool:this.schemaManager.createRootPool.bind(this.schemaManager),sharedSchema:e.schemas.shared}):this.sharedSeeder=null;}migrationsTable;schemaManager;driftDetector;seeder;syncManager;migrationExecutor;batchExecutor;cloner;sharedMigrationExecutor;sharedSeeder;async migrateAll(e={}){return this.batchExecutor.migrateAll(e)}async migrateTenant(e,t,r={}){return this.migrationExecutor.migrateTenant(e,t,r)}async migrateTenants(e,t={}){return this.batchExecutor.migrateTenants(e,t)}async getStatus(){return this.batchExecutor.getStatus()}async getTenantStatus(e,t){return this.migrationExecutor.getTenantStatus(e,t)}async createTenant(e,t={}){let{migrate:r=true}=t;await this.schemaManager.createSchema(e),r&&await this.migrateTenant(e);}async dropTenant(e,t={}){await this.schemaManager.dropSchema(e,t);}async tenantExists(e){return this.schemaManager.schemaExists(e)}async cloneTenant(e,t,r={}){return this.cloner.cloneTenant(e,t,r)}async markAsApplied(e,t={}){return this.migrationExecutor.markAsApplied(e,t)}async markAllAsApplied(e={}){return this.batchExecutor.markAllAsApplied(e)}async getSyncStatus(){return this.syncManager.getSyncStatus()}async getTenantSyncStatus(e,t){return this.syncManager.getTenantSyncStatus(e,t)}async markMissing(e){return this.syncManager.markMissing(e)}async markAllMissing(e={}){return this.syncManager.markAllMissing(e)}async cleanOrphans(e){return this.syncManager.cleanOrphans(e)}async cleanAllOrphans(e={}){return this.syncManager.cleanAllOrphans(e)}async seedTenant(e,t){return this.seeder.seedTenant(e,t)}async seedAll(e,t={}){return this.seeder.seedAll(e,t)}async seedTenants(e,t,r={}){return this.seeder.seedTenants(e,t,r)}hasSharedSeeding(){return this.sharedSeeder!==null}async seedShared(e){return this.sharedSeeder?this.sharedSeeder.seed(e):{schemaName:"public",success:false,error:"Shared schema not configured. Set schemas.shared in tenant config.",durationMs:0}}async seedAllWithShared(e,t,r={}){let a=await this.seedShared(e),o=await this.seedAll(t,r);return {shared:a,tenants:o}}async loadMigrations(){let e=await readdir(this.migratorConfig.migrationsFolder),t=[];for(let r of e){if(!r.endsWith(".sql"))continue;let a=join(this.migratorConfig.migrationsFolder,r),o=await readFile(a,"utf-8"),s=r.match(/^(\d+)_/),i=s?.[1]?parseInt(s[1],10):0,c=createHash("sha256").update(o).digest("hex");t.push({name:basename(r,".sql"),path:a,sql:o,timestamp:i,hash:c});}return t.sort((r,a)=>r.timestamp-a.timestamp)}async getOrDetectFormat(e,t){let r=this.migratorConfig.tableFormat??"auto";if(r!=="auto")return Ee(r,this.migrationsTable);let a=await Me(e,t,this.migrationsTable);if(a)return a;let o=this.migratorConfig.defaultFormat??"name";return Ee(o,this.migrationsTable)}async loadSharedMigrations(){if(!this.migratorConfig.sharedMigrationsFolder)return [];let e=await readdir(this.migratorConfig.sharedMigrationsFolder),t=[];for(let r of e){if(!r.endsWith(".sql"))continue;let a=join(this.migratorConfig.sharedMigrationsFolder,r),o=await readFile(a,"utf-8"),s=r.match(/^(\d+)_/),i=s?.[1]?parseInt(s[1],10):0,c=createHash("sha256").update(o).digest("hex");t.push({name:basename(r,".sql"),path:a,sql:o,timestamp:i,hash:c});}return t.sort((r,a)=>r.timestamp-a.timestamp)}async getOrDetectSharedFormat(e,t){let r=this.migratorConfig.sharedMigrationsTable??cr,a=this.migratorConfig.sharedTableFormat??this.migratorConfig.tableFormat??"auto";if(a!=="auto")return Ee(a,r);let o=await Me(e,t,r);if(o)return o;let s=this.migratorConfig.sharedDefaultFormat??this.migratorConfig.defaultFormat??"name";return Ee(s,r)}hasSharedMigrations(){return this.sharedMigrationExecutor!==null}async migrateShared(e={}){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.migrate(e):{schemaName:"public",success:false,appliedMigrations:[],error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",durationMs:0}}async getSharedStatus(){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.getStatus():{schemaName:"public",appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",format:null}}async markSharedAsApplied(e={}){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.markAsApplied(e):{schemaName:"public",success:false,appliedMigrations:[],error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",durationMs:0}}async migrateAllWithShared(e={}){let{sharedOptions:t,...r}=e,a=await this.migrateShared(t??{}),o=await this.migrateAll(r);return {shared:a,tenants:o}}async getSchemaDrift(e={}){return this.driftDetector.detectDrift(e)}async getTenantSchemaDrift(e,t,r={}){return this.driftDetector.compareTenant(e,t,r)}async introspectTenantSchema(e,t={}){return this.driftDetector.introspectSchema(e,t)}};function I(n,e){return new Rt(n,e)}function R(n){return st({text:n,color:"cyan"})}function mr(n){if(n===null)return
|
|
156
|
-
All tenants are up to date.`);let t=[
|
|
157
|
-
Pending migrations:`)];for(let[r,a]of e.entries())t.push(` ${
|
|
155
|
+
ORDER BY id`)).rows.map(s=>{let i=t.columns.timestampType==="bigint"?new Date(Number(s.applied_at)):new Date(s.applied_at);return {identifier:s.identifier,...t.columns.identifier==="name"?{name:s.identifier}:{hash:s.identifier},appliedAt:i}})}isMigrationApplied(e,t,r){return r.columns.identifier==="name"?t.has(e.name):t.has(e.hash)||t.has(e.name)}async applyMigration(e,t,r){let a=await e.connect();try{await a.query("BEGIN"),await a.query(t.sql);let{identifier:o,timestamp:s,timestampType:i}=r.columns,c=o==="name"?t.name:t.hash,l=i==="bigint"?Date.now():new Date;await a.query(`INSERT INTO "${this.schemaName}"."${r.tableName}" ("${o}", "${s}") VALUES ($1, $2)`,[c,l]),await a.query("COMMIT");}catch(o){throw await a.query("ROLLBACK"),o}finally{a.release();}}async recordMigration(e,t,r){let{identifier:a,timestamp:o,timestampType:s}=r.columns,i=a==="name"?t.name:t.hash,c=s==="bigint"?Date.now():new Date;await e.query(`INSERT INTO "${this.schemaName}"."${r.tableName}" ("${a}", "${o}") VALUES ($1, $2)`,[i,c]);}};var Aa="__drizzle_migrations",cr="__drizzle_shared_migrations",Rt=class{constructor(e,t){this.migratorConfig=t;if(this.migrationsTable=t.migrationsTable??Aa,this.schemaManager=new Be(e,this.migrationsTable),this.driftDetector=new Ge(e,this.schemaManager,{migrationsTable:this.migrationsTable,tenantDiscovery:t.tenantDiscovery}),this.seeder=new Je({tenantDiscovery:t.tenantDiscovery},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,tenantSchema:e.schemas.tenant}),this.syncManager=new Ve({tenantDiscovery:t.tenantDiscovery,migrationsFolder:t.migrationsFolder,migrationsTable:this.migrationsTable},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectFormat.bind(this),loadMigrations:this.loadMigrations.bind(this)}),this.migrationExecutor=new Ye({hooks:t.hooks},{createPool:this.schemaManager.createPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectFormat.bind(this),loadMigrations:this.loadMigrations.bind(this)}),this.batchExecutor=new Qe({tenantDiscovery:t.tenantDiscovery},this.migrationExecutor,this.loadMigrations.bind(this)),this.cloner=new Ze({migrationsTable:this.migrationsTable},{createPool:this.schemaManager.createPool.bind(this.schemaManager),createRootPool:this.schemaManager.createRootPool.bind(this.schemaManager),schemaNameTemplate:e.isolation.schemaNameTemplate,schemaExists:this.schemaManager.schemaExists.bind(this.schemaManager),createSchema:this.schemaManager.createSchema.bind(this.schemaManager)}),t.sharedMigrationsFolder&&existsSync(t.sharedMigrationsFolder)){let r=t.sharedMigrationsTable??cr,a=t.sharedHooks,o={schemaName:"public",migrationsTable:r};(a?.beforeMigration||a?.afterApply)&&(o.hooks={},a.beforeMigration&&(o.hooks.beforeMigration=a.beforeMigration),a.afterApply&&(o.hooks.afterMigration=a.afterApply)),this.sharedMigrationExecutor=new Xe(o,{createPool:this.schemaManager.createRootPool.bind(this.schemaManager),migrationsTableExists:this.schemaManager.migrationsTableExists.bind(this.schemaManager),ensureMigrationsTable:this.schemaManager.ensureMigrationsTable.bind(this.schemaManager),getOrDetectFormat:this.getOrDetectSharedFormat.bind(this),loadMigrations:this.loadSharedMigrations.bind(this)});}else this.sharedMigrationExecutor=null;e.schemas.shared?this.sharedSeeder=new Ke({schemaName:"public"},{createPool:this.schemaManager.createRootPool.bind(this.schemaManager),sharedSchema:e.schemas.shared}):this.sharedSeeder=null;}migrationsTable;schemaManager;driftDetector;seeder;syncManager;migrationExecutor;batchExecutor;cloner;sharedMigrationExecutor;sharedSeeder;async migrateAll(e={}){return this.batchExecutor.migrateAll(e)}async migrateTenant(e,t,r={}){return this.migrationExecutor.migrateTenant(e,t,r)}async migrateTenants(e,t={}){return this.batchExecutor.migrateTenants(e,t)}async getStatus(){return this.batchExecutor.getStatus()}async getTenantStatus(e,t){return this.migrationExecutor.getTenantStatus(e,t)}async createTenant(e,t={}){let{migrate:r=true}=t;await this.schemaManager.createSchema(e),r&&await this.migrateTenant(e);}async dropTenant(e,t={}){await this.schemaManager.dropSchema(e,t);}async tenantExists(e){return this.schemaManager.schemaExists(e)}async cloneTenant(e,t,r={}){return this.cloner.cloneTenant(e,t,r)}async markAsApplied(e,t={}){return this.migrationExecutor.markAsApplied(e,t)}async markAllAsApplied(e={}){return this.batchExecutor.markAllAsApplied(e)}async getSyncStatus(){return this.syncManager.getSyncStatus()}async getTenantSyncStatus(e,t){return this.syncManager.getTenantSyncStatus(e,t)}async markMissing(e){return this.syncManager.markMissing(e)}async markAllMissing(e={}){return this.syncManager.markAllMissing(e)}async cleanOrphans(e){return this.syncManager.cleanOrphans(e)}async cleanAllOrphans(e={}){return this.syncManager.cleanAllOrphans(e)}async seedTenant(e,t){return this.seeder.seedTenant(e,t)}async seedAll(e,t={}){return this.seeder.seedAll(e,t)}async seedTenants(e,t,r={}){return this.seeder.seedTenants(e,t,r)}hasSharedSeeding(){return this.sharedSeeder!==null}async seedShared(e){return this.sharedSeeder?this.sharedSeeder.seed(e):{schemaName:"public",success:false,error:"Shared schema not configured. Set schemas.shared in tenant config.",durationMs:0}}async seedAllWithShared(e,t,r={}){let a=await this.seedShared(e),o=await this.seedAll(t,r);return {shared:a,tenants:o}}async loadMigrations(){let e=await readdir(this.migratorConfig.migrationsFolder),t=[];for(let r of e){if(!r.endsWith(".sql"))continue;let a=join(this.migratorConfig.migrationsFolder,r),o=await readFile(a,"utf-8"),s=r.match(/^(\d+)_/),i=s?.[1]?parseInt(s[1],10):0,c=createHash("sha256").update(o).digest("hex");t.push({name:basename(r,".sql"),path:a,sql:o,timestamp:i,hash:c});}return t.sort((r,a)=>r.timestamp-a.timestamp)}async getOrDetectFormat(e,t){let r=this.migratorConfig.tableFormat??"auto";if(r!=="auto")return Ee(r,this.migrationsTable);let a=await Me(e,t,this.migrationsTable);if(a)return a;let o=this.migratorConfig.defaultFormat??"name";return Ee(o,this.migrationsTable)}async loadSharedMigrations(){if(!this.migratorConfig.sharedMigrationsFolder)return [];let e=await readdir(this.migratorConfig.sharedMigrationsFolder),t=[];for(let r of e){if(!r.endsWith(".sql"))continue;let a=join(this.migratorConfig.sharedMigrationsFolder,r),o=await readFile(a,"utf-8"),s=r.match(/^(\d+)_/),i=s?.[1]?parseInt(s[1],10):0,c=createHash("sha256").update(o).digest("hex");t.push({name:basename(r,".sql"),path:a,sql:o,timestamp:i,hash:c});}return t.sort((r,a)=>r.timestamp-a.timestamp)}async getOrDetectSharedFormat(e,t){let r=this.migratorConfig.sharedMigrationsTable??cr,a=this.migratorConfig.sharedTableFormat??this.migratorConfig.tableFormat??"auto";if(a!=="auto")return Ee(a,r);let o=await Me(e,t,r);if(o)return o;let s=this.migratorConfig.sharedDefaultFormat??this.migratorConfig.defaultFormat??"name";return Ee(s,r)}hasSharedMigrations(){return this.sharedMigrationExecutor!==null}async migrateShared(e={}){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.migrate(e):{schemaName:"public",success:false,appliedMigrations:[],error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",durationMs:0}}async getSharedStatus(){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.getStatus():{schemaName:"public",appliedCount:0,pendingCount:0,pendingMigrations:[],status:"error",error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",format:null}}async markSharedAsApplied(e={}){return this.sharedMigrationExecutor?this.sharedMigrationExecutor.markAsApplied(e):{schemaName:"public",success:false,appliedMigrations:[],error:"Shared migrations not configured. Set sharedMigrationsFolder in migrator config.",durationMs:0}}async migrateAllWithShared(e={}){let{sharedOptions:t,...r}=e,a=await this.migrateShared(t??{}),o=await this.migrateAll(r);return {shared:a,tenants:o}}async getSchemaDrift(e={}){return this.driftDetector.detectDrift(e)}async getTenantSchemaDrift(e,t,r={}){return this.driftDetector.compareTenant(e,t,r)}async introspectTenantSchema(e,t={}){return this.driftDetector.introspectSchema(e,t)}};function I(n,e){return new Rt(n,e)}function R(n){return st({text:n,color:"cyan"})}function mr(n){if(n===null)return M.dim("(new)");switch(n){case "name":return M.blue("name");case "hash":return M.magenta("hash");case "drizzle-kit":return M.cyan("drizzle-kit")}}function dr(n){let e=new lr({head:[M.cyan("Tenant"),M.cyan("Schema"),M.cyan("Format"),M.cyan("Applied"),M.cyan("Pending"),M.cyan("Status")],style:{head:[],border:[]}});for(let t of n){let r=Oa(t.status),a=La(t.status);e.push([t.tenantId,M.dim(t.schemaName),mr(t.format),M.green(t.appliedCount.toString()),t.pendingCount>0?M.yellow(t.pendingCount.toString()):M.dim("0"),`${r} ${a}`]);}return e.toString()}function ur(n){let e=new lr({head:[M.cyan("Tenant"),M.cyan("Format"),M.cyan("Migrations"),M.cyan("Duration"),M.cyan("Status")],style:{head:[],border:[]}});for(let t of n){let r=t.success?M.green("\u2713"):M.red("\u2717"),a=t.success?M.green("OK"):M.red(t.error??"Failed");e.push([t.tenantId,mr(t.format??null),t.appliedMigrations.length.toString(),`${t.durationMs}ms`,`${r} ${a}`]);}return e.toString()}function pr(n){let e=new Map;for(let r of n)for(let a of r.pendingMigrations)e.set(a,(e.get(a)||0)+1);if(e.size===0)return M.green(`
|
|
156
|
+
All tenants are up to date.`);let t=[M.yellow(`
|
|
157
|
+
Pending migrations:`)];for(let[r,a]of e.entries())t.push(` ${M.dim("-")} ${r} ${M.dim(`(${a} tenant${a>1?"s":""})`)}`);return t.push(M.dim(`
|
|
158
158
|
Run 'drizzle-multitenant migrate --all' to apply pending migrations.`)),t.join(`
|
|
159
|
-
`)}function Oa(n){switch(n){case "ok":return
|
|
159
|
+
`)}function Oa(n){switch(n){case "ok":return M.green("\u2713");case "behind":return M.yellow("\u26A0");case "error":return M.red("\u2717")}}function La(n){switch(n){case "ok":return M.green("OK");case "behind":return M.yellow("Behind");case "error":return M.red("Error")}}var ja=["tenant.config.ts","tenant.config.js","tenant.config.mjs","drizzle-multitenant.config.ts","drizzle-multitenant.config.js","drizzle-multitenant.config.mjs"],qa=["drizzle.config.ts","drizzle.config.js","drizzle.config.mjs"];async function k(n){let e=process.cwd(),t;if(n){if(t=resolve(e,n),!existsSync(t))throw new Error(`Config file not found: ${t}`)}else for(let h of ja){let g=resolve(e,h);if(existsSync(g)){t=g;break}}if(!t)throw new Error("Config file not found. Create a tenant.config.ts or use --config flag.");extname(t)===".ts"&&await fr();let o=await import(pathToFileURL(t).href),s=o.default??o;if(!s.connection||!s.isolation||!s.schemas)throw new Error("Invalid config file. Expected an object with connection, isolation, and schemas properties.");let i=await Ua(),c,l=null;s.migrations?.sharedFolder?(c=s.migrations.sharedFolder,l="tenant.config.ts"):i?.config.out&&(c=i.config.out,l="drizzle.config.ts");let m=s.migrations?.sharedTable??i?.config.migrations?.table??"__drizzle_migrations",u=s.migrations?.sharedTableFormat??"auto",p=s.migrations?.sharedDefaultFormat;return {config:s,migrationsFolder:s.migrations?.tenantFolder??s.migrations?.folder,migrationsTable:s.migrations?.migrationsTable??s.migrations?.table,tenantDiscovery:s.migrations?.tenantDiscovery,sharedMigrationsFolder:c,sharedMigrationsTable:m,sharedTableFormat:u,sharedDefaultFormat:p,drizzleKitConfig:i?.config??null,drizzleKitConfigFile:i?.fileName??null,sharedConfigSource:l}}async function fr(){try{await import('tsx/esm');}catch{try{await import('ts-node/esm');}catch{throw new Error("TypeScript config requires tsx or ts-node. Install with: npm install -D tsx")}}}async function Ua(){let n=process.cwd();for(let e of qa){let t=resolve(n,e);if(existsSync(t)){extname(t)===".ts"&&await fr();try{let o=await import(pathToFileURL(t).href),s=o.default??o;return {config:{out:s.out,schema:s.schema,dialect:s.dialect,dbCredentials:s.dbCredentials,migrations:s.migrations},fileName:e}}catch(a){return console.warn(`Warning: Found ${e} but failed to load it:`,a instanceof Error?a.message:a),null}}}return null}function O(n){let e=process.cwd(),r=resolve(e,n??"./drizzle/tenant");if(!existsSync(r))throw new Error(`Migrations folder not found: ${r}`);return r}var re={isInteractive:process.stdout.isTTY??false,jsonMode:false,verbose:false,quiet:false,noColor:false};function yr(n){re={isInteractive:process.stdout.isTTY??false,jsonMode:n.json??false,verbose:n.verbose??false,quiet:n.quiet??false,noColor:n.noColor??!process.stdout.isTTY},re.noColor&&(M.level=0);}function L(){return re}function me(){return re.isInteractive&&!re.jsonMode&&!re.quiet}function Wa(){return !re.jsonMode&&!re.quiet}function Ha(){return re.verbose&&!re.jsonMode}function d(n){Wa()&&console.log(n);}function H(n){Ha()&&console.log(M.dim(`[debug] ${n}`));}function N(n){re.jsonMode&&console.log(JSON.stringify(n,null,2));}function E(n){return M.green("\u2713 ")+n}function v(n){return M.red("\u2717 ")+n}function C(n){return M.yellow("\u26A0 ")+n}function J(n){return M.blue("\u2139 ")+n}function f(n){return M.dim(n)}function T(n){return M.bold(n)}function G(n){return M.cyan(n)}function Y(n){return M.green(n)}function te(n){return M.red(n)}function Z(n){return M.yellow(n)}function ae(n){let{total:e}=n;if(!me())return {start:()=>{},update:()=>{},increment:()=>{},stop:()=>{}};let t=n.format||`${M.cyan("Migrating")} ${M.cyan("{bar}")} ${M.yellow("{percentage}%")} | {value}/{total} | {tenant} | ${f("{eta}s")}`,r=new Sr.SingleBar({format:t,barCompleteChar:"\u2588",barIncompleteChar:"\u2591",hideCursor:true,clearOnComplete:false,stopOnComplete:true,etaBuffer:10},Sr.Presets.shades_classic),a=0;return {start(){r.start(e,0,{tenant:"starting...",status:""});},update(o,s){a=o;let i=s?.status==="success"?Y("\u2713"):s?.status==="error"?te("\u2717"):"";r.update(o,{tenant:s?.tenant?`${i} ${s.tenant}`:""});},increment(o){a++;let s=o?.status==="success"?Y("\u2713"):o?.status==="error"?te("\u2717"):"";r.update(a,{tenant:o?.tenant?`${s} ${o.tenant}`:""});},stop(){r.stop();}}}var X=class extends Error{constructor(t,r,a,o){super(t);this.suggestion=r;this.example=a;this.docs=o;this.name="CLIError";}format(){let t=[v(this.message)];return this.suggestion&&(t.push(""),t.push(f(" Suggestion: ")+this.suggestion)),this.example&&(t.push(""),t.push(f(" Example:")),t.push(G(" "+this.example))),this.docs&&(t.push(""),t.push(f(" Docs: ")+this.docs)),t.join(`
|
|
160
160
|
`)}toJSON(){return {error:this.message,suggestion:this.suggestion,example:this.example,docs:this.docs}}},Q={configNotFound:n=>new X("Configuration file not found","Create a tenant.config.ts file or use --config <path>",`export default defineConfig({
|
|
161
161
|
connection: process.env.DATABASE_URL,
|
|
162
162
|
isolation: { type: 'schema', schemaNameTemplate: (id) => \`tenant_\${id}\` },
|
|
@@ -184,34 +184,34 @@ Examples:
|
|
|
184
184
|
$ drizzle-multitenant migrate --all --concurrency=5
|
|
185
185
|
$ drizzle-multitenant migrate --all --json
|
|
186
186
|
$ drizzle-multitenant migrate --all --mark-applied
|
|
187
|
-
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config),c=n.migrationsFolder?O(n.migrationsFolder):O(o);H(`Using migrations folder: ${c}`);let l,m;if(n.tenant)l=async()=>[n.tenant],m=[n.tenant];else if(n.tenants){let x=n.tenants.split(",").map(
|
|
188
|
-
`));let x=await i();if(x.length===0){d(
|
|
187
|
+
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config),c=n.migrationsFolder?O(n.migrationsFolder):O(o);H(`Using migrations folder: ${c}`);let l,m;if(n.tenant)l=async()=>[n.tenant],m=[n.tenant];else if(n.tenants){let x=n.tenants.split(",").map(y=>y.trim());l=async()=>x,m=x;}else if(n.all){if(!i)throw Q.noTenantDiscovery();l=i,r.text="Discovering tenants...",m=await i();}else if(r.stop(),me()&&i){d(J(`No tenants specified. Fetching available tenants...
|
|
188
|
+
`));let x=await i();if(x.length===0){d(C("No tenants found."));return}let y=await checkbox({message:"Select tenants to migrate:",choices:x.map(w=>({name:w,value:w})),pageSize:15});if(y.length===0){d(C("No tenants selected. Aborting."));return}l=async()=>y,m=y;}else throw Q.noTenantSpecified();if(m.length===0){r.stop(),d(C("No tenants found."));return}r.text=`Found ${m.length} tenant${m.length>1?"s":""}`,r.succeed(),n.dryRun&&d(J(T(`
|
|
189
189
|
Dry run mode - no changes will be made
|
|
190
190
|
`))),n.markApplied&&d(J(T(`
|
|
191
191
|
Mark-applied mode - migrations will be recorded without executing SQL
|
|
192
192
|
`)));let u=n.markApplied?"Marking":"Migrating";d(J(`${u} ${m.length} tenant${m.length>1?"s":""}...
|
|
193
|
-
`));let p=I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:l}),h=parseInt(n.concurrency||"10",10),g=ae({total:m.length});g.start();let
|
|
194
|
-
`+T("Results:")),d(ur(
|
|
195
|
-
`+T("Summary:")),d(` Total: ${
|
|
196
|
-
`+T("Failed tenants:"));for(let x of
|
|
197
|
-
`+f("Run with --verbose to see more details."));}
|
|
193
|
+
`));let p=I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:l}),h=parseInt(n.concurrency||"10",10),g=ae({total:m.length});g.start();let $={concurrency:h,onProgress:(x,y,w)=>{if(y==="completed")g.increment({tenant:x,status:"success"}),H(`Completed: ${x}`);else if(y==="failed")g.increment({tenant:x,status:"error"}),H(`Failed: ${x}`);else if(y==="migrating"&&w){let D=n.markApplied?"Marking":"Applying";H(`${x}: ${D} ${w}`);}},onError:(x,y)=>(H(`Error on ${x}: ${y.message}`),"continue")},S=n.markApplied?await p.markAllAsApplied($):await p.migrateAll({...$,dryRun:!!n.dryRun});g.stop();let b=Date.now()-e;if(t.jsonMode){let x={results:S.details.map(y=>({tenantId:y.tenantId,schema:y.schemaName,success:y.success,appliedMigrations:y.appliedMigrations,durationMs:y.durationMs,format:y.format,error:y.error})),summary:{total:S.total,succeeded:S.succeeded,failed:S.failed,skipped:S.skipped,durationMs:b,averageMs:S.total>0?Math.round(b/S.total):void 0}};N(x),process.exit(S.failed>0?1:0);}if(d(`
|
|
194
|
+
`+T("Results:")),d(ur(S.details)),d(`
|
|
195
|
+
`+T("Summary:")),d(` Total: ${S.total}`),d(` Succeeded: ${E(S.succeeded.toString())}`),S.failed>0&&d(` Failed: ${v(S.failed.toString())}`),S.skipped>0&&d(` Skipped: ${C(S.skipped.toString())}`),d(` Duration: ${f(br(b))}`),S.total>0&&d(` Average: ${f(br(Math.round(b/S.total))+"/tenant")}`),S.failed>0){d(`
|
|
196
|
+
`+T("Failed tenants:"));for(let x of S.details.filter(y=>!y.success))d(` ${v(x.tenantId)}: ${f(x.error||"Unknown error")}`);d(`
|
|
197
|
+
`+f("Run with --verbose to see more details."));}S.failed>0&&process.exit(1);}catch(a){r.fail(a.message),j(a);}});function br(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}var _t=new Command("migrate:shared").description("Apply pending migrations to the shared schema (public)").option("-c, --config <path>","Path to config file").option("--migrations-folder <path>","Path to shared migrations folder").option("--dry-run","Show what would be applied without executing").option("--mark-applied","Mark migrations as applied without executing SQL").addHelpText("after",`
|
|
198
198
|
Examples:
|
|
199
199
|
$ drizzle-multitenant migrate:shared
|
|
200
200
|
$ drizzle-multitenant migrate:shared --dry-run
|
|
201
201
|
$ drizzle-multitenant migrate:shared --mark-applied
|
|
202
202
|
$ drizzle-multitenant migrate:shared --migrations-folder=./drizzle/shared-migrations
|
|
203
203
|
$ drizzle-multitenant migrate:shared --json
|
|
204
|
-
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let a=await k(n.config),{config:o,sharedMigrationsFolder:s,migrationsTable:i,tenantDiscovery:c}=a,
|
|
205
|
-
No pending migrations.`)),t.jsonMode){let y={schemaName:
|
|
204
|
+
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let a=await k(n.config),{config:o,sharedMigrationsFolder:s,migrationsTable:i,tenantDiscovery:c,sharedMigrationsTable:l,sharedTableFormat:m,sharedDefaultFormat:u}=a,p=n.migrationsFolder?O(n.migrationsFolder):s?O(s):null;if(!p)throw r.fail("Shared migrations folder not configured"),Q.create("Shared migrations folder not configured","Set sharedMigrationsFolder in your config file or use --migrations-folder option","drizzle-multitenant migrate:shared --migrations-folder=./drizzle/shared-migrations");H(`Using shared migrations folder: ${p}`),r.text="Checking shared migrations...";let h=I(o,{migrationsFolder:a.migrationsFolder??"./drizzle/tenant-migrations",sharedMigrationsFolder:p,...i&&{migrationsTable:i},...l&&{sharedMigrationsTable:l},...m&&m!=="auto"&&{sharedTableFormat:m},...u&&{sharedDefaultFormat:u},tenantDiscovery:c??(async()=>[])});if(!h.hasSharedMigrations())throw r.fail("Shared migrations not available"),Q.create("Shared migrations folder does not exist or is empty",`Create the folder at: ${p}`,`mkdir -p ${p}`);let g=await h.getSharedStatus();if(g.status==="error")throw r.fail("Failed to get shared migration status"),new Error(g.error);if(g.pendingCount===0){if(r.succeed("Shared schema is up to date"),d(J(`
|
|
205
|
+
No pending migrations.`)),t.jsonMode){let y={schemaName:g.schemaName,success:!0,appliedMigrations:[],durationMs:Date.now()-e,format:g.format??void 0};N(y);}return}r.succeed(`Found ${g.pendingCount} pending migration${g.pendingCount>1?"s":""}`),n.dryRun&&d(J(T(`
|
|
206
206
|
Dry run mode - no changes will be made
|
|
207
207
|
`))),n.markApplied&&d(J(T(`
|
|
208
208
|
Mark-applied mode - migrations will be recorded without executing SQL
|
|
209
|
-
`)));let
|
|
210
|
-
${
|
|
211
|
-
`));for(let y of
|
|
212
|
-
`+T("Applied migrations:"));for(let y of
|
|
213
|
-
${f(`Duration: ${Ka(
|
|
214
|
-
${v("Error:")} ${
|
|
209
|
+
`)));let $=n.markApplied?"Marking":"Applying";d(J(`
|
|
210
|
+
${$} ${g.pendingCount} migration${g.pendingCount>1?"s":""} to shared schema...
|
|
211
|
+
`));for(let y of g.pendingMigrations)d(f(` - ${y}`));d("");let S=R(`${$} migrations...`);S.start();let b=n.markApplied?await h.markSharedAsApplied({onProgress:(y,w)=>{y==="migrating"&&w&&(S.text=`Marking: ${w}`);}}):await h.migrateShared({dryRun:n.dryRun,onProgress:(y,w)=>{y==="migrating"&&w&&(S.text=`Applying: ${w}`);}}),x=Date.now()-e;if(b.success?S.succeed(`${n.dryRun?"Would apply":"Applied"} ${b.appliedMigrations.length} migration${b.appliedMigrations.length>1?"s":""}`):S.fail("Migration failed"),t.jsonMode){let y={schemaName:b.schemaName,success:b.success,appliedMigrations:b.appliedMigrations,durationMs:x,format:b.format,error:b.error};N(y),process.exit(b.success?0:1);}if(b.success){d(`
|
|
212
|
+
`+T("Applied migrations:"));for(let y of b.appliedMigrations)d(` ${E("\u2713")} ${y}`);d(`
|
|
213
|
+
${f(`Duration: ${Ka(x)}`)}`);}else d(`
|
|
214
|
+
${v("Error:")} ${b.error}`),process.exit(1);}catch(a){r.fail(a.message),j(a);}});function Ka(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}var Pt=new Command("status").description("Show migration status for all tenants").option("-c, --config <path>","Path to config file").option("--migrations-folder <path>","Path to migrations folder").addHelpText("after",`
|
|
215
215
|
Examples:
|
|
216
216
|
$ drizzle-multitenant status
|
|
217
217
|
$ drizzle-multitenant status --json
|
|
@@ -228,18 +228,18 @@ Examples:
|
|
|
228
228
|
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config);if(!i)throw Q.noTenantDiscovery();let c=n.migrationsFolder?O(n.migrationsFolder):O(o);H(`Using migrations folder: ${c}`);let l=I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:i});if(n.status||!n.markMissing&&!n.cleanOrphans){r.text="Fetching sync status...";let p=await l.getSyncStatus();if(r.succeed(`Found ${p.total} tenant${p.total>1?"s":""}`),t.jsonMode){let h={tenants:p.details.map(g=>({id:g.tenantId,schema:g.schemaName,format:g.format,inSync:g.inSync,missing:g.missing,orphans:g.orphans,error:g.error})),summary:{total:p.total,inSync:p.inSync,outOfSync:p.outOfSync,error:p.error}};N(h);return}d(`
|
|
229
229
|
`+T("Sync Status:")),d(Qa(p.details)),d(Za(p));return}let u=parseInt(n.concurrency||"10",10);if(n.markMissing){r.text="Marking missing migrations...",r.succeed();let p=await i();d(J(`
|
|
230
230
|
Marking missing migrations for ${p.length} tenant${p.length>1?"s":""}...
|
|
231
|
-
`));let h=ae({total:p.length});h.start();let g=await l.markAllMissing({concurrency:u,onProgress:(
|
|
231
|
+
`));let h=ae({total:p.length});h.start();let g=await l.markAllMissing({concurrency:u,onProgress:(S,b)=>{b==="completed"?h.increment({tenant:S,status:"success"}):b==="failed"&&h.increment({tenant:S,status:"error"});},onError:(S,b)=>(H(`Error on ${S}: ${b.message}`),"continue")});h.stop();let $=Date.now()-e;t.jsonMode&&(N({action:"mark-missing",results:g.details.map(S=>({tenantId:S.tenantId,schema:S.schemaName,success:S.success,markedMigrations:S.markedMigrations,durationMs:S.durationMs,error:S.error})),summary:{total:g.total,succeeded:g.succeeded,failed:g.failed,durationMs:$}}),process.exit(g.failed>0?1:0)),d(`
|
|
232
232
|
`+T("Results:")),d(xr(g.details,"mark-missing")),d(`
|
|
233
|
-
`+T("Summary:")),d(` Total: ${g.total}`),d(` Succeeded: ${E(g.succeeded.toString())}`),g.failed>0&&d(` Failed: ${v(g.failed.toString())}`),d(` Duration: ${f(Tr(
|
|
233
|
+
`+T("Summary:")),d(` Total: ${g.total}`),d(` Succeeded: ${E(g.succeeded.toString())}`),g.failed>0&&d(` Failed: ${v(g.failed.toString())}`),d(` Duration: ${f(Tr($))}`);}if(n.cleanOrphans){n.markMissing&&d(`
|
|
234
234
|
`),r.text="Cleaning orphan records...",r.succeed();let p=await i();d(J(`
|
|
235
235
|
Cleaning orphan records for ${p.length} tenant${p.length>1?"s":""}...
|
|
236
|
-
`));let h=ae({total:p.length});h.start();let g=await l.cleanAllOrphans({concurrency:u,onProgress:(
|
|
236
|
+
`));let h=ae({total:p.length});h.start();let g=await l.cleanAllOrphans({concurrency:u,onProgress:(S,b)=>{b==="completed"?h.increment({tenant:S,status:"success"}):b==="failed"&&h.increment({tenant:S,status:"error"});},onError:(S,b)=>(H(`Error on ${S}: ${b.message}`),"continue")});h.stop();let $=Date.now()-e;t.jsonMode&&(N({action:"clean-orphans",results:g.details.map(S=>({tenantId:S.tenantId,schema:S.schemaName,success:S.success,removedOrphans:S.removedOrphans,durationMs:S.durationMs,error:S.error})),summary:{total:g.total,succeeded:g.succeeded,failed:g.failed,durationMs:$}}),process.exit(g.failed>0?1:0)),d(`
|
|
237
237
|
`+T("Results:")),d(xr(g.details,"clean-orphans")),d(`
|
|
238
|
-
`+T("Summary:")),d(` Total: ${g.total}`),d(` Succeeded: ${E(g.succeeded.toString())}`),g.failed>0&&d(` Failed: ${v(g.failed.toString())}`),d(` Duration: ${f(Tr(
|
|
239
|
-
`;let e=[];for(let t of n)if(t.error)e.push(` ${v(t.tenantId)}: ${f(t.error)}`);else if(t.inSync)e.push(` ${E(t.tenantId)}: ${f("In sync")}`);else {let r=[];t.missing.length>0&&r.push(`${t.missing.length} missing`),t.orphans.length>0&&r.push(`${t.orphans.length} orphan${t.orphans.length>1?"s":""}`),e.push(` ${
|
|
238
|
+
`+T("Summary:")),d(` Total: ${g.total}`),d(` Succeeded: ${E(g.succeeded.toString())}`),g.failed>0&&d(` Failed: ${v(g.failed.toString())}`),d(` Duration: ${f(Tr($))}`);}}catch(a){r.fail(a.message),j(a);}});function Tr(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}function Qa(n){if(n.length===0)return ` No tenants found.
|
|
239
|
+
`;let e=[];for(let t of n)if(t.error)e.push(` ${v(t.tenantId)}: ${f(t.error)}`);else if(t.inSync)e.push(` ${E(t.tenantId)}: ${f("In sync")}`);else {let r=[];t.missing.length>0&&r.push(`${t.missing.length} missing`),t.orphans.length>0&&r.push(`${t.orphans.length} orphan${t.orphans.length>1?"s":""}`),e.push(` ${C(t.tenantId)}: ${r.join(", ")}`),t.missing.length>0&&e.push(` ${f("Missing:")} ${t.missing.slice(0,3).join(", ")}${t.missing.length>3?`, +${t.missing.length-3} more`:""}`),t.orphans.length>0&&e.push(` ${f("Orphans:")} ${t.orphans.slice(0,3).join(", ")}${t.orphans.length>3?`, +${t.orphans.length-3} more`:""}`);}return e.join(`
|
|
240
240
|
`)+`
|
|
241
241
|
`}function Za(n){let e=[];return e.push(`
|
|
242
|
-
`+T("Summary:")),e.push(` Total: ${n.total}`),e.push(` In Sync: ${E(n.inSync.toString())}`),n.outOfSync>0&&e.push(` Out of Sync: ${
|
|
242
|
+
`+T("Summary:")),e.push(` Total: ${n.total}`),e.push(` In Sync: ${E(n.inSync.toString())}`),n.outOfSync>0&&e.push(` Out of Sync: ${C(n.outOfSync.toString())}`),n.error>0&&e.push(` Errors: ${v(n.error.toString())}`),n.outOfSync>0&&(e.push(`
|
|
243
243
|
`+f("Run with --mark-missing to mark missing migrations as applied.")),e.push(f("Run with --clean-orphans to remove orphan records."))),e.join(`
|
|
244
244
|
`)}function xr(n,e){if(n.length===0)return ` No tenants processed.
|
|
245
245
|
`;let t=[];for(let r of n)r.error?t.push(` ${v(r.tenantId)}: ${f(r.error)}`):e==="mark-missing"?r.markedMigrations.length>0?t.push(` ${E(r.tenantId)}: Marked ${r.markedMigrations.length} migration${r.markedMigrations.length>1?"s":""}`):t.push(` ${f(r.tenantId)}: Nothing to mark`):r.removedOrphans.length>0?t.push(` ${E(r.tenantId)}: Removed ${r.removedOrphans.length} orphan${r.removedOrphans.length>1?"s":""}`):t.push(` ${f(r.tenantId)}: No orphans found`);return t.join(`
|
|
@@ -253,18 +253,18 @@ Examples:
|
|
|
253
253
|
$ drizzle-multitenant diff --no-indexes --no-constraints
|
|
254
254
|
$ drizzle-multitenant diff --exclude-tables=logs,audit
|
|
255
255
|
$ drizzle-multitenant diff --json
|
|
256
|
-
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config);if(!i)throw Q.noTenantDiscovery();H(`Using config from: ${n.config||"default location"}`);let c=I(a,{migrationsFolder:o??"./drizzle",...s&&{migrationsTable:s},tenantDiscovery:i}),l=parseInt(n.concurrency||"10",10),m=n.indexes!==!1,u=n.constraints!==!1,p=n.excludeTables?n.excludeTables.split(",").map(D=>D.trim()):[],h;n.tenant?h=[n.tenant]:n.tenants&&(h=n.tenants.split(",").map(D=>D.trim())),r.text="Detecting schema drift...";let g=await i()
|
|
257
|
-
Using "${
|
|
258
|
-
`));let b=ae({total
|
|
259
|
-
`+T("Schema Drift Status:")),d(to(x.details,x.referenceTenant)),d(no(x,
|
|
260
|
-
`+T("Drift Details:"));for(let D of
|
|
261
|
-
${
|
|
262
|
-
`;let t=[];for(let r of n){let o=r.tenantId===e?f("(ref) "):"";r.error?t.push(` ${v(r.tenantId)}: ${o}${f(r.error)}`):r.hasDrift?t.push(` ${
|
|
256
|
+
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config);if(!i)throw Q.noTenantDiscovery();H(`Using config from: ${n.config||"default location"}`);let c=I(a,{migrationsFolder:o??"./drizzle",...s&&{migrationsTable:s},tenantDiscovery:i}),l=parseInt(n.concurrency||"10",10),m=n.indexes!==!1,u=n.constraints!==!1,p=n.excludeTables?n.excludeTables.split(",").map(D=>D.trim()):[],h;n.tenant?h=[n.tenant]:n.tenants&&(h=n.tenants.split(",").map(D=>D.trim())),r.text="Detecting schema drift...";let g=await i(),$=h??g;if($.length===0){r.fail("No tenants found");return}r.succeed(`Found ${g.length} tenant${g.length>1?"s":""}`);let S=n.reference??$[0];d(J(`
|
|
257
|
+
Using "${S}" as reference tenant
|
|
258
|
+
`));let b=ae({total:$.length});b.start();let x=await c.getSchemaDrift({referenceTenant:S,tenantIds:$,concurrency:l,includeIndexes:m,includeConstraints:u,excludeTables:p,onProgress:(D,W)=>{W==="completed"?b.increment({tenant:D,status:"success"}):W==="failed"&&b.increment({tenant:D,status:"error"});}});b.stop();let y=Date.now()-e;if(t.jsonMode){let D={referenceTenant:x.referenceTenant,tenants:x.details.map(W=>({id:W.tenantId,schema:W.schemaName,hasDrift:W.hasDrift,issueCount:W.issueCount,tables:W.tables.map(z=>({name:z.table,status:z.status,columns:z.columns,indexes:z.indexes,constraints:z.constraints})),error:W.error})),summary:{total:x.total,noDrift:x.noDrift,withDrift:x.withDrift,error:x.error,durationMs:y}};N(D),process.exit(x.withDrift>0?1:0);}d(`
|
|
259
|
+
`+T("Schema Drift Status:")),d(to(x.details,x.referenceTenant)),d(no(x,y));let w=x.details.filter(D=>D.hasDrift);if(w.length>0){d(`
|
|
260
|
+
`+T("Drift Details:"));for(let D of w){d(`
|
|
261
|
+
${C(D.tenantId)} (${D.schemaName}):`);for(let W of D.tables)if(W.status==="missing")d(` ${v("\u2717")} Table "${W.table}" is missing`);else if(W.status==="extra")d(` ${C("+")} Table "${W.table}" is extra (not in reference)`);else if(W.status==="drifted"){d(` ${C("~")} Table "${W.table}":`);for(let z of W.columns){let B=z.type==="missing"?v("\u2717"):z.type==="extra"?C("+"):C("~");d(` ${B} ${z.description}`);}for(let z of W.indexes){let B=z.type==="missing"?v("\u2717"):z.type==="extra"?C("+"):C("~");d(` ${B} ${z.description}`);}for(let z of W.constraints){let B=z.type==="missing"?v("\u2717"):z.type==="extra"?C("+"):C("~");d(` ${B} ${z.description}`);}}}}process.exit(x.withDrift>0?1:0);}catch(a){r.fail(a.message),j(a);}});function eo(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}function to(n,e){if(n.length===0)return ` No tenants found.
|
|
262
|
+
`;let t=[];for(let r of n){let o=r.tenantId===e?f("(ref) "):"";r.error?t.push(` ${v(r.tenantId)}: ${o}${f(r.error)}`):r.hasDrift?t.push(` ${C(r.tenantId)}: ${o}${r.issueCount} issue${r.issueCount>1?"s":""} detected`):t.push(` ${E(r.tenantId)}: ${o}${f("No drift")}`);}return t.join(`
|
|
263
263
|
`)+`
|
|
264
264
|
`}function no(n,e){let t=[];return t.push(`
|
|
265
|
-
`+T("Summary:")),t.push(` Total: ${n.total}`),t.push(` No Drift: ${E(n.noDrift.toString())}`),n.withDrift>0&&t.push(` With Drift: ${
|
|
265
|
+
`+T("Summary:")),t.push(` Total: ${n.total}`),t.push(` No Drift: ${E(n.noDrift.toString())}`),n.withDrift>0&&t.push(` With Drift: ${C(n.withDrift.toString())}`),n.error>0&&t.push(` Errors: ${v(n.error.toString())}`),t.push(` Duration: ${f(eo(e))}`),n.withDrift>0&&t.push(`
|
|
266
266
|
`+f("Run migrations to fix drift: drizzle-multitenant migrate --all")),t.join(`
|
|
267
|
-
`)}var It=new Command("generate").description("Generate a new migration file").requiredOption("-n, --name <name>","Migration name").option("-c, --config <path>","Path to config file").option("--type <type>","Migration type: tenant or shared","tenant").option("--migrations-folder <path>","Path to migrations folder").action(async n=>{let e=R("Loading configuration...");try{e.start();let{migrationsFolder:t}=await k(n.config),r=process.cwd(),a;n.migrationsFolder?a=resolve(r,n.migrationsFolder):t?a=resolve(r,t):a=resolve(r,n.type==="shared"?"./drizzle/shared":"./drizzle/tenant"),existsSync(a)||(await mkdir(a,{recursive:!0}),e.text=`Created migrations folder: ${a}`),e.text="Generating migration...";let s=(existsSync(a)?await readdir(a):[]).filter(h=>h.endsWith(".sql")),i=0;for(let h of s){let g=h.match(/^(\d+)_/);if(g?.[1]){let
|
|
267
|
+
`)}var It=new Command("generate").description("Generate a new migration file").requiredOption("-n, --name <name>","Migration name").option("-c, --config <path>","Path to config file").option("--type <type>","Migration type: tenant or shared","tenant").option("--migrations-folder <path>","Path to migrations folder").action(async n=>{let e=R("Loading configuration...");try{e.start();let{migrationsFolder:t}=await k(n.config),r=process.cwd(),a;n.migrationsFolder?a=resolve(r,n.migrationsFolder):t?a=resolve(r,t):a=resolve(r,n.type==="shared"?"./drizzle/shared":"./drizzle/tenant"),existsSync(a)||(await mkdir(a,{recursive:!0}),e.text=`Created migrations folder: ${a}`),e.text="Generating migration...";let s=(existsSync(a)?await readdir(a):[]).filter(h=>h.endsWith(".sql")),i=0;for(let h of s){let g=h.match(/^(\d+)_/);if(g?.[1]){let $=parseInt(g[1],10);$>i&&(i=$);}}let c=(i+1).toString().padStart(4,"0"),l=n.name.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,""),m=`${c}_${l}.sql`,u=join(a,m),p=`-- Migration: ${n.name}
|
|
268
268
|
-- Created at: ${new Date().toISOString()}
|
|
269
269
|
-- Type: ${n.type}
|
|
270
270
|
|
|
@@ -279,7 +279,7 @@ Examples:
|
|
|
279
279
|
$ drizzle-multitenant generate:shared --name add-permissions --migrations-folder=./drizzle/shared
|
|
280
280
|
|
|
281
281
|
Alias for: drizzle-multitenant generate --name <name> --type shared
|
|
282
|
-
`).action(async n=>{let e=R("Loading configuration...");try{e.start();let{sharedMigrationsFolder:t}=await k(n.config),r=process.cwd(),a;n.migrationsFolder?a=resolve(r,n.migrationsFolder):t?a=resolve(r,t):a=resolve(r,"./drizzle/shared-migrations"),existsSync(a)||(await mkdir(a,{recursive:!0}),e.text=`Created shared migrations folder: ${a}`),e.text="Generating shared migration...";let s=(existsSync(a)?await readdir(a):[]).filter(h=>h.endsWith(".sql")),i=0;for(let h of s){let g=h.match(/^(\d+)_/);if(g?.[1]){let
|
|
282
|
+
`).action(async n=>{let e=R("Loading configuration...");try{e.start();let{sharedMigrationsFolder:t}=await k(n.config),r=process.cwd(),a;n.migrationsFolder?a=resolve(r,n.migrationsFolder):t?a=resolve(r,t):a=resolve(r,"./drizzle/shared-migrations"),existsSync(a)||(await mkdir(a,{recursive:!0}),e.text=`Created shared migrations folder: ${a}`),e.text="Generating shared migration...";let s=(existsSync(a)?await readdir(a):[]).filter(h=>h.endsWith(".sql")),i=0;for(let h of s){let g=h.match(/^(\d+)_/);if(g?.[1]){let $=parseInt(g[1],10);$>i&&(i=$);}}let c=(i+1).toString().padStart(4,"0"),l=n.name.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,""),m=`${c}_${l}.sql`,u=join(a,m),p=`-- Migration: ${n.name}
|
|
283
283
|
-- Created at: ${new Date().toISOString()}
|
|
284
284
|
-- Type: shared (public schema)
|
|
285
285
|
|
|
@@ -289,12 +289,12 @@ Alias for: drizzle-multitenant generate --name <name> --type shared
|
|
|
289
289
|
`;await writeFile(u,p,"utf-8"),e.succeed("Shared migration generated"),console.log(`
|
|
290
290
|
`+E(`Created: ${f(u)}`)),console.log(f(`
|
|
291
291
|
Edit this file to add your shared schema migration SQL.`)),console.log(f("Run `drizzle-multitenant migrate:shared` to apply."));}catch(t){e.fail(t.message),process.exit(1);}});var Ot=new Command("tenant:create").description("Create a new tenant schema and apply all migrations").requiredOption("--id <tenantId>","Tenant ID").option("-c, --config <path>","Path to config file").option("--migrations-folder <path>","Path to migrations folder").option("--no-migrate","Skip applying migrations after creating schema").action(async n=>{let e=R("Loading configuration...");try{e.start();let{config:t,migrationsFolder:r,migrationsTable:a}=await k(n.config),o=n.migrationsFolder?O(n.migrationsFolder):O(r),s=I(t,{migrationsFolder:o,...a&&{migrationsTable:a},tenantDiscovery:async()=>[]}),i=t.isolation.schemaNameTemplate(n.id);if(e.text=`Checking if tenant ${n.id} exists...`,await s.tenantExists(n.id)){if(e.warn(`Tenant ${n.id} already exists (${i})`),n.migrate){e.start(),e.text="Applying pending migrations...";let l=await s.migrateTenant(n.id);if(l.appliedMigrations.length>0){e.succeed(`Applied ${l.appliedMigrations.length} migration(s)`);for(let m of l.appliedMigrations)console.log(` ${f("-")} ${m}`);}else e.succeed("No pending migrations");}return}e.text=`Creating tenant schema ${i}...`,await s.createTenant(n.id,{migrate:n.migrate}),e.succeed(`Tenant ${n.id} created`),console.log(`
|
|
292
|
-
`+E("Schema created: ")+f(i)),n.migrate?console.log(E("All migrations applied")):console.log(
|
|
292
|
+
`+E("Schema created: ")+f(i)),n.migrate?console.log(E("All migrations applied")):console.log(C("Migrations skipped. Run migrate to apply.")),console.log(f(`
|
|
293
293
|
You can now use this tenant:`)),console.log(f(` const db = tenants.getDb('${n.id}');`));}catch(t){e.fail(t.message),process.exit(1);}});var Lt=new Command("tenant:drop").description("Drop a tenant schema (DESTRUCTIVE)").requiredOption("--id <tenantId>","Tenant ID").option("-c, --config <path>","Path to config file").option("--migrations-folder <path>","Path to migrations folder").option("-f, --force","Skip confirmation prompt").option("--no-cascade","Use RESTRICT instead of CASCADE").action(async n=>{let e=R("Loading configuration...");try{e.start();let{config:t,migrationsFolder:r,migrationsTable:a}=await k(n.config),o=n.migrationsFolder?O(n.migrationsFolder):O(r),s=I(t,{migrationsFolder:o,...a&&{migrationsTable:a},tenantDiscovery:async()=>[]}),i=t.isolation.schemaNameTemplate(n.id);if(e.text=`Checking if tenant ${n.id} exists...`,!await s.tenantExists(n.id)){e.warn(`Tenant ${n.id} does not exist`);return}if(e.stop(),!n.force&&(console.log(te(T(`
|
|
294
294
|
\u26A0\uFE0F WARNING: This action is DESTRUCTIVE and IRREVERSIBLE!`))),console.log(f(`
|
|
295
295
|
You are about to drop schema: ${i}`)),console.log(f(`All tables and data in this schema will be permanently deleted.
|
|
296
296
|
`)),!await yo(`Type "${n.id}" to confirm deletion: `,n.id))){console.log(`
|
|
297
|
-
|
|
297
|
+
`+C("Operation cancelled."));return}e.start(),e.text=`Dropping tenant schema ${i}...`,await s.dropTenant(n.id,{cascade:n.cascade}),e.succeed(`Tenant ${n.id} dropped`),console.log(`
|
|
298
298
|
`+E("Schema deleted: ")+f(i));}catch(t){e.fail(t.message),process.exit(1);}});async function yo(n,e){let t=createInterface({input:process.stdin,output:process.stdout});return new Promise(r=>{t.question(n,a=>{t.close(),r(a.trim()===e);});})}var jt=new Command("tenant:clone").description("Clone a tenant schema to a new tenant").requiredOption("--from <tenantId>","Source tenant ID").requiredOption("--to <tenantId>","Target tenant ID").option("-c, --config <path>","Path to config file").option("--migrations-folder <path>","Path to migrations folder").option("--include-data","Include data in clone (default: schema only)").option("--anonymize","Anonymize sensitive data (requires --include-data)").option("--anonymize-config <path>","Path to anonymization config file").option("-f, --force","Skip confirmation prompt").option("--json","Output as JSON").addHelpText("after",`
|
|
299
299
|
Examples:
|
|
300
300
|
# Clone schema only
|
|
@@ -308,13 +308,13 @@ Examples:
|
|
|
308
308
|
|
|
309
309
|
# Clone with custom anonymization rules
|
|
310
310
|
$ drizzle-multitenant tenant:clone --from=prod --to=dev --include-data --anonymize --anonymize-config=./anonymize.config.js
|
|
311
|
-
`).action(async n=>{let e=R("Loading configuration...");try{e.start();let{config:t,migrationsFolder:r,migrationsTable:a}=await k(n.config),o=n.migrationsFolder?O(n.migrationsFolder):O(r),s=I(t,{migrationsFolder:o,...a&&{migrationsTable:a},tenantDiscovery:async()=>[]}),i=t.isolation.schemaNameTemplate(n.from),c=t.isolation.schemaNameTemplate(n.to);if(e.text=`Checking source tenant ${n.from}...`,await s.tenantExists(n.from)||(e.fail(`Source tenant ${n.from} does not exist`),process.exit(1)),await s.tenantExists(n.to)&&(e.fail(`Target tenant ${n.to} already exists`),process.exit(1)),e.stop(),!n.force&&(console.log(""),console.log(T("Clone Configuration:")),console.log(f(` Source: ${n.from} (${i})`)),console.log(f(` Target: ${n.to} (${c})`)),n.includeData?(console.log(
|
|
311
|
+
`).action(async n=>{let e=R("Loading configuration...");try{e.start();let{config:t,migrationsFolder:r,migrationsTable:a}=await k(n.config),o=n.migrationsFolder?O(n.migrationsFolder):O(r),s=I(t,{migrationsFolder:o,...a&&{migrationsTable:a},tenantDiscovery:async()=>[]}),i=t.isolation.schemaNameTemplate(n.from),c=t.isolation.schemaNameTemplate(n.to);if(e.text=`Checking source tenant ${n.from}...`,await s.tenantExists(n.from)||(e.fail(`Source tenant ${n.from} does not exist`),process.exit(1)),await s.tenantExists(n.to)&&(e.fail(`Target tenant ${n.to} already exists`),process.exit(1)),e.stop(),!n.force&&(console.log(""),console.log(T("Clone Configuration:")),console.log(f(` Source: ${n.from} (${i})`)),console.log(f(` Target: ${n.to} (${c})`)),n.includeData?(console.log(C(`
|
|
312
312
|
This will copy all data from source tenant.`)),n.anonymize&&console.log(f(" Sensitive data will be anonymized."))):console.log(f(`
|
|
313
313
|
Schema only (no data will be copied).`)),!await To(`
|
|
314
314
|
Type "${n.to}" to confirm: `,n.to))){console.log(`
|
|
315
|
-
|
|
315
|
+
`+C("Operation cancelled."));return}let u;if(n.anonymize&&n.anonymizeConfig)try{let g=await import(await import('path').then($=>$.resolve(process.cwd(),n.anonymizeConfig)));u=g.default||g;}catch(h){e.fail(`Failed to load anonymization config: ${h.message}`),process.exit(1);}e.start(),e.text=`Cloning ${n.from} to ${n.to}...`;let p=await s.cloneTenant(n.from,n.to,{includeData:n.includeData,anonymize:n.anonymize?{enabled:!0,rules:u}:void 0,onProgress:(h,g)=>{switch(h){case "introspecting":e.text="Introspecting source schema...";break;case "creating_schema":e.text="Creating target schema...";break;case "creating_tables":e.text="Creating tables...";break;case "creating_indexes":e.text="Creating indexes...";break;case "creating_constraints":e.text="Creating constraints...";break;case "copying_data":g?.table?e.text=`Copying data: ${g.table} (${g.progress}/${g.total})...`:e.text="Copying data...";break}}});if(n.json){e.stop(),console.log(JSON.stringify(p,null,2));return}p.success||(e.fail(p.error??"Clone failed"),process.exit(1)),e.succeed(`Tenant ${n.to} cloned successfully`),console.log(""),console.log(E("Source: ")+f(`${n.from} (${i})`)),console.log(E("Target: ")+f(`${n.to} (${c})`)),console.log(E("Tables: ")+f(p.tables.length.toString())),n.includeData&&p.rowsCopied!==void 0&&(console.log(E("Rows copied: ")+f(p.rowsCopied.toLocaleString())),n.anonymize&&console.log(E("Data anonymized: ")+f("Yes"))),console.log(f(`
|
|
316
316
|
Duration: ${p.durationMs}ms`)),console.log(f(`
|
|
317
|
-
Usage:`)),console.log(f(` const db = tenants.getDb('${n.to}');`));}catch(t){e.fail(t.message),process.exit(1);}});async function To(n,e){let t=createInterface({input:process.stdin,output:process.stdout});return new Promise(r=>{t.question(n,a=>{t.close(),r(a.trim()===e);});})}async function Do(n){let e=await readdir(n),t=[];for(let r of e){if(!r.endsWith(".sql"))continue;let a=join(n,r),o=await readFile(a,"utf-8"),s=createHash("sha256").update(o).digest("hex");t.push({name:basename(r,".sql"),hash:s});}return t}async function vo(n,e,t,r,a,o,s){let i=new Map(r.map(m=>[m.hash,m.name])),c=new Map(r.map(m=>[m.name,m.hash])),l=await n.connect();try{let m=a.columns.identifier,u=await l.query(`SELECT id, "${m}" as identifier FROM "${e}"."${t}" ORDER BY id`);if(s){let p=[];for(let h of u.rows)if(o==="name"&&a.columns.identifier==="hash"){let g=i.get(h.identifier);g?p.push(` ${f(h.identifier.slice(0,8))}... -> ${g}`):p.push(` ${
|
|
317
|
+
Usage:`)),console.log(f(` const db = tenants.getDb('${n.to}');`));}catch(t){e.fail(t.message),process.exit(1);}});async function To(n,e){let t=createInterface({input:process.stdin,output:process.stdout});return new Promise(r=>{t.question(n,a=>{t.close(),r(a.trim()===e);});})}async function Do(n){let e=await readdir(n),t=[];for(let r of e){if(!r.endsWith(".sql"))continue;let a=join(n,r),o=await readFile(a,"utf-8"),s=createHash("sha256").update(o).digest("hex");t.push({name:basename(r,".sql"),hash:s});}return t}async function vo(n,e,t,r,a,o,s){let i=new Map(r.map(m=>[m.hash,m.name])),c=new Map(r.map(m=>[m.name,m.hash])),l=await n.connect();try{let m=a.columns.identifier,u=await l.query(`SELECT id, "${m}" as identifier FROM "${e}"."${t}" ORDER BY id`);if(s){let p=[];for(let h of u.rows)if(o==="name"&&a.columns.identifier==="hash"){let g=i.get(h.identifier);g?p.push(` ${f(h.identifier.slice(0,8))}... -> ${g}`):p.push(` ${C(h.identifier.slice(0,8))}... -> ${v("unknown")}`);}else if(o!=="name"&&a.columns.identifier==="name"){let g=c.get(h.identifier);g?p.push(` ${h.identifier} -> ${f(g.slice(0,16))}...`):p.push(` ${h.identifier} -> ${v("unknown")}`);}return {success:!0,message:p.length>0?p.join(`
|
|
318
318
|
`):" No conversions needed"}}await l.query("BEGIN");try{if(o==="name"&&a.columns.identifier==="hash"){await l.query(`
|
|
319
319
|
ALTER TABLE "${e}"."${t}"
|
|
320
320
|
ADD COLUMN IF NOT EXISTS name VARCHAR(255)
|
|
@@ -352,11 +352,11 @@ Usage:`)),console.log(f(` const db = tenants.getDb('${n.to}');`));}catch(t){e.f
|
|
|
352
352
|
ALTER TABLE "${e}"."${t}"
|
|
353
353
|
ALTER COLUMN created_at TYPE BIGINT
|
|
354
354
|
USING (EXTRACT(EPOCH FROM created_at) * 1000)::BIGINT
|
|
355
|
-
`)),await l.query("COMMIT"),{success:!0,message:`Converted ${p} records to ${o} format`}}return await l.query("COMMIT"),{success:!0,message:"No conversion needed"}}catch(p){throw await l.query("ROLLBACK"),p}}finally{l.release();}}var qt=new Command("convert-format").description("Convert migration table format between name/hash/drizzle-kit").requiredOption("--to <format>","Target format: name, hash, or drizzle-kit").option("-c, --config <path>","Path to config file").option("-t, --tenant <id>","Convert a specific tenant only").option("--dry-run","Preview changes without applying").option("--migrations-folder <path>","Path to migrations folder").action(async n=>{let e=R("Loading configuration...");try{let t=n.to;if(!["name","hash","drizzle-kit"].includes(t))throw new Error(`Invalid format: ${n.to}. Use: name, hash, or drizzle-kit`);e.start();let{config:r,migrationsFolder:a,migrationsTable:o,tenantDiscovery:s}=await k(n.config),i=o??"__drizzle_migrations",c=n.migrationsFolder?O(n.migrationsFolder):O(a);e.text="Loading migration files...";let l=await Do(c);e.text="Discovering tenants...";let m;if(n.tenant)m=[n.tenant];else {if(!s)throw new Error("No tenant discovery function configured. Add migrations.tenantDiscovery to your config.");m=await s();}e.succeed(`Found ${m.length} tenant${m.length>1?"s":""}`),n.dryRun&&console.log(
|
|
355
|
+
`)),await l.query("COMMIT"),{success:!0,message:`Converted ${p} records to ${o} format`}}return await l.query("COMMIT"),{success:!0,message:"No conversion needed"}}catch(p){throw await l.query("ROLLBACK"),p}}finally{l.release();}}var qt=new Command("convert-format").description("Convert migration table format between name/hash/drizzle-kit").requiredOption("--to <format>","Target format: name, hash, or drizzle-kit").option("-c, --config <path>","Path to config file").option("-t, --tenant <id>","Convert a specific tenant only").option("--dry-run","Preview changes without applying").option("--migrations-folder <path>","Path to migrations folder").action(async n=>{let e=R("Loading configuration...");try{let t=n.to;if(!["name","hash","drizzle-kit"].includes(t))throw new Error(`Invalid format: ${n.to}. Use: name, hash, or drizzle-kit`);e.start();let{config:r,migrationsFolder:a,migrationsTable:o,tenantDiscovery:s}=await k(n.config),i=o??"__drizzle_migrations",c=n.migrationsFolder?O(n.migrationsFolder):O(a);e.text="Loading migration files...";let l=await Do(c);e.text="Discovering tenants...";let m;if(n.tenant)m=[n.tenant];else {if(!s)throw new Error("No tenant discovery function configured. Add migrations.tenantDiscovery to your config.");m=await s();}e.succeed(`Found ${m.length} tenant${m.length>1?"s":""}`),n.dryRun&&console.log(C(T(`
|
|
356
356
|
Dry run mode - no changes will be made
|
|
357
357
|
`))),console.log(T(`
|
|
358
358
|
Converting to ${t} format:
|
|
359
|
-
`));let u=0,p=0,h=0;for(let g of m){let
|
|
359
|
+
`));let u=0,p=0,h=0;for(let g of m){let $=r.isolation.schemaNameTemplate(g),S=new Pool({connectionString:r.connection.url,...r.connection.poolConfig});try{let b=await Me(S,$,i);if(!b){console.log(`${f(g)}: ${f("No migrations table found, skipping")}`),p++;continue}if(b.format===t){console.log(`${f(g)}: ${f(`Already using ${t} format`)}`),p++;continue}console.log(`${T(g)}: ${b.format} -> ${t}`);let x=await vo(S,$,i,l,b,t,n.dryRun);x.success?(n.dryRun?console.log(x.message):console.log(` ${E(x.message)}`),u++):(console.log(` ${v(x.message)}`),h++);}catch(b){console.log(` ${v(b.message)}`),h++;}finally{await S.end();}}console.log(T(`
|
|
360
360
|
Summary:`)),console.log(` Converted: ${E(u.toString())}`),console.log(` Skipped: ${f(p.toString())}`),h>0&&(console.log(` Failed: ${v(h.toString())}`),process.exit(1));}catch(t){e.fail(t.message),process.exit(1);}});var ce={minimal:{name:"Minimal (config only)",description:"Just the configuration file - you set up everything else"},standard:{name:"Standard (config + folder structure)",description:"Configuration with organized folder structure for migrations and seeds"},full:{name:"Full (config + folders + example schemas)",description:"Complete setup with example tenant and shared schemas"},enterprise:{name:"Enterprise (full + CI/CD + Docker)",description:"Production-ready setup with Docker, CI/CD, and monitoring"}},oe={none:{name:"None (standalone)",description:"No framework integration - use directly with Drizzle"},express:{name:"Express",description:"Middleware for Express.js applications"},fastify:{name:"Fastify",description:"Plugin for Fastify applications"},nestjs:{name:"NestJS",description:"Full module with decorators and guards"},hono:{name:"Hono",description:"Middleware for Hono applications"}},Cr={sharedSchema:{name:"Shared schema support",description:"Support for shared tables (plans, roles, etc.)",default:true},crossSchemaQueries:{name:"Cross-schema queries",description:"Type-safe queries joining tenant and shared schemas",default:true},healthChecks:{name:"Health check endpoints",description:"Pool health monitoring and metrics",default:true},metrics:{name:"Metrics (Prometheus)",description:"Prometheus-compatible metrics endpoint",default:false},debug:{name:"Debug mode",description:"Structured logging for development",default:false}},he={manual:{name:"I'll configure manually",description:"Set up DATABASE_URL yourself"},"docker-compose":{name:"Generate docker-compose.yml",description:"Create a Docker Compose file for PostgreSQL"},"existing-url":{name:"Use existing DATABASE_URL",description:"Use your current database connection"}};function Mr(n){let e=n.useTypeScript?_o(n):Po(n);return {path:n.useTypeScript?"tenant.config.ts":"tenant.config.js",content:e}}function _o(n){let e=zo(n),t=No(n),r=Er(n);return `${e}
|
|
361
361
|
${t}
|
|
362
362
|
|
|
@@ -1459,9 +1459,9 @@ Examples:
|
|
|
1459
1459
|
$ drizzle-multitenant init --template=full
|
|
1460
1460
|
$ drizzle-multitenant init --template=enterprise --framework=nestjs
|
|
1461
1461
|
$ drizzle-multitenant init --force --no-typescript
|
|
1462
|
-
`).action(async n=>{try{!me()&&!n.template&&(d(
|
|
1462
|
+
`).action(async n=>{try{!me()&&!n.template&&(d(C("Interactive mode required. Please run in a terminal or use --template flag.")),process.exit(1)),d(T(`
|
|
1463
1463
|
\u{1F680} drizzle-multitenant Setup Wizard
|
|
1464
|
-
`));let e=os();if(e&&!n.force&&(d(
|
|
1464
|
+
`));let e=os();if(e&&!n.force&&(d(C(`Configuration file already exists: ${e}`)),!await confirm({message:"Do you want to overwrite it?",default:!1}))){d(J("Setup cancelled."));return}let t=await ss(n),r=await us(t);await ps(r,t),hs(t);}catch(e){if(e.name==="ExitPromptError"){d(J(`
|
|
1465
1465
|
Setup cancelled.`));return}j(e);}});function os(){return ["tenant.config.ts","tenant.config.js","drizzle-multitenant.config.ts","drizzle-multitenant.config.js"].find(e=>existsSync(join(process.cwd(),e)))||null}async function ss(n){let e=n.template||await is(),t=n.framework||await cs(),r=await ls(e),a=await ms(),o=await ds(),s=await input({message:"Environment variable for database connection:",default:"DATABASE_URL"}),i;a==="existing-url"&&(i=await input({message:"Enter your database URL:",default:process.env.DATABASE_URL||"postgresql://postgres:postgres@localhost:5432/myapp"}));let c=await input({message:"Tenant migrations folder path:",default:"./drizzle/tenant-migrations"}),l="./drizzle/shared-migrations";r.sharedSchema&&(l=await input({message:"Shared migrations folder path:",default:"./drizzle/shared-migrations"}));let m="tenant_${id}";o==="schema"&&(m=await input({message:"Schema name template (use ${id} for tenant ID):",default:"tenant_${id}"}));let u=n.typescript!==void 0?n.typescript:await confirm({message:"Use TypeScript for configuration?",default:true});return {template:e,framework:t,features:r,databaseSetup:a,isolationType:o,dbEnvVar:s,databaseUrl:i,migrationsFolder:c,sharedMigrationsFolder:l,schemaTemplate:m,useTypeScript:u}}async function is(){return await select({message:"Project template:",choices:[{name:ce.minimal.name,value:"minimal",description:ce.minimal.description},{name:ce.standard.name,value:"standard",description:ce.standard.description},{name:ce.full.name,value:"full",description:ce.full.description},{name:ce.enterprise.name,value:"enterprise",description:ce.enterprise.description}]})}async function cs(){return await select({message:"Framework integration:",choices:[{name:oe.none.name,value:"none",description:oe.none.description},{name:oe.express.name,value:"express",description:oe.express.description},{name:oe.fastify.name,value:"fastify",description:oe.fastify.description},{name:oe.nestjs.name,value:"nestjs",description:oe.nestjs.description},{name:oe.hono.name,value:"hono",description:oe.hono.description}]})}async function ls(n){if(n==="minimal")return {sharedSchema:false,crossSchemaQueries:false,healthChecks:false,metrics:false,debug:false};let e=Object.entries(Cr).map(([r,a])=>({name:`${a.name}`,value:r,checked:a.default})),t=await checkbox({message:"Features to include:",choices:e});return {sharedSchema:t.includes("sharedSchema"),crossSchemaQueries:t.includes("crossSchemaQueries"),healthChecks:t.includes("healthChecks"),metrics:t.includes("metrics"),debug:t.includes("debug")}}async function ms(){return await select({message:"Database setup:",choices:[{name:he.manual.name,value:"manual",description:he.manual.description},{name:he["docker-compose"].name,value:"docker-compose",description:he["docker-compose"].description},{name:he["existing-url"].name,value:"existing-url",description:he["existing-url"].description}]})}async function ds(){return await select({message:"Which isolation strategy do you want to use?",choices:[{name:"Schema-based isolation (recommended)",value:"schema",description:"Each tenant has its own PostgreSQL schema"},{name:"Row-level security (RLS)",value:"rls",description:"Shared tables with tenant_id column and RLS policies"}]})}async function us(n){let e={folders:[],files:[],dependencies:[],devDependencies:[]},t=Mr(n);e.files.push(t);let r=Dr(n);if(e.folders.push(...r.folders),e.files.push(...r.files),n.databaseSetup==="docker-compose"&&(e.files.push(Pr()),e.files.push(zr())),n.framework!=="none"){let a=kr(n);e.files.push(...a);let o=Ut(n.framework);e.dependencies.push(...o.dependencies),e.devDependencies.push(...o.devDependencies),(n.framework==="express"||n.framework==="fastify"||n.framework==="hono")&&e.folders.push("src/middleware"),n.framework==="fastify"&&e.folders.push("src/plugins"),n.framework==="nestjs"&&(e.folders.push("src/tenant"),e.folders.push("src/users"),e.folders.push("src/health"));}return n.template!=="minimal"&&e.folders.push("src/db"),e}async function ps(n,e){let t=process.cwd(),r=[...new Set(n.folders)].sort();for(let o of r){let s=join(t,o);existsSync(s)||(mkdirSync(s,{recursive:true}),d(E(`Created folder: ${o}`)));}let a=0;for(let o of n.files){let s=join(t,o.path),i=join(t,o.path.split("/").slice(0,-1).join("/"));if(i&&!existsSync(i)&&mkdirSync(i,{recursive:true}),basename(o.path)===".gitkeep"){existsSync(s)||writeFileSync(s,o.content);continue}writeFileSync(s,o.content),a++,d(E(`Created: ${o.path}`));}d(f(`
|
|
1466
1466
|
${a} files created
|
|
1467
1467
|
`));}function hs(n){d(T(`\u2728 Setup complete!
|
|
@@ -1475,7 +1475,7 @@ Examples:
|
|
|
1475
1475
|
After adding the completion script, restart your shell or run:
|
|
1476
1476
|
$ source ~/.bashrc # for bash
|
|
1477
1477
|
$ source ~/.zshrc # for zsh
|
|
1478
|
-
`).action(n=>{switch(n.toLowerCase()){case "bash":console.log(fs());break;case "zsh":console.log(ys());break;case "fish":console.log(Ss());break;default:d(
|
|
1478
|
+
`).action(n=>{switch(n.toLowerCase()){case "bash":console.log(fs());break;case "zsh":console.log(ys());break;case "fish":console.log(Ss());break;default:d(C(`Unknown shell: ${n}`)),d(J("Supported shells: bash, zsh, fish")),process.exit(1);}});function fs(){return `# drizzle-multitenant bash completion
|
|
1479
1479
|
# Add this to ~/.bashrc or ~/.bash_completion
|
|
1480
1480
|
|
|
1481
1481
|
_drizzle_multitenant() {
|
|
@@ -1701,62 +1701,62 @@ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from convert-format"
|
|
|
1701
1701
|
|
|
1702
1702
|
# completion options
|
|
1703
1703
|
complete -c drizzle-multitenant -n "__fish_seen_subcommand_from completion" -a "bash zsh fish" -d "Shell type"
|
|
1704
|
-
`}function Fr(){let e="Drizzle Multitenant CLI",t="Multi-tenancy toolkit for Drizzle",r=Math.floor((43-e.length)/2),a=Math.floor((43-t.length)/2);console.log(""),console.log(
|
|
1704
|
+
`}function Fr(){let e="Drizzle Multitenant CLI",t="Multi-tenancy toolkit for Drizzle",r=Math.floor((43-e.length)/2),a=Math.floor((43-t.length)/2);console.log(""),console.log(M.cyan.bold(" \u2554"+"\u2550".repeat(43)+"\u2557")),console.log(M.cyan.bold(" \u2551")+M.white.bold(" ".repeat(r)+e+" ".repeat(43-r-e.length))+M.cyan.bold("\u2551")),console.log(M.cyan.bold(" \u2551")+M.dim(" ".repeat(a)+t+" ".repeat(43-a-t.length))+M.cyan.bold("\u2551")),console.log(M.cyan.bold(" \u255A"+"\u2550".repeat(43)+"\u255D")),console.log("");}var V=class{clearScreen(){console.clear();}showHeader(e){console.log(""),console.log(M.cyan.bold(` ${e}`)),console.log(M.dim(" "+"\u2500".repeat(e.length+4))),console.log("");}showStatus(e,t="info"){let r={success:M.green("\u2713"),warning:M.yellow("\u26A0"),error:M.red("\u2717"),info:M.blue("\u2139")},a={success:M.green,warning:M.yellow,error:M.red,info:M.blue};console.log(`
|
|
1705
1705
|
${r[t]} ${a[t](e)}
|
|
1706
|
-
`);}formatTenantStatus(e){switch(e){case "ok":return
|
|
1707
|
-
`));let r=await checkbox({message:"Select tenants to migrate:",choices:t.map(o=>({name:`${o.tenantId} ${
|
|
1708
|
-
`));let a=await t.migrateAll({concurrency:10,onProgress:(s,i,c)=>{(i==="completed"||i==="failed"||i==="migrating")&&this.renderer.showProgress(s,i,c);},onError:(s,i)=>(this.renderer.showError(i.message),"continue")}),o=Date.now()-r;this.renderer.showResults(a,o),console.log(""),await this.renderer.pressEnterToContinue();}};var fe=class{constructor(e,t){this.ctx=e;this.renderer=t||new V;}renderer;async showCreate(){this.renderer.clearScreen(),this.renderer.showHeader("Create Tenant");let e=await input({message:"Tenant ID:",validate:s=>s.trim()?/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(s)?true:"Invalid tenant ID format (use alphanumeric, dashes, underscores)":"Tenant ID cannot be empty"}),t=this.ctx.config.isolation.schemaNameTemplate(e);console.log(
|
|
1709
|
-
Schema name: ${t}`));let r=await confirm({message:"Apply all migrations after creation?",default:true});if(!await confirm({message:`Create tenant "${e}"?`,default:true}))return {type:"back"};let o=st("Creating tenant...").start();try{let s=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>[]});if(await s.tenantExists(e))return o.warn(`Tenant ${e} already exists`),await this.renderer.pressEnterToContinue(),{type:"back"};await s.createTenant(e,{migrate:r}),o.succeed(`Tenant ${e} created successfully`),console.log(""),console.log(
|
|
1710
|
-
Usage:`)),console.log(
|
|
1711
|
-
${s.message}`));}return console.log(""),await this.renderer.pressEnterToContinue(),{type:"refresh"}}async showDrop(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Drop Tenant"),e.length===0)return this.renderer.showStatus("No tenants found","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=e.map(c=>({name:`${c.tenantId} ${
|
|
1712
|
-
Schema ${a.schemaName} has been removed.`));}catch(c){i.fail("Failed to drop tenant"),console.log(
|
|
1713
|
-
${c.message}`));}return console.log(""),await this.renderer.pressEnterToContinue(),{type:"refresh"}}async showClone(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Clone Tenant"),e.length===0)return this.renderer.showStatus("No tenants found to clone from","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=e.map(u=>({name:`${u.tenantId} ${
|
|
1714
|
-
Target schema: ${s}`));let i=await confirm({message:"Include data in clone?",default:false}),c=false;if(i&&(c=await confirm({message:"Anonymize sensitive data?",default:false})),console.log(""),console.log(
|
|
1715
|
-
Duration: ${p.durationMs}ms`)),console.log(
|
|
1716
|
-
Usage:`)),console.log(
|
|
1717
|
-
${p.error}`)));}catch(u){m.fail("Clone failed"),console.log(
|
|
1718
|
-
${u.message}`));}return console.log(""),await this.renderer.pressEnterToContinue(),{type:"refresh"}}};var ve=class{constructor(e,t){this.ctx=e;this.renderer=t||new V;}renderer;async show(e){this.renderer.clearScreen(),this.renderer.showHeader("Seeding");let t=!!this.ctx.config.schemas?.shared,r=[{name:"Seed tenants",value:"tenants"},...t?[{name:"Seed shared schema",value:"shared"}]:[],...t?[{name:"Seed all (shared + tenants)",value:"all"}]:[],{name:
|
|
1719
|
-
Expected format:`)),console.log(
|
|
1720
|
-
File not found: ${e}`)):console.log(
|
|
1721
|
-
${a.message}`)),await this.renderer.pressEnterToContinue(),null}}async loadSharedSeedFile(e){let t=st("Loading shared seed file...").start();try{let r=resolve(process.cwd(),e),o=await import(pathToFileURL(r).href),s=o.seed||o.default;return typeof s!="function"?(t.fail('Seed file must export a "seed" function or default export'),console.log(
|
|
1722
|
-
Expected format:`)),console.log(
|
|
1723
|
-
File not found: ${e}`)):console.log(
|
|
1724
|
-
${a.message}`)),await this.renderer.pressEnterToContinue(),null}}async runSeeding(e,t){this.renderer.clearScreen(),this.renderer.showHeader("Running Seed");let r=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>e}),a=Date.now();console.log(
|
|
1725
|
-
`));let o=await r.seedAll(t,{concurrency:10,onProgress:(i,c)=>{c==="completed"?console.log(
|
|
1726
|
-
Error: ${o.error}`)),console.log(""),await this.renderer.pressEnterToContinue();}async runAllSeeding(e,t,r){this.renderer.clearScreen(),this.renderer.showHeader("Running Full Seed");let a=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>r}),o=Date.now();console.log(
|
|
1706
|
+
`);}formatTenantStatus(e){switch(e){case "ok":return M.green("\u25CF up to date");case "behind":return M.yellow("\u25CF pending");case "error":return M.red("\u25CF error")}}formatPendingCount(e){return e===0?M.dim("0"):M.yellow(`${e} pending`)}formatDuration(e){if(e<1e3)return `${e}ms`;if(e<6e4)return `${(e/1e3).toFixed(1)}s`;let t=Math.floor(e/6e4),r=Math.round(e%6e4/1e3);return `${t}m ${r}s`}createStatusTable(e){let t=new lr({head:[M.cyan("Tenant"),M.cyan("Schema"),M.cyan("Applied"),M.cyan("Pending"),M.cyan("Status")],style:{head:[],border:[]}});for(let r of e)t.push([r.tenantId,M.dim(r.schemaName),M.green(r.appliedCount.toString()),this.formatPendingCount(r.pendingCount),this.formatTenantStatus(r.status)]);return t}getStatusSummary(e){let t=e.filter(s=>s.status==="ok").length,r=e.filter(s=>s.status==="behind").length,a=e.filter(s=>s.status==="error").length,o=e.reduce((s,i)=>s+i.pendingCount,0);return {upToDate:t,behind:r,error:a,totalPending:o}}showSummary(e){let t=this.getStatusSummary(e);console.log(""),console.log(M.bold(" Summary:")),console.log(` Total tenants: ${e.length}`),console.log(` Up to date: ${M.green(t.upToDate.toString())}`),t.behind>0&&console.log(` Behind: ${M.yellow(t.behind.toString())}`),t.error>0&&console.log(` Errors: ${M.red(t.error.toString())}`);}showPendingMigrations(e){let t=new Map;for(let r of e)for(let a of r.pendingMigrations)t.set(a,(t.get(a)||0)+1);if(t.size>0){console.log(""),console.log(M.yellow(" Pending migrations:"));for(let[r,a]of t.entries())console.log(` ${M.dim("-")} ${r} ${M.dim(`(${a} tenant${a>1?"s":""})`)}`);}}showResults(e,t){if(console.log(""),console.log(M.bold(" Results:")),console.log(` Succeeded: ${M.green(e.succeeded.toString())}`),e.failed>0&&console.log(` Failed: ${M.red(e.failed.toString())}`),console.log(` Duration: ${M.dim(this.formatDuration(t))}`),e.failed>0){console.log(""),console.log(M.red(" Failed tenants:"));for(let r of e.details.filter(a=>!a.success))console.log(` ${M.red("\u2717")} ${r.tenantId}: ${M.dim(r.error||"Unknown error")}`);}}async pressEnterToContinue(){await select({message:"Press Enter to continue...",choices:[{name:"Continue",value:"continue"}]});}showProgress(e,t,r){t==="completed"?console.log(M.green(` \u2713 ${e}`)):t==="failed"?console.log(M.red(` \u2717 ${e}`)):t==="migrating"&&r&&console.log(M.dim(` Applying: ${r}`));}showError(e){console.log(M.red(` Error: ${e}`));}};var De=class{renderer;constructor(e,t){this.renderer=t||new V;}async show(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Migration Status"),e.length===0)return this.renderer.showStatus("No tenants found","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=this.renderer.createStatusTable(e);console.log(t.toString()),this.renderer.showSummary(e),this.renderer.showPendingMigrations(e),console.log("");let r=this.renderer.getStatusSummary(e),a=await select({message:"What would you like to do?",choices:[{name:"Migrate all pending",value:"migrate",disabled:r.totalPending===0},{name:"View tenant details",value:"details"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]});return a==="migrate"?{type:"navigate",screen:"migrate",params:{statuses:e}}:a==="details"?this.showTenantSelection(e):{type:"back"}}async showTenantSelection(e){this.renderer.clearScreen(),this.renderer.showHeader("Select Tenant");let t=e.map(o=>({name:`${this.renderer.formatTenantStatus(o.status).split(" ")[0]} ${o.tenantId} ${M.dim(`(${o.schemaName})`)}`,value:o.tenantId}));t.push({name:M.gray("\u2190 Back"),value:"back"});let r=await select({message:"Select a tenant to view details:",choices:t});if(r==="back")return this.show(e);let a=e.find(o=>o.tenantId===r);return a?this.showTenantDetails(a,e):this.show(e)}async showTenantDetails(e,t){if(this.renderer.clearScreen(),this.renderer.showHeader(`Tenant: ${e.tenantId}`),console.log(M.bold(" Details:")),console.log(` Schema: ${e.schemaName}`),console.log(` Format: ${e.format||M.dim("(new)")}`),console.log(` Applied: ${M.green(e.appliedCount.toString())}`),console.log(` Pending: ${this.renderer.formatPendingCount(e.pendingCount)}`),console.log(` Status: ${this.renderer.formatTenantStatus(e.status)}`),e.pendingMigrations.length>0){console.log(""),console.log(M.yellow(" Pending migrations:"));for(let a of e.pendingMigrations)console.log(` ${M.dim("-")} ${a}`);}return e.error&&(console.log(""),console.log(M.red(` Error: ${e.error}`))),console.log(""),await select({message:"What would you like to do?",choices:[{name:"Migrate this tenant",value:"migrate",disabled:e.pendingCount===0},{name:M.gray("\u2190 Back to list"),value:"back"}]})==="migrate"?{type:"navigate",screen:"migrate-single",params:{tenantId:e.tenantId}}:this.showTenantSelection(t)}};var ge=class{constructor(e,t){this.ctx=e;this.renderer=t||new V;}renderer;async show(e){this.renderer.clearScreen(),this.renderer.showHeader("Migrate Tenants");let t=e.filter(o=>o.pendingCount>0);if(t.length===0)return this.renderer.showStatus("All tenants are up to date","success"),await this.renderer.pressEnterToContinue(),{type:"back"};console.log(M.dim(` Found ${t.length} tenant(s) with pending migrations
|
|
1707
|
+
`));let r=await checkbox({message:"Select tenants to migrate:",choices:t.map(o=>({name:`${o.tenantId} ${M.yellow(`(${o.pendingCount} pending)`)}`,value:o.tenantId,checked:true})),pageSize:15});return r.length===0?(this.renderer.showStatus("No tenants selected","warning"),await this.renderer.pressEnterToContinue(),{type:"back"}):await confirm({message:`Migrate ${r.length} tenant(s)?`,default:true})?(await this.runMigration(r),{type:"refresh"}):{type:"back"}}async migrateSingle(e){return await this.runMigration([e]),{type:"refresh"}}async runMigration(e){this.renderer.clearScreen(),this.renderer.showHeader("Running Migrations");let t=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>e}),r=Date.now();console.log(M.dim(` Migrating ${e.length} tenant(s)...
|
|
1708
|
+
`));let a=await t.migrateAll({concurrency:10,onProgress:(s,i,c)=>{(i==="completed"||i==="failed"||i==="migrating")&&this.renderer.showProgress(s,i,c);},onError:(s,i)=>(this.renderer.showError(i.message),"continue")}),o=Date.now()-r;this.renderer.showResults(a,o),console.log(""),await this.renderer.pressEnterToContinue();}};var fe=class{constructor(e,t){this.ctx=e;this.renderer=t||new V;}renderer;async showCreate(){this.renderer.clearScreen(),this.renderer.showHeader("Create Tenant");let e=await input({message:"Tenant ID:",validate:s=>s.trim()?/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(s)?true:"Invalid tenant ID format (use alphanumeric, dashes, underscores)":"Tenant ID cannot be empty"}),t=this.ctx.config.isolation.schemaNameTemplate(e);console.log(M.dim(`
|
|
1709
|
+
Schema name: ${t}`));let r=await confirm({message:"Apply all migrations after creation?",default:true});if(!await confirm({message:`Create tenant "${e}"?`,default:true}))return {type:"back"};let o=st("Creating tenant...").start();try{let s=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>[]});if(await s.tenantExists(e))return o.warn(`Tenant ${e} already exists`),await this.renderer.pressEnterToContinue(),{type:"back"};await s.createTenant(e,{migrate:r}),o.succeed(`Tenant ${e} created successfully`),console.log(""),console.log(M.green(" \u2713 Schema created: ")+M.dim(t)),r&&console.log(M.green(" \u2713 All migrations applied")),console.log(M.dim(`
|
|
1710
|
+
Usage:`)),console.log(M.dim(` const db = tenants.getDb('${e}');`));}catch(s){o.fail("Failed to create tenant"),console.log(M.red(`
|
|
1711
|
+
${s.message}`));}return console.log(""),await this.renderer.pressEnterToContinue(),{type:"refresh"}}async showDrop(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Drop Tenant"),e.length===0)return this.renderer.showStatus("No tenants found","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=e.map(c=>({name:`${c.tenantId} ${M.dim(`(${c.schemaName})`)}`,value:c.tenantId}));t.push({name:M.gray("\u2190 Cancel"),value:"cancel"});let r=await select({message:"Select tenant to drop:",choices:t});if(r==="cancel")return {type:"back"};let a=e.find(c=>c.tenantId===r);if(!a)return {type:"back"};if(console.log(""),console.log(M.red.bold(" WARNING: This action is irreversible!")),console.log(M.dim(` Schema ${a.schemaName} and all its data will be deleted.`)),console.log(""),!await confirm({message:`Are you sure you want to drop tenant "${r}"?`,default:false}))return {type:"back"};if(await input({message:`Type "${r}" to confirm deletion:`})!==r)return this.renderer.showStatus("Tenant ID does not match. Operation cancelled.","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let i=st("Dropping tenant...").start();try{await I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>[]}).dropTenant(r),i.succeed(`Tenant ${r} dropped successfully`),console.log(M.dim(`
|
|
1712
|
+
Schema ${a.schemaName} has been removed.`));}catch(c){i.fail("Failed to drop tenant"),console.log(M.red(`
|
|
1713
|
+
${c.message}`));}return console.log(""),await this.renderer.pressEnterToContinue(),{type:"refresh"}}async showClone(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Clone Tenant"),e.length===0)return this.renderer.showStatus("No tenants found to clone from","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=e.map(u=>({name:`${u.tenantId} ${M.dim(`(${u.schemaName})`)}`,value:u.tenantId}));t.push({name:M.gray("\u2190 Cancel"),value:"cancel"});let r=await select({message:"Select source tenant:",choices:t});if(r==="cancel")return {type:"back"};let a=new Set(e.map(u=>u.tenantId)),o=await input({message:"New tenant ID:",validate:u=>u.trim()?/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(u)?a.has(u)?"Tenant already exists":true:"Invalid tenant ID format (use alphanumeric, dashes, underscores)":"Tenant ID cannot be empty"}),s=this.ctx.config.isolation.schemaNameTemplate(o);console.log(M.dim(`
|
|
1714
|
+
Target schema: ${s}`));let i=await confirm({message:"Include data in clone?",default:false}),c=false;if(i&&(c=await confirm({message:"Anonymize sensitive data?",default:false})),console.log(""),console.log(M.dim(" Clone configuration:")),console.log(M.dim(` Source: ${r}`)),console.log(M.dim(` Target: ${o} (${s})`)),console.log(M.dim(` Data: ${i?c?"Yes (anonymized)":"Yes":"No (schema only)"}`)),console.log(""),!await confirm({message:`Clone "${r}" to "${o}"?`,default:true}))return {type:"back"};let m=st("Cloning tenant...").start();try{let p=await I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>[]}).cloneTenant(r,o,{includeData:i,anonymize:c?{enabled:!0}:void 0,onProgress:(h,g)=>{switch(h){case "introspecting":m.text="Introspecting source schema...";break;case "creating_schema":m.text="Creating target schema...";break;case "creating_tables":m.text="Creating tables...";break;case "creating_indexes":m.text="Creating indexes...";break;case "creating_constraints":m.text="Creating constraints...";break;case "copying_data":g?.table?m.text=`Copying: ${g.table} (${g.progress}/${g.total})...`:m.text="Copying data...";break}}});p.success?(m.succeed(`Tenant ${o} cloned successfully`),console.log(""),console.log(M.green(" \u2713 Tables: ")+M.dim(p.tables.length.toString())),i&&p.rowsCopied!==void 0&&(console.log(M.green(" \u2713 Rows copied: ")+M.dim(p.rowsCopied.toLocaleString())),c&&console.log(M.green(" \u2713 Data anonymized"))),console.log(M.dim(`
|
|
1715
|
+
Duration: ${p.durationMs}ms`)),console.log(M.dim(`
|
|
1716
|
+
Usage:`)),console.log(M.dim(` const db = tenants.getDb('${o}');`))):(m.fail("Clone failed"),console.log(M.red(`
|
|
1717
|
+
${p.error}`)));}catch(u){m.fail("Clone failed"),console.log(M.red(`
|
|
1718
|
+
${u.message}`));}return console.log(""),await this.renderer.pressEnterToContinue(),{type:"refresh"}}};var ve=class{constructor(e,t){this.ctx=e;this.renderer=t||new V;}renderer;async show(e){this.renderer.clearScreen(),this.renderer.showHeader("Seeding");let t=!!this.ctx.config.schemas?.shared,r=[{name:"Seed tenants",value:"tenants"},...t?[{name:"Seed shared schema",value:"shared"}]:[],...t?[{name:"Seed all (shared + tenants)",value:"all"}]:[],{name:M.dim("\u2190 Back"),value:"back"}],a=await select({message:"Select seeding action:",choices:r});return a==="back"?{type:"back"}:a==="shared"?this.showSharedSeeding():a==="all"?this.showAllSeeding(e):this.showTenantSeeding(e)}async showTenantSeeding(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Seed Tenants"),e.length===0)return this.renderer.showStatus("No tenants found","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=await input({message:"Seed file path:",default:"./seeds/tenant/initial.ts",validate:s=>s.trim()?true:"Seed file path cannot be empty"}),r=await this.loadSeedFile(t);if(!r)return {type:"back"};console.log("");let a=await checkbox({message:"Select tenants to seed:",choices:e.map(s=>({name:`${s.tenantId} ${M.dim(`(${s.schemaName})`)}`,value:s.tenantId,checked:true})),pageSize:15});return a.length===0?(this.renderer.showStatus("No tenants selected","warning"),await this.renderer.pressEnterToContinue(),{type:"back"}):await confirm({message:`Seed ${a.length} tenant(s)?`,default:true})?(await this.runSeeding(a,r),{type:"back"}):{type:"back"}}async showSharedSeeding(){this.renderer.clearScreen(),this.renderer.showHeader("Seed Shared Schema");let e=await input({message:"Shared seed file path:",default:"./seeds/shared/plans.ts",validate:a=>a.trim()?true:"Seed file path cannot be empty"}),t=await this.loadSharedSeedFile(e);return t?await confirm({message:"Seed shared schema (public)?",default:true})?(await this.runSharedSeeding(t),{type:"back"}):{type:"back"}:{type:"back"}}async showAllSeeding(e){if(this.renderer.clearScreen(),this.renderer.showHeader("Seed All (Shared + Tenants)"),e.length===0)return this.renderer.showStatus("No tenants found","warning"),await this.renderer.pressEnterToContinue(),{type:"back"};let t=await input({message:"Shared seed file path:",default:"./seeds/shared/plans.ts",validate:i=>i.trim()?true:"Seed file path cannot be empty"}),r=await this.loadSharedSeedFile(t);if(!r)return {type:"back"};console.log("");let a=await input({message:"Tenant seed file path:",default:"./seeds/tenant/initial.ts",validate:i=>i.trim()?true:"Seed file path cannot be empty"}),o=await this.loadSeedFile(a);return o?(console.log(""),await confirm({message:`Seed shared schema and ${e.length} tenant(s)?`,default:true})?(await this.runAllSeeding(r,o,e.map(i=>i.tenantId)),{type:"back"}):{type:"back"}):{type:"back"}}async loadSeedFile(e){let t=st("Loading seed file...").start();try{let r=resolve(process.cwd(),e),o=await import(pathToFileURL(r).href),s=o.seed||o.default;return typeof s!="function"?(t.fail('Seed file must export a "seed" function or default export'),console.log(M.dim(`
|
|
1719
|
+
Expected format:`)),console.log(M.dim(" export const seed: SeedFunction = async (db, tenantId) => { ... };")),await this.renderer.pressEnterToContinue(),null):(t.succeed("Seed file loaded"),s)}catch(r){t.fail("Failed to load seed file");let a=r;return a.message.includes("Cannot find module")?console.log(M.red(`
|
|
1720
|
+
File not found: ${e}`)):console.log(M.red(`
|
|
1721
|
+
${a.message}`)),await this.renderer.pressEnterToContinue(),null}}async loadSharedSeedFile(e){let t=st("Loading shared seed file...").start();try{let r=resolve(process.cwd(),e),o=await import(pathToFileURL(r).href),s=o.seed||o.default;return typeof s!="function"?(t.fail('Seed file must export a "seed" function or default export'),console.log(M.dim(`
|
|
1722
|
+
Expected format:`)),console.log(M.dim(" export const seed: SharedSeedFunction = async (db) => { ... };")),await this.renderer.pressEnterToContinue(),null):(t.succeed("Shared seed file loaded"),s)}catch(r){t.fail("Failed to load shared seed file");let a=r;return a.message.includes("Cannot find module")?console.log(M.red(`
|
|
1723
|
+
File not found: ${e}`)):console.log(M.red(`
|
|
1724
|
+
${a.message}`)),await this.renderer.pressEnterToContinue(),null}}async runSeeding(e,t){this.renderer.clearScreen(),this.renderer.showHeader("Running Seed");let r=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>e}),a=Date.now();console.log(M.dim(` Seeding ${e.length} tenant(s)...
|
|
1725
|
+
`));let o=await r.seedAll(t,{concurrency:10,onProgress:(i,c)=>{c==="completed"?console.log(M.green(` \u2713 ${i}`)):c==="failed"&&console.log(M.red(` \u2717 ${i}`));},onError:(i,c)=>(console.log(M.red(` Error: ${c.message}`)),"continue")}),s=Date.now()-a;this.renderer.showResults(o,s),console.log(""),await this.renderer.pressEnterToContinue();}async runSharedSeeding(e){this.renderer.clearScreen(),this.renderer.showHeader("Running Shared Seed");let t=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>[]}),r=st("Seeding shared schema...").start(),a=Date.now(),o=await t.seedShared(e),s=Date.now()-a;o.success?r.succeed(`Shared schema seeded successfully in ${s}ms`):r.fail(`Failed to seed shared schema: ${o.error}`),console.log(""),console.log(M.bold("Summary:")),console.log(` Schema: ${o.schemaName}`),console.log(` Status: ${o.success?M.green("success"):M.red("failed")}`),console.log(` Duration: ${M.dim(`${s}ms`)}`),o.error&&console.log(M.red(`
|
|
1726
|
+
Error: ${o.error}`)),console.log(""),await this.renderer.pressEnterToContinue();}async runAllSeeding(e,t,r){this.renderer.clearScreen(),this.renderer.showHeader("Running Full Seed");let a=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:async()=>r}),o=Date.now();console.log(M.bold(`
|
|
1727
1727
|
[1/2] Seeding shared schema...
|
|
1728
|
-
`));let s=st("Seeding shared schema...").start(),i=await a.seedShared(e);i.success?s.succeed(`Shared schema seeded in ${i.durationMs}ms`):s.fail(`Failed: ${i.error}`),console.log(
|
|
1728
|
+
`));let s=st("Seeding shared schema...").start(),i=await a.seedShared(e);i.success?s.succeed(`Shared schema seeded in ${i.durationMs}ms`):s.fail(`Failed: ${i.error}`),console.log(M.bold(`
|
|
1729
1729
|
[2/2] Seeding ${r.length} tenant(s)...
|
|
1730
|
-
`));let c=await a.seedAll(t,{concurrency:10,onProgress:(m,u)=>{u==="completed"?console.log(
|
|
1731
|
-
Tenants:`)),console.log(` Total: ${c.total}`),console.log(` Succeeded: ${
|
|
1732
|
-
Total:`)),console.log(` Duration: ${C.dim(`${l}ms`)}`),console.log(""),await this.renderer.pressEnterToContinue();}};var Te=class{renderer;constructor(e,t){this.renderer=t||new V;}async show(){this.renderer.clearScreen(),this.renderer.showHeader("Generate Migration");let e=await input({message:"Migration name:",validate:t=>t.trim()?/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(t)?true:"Invalid migration name format":"Migration name cannot be empty"});return console.log(""),console.log(C.dim(" To generate a migration, run:")),console.log(""),console.log(C.cyan(` npx drizzle-kit generate --name=${e}`)),console.log(""),console.log(C.dim(" Or use drizzle-multitenant generate:")),console.log(""),console.log(C.cyan(` npx drizzle-multitenant generate --name=${e}`)),console.log(""),await this.renderer.pressEnterToContinue(),{type:"back"}}async showShared(){this.renderer.clearScreen(),this.renderer.showHeader("Generate Shared Migration");let e=await input({message:"Shared migration name:",validate:t=>t.trim()?/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(t)?true:"Invalid migration name format":"Migration name cannot be empty"});return console.log(""),console.log(C.dim(" To generate a shared migration, run:")),console.log(""),console.log(C.cyan(` npx drizzle-multitenant generate:shared --name=${e}`)),console.log(""),console.log(C.dim(" This will create a migration for the public schema.")),console.log(""),await this.renderer.pressEnterToContinue(),{type:"back"}}};var Yt={"table-naming":["warn",{style:"snake_case"}],"column-naming":["warn",{style:"snake_case"}],"require-primary-key":"error","prefer-uuid-pk":"warn","require-timestamps":"off","index-foreign-keys":"warn","no-cascade-delete":"off","require-soft-delete":"off"};function Ms(n){return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(n)}function Es(n){return /^[a-z][a-zA-Z0-9]*$/.test(n)&&!n.includes("_")}function Rs(n){return /^[A-Z][a-zA-Z0-9]*$/.test(n)&&!n.includes("_")}function Ds(n){return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(n)}function Ur(n,e){switch(e){case "snake_case":return Ms(n);case "camelCase":return Es(n);case "PascalCase":return Rs(n);case "kebab-case":return Ds(n);default:return true}}function Wr(n,e){let t=n.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/-/g,"_").toLowerCase().split("_").filter(Boolean);switch(e){case "snake_case":return t.join("_");case "camelCase":return t.map((r,a)=>a===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join("");case "PascalCase":return t.map(r=>r.charAt(0).toUpperCase()+r.slice(1)).join("");case "kebab-case":return t.join("-");default:return n}}function Hr(n,e){return e?.length?e.some(t=>new RegExp("^"+t.replace(/\*/g,".*")+"$").test(n)):false}var Qt={name:"table-naming",description:"Enforce consistent table naming convention",defaultSeverity:"warn",validateTable(n,e){let t=e.options,r=t.style??"snake_case",a=t.exceptions??[];if(!Hr(n.name,a)&&!Ur(n.name,r)){let o=Wr(n.name,r);e.report({message:`Table "${n.name}" does not follow ${r} convention`,filePath:n.filePath,table:n.name,suggestion:`Consider renaming to "${o}"`});}}},Zt={name:"column-naming",description:"Enforce consistent column naming convention",defaultSeverity:"warn",validateColumn(n,e,t){let r=t.options,a=r.style??"snake_case",o=r.exceptions??[];if(!Hr(n.name,o)&&!Ur(n.name,a)){let s=Wr(n.name,a);t.report({message:`Column "${n.name}" in table "${e.name}" does not follow ${a} convention`,filePath:e.filePath,table:e.name,column:n.name,suggestion:`Consider renaming to "${s}"`});}}},it=[Qt,Zt];var Xt={name:"require-primary-key",description:"Require every table to have a primary key",defaultSeverity:"error",validateTable(n,e){n.columns.some(r=>r.isPrimaryKey)||e.report({message:`Table "${n.name}" does not have a primary key`,filePath:n.filePath,table:n.name,suggestion:"Add a primary key column (e.g., id: uuid().primaryKey())"});}},en={name:"prefer-uuid-pk",description:"Prefer UUID over serial/integer for primary keys",defaultSeverity:"warn",validateColumn(n,e,t){if(!n.isPrimaryKey)return;let r=["serial","bigserial","smallserial","integer","bigint","smallint"],a=n.dataType.toLowerCase();r.some(o=>a.includes(o))&&t.report({message:`Table "${e.name}" uses ${n.dataType} for primary key instead of UUID`,filePath:e.filePath,table:e.name,column:n.name,suggestion:"Consider using uuid() for better distribution and security"});}},tn={name:"require-timestamps",description:"Require timestamp columns (created_at, updated_at) on tables",defaultSeverity:"warn",validateTable(n,e){let r=e.options.columns??["created_at","updated_at"],a=n.columns.map(o=>o.name.toLowerCase());for(let o of r){let s=o.toLowerCase(),i=o.replace(/_([a-z])/g,(c,l)=>l.toUpperCase()).toLowerCase();!a.includes(s)&&!a.includes(i)&&e.report({message:`Table "${n.name}" is missing "${o}" column`,filePath:n.filePath,table:n.name,suggestion:`Add ${s}: timestamp('${s}').defaultNow().notNull()`});}}},nn={name:"index-foreign-keys",description:"Require indexes on foreign key columns",defaultSeverity:"warn",validateTable(n,e){let t=n.columns.filter(o=>o.references),r=new Set;for(let o of n.indexes){let s=o.columns[0];s&&r.add(s.toLowerCase());}let a=n.columns.filter(o=>o.isPrimaryKey);for(let o of a)r.add(o.name.toLowerCase());for(let o of t)r.has(o.name.toLowerCase())||e.report({message:`Foreign key column "${o.name}" in table "${n.name}" is not indexed`,filePath:n.filePath,table:n.name,column:o.name,suggestion:`Add index: index('${n.name}_${o.name}_idx').on(${n.name}.${o.name})`});}},ct=[Xt,en,tn,nn];var rn={name:"no-cascade-delete",description:"Warn about CASCADE DELETE on foreign keys",defaultSeverity:"warn",validateColumn(n,e,t){if(!n.references)return;n.references.onDelete?.toLowerCase()==="cascade"&&t.report({message:`Foreign key "${n.name}" in table "${e.name}" uses CASCADE DELETE`,filePath:e.filePath,table:e.name,column:n.name,suggestion:"Consider using SET NULL, RESTRICT, or implementing soft delete to prevent accidental data loss"});}},an={name:"require-soft-delete",description:"Require soft delete column on tables",defaultSeverity:"off",validateTable(n,e){let r=e.options.column??"deleted_at",a=r.toLowerCase(),o=r.replace(/_([a-z])/g,(i,c)=>c.toUpperCase()).toLowerCase(),s=n.columns.map(i=>i.name.toLowerCase());!s.includes(a)&&!s.includes(o)&&e.report({message:`Table "${n.name}" does not have a soft delete column ("${r}")`,filePath:n.filePath,table:n.name,suggestion:`Add ${a}: timestamp('${a}') for soft delete support`});}},lt=[rn,an];var _e=[...it,...ct,...lt];var vs=Symbol.for("drizzle:Table"),_s=Symbol.for("drizzle:Column");function Ps(n){if(!n||typeof n!="object")return false;let e=n;if(e._&&typeof e._=="object"){let t=e._;return typeof t.name=="string"&&(t.schema===void 0||typeof t.schema=="string")}return !!Object.getOwnPropertySymbols(e).some(t=>t===vs)}function zs(n){if(!n||typeof n!="object")return false;let e=n;return typeof e.name=="string"||typeof e.columnType=="string"||typeof e.dataType=="string"||Object.getOwnPropertySymbols(e).some(t=>t===_s)}function ks(n,e){let t=e.name??n,r="unknown";if(typeof e.dataType=="string")r=e.dataType;else if(typeof e.columnType=="string")r=e.columnType;else if(typeof e.getSQLType=="function")try{r=e.getSQLType();}catch{}let a=e.primary===true||e.isPrimaryKey===true||typeof e.primaryKey=="function"&&e._isPrimaryKey===true,o=e.notNull!==true&&e.isNotNull!==true,s=e.hasDefault===true||e.default!==void 0,i=e.default!==void 0?String(e.default):null,c;if(e.references&&typeof e.references=="object"){let l=e.references;c={table:l.table??"unknown",column:l.column??"unknown",onDelete:l.onDelete,onUpdate:l.onUpdate};}return {name:t,dataType:r,isPrimaryKey:a,isNullable:o,hasDefault:s,defaultValue:i,references:c}}function Ns(n){let e=[],t=n._indexes??n.indexes;if(t&&typeof t=="object")for(let[r,a]of Object.entries(t)){if(!a||typeof a!="object")continue;let o=a,s=[];if(Array.isArray(o.columns))for(let i of o.columns)typeof i=="string"?s.push(i):i&&typeof i=="object"&&"name"in i&&s.push(String(i.name));e.push({name:r,columns:s,isUnique:o.isUnique===true||o.unique===true});}return e}function Se(n,e,t){let r=[];for(let[a,o]of Object.entries(n)){if(!Ps(o))continue;let s=o,c=s._?.name??a,l=[];for(let[u,p]of Object.entries(s)){if(u==="_"||u.startsWith("_")||!zs(p))continue;let h=ks(u,p);l.push(h);}let m=Ns(s);r.push({name:c,schemaType:t,columns:l,indexes:m,filePath:e});}return r}async function ue(n,e){let{glob:t}=await import('glob'),{resolve:r}=await import('path'),a=r(n,"**/*.ts");return (await t(a,{ignore:["**/*.test.ts","**/*.spec.ts","**/node_modules/**"]})).map(s=>({filePath:s,type:e}))}async function pe(n,e){try{let t=await import(n);return Se(t,n,e)}catch(t){return console.warn(`Warning: Could not parse schema file ${n}:`,t.message),[]}}function As(n){return n?typeof n=="string"?n:n[0]:"off"}function Fs(n){return n?typeof n=="string"?{}:n[1]??{}:{}}var mt=class{rules=new Map;config;constructor(e){this.config={...Yt,...e?.rules};for(let t of _e)this.rules.set(t.name,t);}getEffectiveSeverity(e){let t=this.config[e];return As(t)}getRuleOptions(e){let t=this.config[e];return Fs(t)}isRuleEnabled(e){return this.getEffectiveSeverity(e)!=="off"}lintTable(e){let t=[];for(let[r,a]of this.rules){let o=this.getEffectiveSeverity(r);if(o==="off")continue;let s=this.getRuleOptions(r),i={severity:o,options:s,report:c=>{t.push({rule:r,severity:o,...c});}};if(a.validateTable&&a.validateTable(e,i),a.validateColumn)for(let c of e.columns)a.validateColumn(c,e,i);}return t}lintTables(e){let t=Date.now(),r=new Map;for(let i of e){r.has(i.filePath)||r.set(i.filePath,{filePath:i.filePath,issues:[],tables:0,columns:0});let c=r.get(i.filePath);c.tables++,c.columns+=i.columns.length;let l=this.lintTable(i);c.issues.push(...l);}for(let[i,c]of this.rules){if(!c.validateSchema)continue;let l=this.getEffectiveSeverity(i);if(l==="off")continue;let m=this.getRuleOptions(i),u={severity:l,options:m,report:p=>{let h=p.filePath||"schema";r.has(h)||r.set(h,{filePath:h,issues:[],tables:0,columns:0}),r.get(h).issues.push({rule:i,severity:l,...p});}};c.validateSchema(e,u);}let a=Array.from(r.values()),o=a.flatMap(i=>i.issues),s={totalFiles:a.length,totalTables:a.reduce((i,c)=>i+c.tables,0),totalColumns:a.reduce((i,c)=>i+c.columns,0),errors:o.filter(i=>i.severity==="error").length,warnings:o.filter(i=>i.severity==="warn").length};return {files:a,summary:s,durationMs:Date.now()-t,passed:s.errors===0}}lintModule(e,t,r="tenant"){let a=Se(e,t,r);return this.lintTables(a)}async lintDirectory(e,t="tenant"){let r=Date.now(),a=await ue(e,t),o=[];for(let i of a){let c=await pe(i.filePath,i.type);o.push(...c);}let s=this.lintTables(o);return s.durationMs=Date.now()-r,s}async lintDirectories(e){let t=Date.now(),r=[];if(e.tenant){let o=await ue(e.tenant,"tenant");for(let s of o){let i=await pe(s.filePath,s.type);r.push(...i);}}if(e.shared){let o=await ue(e.shared,"shared");for(let s of o){let i=await pe(s.filePath,s.type);r.push(...i);}}let a=this.lintTables(r);return a.durationMs=Date.now()-t,a}getRules(){return Array.from(this.rules.values())}getRule(e){return this.rules.get(e)}registerRule(e){this.rules.set(e.name,e);}setRuleConfig(e,t){this.config[e]=t;}getConfig(){return {...this.config}}};function Pe(n){return new mt(n)}var Br={reset:"\x1B[0m",red:"\x1B[31m",yellow:"\x1B[33m",green:"\x1B[32m",cyan:"\x1B[36m",dim:"\x1B[2m",bold:"\x1B[1m"};function se(n,e,t){return t?`${Br[e]}${n}${Br.reset}`:n}function Os(n,e){let t=n.severity==="error"?se("error","red",e):se("warn","yellow",e),r=n.column?`${n.table}.${n.column}`:n.table??n.filePath,a=se(`(${n.rule})`,"dim",e),o=` ${t} ${n.message} ${a}`;return o+=`
|
|
1730
|
+
`));let c=await a.seedAll(t,{concurrency:10,onProgress:(m,u)=>{u==="completed"?console.log(M.green(` \u2713 ${m}`)):u==="failed"&&console.log(M.red(` \u2717 ${m}`));},onError:(m,u)=>(console.log(M.red(` Error: ${u.message}`)),"continue")}),l=Date.now()-o;console.log(""),console.log(M.bold("Summary:")),console.log(M.bold(" Shared:")),console.log(` Schema: ${i.schemaName}`),console.log(` Status: ${i.success?M.green("success"):M.red("failed")}`),console.log(` Duration: ${M.dim(`${i.durationMs}ms`)}`),console.log(M.bold(`
|
|
1731
|
+
Tenants:`)),console.log(` Total: ${c.total}`),console.log(` Succeeded: ${M.green(c.succeeded.toString())}`),c.failed>0&&console.log(` Failed: ${M.red(c.failed.toString())}`),c.skipped>0&&console.log(` Skipped: ${M.yellow(c.skipped.toString())}`),console.log(M.bold(`
|
|
1732
|
+
Total:`)),console.log(` Duration: ${M.dim(`${l}ms`)}`),console.log(""),await this.renderer.pressEnterToContinue();}};var Te=class{renderer;constructor(e,t){this.renderer=t||new V;}async show(){this.renderer.clearScreen(),this.renderer.showHeader("Generate Migration");let e=await input({message:"Migration name:",validate:t=>t.trim()?/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(t)?true:"Invalid migration name format":"Migration name cannot be empty"});return console.log(""),console.log(M.dim(" To generate a migration, run:")),console.log(""),console.log(M.cyan(` npx drizzle-kit generate --name=${e}`)),console.log(""),console.log(M.dim(" Or use drizzle-multitenant generate:")),console.log(""),console.log(M.cyan(` npx drizzle-multitenant generate --name=${e}`)),console.log(""),await this.renderer.pressEnterToContinue(),{type:"back"}}async showShared(){this.renderer.clearScreen(),this.renderer.showHeader("Generate Shared Migration");let e=await input({message:"Shared migration name:",validate:t=>t.trim()?/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(t)?true:"Invalid migration name format":"Migration name cannot be empty"});return console.log(""),console.log(M.dim(" To generate a shared migration, run:")),console.log(""),console.log(M.cyan(` npx drizzle-multitenant generate:shared --name=${e}`)),console.log(""),console.log(M.dim(" This will create a migration for the public schema.")),console.log(""),await this.renderer.pressEnterToContinue(),{type:"back"}}};var Yt={"table-naming":["warn",{style:"snake_case"}],"column-naming":["warn",{style:"snake_case"}],"require-primary-key":"error","prefer-uuid-pk":"warn","require-timestamps":"off","index-foreign-keys":"warn","no-cascade-delete":"off","require-soft-delete":"off"};function Ms(n){return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(n)}function Es(n){return /^[a-z][a-zA-Z0-9]*$/.test(n)&&!n.includes("_")}function Rs(n){return /^[A-Z][a-zA-Z0-9]*$/.test(n)&&!n.includes("_")}function Ds(n){return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(n)}function Ur(n,e){switch(e){case "snake_case":return Ms(n);case "camelCase":return Es(n);case "PascalCase":return Rs(n);case "kebab-case":return Ds(n);default:return true}}function Wr(n,e){let t=n.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/-/g,"_").toLowerCase().split("_").filter(Boolean);switch(e){case "snake_case":return t.join("_");case "camelCase":return t.map((r,a)=>a===0?r:r.charAt(0).toUpperCase()+r.slice(1)).join("");case "PascalCase":return t.map(r=>r.charAt(0).toUpperCase()+r.slice(1)).join("");case "kebab-case":return t.join("-");default:return n}}function Hr(n,e){return e?.length?e.some(t=>new RegExp("^"+t.replace(/\*/g,".*")+"$").test(n)):false}var Qt={name:"table-naming",description:"Enforce consistent table naming convention",defaultSeverity:"warn",validateTable(n,e){let t=e.options,r=t.style??"snake_case",a=t.exceptions??[];if(!Hr(n.name,a)&&!Ur(n.name,r)){let o=Wr(n.name,r);e.report({message:`Table "${n.name}" does not follow ${r} convention`,filePath:n.filePath,table:n.name,suggestion:`Consider renaming to "${o}"`});}}},Zt={name:"column-naming",description:"Enforce consistent column naming convention",defaultSeverity:"warn",validateColumn(n,e,t){let r=t.options,a=r.style??"snake_case",o=r.exceptions??[];if(!Hr(n.name,o)&&!Ur(n.name,a)){let s=Wr(n.name,a);t.report({message:`Column "${n.name}" in table "${e.name}" does not follow ${a} convention`,filePath:e.filePath,table:e.name,column:n.name,suggestion:`Consider renaming to "${s}"`});}}},it=[Qt,Zt];var Xt={name:"require-primary-key",description:"Require every table to have a primary key",defaultSeverity:"error",validateTable(n,e){n.columns.some(r=>r.isPrimaryKey)||e.report({message:`Table "${n.name}" does not have a primary key`,filePath:n.filePath,table:n.name,suggestion:"Add a primary key column (e.g., id: uuid().primaryKey())"});}},en={name:"prefer-uuid-pk",description:"Prefer UUID over serial/integer for primary keys",defaultSeverity:"warn",validateColumn(n,e,t){if(!n.isPrimaryKey)return;let r=["serial","bigserial","smallserial","integer","bigint","smallint"],a=n.dataType.toLowerCase();r.some(o=>a.includes(o))&&t.report({message:`Table "${e.name}" uses ${n.dataType} for primary key instead of UUID`,filePath:e.filePath,table:e.name,column:n.name,suggestion:"Consider using uuid() for better distribution and security"});}},tn={name:"require-timestamps",description:"Require timestamp columns (created_at, updated_at) on tables",defaultSeverity:"warn",validateTable(n,e){let r=e.options.columns??["created_at","updated_at"],a=n.columns.map(o=>o.name.toLowerCase());for(let o of r){let s=o.toLowerCase(),i=o.replace(/_([a-z])/g,(c,l)=>l.toUpperCase()).toLowerCase();!a.includes(s)&&!a.includes(i)&&e.report({message:`Table "${n.name}" is missing "${o}" column`,filePath:n.filePath,table:n.name,suggestion:`Add ${s}: timestamp('${s}').defaultNow().notNull()`});}}},nn={name:"index-foreign-keys",description:"Require indexes on foreign key columns",defaultSeverity:"warn",validateTable(n,e){let t=n.columns.filter(o=>o.references),r=new Set;for(let o of n.indexes){let s=o.columns[0];s&&r.add(s.toLowerCase());}let a=n.columns.filter(o=>o.isPrimaryKey);for(let o of a)r.add(o.name.toLowerCase());for(let o of t)r.has(o.name.toLowerCase())||e.report({message:`Foreign key column "${o.name}" in table "${n.name}" is not indexed`,filePath:n.filePath,table:n.name,column:o.name,suggestion:`Add index: index('${n.name}_${o.name}_idx').on(${n.name}.${o.name})`});}},ct=[Xt,en,tn,nn];var rn={name:"no-cascade-delete",description:"Warn about CASCADE DELETE on foreign keys",defaultSeverity:"warn",validateColumn(n,e,t){if(!n.references)return;n.references.onDelete?.toLowerCase()==="cascade"&&t.report({message:`Foreign key "${n.name}" in table "${e.name}" uses CASCADE DELETE`,filePath:e.filePath,table:e.name,column:n.name,suggestion:"Consider using SET NULL, RESTRICT, or implementing soft delete to prevent accidental data loss"});}},an={name:"require-soft-delete",description:"Require soft delete column on tables",defaultSeverity:"off",validateTable(n,e){let r=e.options.column??"deleted_at",a=r.toLowerCase(),o=r.replace(/_([a-z])/g,(i,c)=>c.toUpperCase()).toLowerCase(),s=n.columns.map(i=>i.name.toLowerCase());!s.includes(a)&&!s.includes(o)&&e.report({message:`Table "${n.name}" does not have a soft delete column ("${r}")`,filePath:n.filePath,table:n.name,suggestion:`Add ${a}: timestamp('${a}') for soft delete support`});}},lt=[rn,an];var _e=[...it,...ct,...lt];var vs=Symbol.for("drizzle:Table"),_s=Symbol.for("drizzle:Column");function Ps(n){if(!n||typeof n!="object")return false;let e=n;if(e._&&typeof e._=="object"){let t=e._;return typeof t.name=="string"&&(t.schema===void 0||typeof t.schema=="string")}return !!Object.getOwnPropertySymbols(e).some(t=>t===vs)}function zs(n){if(!n||typeof n!="object")return false;let e=n;return typeof e.name=="string"||typeof e.columnType=="string"||typeof e.dataType=="string"||Object.getOwnPropertySymbols(e).some(t=>t===_s)}function ks(n,e){let t=e.name??n,r="unknown";if(typeof e.dataType=="string")r=e.dataType;else if(typeof e.columnType=="string")r=e.columnType;else if(typeof e.getSQLType=="function")try{r=e.getSQLType();}catch{}let a=e.primary===true||e.isPrimaryKey===true||typeof e.primaryKey=="function"&&e._isPrimaryKey===true,o=e.notNull!==true&&e.isNotNull!==true,s=e.hasDefault===true||e.default!==void 0,i=e.default!==void 0?String(e.default):null,c;if(e.references&&typeof e.references=="object"){let l=e.references;c={table:l.table??"unknown",column:l.column??"unknown",onDelete:l.onDelete,onUpdate:l.onUpdate};}return {name:t,dataType:r,isPrimaryKey:a,isNullable:o,hasDefault:s,defaultValue:i,references:c}}function Ns(n){let e=[],t=n._indexes??n.indexes;if(t&&typeof t=="object")for(let[r,a]of Object.entries(t)){if(!a||typeof a!="object")continue;let o=a,s=[];if(Array.isArray(o.columns))for(let i of o.columns)typeof i=="string"?s.push(i):i&&typeof i=="object"&&"name"in i&&s.push(String(i.name));e.push({name:r,columns:s,isUnique:o.isUnique===true||o.unique===true});}return e}function Se(n,e,t){let r=[];for(let[a,o]of Object.entries(n)){if(!Ps(o))continue;let s=o,c=s._?.name??a,l=[];for(let[u,p]of Object.entries(s)){if(u==="_"||u.startsWith("_")||!zs(p))continue;let h=ks(u,p);l.push(h);}let m=Ns(s);r.push({name:c,schemaType:t,columns:l,indexes:m,filePath:e});}return r}async function ue(n,e){let{glob:t}=await import('glob'),{resolve:r}=await import('path'),a=r(n,"**/*.ts");return (await t(a,{ignore:["**/*.test.ts","**/*.spec.ts","**/node_modules/**"]})).map(s=>({filePath:s,type:e}))}async function pe(n,e){try{let t=await import(n);return Se(t,n,e)}catch(t){return console.warn(`Warning: Could not parse schema file ${n}:`,t.message),[]}}function As(n){return n?typeof n=="string"?n:n[0]:"off"}function Fs(n){return n?typeof n=="string"?{}:n[1]??{}:{}}var mt=class{rules=new Map;config;constructor(e){this.config={...Yt,...e?.rules};for(let t of _e)this.rules.set(t.name,t);}getEffectiveSeverity(e){let t=this.config[e];return As(t)}getRuleOptions(e){let t=this.config[e];return Fs(t)}isRuleEnabled(e){return this.getEffectiveSeverity(e)!=="off"}lintTable(e){let t=[];for(let[r,a]of this.rules){let o=this.getEffectiveSeverity(r);if(o==="off")continue;let s=this.getRuleOptions(r),i={severity:o,options:s,report:c=>{t.push({rule:r,severity:o,...c});}};if(a.validateTable&&a.validateTable(e,i),a.validateColumn)for(let c of e.columns)a.validateColumn(c,e,i);}return t}lintTables(e){let t=Date.now(),r=new Map;for(let i of e){r.has(i.filePath)||r.set(i.filePath,{filePath:i.filePath,issues:[],tables:0,columns:0});let c=r.get(i.filePath);c.tables++,c.columns+=i.columns.length;let l=this.lintTable(i);c.issues.push(...l);}for(let[i,c]of this.rules){if(!c.validateSchema)continue;let l=this.getEffectiveSeverity(i);if(l==="off")continue;let m=this.getRuleOptions(i),u={severity:l,options:m,report:p=>{let h=p.filePath||"schema";r.has(h)||r.set(h,{filePath:h,issues:[],tables:0,columns:0}),r.get(h).issues.push({rule:i,severity:l,...p});}};c.validateSchema(e,u);}let a=Array.from(r.values()),o=a.flatMap(i=>i.issues),s={totalFiles:a.length,totalTables:a.reduce((i,c)=>i+c.tables,0),totalColumns:a.reduce((i,c)=>i+c.columns,0),errors:o.filter(i=>i.severity==="error").length,warnings:o.filter(i=>i.severity==="warn").length};return {files:a,summary:s,durationMs:Date.now()-t,passed:s.errors===0}}lintModule(e,t,r="tenant"){let a=Se(e,t,r);return this.lintTables(a)}async lintDirectory(e,t="tenant"){let r=Date.now(),a=await ue(e,t),o=[];for(let i of a){let c=await pe(i.filePath,i.type);o.push(...c);}let s=this.lintTables(o);return s.durationMs=Date.now()-r,s}async lintDirectories(e){let t=Date.now(),r=[];if(e.tenant){let o=await ue(e.tenant,"tenant");for(let s of o){let i=await pe(s.filePath,s.type);r.push(...i);}}if(e.shared){let o=await ue(e.shared,"shared");for(let s of o){let i=await pe(s.filePath,s.type);r.push(...i);}}let a=this.lintTables(r);return a.durationMs=Date.now()-t,a}getRules(){return Array.from(this.rules.values())}getRule(e){return this.rules.get(e)}registerRule(e){this.rules.set(e.name,e);}setRuleConfig(e,t){this.config[e]=t;}getConfig(){return {...this.config}}};function Pe(n){return new mt(n)}var Br={reset:"\x1B[0m",red:"\x1B[31m",yellow:"\x1B[33m",green:"\x1B[32m",cyan:"\x1B[36m",dim:"\x1B[2m",bold:"\x1B[1m"};function se(n,e,t){return t?`${Br[e]}${n}${Br.reset}`:n}function Os(n,e){let t=n.severity==="error"?se("error","red",e):se("warn","yellow",e),r=n.column?`${n.table}.${n.column}`:n.table??n.filePath,a=se(`(${n.rule})`,"dim",e),o=` ${t} ${n.message} ${a}`;return o+=`
|
|
1733
1733
|
${se(r,"dim",e)}`,n.suggestion&&(o+=`
|
|
1734
1734
|
${se("\u2192 "+n.suggestion,"cyan",e)}`),o}function Ls(n,e){let t=e.colors!==false,r=[];for(let a of n.files)if(!(a.issues.length===0&&!e.verbose)){if(r.push(""),r.push(se(a.filePath,"bold",t)),a.issues.length===0){r.push(se(" No issues found","green",t));continue}for(let o of a.issues)r.push(Os(o,t));}if(!e.quiet){r.push("");let{errors:a,warnings:o,totalTables:s}=n.summary;if(a===0&&o===0)r.push(se(`\u2713 ${s} tables validated, no issues found`,"green",t));else {let i=[];i.push(`${s} tables validated`),o>0&&i.push(se(`${o} warning${o>1?"s":""}`,"yellow",t)),a>0&&i.push(se(`${a} error${a>1?"s":""}`,"red",t)),r.push(i.join(", "));}r.push(se(`Completed in ${n.durationMs}ms`,"dim",t));}return r.join(`
|
|
1735
1735
|
`)}function js(n){return JSON.stringify({passed:n.passed,summary:n.summary,files:n.files.map(e=>({filePath:e.filePath,tables:e.tables,columns:e.columns,issues:e.issues})),durationMs:n.durationMs},null,2)}function qs(n){let e=[];for(let o of n.files)for(let s of o.issues){let i=s.severity==="error"?"error":"warning",c=`[${s.rule}]`,l=s.filePath,m=s.line??1,u=`::${i} file=${l},line=${m},title=${c}::${s.message}`;s.suggestion&&(u+=` (${s.suggestion})`),e.push(u);}let{errors:t,warnings:r,totalTables:a}=n.summary;return t>0||r>0?e.push(`::notice::Schema lint: ${a} tables, ${t} errors, ${r} warnings`):e.push(`::notice::Schema lint: ${a} tables validated, no issues`),e.join(`
|
|
1736
|
-
`)}function dt(n,e){switch(e.format){case "json":return js(n);case "github":return qs(n);default:return Ls(n,e)}}var ze=class{renderer;ctx;constructor(e,t){this.ctx=e,this.renderer=t||new V;}async show(){switch(this.renderer.clearScreen(),this.renderer.showHeader("Schema Lint"),await select({message:"What would you like to do?",choices:[{name:"Lint schemas with default rules",value:"default"},{name:"Configure rules and lint",value:"configure"},{name:"View available rules",value:"rules"},{name:C.gray("\u2190 Back to main menu"),value:"back"}]})){case "default":return this.runLint();case "configure":return this.configureAndLint();case "rules":return this.showRules();default:return {type:"back"}}}async runLint(e){this.renderer.clearScreen(),this.renderer.showHeader("Linting Schemas...");let{tenantDir:t,sharedDir:r}=await this.findSchemaDirectories();if(!t&&!r)return this.renderer.showStatus("No schema directories found","error"),console.log(""),console.log(" Please specify schema directories or create them in:"),console.log(" - ./src/db/schema/tenant"),console.log(" - ./src/db/schema/shared"),console.log(""),await this.renderer.pressEnterToContinue(),this.show();console.log(` Tenant schemas: ${t??C.dim("not found")}`),console.log(` Shared schemas: ${r??C.dim("not found")}`),console.log("");try{let o=await Pe({rules:e}).lintDirectories({tenant:t,shared:r});return this.showResults(o)}catch(a){return this.renderer.showStatus(a.message,"error"),await this.renderer.pressEnterToContinue(),this.show()}}async findSchemaDirectories(){let e={tenant:["./src/db/schema/tenant","./src/schema/tenant","./drizzle/schema/tenant","./db/schema/tenant"],shared:["./src/db/schema/shared","./src/schema/shared","./drizzle/schema/shared","./db/schema/shared"]},t,r;for(let a of e.tenant){let o=resolve(process.cwd(),a);if(existsSync(o)){t=o;break}}for(let a of e.shared){let o=resolve(process.cwd(),a);if(existsSync(o)){r=o;break}}return {tenantDir:t,sharedDir:r}}async configureAndLint(){this.renderer.clearScreen(),this.renderer.showHeader("Configure Lint Rules");let e=await checkbox({message:"Select rules to enable:",choices:_e.map(a=>({name:`${a.name} - ${a.description}`,value:a.name,checked:a.defaultSeverity!=="off"}))});if(e.length===0)return this.renderer.showStatus("No rules selected","warning"),await this.renderer.pressEnterToContinue(),this.show();let t=await select({message:"Default severity for selected rules:",choices:[{name:"Error (fail on issues)",value:"error"},{name:"Warning (report but pass)",value:"warn"}]}),r={};for(let a of e)r[a]=t;return this.runLint(r)}async showResults(e){this.renderer.clearScreen(),this.renderer.showHeader("Lint Results");let{errors:t,warnings:r,totalTables:a,totalFiles:o}=e.summary;if(t===0&&r===0)console.log(C.green.bold(" \u2713 All schemas passed validation!")),console.log(""),console.log(` Files: ${o}`),console.log(` Tables: ${a}`),console.log(` Issues: ${C.green("0")}`),console.log(""),console.log(C.dim(` Completed in ${e.durationMs}ms`));else {for(let i of e.files)if(i.issues.length!==0){console.log(""),console.log(C.bold(i.filePath));for(let c of i.issues)this.printIssue(c);}console.log(""),console.log(C.bold(" Summary:")),console.log(` Files: ${o}`),console.log(` Tables: ${a}`),console.log(` Errors: ${t>0?C.red(t.toString()):C.green("0")}`),console.log(` Warnings: ${r>0?C.yellow(r.toString()):C.green("0")}`),console.log(""),console.log(C.dim(` Completed in ${e.durationMs}ms`)),console.log(""),e.passed?console.log(C.yellow.bold(" \u26A0 Schema validation passed with warnings")):console.log(C.red.bold(" \u2717 Schema validation failed"));}switch(console.log(""),await select({message:"What would you like to do?",choices:[{name:"Run lint again",value:"again"},{name:"Configure rules and re-run",value:"configure"},{name:"View rule details",value:"rules"},{name:C.gray("\u2190 Back to main menu"),value:"back"}]})){case "again":return this.runLint();case "configure":return this.configureAndLint();case "rules":return this.showRules();default:return {type:"back"}}}printIssue(e){let t=e.severity==="error"?C.red("\u2717"):C.yellow("\u26A0"),r=e.severity==="error"?C.red("error"):C.yellow("warn"),a=C.dim(`(${e.rule})`);if(console.log(` ${t} ${r} ${e.message} ${a}`),e.table){let o=e.column?`${e.table}.${e.column}`:e.table;console.log(C.dim(` ${o}`));}e.suggestion&&console.log(C.cyan(` \u2192 ${e.suggestion}`));}async showRules(){return this.renderer.clearScreen(),this.renderer.showHeader("Available Lint Rules"),console.log(C.bold(" Naming Rules:")),console.log(` ${C.cyan("table-naming")} Enforce table naming convention`),console.log(` ${C.cyan("column-naming")} Enforce column naming convention`),console.log(""),console.log(C.bold(" Convention Rules:")),console.log(` ${C.cyan("require-primary-key")} Require every table to have a PK`),console.log(` ${C.cyan("prefer-uuid-pk")} Prefer UUID over serial for PKs`),console.log(` ${C.cyan("require-timestamps")} Require created_at/updated_at`),console.log(` ${C.cyan("index-foreign-keys")} Require indexes on FK columns`),console.log(""),console.log(C.bold(" Security Rules:")),console.log(` ${C.cyan("no-cascade-delete")} Warn about CASCADE DELETE`),console.log(` ${C.cyan("require-soft-delete")} Require soft delete column`),console.log(""),console.log(C.dim(" Configure rules in tenant.config.ts:")),console.log(C.dim(" lint: {")),console.log(C.dim(" rules: {")),console.log(C.dim(" 'table-naming': ['error', { style: 'snake_case' }],")),console.log(C.dim(" 'require-primary-key': 'error',")),console.log(C.dim(" }")),console.log(C.dim(" }")),console.log(""),await this.renderer.pressEnterToContinue(),this.show()}};var ee={maxPools:50,poolTtlMs:36e5,cleanupIntervalMs:6e4,poolConfig:{max:10,idleTimeoutMillis:3e4,connectionTimeoutMillis:5e3},retry:{maxAttempts:3,initialDelayMs:100,maxDelayMs:5e3,backoffMultiplier:2,jitter:true}};var le="[drizzle-multitenant]";var sn=class{enabled;logQueries;logPoolEvents;slowQueryThreshold;logger;constructor(e){this.enabled=e?.enabled??false,this.logQueries=e?.logQueries??true,this.logPoolEvents=e?.logPoolEvents??true,this.slowQueryThreshold=e?.slowQueryThreshold??1e3,this.logger=e?.logger??this.defaultLogger;}isEnabled(){return this.enabled}logQuery(e,t,r){if(!this.enabled||!this.logQueries)return;let a=r>=this.slowQueryThreshold,s={type:a?"slow_query":"query",tenantId:e,query:this.truncateQuery(t),durationMs:r};a?this.logger(`${le} tenant=${e} SLOW_QUERY duration=${r}ms query="${this.truncateQuery(t)}"`,s):this.logger(`${le} tenant=${e} query="${this.truncateQuery(t)}" duration=${r}ms`,s);}logPoolCreated(e,t){if(!this.enabled||!this.logPoolEvents)return;let r={type:"pool_created",tenantId:e,schemaName:t};this.logger(`${le} tenant=${e} POOL_CREATED schema=${t}`,r);}logPoolEvicted(e,t,r){if(!this.enabled||!this.logPoolEvents)return;let a={type:"pool_evicted",tenantId:e,schemaName:t,metadata:r?{reason:r}:void 0},o=r?` reason=${r}`:"";this.logger(`${le} tenant=${e} POOL_EVICTED schema=${t}${o}`,a);}logPoolError(e,t){if(!this.enabled||!this.logPoolEvents)return;let r={type:"pool_error",tenantId:e,error:t.message};this.logger(`${le} tenant=${e} POOL_ERROR error="${t.message}"`,r);}logWarmup(e,t,r,a){if(!this.enabled||!this.logPoolEvents)return;let o={type:"warmup",tenantId:e,durationMs:r,metadata:{success:t,alreadyWarm:a}},s=a?"already_warm":t?"success":"failed";this.logger(`${le} tenant=${e} WARMUP status=${s} duration=${r}ms`,o);}logConnectionRetry(e,t,r,a,o){if(!this.enabled||!this.logPoolEvents)return;let s={type:"connection_retry",tenantId:e,error:a.message,metadata:{attempt:t,maxAttempts:r,delayMs:o}};this.logger(`${le} tenant=${e} CONNECTION_RETRY attempt=${t}/${r} delay=${o}ms error="${a.message}"`,s);}logConnectionSuccess(e,t,r){if(!this.enabled||!this.logPoolEvents)return;let a={type:"pool_created",tenantId:e,durationMs:r,metadata:{attempts:t}};t>1&&this.logger(`${le} tenant=${e} CONNECTION_SUCCESS attempts=${t} totalTime=${r}ms`,a);}log(e,t){this.enabled&&this.logger(`${le} ${e}`,t);}defaultLogger(e,t){console.log(e);}truncateQuery(e,t=100){let r=e.replace(/\s+/g," ").trim();return r.length<=t?r:r.substring(0,t-3)+"..."}};function Kr(n){return new sn(n)}var ke=class{cache;poolTtlMs;onDispose;constructor(e){this.poolTtlMs=e.poolTtlMs,this.onDispose=e.onDispose,this.cache=new LRUCache({max:e.maxPools,dispose:(t,r)=>{this.handleDispose(r,t);},noDisposeOnSet:true});}get(e){return this.cache.get(e)}set(e,t){this.cache.set(e,t);}has(e){return this.cache.has(e)}delete(e){return this.cache.delete(e)}size(){return this.cache.size}keys(){return Array.from(this.cache.keys())}*entries(){for(let[e,t]of this.cache.entries())yield [e,t];}async clear(){this.cache.clear(),await Promise.resolve();}evictLRU(){let e=Array.from(this.cache.keys());if(e.length===0)return;let t=e[e.length-1];return this.cache.delete(t),t}async evictExpired(){if(!this.poolTtlMs)return [];let e=Date.now(),t=[];for(let[r,a]of this.cache.entries())e-a.lastAccess>this.poolTtlMs&&t.push(r);for(let r of t)this.cache.delete(r);return t}touch(e){let t=this.cache.get(e);t&&(t.lastAccess=Date.now());}getMaxPools(){return this.cache.max}getTtlMs(){return this.poolTtlMs}isExpired(e){return this.poolTtlMs?Date.now()-e.lastAccess>this.poolTtlMs:false}async handleDispose(e,t){this.onDispose&&await this.onDispose(e,t);}};function cn(n){let e=n.message.toLowerCase();return !!(e.includes("econnrefused")||e.includes("econnreset")||e.includes("etimedout")||e.includes("enotfound")||e.includes("connection refused")||e.includes("connection reset")||e.includes("connection terminated")||e.includes("connection timed out")||e.includes("timeout expired")||e.includes("socket hang up")||e.includes("too many connections")||e.includes("sorry, too many clients")||e.includes("the database system is starting up")||e.includes("the database system is shutting down")||e.includes("server closed the connection unexpectedly")||e.includes("could not connect to server")||e.includes("ssl connection")||e.includes("ssl handshake"))}function Hs(n){return new Promise(e=>setTimeout(e,n))}var Ne=class{config;constructor(e){this.config={maxAttempts:e?.maxAttempts??ee.retry.maxAttempts,initialDelayMs:e?.initialDelayMs??ee.retry.initialDelayMs,maxDelayMs:e?.maxDelayMs??ee.retry.maxDelayMs,backoffMultiplier:e?.backoffMultiplier??ee.retry.backoffMultiplier,jitter:e?.jitter??ee.retry.jitter,isRetryable:e?.isRetryable??cn,onRetry:e?.onRetry};}async withRetry(e,t){let r=t?{...this.config,...t}:this.config,a=Date.now(),o=null;for(let s=0;s<r.maxAttempts;s++)try{return {result:await e(),attempts:s+1,totalTimeMs:Date.now()-a}}catch(i){o=i;let c=s>=r.maxAttempts-1,l=r.isRetryable??this.isRetryable;if(c||!l(o))throw o;let m=this.calculateDelay(s,r);r.onRetry?.(s+1,o,m),await Hs(m);}throw o??new Error("Retry failed with no error")}calculateDelay(e,t){let r=t?{...this.config,...t}:this.config,a=r.initialDelayMs*Math.pow(r.backoffMultiplier,e),o=Math.min(a,r.maxDelayMs);if(r.jitter){let s=1+Math.random()*.25;return Math.floor(o*s)}return Math.floor(o)}isRetryable(e){return (this.config.isRetryable??cn)(e)}getConfig(){return {...this.config}}getMaxAttempts(){return this.config.maxAttempts}};var Ie=class{constructor(e){this.deps=e;}async checkHealth(e={}){let t=Date.now(),{ping:r=true,pingTimeoutMs:a=5e3,includeShared:o=true,tenantIds:s}=e,i=[],c="ok",l,m,p=this.getPoolsToCheck(s).map(async({schemaName:x,tenantId:S,entry:M})=>this.checkPoolHealth(S,x,M,r,a));i.push(...await Promise.all(p));let h=this.deps.getSharedPool();if(o&&h){let x=await this.checkSharedDbHealth(h,r,a);c=x.status,l=x.responseTimeMs,m=x.error;}let g=i.filter(x=>x.status==="degraded").length,w=i.filter(x=>x.status==="unhealthy").length,b={healthy:w===0&&c!=="unhealthy",pools:i,sharedDb:c,totalPools:i.length,degradedPools:g,unhealthyPools:w,timestamp:new Date().toISOString(),durationMs:Date.now()-t};return l!==void 0&&(b.sharedDbResponseTimeMs=l),m!==void 0&&(b.sharedDbError=m),b}getPoolsToCheck(e){let t=[];if(e&&e.length>0)for(let r of e){let a=this.deps.getSchemaName(r),o=this.deps.getPoolEntry(a);o&&t.push({schemaName:a,tenantId:r,entry:o});}else for(let[r,a]of this.deps.getPoolEntries()){let o=this.deps.getTenantIdBySchema(r)??r;t.push({schemaName:r,tenantId:o,entry:a});}return t}async checkPoolHealth(e,t,r,a,o){let s=r.pool,i=s.totalCount,c=s.idleCount,l=s.waitingCount,m="ok",u,p;if(l>0&&(m="degraded"),a){let g=await this.executePingQuery(s,o);u=g.responseTimeMs,g.success?g.responseTimeMs&&g.responseTimeMs>o/2&&m==="ok"&&(m="degraded"):(m="unhealthy",p=g.error);}let h={tenantId:e,schemaName:t,status:m,totalConnections:i,idleConnections:c,waitingRequests:l};return u!==void 0&&(h.responseTimeMs=u),p!==void 0&&(h.error=p),h}async checkSharedDbHealth(e,t,r){let a="ok",o,s;if(e.waitingCount>0&&(a="degraded"),t){let l=await this.executePingQuery(e,r);o=l.responseTimeMs,l.success?l.responseTimeMs&&l.responseTimeMs>r/2&&a==="ok"&&(a="degraded"):(a="unhealthy",s=l.error);}let c={status:a};return o!==void 0&&(c.responseTimeMs=o),s!==void 0&&(c.error=s),c}async executePingQuery(e,t){let r=Date.now();try{let a=new Promise((s,i)=>{setTimeout(()=>i(new Error("Health check ping timeout")),t);}),o=e.query("SELECT 1");return await Promise.race([o,a]),{success:!0,responseTimeMs:Date.now()-r}}catch(a){return {success:false,responseTimeMs:Date.now()-r,error:a.message}}}determineOverallHealth(e,t="ok"){return e.filter(a=>a.status==="unhealthy").length===0&&t!=="unhealthy"}};var xe=class{constructor(e){this.config=e;let t=e.isolation.maxPools??ee.maxPools,r=e.isolation.poolTtlMs??ee.poolTtlMs;this.debugLogger=Kr(e.debug),this.retryHandler=new Ne(e.connection.retry),this.poolCache=new ke({maxPools:t,poolTtlMs:r,onDispose:(a,o)=>{this.disposePoolEntry(o,a);}}),this.healthChecker=new Ie({getPoolEntries:()=>this.poolCache.entries(),getTenantIdBySchema:a=>this.tenantIdBySchema.get(a),getPoolEntry:a=>this.poolCache.get(a),getSchemaName:a=>this.config.isolation.schemaNameTemplate(a),getSharedPool:()=>this.sharedPool});}poolCache;tenantIdBySchema=new Map;pendingConnections=new Map;sharedPool=null;sharedDb=null;sharedDbPending=null;cleanupInterval=null;disposed=false;debugLogger;retryHandler;healthChecker;getDb(e){this.ensureNotDisposed();let t=this.config.isolation.schemaNameTemplate(e),r=this.poolCache.get(t);return r||(r=this.createPoolEntry(e,t),this.poolCache.set(t,r),this.tenantIdBySchema.set(t,e),this.debugLogger.logPoolCreated(e,t),this.config.hooks?.onPoolCreated?.(e)),this.poolCache.touch(t),r.db}async getDbAsync(e){this.ensureNotDisposed();let t=this.config.isolation.schemaNameTemplate(e),r=this.poolCache.get(t);if(r)return this.poolCache.touch(t),r.db;let a=this.pendingConnections.get(t);if(a)return r=await a,this.poolCache.touch(t),r.db;let o=this.connectWithRetry(e,t);this.pendingConnections.set(t,o);try{return r=await o,this.poolCache.set(t,r),this.tenantIdBySchema.set(t,e),this.debugLogger.logPoolCreated(e,t),this.config.hooks?.onPoolCreated?.(e),this.poolCache.touch(t),r.db}finally{this.pendingConnections.delete(t);}}async connectWithRetry(e,t){let r=this.retryHandler.getConfig(),a=r.maxAttempts,o=await this.retryHandler.withRetry(async()=>{let s=this.createPoolEntry(e,t);try{return await s.pool.query("SELECT 1"),s}catch(i){try{await s.pool.end();}catch{}throw i}},{onRetry:(s,i,c)=>{this.debugLogger.logConnectionRetry(e,s,a,i,c),r.onRetry?.(s,i,c);}});return this.debugLogger.logConnectionSuccess(e,o.attempts,o.totalTimeMs),o.result}getSharedDb(){return this.ensureNotDisposed(),this.sharedDb||(this.sharedPool=new Pool({connectionString:this.config.connection.url,...ee.poolConfig,...this.config.connection.poolConfig}),this.sharedPool.on("error",e=>{this.config.hooks?.onError?.("shared",e);}),this.sharedDb=drizzle(this.sharedPool,{schema:this.config.schemas.shared})),this.sharedDb}async getSharedDbAsync(){if(this.ensureNotDisposed(),this.sharedDb)return this.sharedDb;if(this.sharedDbPending)return this.sharedDbPending;this.sharedDbPending=this.connectSharedWithRetry();try{return await this.sharedDbPending}finally{this.sharedDbPending=null;}}async connectSharedWithRetry(){let e=this.retryHandler.getConfig(),t=e.maxAttempts,r=await this.retryHandler.withRetry(async()=>{let a=new Pool({connectionString:this.config.connection.url,...ee.poolConfig,...this.config.connection.poolConfig});try{return await a.query("SELECT 1"),a.on("error",o=>{this.config.hooks?.onError?.("shared",o);}),this.sharedPool=a,this.sharedDb=drizzle(a,{schema:this.config.schemas.shared}),this.sharedDb}catch(o){try{await a.end();}catch{}throw o}},{onRetry:(a,o,s)=>{this.debugLogger.logConnectionRetry("shared",a,t,o,s),e.onRetry?.(a,o,s);}});return this.debugLogger.logConnectionSuccess("shared",r.attempts,r.totalTimeMs),r.result}getSchemaName(e){return this.config.isolation.schemaNameTemplate(e)}hasPool(e){let t=this.config.isolation.schemaNameTemplate(e);return this.poolCache.has(t)}getPoolCount(){return this.poolCache.size()}getActiveTenantIds(){return Array.from(this.tenantIdBySchema.values())}getRetryConfig(){return this.retryHandler.getConfig()}async warmup(e,t={}){this.ensureNotDisposed();let r=Date.now(),{concurrency:a=10,ping:o=true,onProgress:s}=t,i=[];for(let c=0;c<e.length;c+=a){let l=e.slice(c,c+a),m=await Promise.all(l.map(async u=>{let p=Date.now();s?.(u,"starting");try{let h=this.hasPool(u);o?await this.getDbAsync(u):this.getDb(u);let g=Date.now()-p;return s?.(u,"completed"),this.debugLogger.logWarmup(u,!0,g,h),{tenantId:u,success:!0,alreadyWarm:h,durationMs:g}}catch(h){let g=Date.now()-p;return s?.(u,"failed"),this.debugLogger.logWarmup(u,false,g,false),{tenantId:u,success:false,alreadyWarm:false,durationMs:g,error:h.message}}}));i.push(...m);}return {total:i.length,succeeded:i.filter(c=>c.success).length,failed:i.filter(c=>!c.success).length,alreadyWarm:i.filter(c=>c.alreadyWarm).length,durationMs:Date.now()-r,details:i}}getMetrics(){this.ensureNotDisposed();let e=this.config.isolation.maxPools??ee.maxPools,t=[];for(let[r,a]of this.poolCache.entries()){let o=this.tenantIdBySchema.get(r)??r,s=a.pool;t.push({tenantId:o,schemaName:r,connections:{total:s.totalCount,idle:s.idleCount,waiting:s.waitingCount},lastAccessedAt:new Date(a.lastAccess).toISOString()});}return {pools:{total:t.length,maxPools:e,tenants:t},shared:{initialized:this.sharedPool!==null,connections:this.sharedPool?{total:this.sharedPool.totalCount,idle:this.sharedPool.idleCount,waiting:this.sharedPool.waitingCount}:null},timestamp:new Date().toISOString()}}async healthCheck(e={}){return this.ensureNotDisposed(),this.healthChecker.checkHealth(e)}async evictPool(e,t="manual"){let r=this.config.isolation.schemaNameTemplate(e),a=this.poolCache.get(r);a&&(this.debugLogger.logPoolEvicted(e,r,t),this.poolCache.delete(r),this.tenantIdBySchema.delete(r),await this.closePool(a.pool,e));}startCleanup(){if(this.cleanupInterval)return;let e=ee.cleanupIntervalMs;this.cleanupInterval=setInterval(()=>{this.cleanupIdlePools();},e),this.cleanupInterval.unref();}stopCleanup(){this.cleanupInterval&&(clearInterval(this.cleanupInterval),this.cleanupInterval=null);}async dispose(){if(this.disposed)return;this.disposed=true,this.stopCleanup();let e=[];for(let[t,r]of this.poolCache.entries()){let a=this.tenantIdBySchema.get(t);e.push(this.closePool(r.pool,a??t));}await this.poolCache.clear(),this.tenantIdBySchema.clear(),this.sharedPool&&(e.push(this.closePool(this.sharedPool,"shared")),this.sharedPool=null,this.sharedDb=null),await Promise.all(e);}createPoolEntry(e,t){let r=new Pool({connectionString:this.config.connection.url,...ee.poolConfig,...this.config.connection.poolConfig,options:`-c search_path=${t},public`});return r.on("error",async o=>{this.debugLogger.logPoolError(e,o),this.config.hooks?.onError?.(e,o),await this.evictPool(e,"error");}),{db:drizzle(r,{schema:this.config.schemas.tenant}),pool:r,lastAccess:Date.now(),schemaName:t}}disposePoolEntry(e,t){let r=this.tenantIdBySchema.get(t);this.tenantIdBySchema.delete(t),r&&this.debugLogger.logPoolEvicted(r,t,"lru_eviction"),this.closePool(e.pool,r??t).then(()=>{r&&this.config.hooks?.onPoolEvicted?.(r);});}async closePool(e,t){try{await e.end();}catch(r){this.config.hooks?.onError?.(t,r);}}async cleanupIdlePools(){let e=await this.poolCache.evictExpired();for(let t of e){let r=this.tenantIdBySchema.get(t);r&&(this.debugLogger.logPoolEvicted(r,t,"ttl_expired"),this.tenantIdBySchema.delete(t));}}ensureNotDisposed(){if(this.disposed)throw new Error("[drizzle-multitenant] TenantManager has been disposed")}};var we=class{constructor(e){this.tenantManager=e;}async collect(e={}){let{includeHealth:t=false,healthPingTimeoutMs:r=5e3,tenantIds:a}=e,o=this.tenantManager.getMetrics(),s;return t&&(s=await this.tenantManager.healthCheck({ping:true,pingTimeoutMs:r,includeShared:true,...a&&{tenantIds:a}})),{pools:o,health:s,collectedAt:new Date().toISOString()}}getPoolMetrics(){return this.tenantManager.getMetrics()}async getHealthMetrics(e){return this.tenantManager.healthCheck({ping:true,pingTimeoutMs:e?.pingTimeoutMs??5e3,includeShared:true,...e?.tenantIds&&{tenantIds:e.tenantIds}})}getRuntimeMetrics(){let e=process.memoryUsage();return {uptimeSeconds:process.uptime(),memoryUsage:{heapTotal:e.heapTotal,heapUsed:e.heapUsed,external:e.external,rss:e.rss},activeHandles:process._getActiveHandles?.()?.length??0,activeRequests:process._getActiveRequests?.()?.length??0}}calculateSummary(e){let t=e.pools,r=e.health,a=0,o=0,s=0;for(let m of t.pools.tenants)a+=m.connections.total,o+=m.connections.idle,s+=m.connections.waiting;t.shared.connections&&(a+=t.shared.connections.total,o+=t.shared.connections.idle,s+=t.shared.connections.waiting);let i=r?r.pools.filter(m=>m.status==="ok").length:t.pools.total,c=r?.degradedPools??0,l=r?.unhealthyPools??0;return {activePools:t.pools.total,totalConnections:a,idleConnections:o,waitingRequests:s,healthyPools:i,degradedPools:c,unhealthyPools:l}}};var Bs="drizzle_multitenant",Gs=[{name:"pools_active",help:"Number of active database pools",type:"gauge",extract:n=>[{value:n.pools.pools.total}]},{name:"pools_max",help:"Maximum number of pools allowed",type:"gauge",extract:n=>[{value:n.pools.pools.maxPools}]},{name:"pool_connections_total",help:"Total connections in pool",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:e.connections.total}))},{name:"pool_connections_idle",help:"Idle connections in pool",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:e.connections.idle}))},{name:"pool_connections_waiting",help:"Waiting requests in pool queue",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:e.connections.waiting}))},{name:"pool_last_access_timestamp",help:"Last access timestamp for pool (unix epoch)",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:new Date(e.lastAccessedAt).getTime()/1e3}))},{name:"shared_pool_initialized",help:"Whether shared database pool is initialized (1=yes, 0=no)",type:"gauge",extract:n=>[{value:n.pools.shared.initialized?1:0}]},{name:"shared_pool_connections_total",help:"Total connections in shared pool",type:"gauge",extract:n=>[{value:n.pools.shared.connections?.total??0}]},{name:"shared_pool_connections_idle",help:"Idle connections in shared pool",type:"gauge",extract:n=>[{value:n.pools.shared.connections?.idle??0}]},{name:"shared_pool_connections_waiting",help:"Waiting requests in shared pool queue",type:"gauge",extract:n=>[{value:n.pools.shared.connections?.waiting??0}]},{name:"health_status",help:"Overall health status (1=healthy, 0=unhealthy)",type:"gauge",extract:n=>n.health?[{value:n.health.healthy?1:0}]:[]},{name:"health_pools_total",help:"Total number of pools checked",type:"gauge",extract:n=>n.health?[{value:n.health.totalPools}]:[]},{name:"health_pools_degraded",help:"Number of degraded pools",type:"gauge",extract:n=>n.health?[{value:n.health.degradedPools}]:[]},{name:"health_pools_unhealthy",help:"Number of unhealthy pools",type:"gauge",extract:n=>n.health?[{value:n.health.unhealthyPools}]:[]},{name:"health_check_duration_seconds",help:"Duration of health check in seconds",type:"gauge",extract:n=>n.health?[{value:n.health.durationMs/1e3}]:[]},{name:"pool_health_status",help:"Health status per pool (1=ok, 0.5=degraded, 0=unhealthy)",type:"gauge",labels:["tenant","schema","status"],extract:n=>n.health?.pools.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName,status:e.status},value:e.status==="ok"?1:e.status==="degraded"?.5:0}))??[]},{name:"pool_response_time_seconds",help:"Pool ping response time in seconds",type:"gauge",labels:["tenant","schema"],extract:n=>n.health?.pools.filter(e=>e.responseTimeMs!==void 0).map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:(e.responseTimeMs??0)/1e3}))??[]},{name:"shared_db_health_status",help:"Shared database health status (1=ok, 0.5=degraded, 0=unhealthy)",type:"gauge",extract:n=>{if(!n.health)return [];let e=n.health.sharedDb;return [{value:e==="ok"?1:e==="degraded"?.5:0}]}},{name:"shared_db_response_time_seconds",help:"Shared database ping response time in seconds",type:"gauge",extract:n=>n.health?.sharedDbResponseTimeMs!==void 0?[{value:n.health.sharedDbResponseTimeMs/1e3}]:[]}],Js=[{name:"process_uptime_seconds",help:"Process uptime in seconds",type:"gauge",extract:n=>[{value:n.uptimeSeconds}]},{name:"process_heap_bytes_total",help:"Total heap memory in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.heapTotal}]},{name:"process_heap_bytes_used",help:"Used heap memory in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.heapUsed}]},{name:"process_external_bytes",help:"External memory in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.external}]},{name:"process_rss_bytes",help:"Resident Set Size in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.rss}]},{name:"process_active_handles",help:"Number of active handles",type:"gauge",extract:n=>[{value:n.activeHandles}]},{name:"process_active_requests",help:"Number of active requests",type:"gauge",extract:n=>[{value:n.activeRequests}]}],$e=class{prefix;includeTenantLabels;includeTimestamps;defaultLabels;constructor(e={}){this.prefix=e.prefix??Bs,this.includeTenantLabels=e.includeTenantLabels??true,this.includeTimestamps=e.includeTimestamps??false,this.defaultLabels=e.defaultLabels??{};}export(e,t){let r=this.toPrometheusMetrics(e),a=[];for(let o of r)a.push(...this.formatMetric(o));if(t){let o=this.extractRuntimeMetrics(t);for(let s of o)a.push(...this.formatMetric(s));}return a.join(`
|
|
1736
|
+
`)}function dt(n,e){switch(e.format){case "json":return js(n);case "github":return qs(n);default:return Ls(n,e)}}var ze=class{renderer;ctx;constructor(e,t){this.ctx=e,this.renderer=t||new V;}async show(){switch(this.renderer.clearScreen(),this.renderer.showHeader("Schema Lint"),await select({message:"What would you like to do?",choices:[{name:"Lint schemas with default rules",value:"default"},{name:"Configure rules and lint",value:"configure"},{name:"View available rules",value:"rules"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "default":return this.runLint();case "configure":return this.configureAndLint();case "rules":return this.showRules();default:return {type:"back"}}}async runLint(e){this.renderer.clearScreen(),this.renderer.showHeader("Linting Schemas...");let{tenantDir:t,sharedDir:r}=await this.findSchemaDirectories();if(!t&&!r)return this.renderer.showStatus("No schema directories found","error"),console.log(""),console.log(" Please specify schema directories or create them in:"),console.log(" - ./src/db/schema/tenant"),console.log(" - ./src/db/schema/shared"),console.log(""),await this.renderer.pressEnterToContinue(),this.show();console.log(` Tenant schemas: ${t??M.dim("not found")}`),console.log(` Shared schemas: ${r??M.dim("not found")}`),console.log("");try{let o=await Pe({rules:e}).lintDirectories({tenant:t,shared:r});return this.showResults(o)}catch(a){return this.renderer.showStatus(a.message,"error"),await this.renderer.pressEnterToContinue(),this.show()}}async findSchemaDirectories(){let e={tenant:["./src/db/schema/tenant","./src/schema/tenant","./drizzle/schema/tenant","./db/schema/tenant"],shared:["./src/db/schema/shared","./src/schema/shared","./drizzle/schema/shared","./db/schema/shared"]},t,r;for(let a of e.tenant){let o=resolve(process.cwd(),a);if(existsSync(o)){t=o;break}}for(let a of e.shared){let o=resolve(process.cwd(),a);if(existsSync(o)){r=o;break}}return {tenantDir:t,sharedDir:r}}async configureAndLint(){this.renderer.clearScreen(),this.renderer.showHeader("Configure Lint Rules");let e=await checkbox({message:"Select rules to enable:",choices:_e.map(a=>({name:`${a.name} - ${a.description}`,value:a.name,checked:a.defaultSeverity!=="off"}))});if(e.length===0)return this.renderer.showStatus("No rules selected","warning"),await this.renderer.pressEnterToContinue(),this.show();let t=await select({message:"Default severity for selected rules:",choices:[{name:"Error (fail on issues)",value:"error"},{name:"Warning (report but pass)",value:"warn"}]}),r={};for(let a of e)r[a]=t;return this.runLint(r)}async showResults(e){this.renderer.clearScreen(),this.renderer.showHeader("Lint Results");let{errors:t,warnings:r,totalTables:a,totalFiles:o}=e.summary;if(t===0&&r===0)console.log(M.green.bold(" \u2713 All schemas passed validation!")),console.log(""),console.log(` Files: ${o}`),console.log(` Tables: ${a}`),console.log(` Issues: ${M.green("0")}`),console.log(""),console.log(M.dim(` Completed in ${e.durationMs}ms`));else {for(let i of e.files)if(i.issues.length!==0){console.log(""),console.log(M.bold(i.filePath));for(let c of i.issues)this.printIssue(c);}console.log(""),console.log(M.bold(" Summary:")),console.log(` Files: ${o}`),console.log(` Tables: ${a}`),console.log(` Errors: ${t>0?M.red(t.toString()):M.green("0")}`),console.log(` Warnings: ${r>0?M.yellow(r.toString()):M.green("0")}`),console.log(""),console.log(M.dim(` Completed in ${e.durationMs}ms`)),console.log(""),e.passed?console.log(M.yellow.bold(" \u26A0 Schema validation passed with warnings")):console.log(M.red.bold(" \u2717 Schema validation failed"));}switch(console.log(""),await select({message:"What would you like to do?",choices:[{name:"Run lint again",value:"again"},{name:"Configure rules and re-run",value:"configure"},{name:"View rule details",value:"rules"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "again":return this.runLint();case "configure":return this.configureAndLint();case "rules":return this.showRules();default:return {type:"back"}}}printIssue(e){let t=e.severity==="error"?M.red("\u2717"):M.yellow("\u26A0"),r=e.severity==="error"?M.red("error"):M.yellow("warn"),a=M.dim(`(${e.rule})`);if(console.log(` ${t} ${r} ${e.message} ${a}`),e.table){let o=e.column?`${e.table}.${e.column}`:e.table;console.log(M.dim(` ${o}`));}e.suggestion&&console.log(M.cyan(` \u2192 ${e.suggestion}`));}async showRules(){return this.renderer.clearScreen(),this.renderer.showHeader("Available Lint Rules"),console.log(M.bold(" Naming Rules:")),console.log(` ${M.cyan("table-naming")} Enforce table naming convention`),console.log(` ${M.cyan("column-naming")} Enforce column naming convention`),console.log(""),console.log(M.bold(" Convention Rules:")),console.log(` ${M.cyan("require-primary-key")} Require every table to have a PK`),console.log(` ${M.cyan("prefer-uuid-pk")} Prefer UUID over serial for PKs`),console.log(` ${M.cyan("require-timestamps")} Require created_at/updated_at`),console.log(` ${M.cyan("index-foreign-keys")} Require indexes on FK columns`),console.log(""),console.log(M.bold(" Security Rules:")),console.log(` ${M.cyan("no-cascade-delete")} Warn about CASCADE DELETE`),console.log(` ${M.cyan("require-soft-delete")} Require soft delete column`),console.log(""),console.log(M.dim(" Configure rules in tenant.config.ts:")),console.log(M.dim(" lint: {")),console.log(M.dim(" rules: {")),console.log(M.dim(" 'table-naming': ['error', { style: 'snake_case' }],")),console.log(M.dim(" 'require-primary-key': 'error',")),console.log(M.dim(" }")),console.log(M.dim(" }")),console.log(""),await this.renderer.pressEnterToContinue(),this.show()}};var ee={maxPools:50,poolTtlMs:36e5,cleanupIntervalMs:6e4,poolConfig:{max:10,idleTimeoutMillis:3e4,connectionTimeoutMillis:5e3},retry:{maxAttempts:3,initialDelayMs:100,maxDelayMs:5e3,backoffMultiplier:2,jitter:true}};var le="[drizzle-multitenant]";var sn=class{enabled;logQueries;logPoolEvents;slowQueryThreshold;logger;constructor(e){this.enabled=e?.enabled??false,this.logQueries=e?.logQueries??true,this.logPoolEvents=e?.logPoolEvents??true,this.slowQueryThreshold=e?.slowQueryThreshold??1e3,this.logger=e?.logger??this.defaultLogger;}isEnabled(){return this.enabled}logQuery(e,t,r){if(!this.enabled||!this.logQueries)return;let a=r>=this.slowQueryThreshold,s={type:a?"slow_query":"query",tenantId:e,query:this.truncateQuery(t),durationMs:r};a?this.logger(`${le} tenant=${e} SLOW_QUERY duration=${r}ms query="${this.truncateQuery(t)}"`,s):this.logger(`${le} tenant=${e} query="${this.truncateQuery(t)}" duration=${r}ms`,s);}logPoolCreated(e,t){if(!this.enabled||!this.logPoolEvents)return;let r={type:"pool_created",tenantId:e,schemaName:t};this.logger(`${le} tenant=${e} POOL_CREATED schema=${t}`,r);}logPoolEvicted(e,t,r){if(!this.enabled||!this.logPoolEvents)return;let a={type:"pool_evicted",tenantId:e,schemaName:t,metadata:r?{reason:r}:void 0},o=r?` reason=${r}`:"";this.logger(`${le} tenant=${e} POOL_EVICTED schema=${t}${o}`,a);}logPoolError(e,t){if(!this.enabled||!this.logPoolEvents)return;let r={type:"pool_error",tenantId:e,error:t.message};this.logger(`${le} tenant=${e} POOL_ERROR error="${t.message}"`,r);}logWarmup(e,t,r,a){if(!this.enabled||!this.logPoolEvents)return;let o={type:"warmup",tenantId:e,durationMs:r,metadata:{success:t,alreadyWarm:a}},s=a?"already_warm":t?"success":"failed";this.logger(`${le} tenant=${e} WARMUP status=${s} duration=${r}ms`,o);}logConnectionRetry(e,t,r,a,o){if(!this.enabled||!this.logPoolEvents)return;let s={type:"connection_retry",tenantId:e,error:a.message,metadata:{attempt:t,maxAttempts:r,delayMs:o}};this.logger(`${le} tenant=${e} CONNECTION_RETRY attempt=${t}/${r} delay=${o}ms error="${a.message}"`,s);}logConnectionSuccess(e,t,r){if(!this.enabled||!this.logPoolEvents)return;let a={type:"pool_created",tenantId:e,durationMs:r,metadata:{attempts:t}};t>1&&this.logger(`${le} tenant=${e} CONNECTION_SUCCESS attempts=${t} totalTime=${r}ms`,a);}log(e,t){this.enabled&&this.logger(`${le} ${e}`,t);}defaultLogger(e,t){console.log(e);}truncateQuery(e,t=100){let r=e.replace(/\s+/g," ").trim();return r.length<=t?r:r.substring(0,t-3)+"..."}};function Kr(n){return new sn(n)}var ke=class{cache;poolTtlMs;onDispose;constructor(e){this.poolTtlMs=e.poolTtlMs,this.onDispose=e.onDispose,this.cache=new LRUCache({max:e.maxPools,dispose:(t,r)=>{this.handleDispose(r,t);},noDisposeOnSet:true});}get(e){return this.cache.get(e)}set(e,t){this.cache.set(e,t);}has(e){return this.cache.has(e)}delete(e){return this.cache.delete(e)}size(){return this.cache.size}keys(){return Array.from(this.cache.keys())}*entries(){for(let[e,t]of this.cache.entries())yield [e,t];}async clear(){this.cache.clear(),await Promise.resolve();}evictLRU(){let e=Array.from(this.cache.keys());if(e.length===0)return;let t=e[e.length-1];return this.cache.delete(t),t}async evictExpired(){if(!this.poolTtlMs)return [];let e=Date.now(),t=[];for(let[r,a]of this.cache.entries())e-a.lastAccess>this.poolTtlMs&&t.push(r);for(let r of t)this.cache.delete(r);return t}touch(e){let t=this.cache.get(e);t&&(t.lastAccess=Date.now());}getMaxPools(){return this.cache.max}getTtlMs(){return this.poolTtlMs}isExpired(e){return this.poolTtlMs?Date.now()-e.lastAccess>this.poolTtlMs:false}async handleDispose(e,t){this.onDispose&&await this.onDispose(e,t);}};function cn(n){let e=n.message.toLowerCase();return !!(e.includes("econnrefused")||e.includes("econnreset")||e.includes("etimedout")||e.includes("enotfound")||e.includes("connection refused")||e.includes("connection reset")||e.includes("connection terminated")||e.includes("connection timed out")||e.includes("timeout expired")||e.includes("socket hang up")||e.includes("too many connections")||e.includes("sorry, too many clients")||e.includes("the database system is starting up")||e.includes("the database system is shutting down")||e.includes("server closed the connection unexpectedly")||e.includes("could not connect to server")||e.includes("ssl connection")||e.includes("ssl handshake"))}function Hs(n){return new Promise(e=>setTimeout(e,n))}var Ne=class{config;constructor(e){this.config={maxAttempts:e?.maxAttempts??ee.retry.maxAttempts,initialDelayMs:e?.initialDelayMs??ee.retry.initialDelayMs,maxDelayMs:e?.maxDelayMs??ee.retry.maxDelayMs,backoffMultiplier:e?.backoffMultiplier??ee.retry.backoffMultiplier,jitter:e?.jitter??ee.retry.jitter,isRetryable:e?.isRetryable??cn,onRetry:e?.onRetry};}async withRetry(e,t){let r=t?{...this.config,...t}:this.config,a=Date.now(),o=null;for(let s=0;s<r.maxAttempts;s++)try{return {result:await e(),attempts:s+1,totalTimeMs:Date.now()-a}}catch(i){o=i;let c=s>=r.maxAttempts-1,l=r.isRetryable??this.isRetryable;if(c||!l(o))throw o;let m=this.calculateDelay(s,r);r.onRetry?.(s+1,o,m),await Hs(m);}throw o??new Error("Retry failed with no error")}calculateDelay(e,t){let r=t?{...this.config,...t}:this.config,a=r.initialDelayMs*Math.pow(r.backoffMultiplier,e),o=Math.min(a,r.maxDelayMs);if(r.jitter){let s=1+Math.random()*.25;return Math.floor(o*s)}return Math.floor(o)}isRetryable(e){return (this.config.isRetryable??cn)(e)}getConfig(){return {...this.config}}getMaxAttempts(){return this.config.maxAttempts}};var Ie=class{constructor(e){this.deps=e;}async checkHealth(e={}){let t=Date.now(),{ping:r=true,pingTimeoutMs:a=5e3,includeShared:o=true,tenantIds:s}=e,i=[],c="ok",l,m,p=this.getPoolsToCheck(s).map(async({schemaName:x,tenantId:y,entry:w})=>this.checkPoolHealth(y,x,w,r,a));i.push(...await Promise.all(p));let h=this.deps.getSharedPool();if(o&&h){let x=await this.checkSharedDbHealth(h,r,a);c=x.status,l=x.responseTimeMs,m=x.error;}let g=i.filter(x=>x.status==="degraded").length,$=i.filter(x=>x.status==="unhealthy").length,b={healthy:$===0&&c!=="unhealthy",pools:i,sharedDb:c,totalPools:i.length,degradedPools:g,unhealthyPools:$,timestamp:new Date().toISOString(),durationMs:Date.now()-t};return l!==void 0&&(b.sharedDbResponseTimeMs=l),m!==void 0&&(b.sharedDbError=m),b}getPoolsToCheck(e){let t=[];if(e&&e.length>0)for(let r of e){let a=this.deps.getSchemaName(r),o=this.deps.getPoolEntry(a);o&&t.push({schemaName:a,tenantId:r,entry:o});}else for(let[r,a]of this.deps.getPoolEntries()){let o=this.deps.getTenantIdBySchema(r)??r;t.push({schemaName:r,tenantId:o,entry:a});}return t}async checkPoolHealth(e,t,r,a,o){let s=r.pool,i=s.totalCount,c=s.idleCount,l=s.waitingCount,m="ok",u,p;if(l>0&&(m="degraded"),a){let g=await this.executePingQuery(s,o);u=g.responseTimeMs,g.success?g.responseTimeMs&&g.responseTimeMs>o/2&&m==="ok"&&(m="degraded"):(m="unhealthy",p=g.error);}let h={tenantId:e,schemaName:t,status:m,totalConnections:i,idleConnections:c,waitingRequests:l};return u!==void 0&&(h.responseTimeMs=u),p!==void 0&&(h.error=p),h}async checkSharedDbHealth(e,t,r){let a="ok",o,s;if(e.waitingCount>0&&(a="degraded"),t){let l=await this.executePingQuery(e,r);o=l.responseTimeMs,l.success?l.responseTimeMs&&l.responseTimeMs>r/2&&a==="ok"&&(a="degraded"):(a="unhealthy",s=l.error);}let c={status:a};return o!==void 0&&(c.responseTimeMs=o),s!==void 0&&(c.error=s),c}async executePingQuery(e,t){let r=Date.now();try{let a=new Promise((s,i)=>{setTimeout(()=>i(new Error("Health check ping timeout")),t);}),o=e.query("SELECT 1");return await Promise.race([o,a]),{success:!0,responseTimeMs:Date.now()-r}}catch(a){return {success:false,responseTimeMs:Date.now()-r,error:a.message}}}determineOverallHealth(e,t="ok"){return e.filter(a=>a.status==="unhealthy").length===0&&t!=="unhealthy"}};var xe=class{constructor(e){this.config=e;let t=e.isolation.maxPools??ee.maxPools,r=e.isolation.poolTtlMs??ee.poolTtlMs;this.debugLogger=Kr(e.debug),this.retryHandler=new Ne(e.connection.retry),this.poolCache=new ke({maxPools:t,poolTtlMs:r,onDispose:(a,o)=>{this.disposePoolEntry(o,a);}}),this.healthChecker=new Ie({getPoolEntries:()=>this.poolCache.entries(),getTenantIdBySchema:a=>this.tenantIdBySchema.get(a),getPoolEntry:a=>this.poolCache.get(a),getSchemaName:a=>this.config.isolation.schemaNameTemplate(a),getSharedPool:()=>this.sharedPool});}poolCache;tenantIdBySchema=new Map;pendingConnections=new Map;sharedPool=null;sharedDb=null;sharedDbPending=null;cleanupInterval=null;disposed=false;debugLogger;retryHandler;healthChecker;getDb(e){this.ensureNotDisposed();let t=this.config.isolation.schemaNameTemplate(e),r=this.poolCache.get(t);return r||(r=this.createPoolEntry(e,t),this.poolCache.set(t,r),this.tenantIdBySchema.set(t,e),this.debugLogger.logPoolCreated(e,t),this.config.hooks?.onPoolCreated?.(e)),this.poolCache.touch(t),r.db}async getDbAsync(e){this.ensureNotDisposed();let t=this.config.isolation.schemaNameTemplate(e),r=this.poolCache.get(t);if(r)return this.poolCache.touch(t),r.db;let a=this.pendingConnections.get(t);if(a)return r=await a,this.poolCache.touch(t),r.db;let o=this.connectWithRetry(e,t);this.pendingConnections.set(t,o);try{return r=await o,this.poolCache.set(t,r),this.tenantIdBySchema.set(t,e),this.debugLogger.logPoolCreated(e,t),this.config.hooks?.onPoolCreated?.(e),this.poolCache.touch(t),r.db}finally{this.pendingConnections.delete(t);}}async connectWithRetry(e,t){let r=this.retryHandler.getConfig(),a=r.maxAttempts,o=await this.retryHandler.withRetry(async()=>{let s=this.createPoolEntry(e,t);try{return await s.pool.query("SELECT 1"),s}catch(i){try{await s.pool.end();}catch{}throw i}},{onRetry:(s,i,c)=>{this.debugLogger.logConnectionRetry(e,s,a,i,c),r.onRetry?.(s,i,c);}});return this.debugLogger.logConnectionSuccess(e,o.attempts,o.totalTimeMs),o.result}getSharedDb(){return this.ensureNotDisposed(),this.sharedDb||(this.sharedPool=new Pool({connectionString:this.config.connection.url,...ee.poolConfig,...this.config.connection.poolConfig}),this.sharedPool.on("error",e=>{this.config.hooks?.onError?.("shared",e);}),this.sharedDb=drizzle(this.sharedPool,{schema:this.config.schemas.shared})),this.sharedDb}async getSharedDbAsync(){if(this.ensureNotDisposed(),this.sharedDb)return this.sharedDb;if(this.sharedDbPending)return this.sharedDbPending;this.sharedDbPending=this.connectSharedWithRetry();try{return await this.sharedDbPending}finally{this.sharedDbPending=null;}}async connectSharedWithRetry(){let e=this.retryHandler.getConfig(),t=e.maxAttempts,r=await this.retryHandler.withRetry(async()=>{let a=new Pool({connectionString:this.config.connection.url,...ee.poolConfig,...this.config.connection.poolConfig});try{return await a.query("SELECT 1"),a.on("error",o=>{this.config.hooks?.onError?.("shared",o);}),this.sharedPool=a,this.sharedDb=drizzle(a,{schema:this.config.schemas.shared}),this.sharedDb}catch(o){try{await a.end();}catch{}throw o}},{onRetry:(a,o,s)=>{this.debugLogger.logConnectionRetry("shared",a,t,o,s),e.onRetry?.(a,o,s);}});return this.debugLogger.logConnectionSuccess("shared",r.attempts,r.totalTimeMs),r.result}getSchemaName(e){return this.config.isolation.schemaNameTemplate(e)}hasPool(e){let t=this.config.isolation.schemaNameTemplate(e);return this.poolCache.has(t)}getPoolCount(){return this.poolCache.size()}getActiveTenantIds(){return Array.from(this.tenantIdBySchema.values())}getRetryConfig(){return this.retryHandler.getConfig()}async warmup(e,t={}){this.ensureNotDisposed();let r=Date.now(),{concurrency:a=10,ping:o=true,onProgress:s}=t,i=[];for(let c=0;c<e.length;c+=a){let l=e.slice(c,c+a),m=await Promise.all(l.map(async u=>{let p=Date.now();s?.(u,"starting");try{let h=this.hasPool(u);o?await this.getDbAsync(u):this.getDb(u);let g=Date.now()-p;return s?.(u,"completed"),this.debugLogger.logWarmup(u,!0,g,h),{tenantId:u,success:!0,alreadyWarm:h,durationMs:g}}catch(h){let g=Date.now()-p;return s?.(u,"failed"),this.debugLogger.logWarmup(u,false,g,false),{tenantId:u,success:false,alreadyWarm:false,durationMs:g,error:h.message}}}));i.push(...m);}return {total:i.length,succeeded:i.filter(c=>c.success).length,failed:i.filter(c=>!c.success).length,alreadyWarm:i.filter(c=>c.alreadyWarm).length,durationMs:Date.now()-r,details:i}}getMetrics(){this.ensureNotDisposed();let e=this.config.isolation.maxPools??ee.maxPools,t=[];for(let[r,a]of this.poolCache.entries()){let o=this.tenantIdBySchema.get(r)??r,s=a.pool;t.push({tenantId:o,schemaName:r,connections:{total:s.totalCount,idle:s.idleCount,waiting:s.waitingCount},lastAccessedAt:new Date(a.lastAccess).toISOString()});}return {pools:{total:t.length,maxPools:e,tenants:t},shared:{initialized:this.sharedPool!==null,connections:this.sharedPool?{total:this.sharedPool.totalCount,idle:this.sharedPool.idleCount,waiting:this.sharedPool.waitingCount}:null},timestamp:new Date().toISOString()}}async healthCheck(e={}){return this.ensureNotDisposed(),this.healthChecker.checkHealth(e)}async evictPool(e,t="manual"){let r=this.config.isolation.schemaNameTemplate(e),a=this.poolCache.get(r);a&&(this.debugLogger.logPoolEvicted(e,r,t),this.poolCache.delete(r),this.tenantIdBySchema.delete(r),await this.closePool(a.pool,e));}startCleanup(){if(this.cleanupInterval)return;let e=ee.cleanupIntervalMs;this.cleanupInterval=setInterval(()=>{this.cleanupIdlePools();},e),this.cleanupInterval.unref();}stopCleanup(){this.cleanupInterval&&(clearInterval(this.cleanupInterval),this.cleanupInterval=null);}async dispose(){if(this.disposed)return;this.disposed=true,this.stopCleanup();let e=[];for(let[t,r]of this.poolCache.entries()){let a=this.tenantIdBySchema.get(t);e.push(this.closePool(r.pool,a??t));}await this.poolCache.clear(),this.tenantIdBySchema.clear(),this.sharedPool&&(e.push(this.closePool(this.sharedPool,"shared")),this.sharedPool=null,this.sharedDb=null),await Promise.all(e);}createPoolEntry(e,t){let r=new Pool({connectionString:this.config.connection.url,...ee.poolConfig,...this.config.connection.poolConfig,options:`-c search_path=${t},public`});return r.on("error",async o=>{this.debugLogger.logPoolError(e,o),this.config.hooks?.onError?.(e,o),await this.evictPool(e,"error");}),{db:drizzle(r,{schema:this.config.schemas.tenant}),pool:r,lastAccess:Date.now(),schemaName:t}}disposePoolEntry(e,t){let r=this.tenantIdBySchema.get(t);this.tenantIdBySchema.delete(t),r&&this.debugLogger.logPoolEvicted(r,t,"lru_eviction"),this.closePool(e.pool,r??t).then(()=>{r&&this.config.hooks?.onPoolEvicted?.(r);});}async closePool(e,t){try{await e.end();}catch(r){this.config.hooks?.onError?.(t,r);}}async cleanupIdlePools(){let e=await this.poolCache.evictExpired();for(let t of e){let r=this.tenantIdBySchema.get(t);r&&(this.debugLogger.logPoolEvicted(r,t,"ttl_expired"),this.tenantIdBySchema.delete(t));}}ensureNotDisposed(){if(this.disposed)throw new Error("[drizzle-multitenant] TenantManager has been disposed")}};var we=class{constructor(e){this.tenantManager=e;}async collect(e={}){let{includeHealth:t=false,healthPingTimeoutMs:r=5e3,tenantIds:a}=e,o=this.tenantManager.getMetrics(),s;return t&&(s=await this.tenantManager.healthCheck({ping:true,pingTimeoutMs:r,includeShared:true,...a&&{tenantIds:a}})),{pools:o,health:s,collectedAt:new Date().toISOString()}}getPoolMetrics(){return this.tenantManager.getMetrics()}async getHealthMetrics(e){return this.tenantManager.healthCheck({ping:true,pingTimeoutMs:e?.pingTimeoutMs??5e3,includeShared:true,...e?.tenantIds&&{tenantIds:e.tenantIds}})}getRuntimeMetrics(){let e=process.memoryUsage();return {uptimeSeconds:process.uptime(),memoryUsage:{heapTotal:e.heapTotal,heapUsed:e.heapUsed,external:e.external,rss:e.rss},activeHandles:process._getActiveHandles?.()?.length??0,activeRequests:process._getActiveRequests?.()?.length??0}}calculateSummary(e){let t=e.pools,r=e.health,a=0,o=0,s=0;for(let m of t.pools.tenants)a+=m.connections.total,o+=m.connections.idle,s+=m.connections.waiting;t.shared.connections&&(a+=t.shared.connections.total,o+=t.shared.connections.idle,s+=t.shared.connections.waiting);let i=r?r.pools.filter(m=>m.status==="ok").length:t.pools.total,c=r?.degradedPools??0,l=r?.unhealthyPools??0;return {activePools:t.pools.total,totalConnections:a,idleConnections:o,waitingRequests:s,healthyPools:i,degradedPools:c,unhealthyPools:l}}};var Bs="drizzle_multitenant",Gs=[{name:"pools_active",help:"Number of active database pools",type:"gauge",extract:n=>[{value:n.pools.pools.total}]},{name:"pools_max",help:"Maximum number of pools allowed",type:"gauge",extract:n=>[{value:n.pools.pools.maxPools}]},{name:"pool_connections_total",help:"Total connections in pool",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:e.connections.total}))},{name:"pool_connections_idle",help:"Idle connections in pool",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:e.connections.idle}))},{name:"pool_connections_waiting",help:"Waiting requests in pool queue",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:e.connections.waiting}))},{name:"pool_last_access_timestamp",help:"Last access timestamp for pool (unix epoch)",type:"gauge",labels:["tenant","schema"],extract:n=>n.pools.pools.tenants.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:new Date(e.lastAccessedAt).getTime()/1e3}))},{name:"shared_pool_initialized",help:"Whether shared database pool is initialized (1=yes, 0=no)",type:"gauge",extract:n=>[{value:n.pools.shared.initialized?1:0}]},{name:"shared_pool_connections_total",help:"Total connections in shared pool",type:"gauge",extract:n=>[{value:n.pools.shared.connections?.total??0}]},{name:"shared_pool_connections_idle",help:"Idle connections in shared pool",type:"gauge",extract:n=>[{value:n.pools.shared.connections?.idle??0}]},{name:"shared_pool_connections_waiting",help:"Waiting requests in shared pool queue",type:"gauge",extract:n=>[{value:n.pools.shared.connections?.waiting??0}]},{name:"health_status",help:"Overall health status (1=healthy, 0=unhealthy)",type:"gauge",extract:n=>n.health?[{value:n.health.healthy?1:0}]:[]},{name:"health_pools_total",help:"Total number of pools checked",type:"gauge",extract:n=>n.health?[{value:n.health.totalPools}]:[]},{name:"health_pools_degraded",help:"Number of degraded pools",type:"gauge",extract:n=>n.health?[{value:n.health.degradedPools}]:[]},{name:"health_pools_unhealthy",help:"Number of unhealthy pools",type:"gauge",extract:n=>n.health?[{value:n.health.unhealthyPools}]:[]},{name:"health_check_duration_seconds",help:"Duration of health check in seconds",type:"gauge",extract:n=>n.health?[{value:n.health.durationMs/1e3}]:[]},{name:"pool_health_status",help:"Health status per pool (1=ok, 0.5=degraded, 0=unhealthy)",type:"gauge",labels:["tenant","schema","status"],extract:n=>n.health?.pools.map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName,status:e.status},value:e.status==="ok"?1:e.status==="degraded"?.5:0}))??[]},{name:"pool_response_time_seconds",help:"Pool ping response time in seconds",type:"gauge",labels:["tenant","schema"],extract:n=>n.health?.pools.filter(e=>e.responseTimeMs!==void 0).map(e=>({labels:{tenant:e.tenantId,schema:e.schemaName},value:(e.responseTimeMs??0)/1e3}))??[]},{name:"shared_db_health_status",help:"Shared database health status (1=ok, 0.5=degraded, 0=unhealthy)",type:"gauge",extract:n=>{if(!n.health)return [];let e=n.health.sharedDb;return [{value:e==="ok"?1:e==="degraded"?.5:0}]}},{name:"shared_db_response_time_seconds",help:"Shared database ping response time in seconds",type:"gauge",extract:n=>n.health?.sharedDbResponseTimeMs!==void 0?[{value:n.health.sharedDbResponseTimeMs/1e3}]:[]}],Js=[{name:"process_uptime_seconds",help:"Process uptime in seconds",type:"gauge",extract:n=>[{value:n.uptimeSeconds}]},{name:"process_heap_bytes_total",help:"Total heap memory in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.heapTotal}]},{name:"process_heap_bytes_used",help:"Used heap memory in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.heapUsed}]},{name:"process_external_bytes",help:"External memory in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.external}]},{name:"process_rss_bytes",help:"Resident Set Size in bytes",type:"gauge",extract:n=>[{value:n.memoryUsage.rss}]},{name:"process_active_handles",help:"Number of active handles",type:"gauge",extract:n=>[{value:n.activeHandles}]},{name:"process_active_requests",help:"Number of active requests",type:"gauge",extract:n=>[{value:n.activeRequests}]}],$e=class{prefix;includeTenantLabels;includeTimestamps;defaultLabels;constructor(e={}){this.prefix=e.prefix??Bs,this.includeTenantLabels=e.includeTenantLabels??true,this.includeTimestamps=e.includeTimestamps??false,this.defaultLabels=e.defaultLabels??{};}export(e,t){let r=this.toPrometheusMetrics(e),a=[];for(let o of r)a.push(...this.formatMetric(o));if(t){let o=this.extractRuntimeMetrics(t);for(let s of o)a.push(...this.formatMetric(s));}return a.join(`
|
|
1737
1737
|
`)+`
|
|
1738
|
-
`}toPrometheusMetrics(e){let t=[];for(let r of Gs){let a=r.extract(e);if(a.length===0)continue;let s=(this.includeTenantLabels?a:a.map(i=>{if(!i.labels)return i;let{tenant:c,schema:l,...m}=i.labels;return {...i,labels:Object.keys(m).length>0?m:void 0}})).map(i=>({...i,labels:{...this.defaultLabels,...i.labels}}));t.push({name:`${this.prefix}_${r.name}`,help:r.help,type:r.type,labels:r.labels,values:s});}return t}extractRuntimeMetrics(e){let t=[];for(let r of Js){let a=r.extract(e);if(a.length===0)continue;let o=a.map(s=>({...s,labels:{...this.defaultLabels,...s.labels}}));t.push({name:`${this.prefix}_${r.name}`,help:r.help,type:r.type,values:o});}return t}formatMetric(e){let t=[];t.push(`# HELP ${e.name} ${e.help}`),t.push(`# TYPE ${e.name} ${e.type}`);for(let r of e.values){let a=this.formatLabels(r.labels),o=this.includeTimestamps&&r.timestamp?` ${r.timestamp}`:"";t.push(`${e.name}${a} ${r.value}${o}`);}return t}formatLabels(e){return !e||Object.keys(e).length===0?"":`{${Object.entries(e).filter(([,r])=>r!=null).map(([r,a])=>`${r}="${this.escapeLabel(a)}"`).join(",")}}`}escapeLabel(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n")}static get contentType(){return "text/plain; version=0.0.4; charset=utf-8"}};var Fe=class{renderer;ctx;manager=null;collector=null;constructor(e,t){this.ctx=e,this.renderer=t||new V;}async show(){switch(this.renderer.clearScreen(),this.renderer.showHeader("Metrics Dashboard"),await select({message:"What would you like to view?",choices:[{name:"Pool metrics (fast)",value:"pools"},{name:"Health check (includes ping)",value:"health"},{name:"Prometheus export",value:"prometheus"},{name:"Runtime metrics",value:"runtime"},{name:
|
|
1739
|
-
`));try{let t=await this.collector.collect({includeHealth:!1}),r=this.collector.calculateSummary(t);this.printPoolMetrics(t,r);}catch(t){this.renderer.showStatus(t.message,"error");}switch(console.log(""),await select({message:"What next?",choices:[{name:"Refresh metrics",value:"refresh"},{name:"Include health check",value:"health"},{name:"View Prometheus format",value:"prometheus"},{name:
|
|
1740
|
-
`));try{let t=Date.now(),r=await this.collector.collect({includeHealth:!0}),a=this.collector.calculateSummary(r),o=Date.now()-t;this.printHealthMetrics(r,a,o);}catch(t){this.renderer.showStatus(t.message,"error");}switch(console.log(""),await select({message:"What next?",choices:[{name:"Run health check again",value:"refresh"},{name:"View pool metrics only",value:"pools"},{name:"View Prometheus format",value:"prometheus"},{name:
|
|
1741
|
-
`));try{let t=await this.collector.collect({includeHealth:!0}),r=this.collector.getRuntimeMetrics(),o=new $e({prefix:"drizzle_multitenant"}).export(t,r);console.log(
|
|
1742
|
-
`))if(s.startsWith("# HELP"))console.log(
|
|
1738
|
+
`}toPrometheusMetrics(e){let t=[];for(let r of Gs){let a=r.extract(e);if(a.length===0)continue;let s=(this.includeTenantLabels?a:a.map(i=>{if(!i.labels)return i;let{tenant:c,schema:l,...m}=i.labels;return {...i,labels:Object.keys(m).length>0?m:void 0}})).map(i=>({...i,labels:{...this.defaultLabels,...i.labels}}));t.push({name:`${this.prefix}_${r.name}`,help:r.help,type:r.type,labels:r.labels,values:s});}return t}extractRuntimeMetrics(e){let t=[];for(let r of Js){let a=r.extract(e);if(a.length===0)continue;let o=a.map(s=>({...s,labels:{...this.defaultLabels,...s.labels}}));t.push({name:`${this.prefix}_${r.name}`,help:r.help,type:r.type,values:o});}return t}formatMetric(e){let t=[];t.push(`# HELP ${e.name} ${e.help}`),t.push(`# TYPE ${e.name} ${e.type}`);for(let r of e.values){let a=this.formatLabels(r.labels),o=this.includeTimestamps&&r.timestamp?` ${r.timestamp}`:"";t.push(`${e.name}${a} ${r.value}${o}`);}return t}formatLabels(e){return !e||Object.keys(e).length===0?"":`{${Object.entries(e).filter(([,r])=>r!=null).map(([r,a])=>`${r}="${this.escapeLabel(a)}"`).join(",")}}`}escapeLabel(e){return e.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n")}static get contentType(){return "text/plain; version=0.0.4; charset=utf-8"}};var Fe=class{renderer;ctx;manager=null;collector=null;constructor(e,t){this.ctx=e,this.renderer=t||new V;}async show(){switch(this.renderer.clearScreen(),this.renderer.showHeader("Metrics Dashboard"),await select({message:"What would you like to view?",choices:[{name:"Pool metrics (fast)",value:"pools"},{name:"Health check (includes ping)",value:"health"},{name:"Prometheus export",value:"prometheus"},{name:"Runtime metrics",value:"runtime"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "pools":return this.showPoolMetrics();case "health":return this.showHealthMetrics();case "prometheus":return this.showPrometheusExport();case "runtime":return this.showRuntimeMetrics();default:return {type:"back"}}}async ensureInitialized(){if(this.collector)return true;try{return this.manager=new xe(this.ctx.config),this.collector=new we(this.manager),!0}catch(e){return this.renderer.showStatus(e.message,"error"),await this.renderer.pressEnterToContinue(),false}}async cleanup(){this.manager&&(await this.manager.dispose(),this.manager=null,this.collector=null);}async showPoolMetrics(){if(this.renderer.clearScreen(),this.renderer.showHeader("Pool Metrics"),!await this.ensureInitialized())return this.show();console.log(M.dim(` Collecting pool metrics...
|
|
1739
|
+
`));try{let t=await this.collector.collect({includeHealth:!1}),r=this.collector.calculateSummary(t);this.printPoolMetrics(t,r);}catch(t){this.renderer.showStatus(t.message,"error");}switch(console.log(""),await select({message:"What next?",choices:[{name:"Refresh metrics",value:"refresh"},{name:"Include health check",value:"health"},{name:"View Prometheus format",value:"prometheus"},{name:M.gray("\u2190 Back to metrics menu"),value:"menu"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "refresh":return this.showPoolMetrics();case "health":return this.showHealthMetrics();case "prometheus":return this.showPrometheusExport();case "menu":return this.show();default:return await this.cleanup(),{type:"back"}}}async showHealthMetrics(){if(this.renderer.clearScreen(),this.renderer.showHeader("Health Check"),!await this.ensureInitialized())return this.show();console.log(M.dim(` Running health check (pinging all pools)...
|
|
1740
|
+
`));try{let t=Date.now(),r=await this.collector.collect({includeHealth:!0}),a=this.collector.calculateSummary(r),o=Date.now()-t;this.printHealthMetrics(r,a,o);}catch(t){this.renderer.showStatus(t.message,"error");}switch(console.log(""),await select({message:"What next?",choices:[{name:"Run health check again",value:"refresh"},{name:"View pool metrics only",value:"pools"},{name:"View Prometheus format",value:"prometheus"},{name:M.gray("\u2190 Back to metrics menu"),value:"menu"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "refresh":return this.showHealthMetrics();case "pools":return this.showPoolMetrics();case "prometheus":return this.showPrometheusExport();case "menu":return this.show();default:return await this.cleanup(),{type:"back"}}}async showPrometheusExport(){if(this.renderer.clearScreen(),this.renderer.showHeader("Prometheus Export"),!await this.ensureInitialized())return this.show();console.log(M.dim(` Collecting metrics for Prometheus export...
|
|
1741
|
+
`));try{let t=await this.collector.collect({includeHealth:!0}),r=this.collector.getRuntimeMetrics(),o=new $e({prefix:"drizzle_multitenant"}).export(t,r);console.log(M.bold(" Prometheus Text Format:")),console.log("");for(let s of o.split(`
|
|
1742
|
+
`))if(s.startsWith("# HELP"))console.log(M.dim(` ${s}`));else if(s.startsWith("# TYPE"))console.log(M.dim(` ${s}`));else if(s.trim()){let[i,c]=s.split(" ");if(i&&c!==void 0){let l=i.includes("{")?i.replace(/\{([^}]+)\}/,(m,u)=>`{${M.cyan(u)}}`):i;console.log(` ${M.green(l)} ${M.yellow(c)}`);}else console.log(` ${s}`);}console.log(""),console.log(M.dim(' Tip: Use "drizzle-multitenant metrics --prometheus" to pipe to curl'));}catch(t){this.renderer.showStatus(t.message,"error");}switch(console.log(""),await select({message:"What next?",choices:[{name:"Refresh export",value:"refresh"},{name:"View pool metrics",value:"pools"},{name:"View health check",value:"health"},{name:M.gray("\u2190 Back to metrics menu"),value:"menu"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "refresh":return this.showPrometheusExport();case "pools":return this.showPoolMetrics();case "health":return this.showHealthMetrics();case "menu":return this.show();default:return await this.cleanup(),{type:"back"}}}async showRuntimeMetrics(){if(this.renderer.clearScreen(),this.renderer.showHeader("Runtime Metrics"),!await this.ensureInitialized())return this.show();try{let t=this.collector.getRuntimeMetrics();console.log(M.bold(" Node.js Process Metrics:")),console.log(""),console.log(` Uptime: ${M.cyan(this.formatUptime(t.uptimeSeconds))}`),console.log(""),console.log(M.bold(" Memory Usage:")),console.log(` Heap Total: ${M.cyan(this.formatBytes(t.memoryUsage.heapTotal))}`),console.log(` Heap Used: ${M.cyan(this.formatBytes(t.memoryUsage.heapUsed))}`),console.log(` External: ${M.cyan(this.formatBytes(t.memoryUsage.external))}`),console.log(` RSS: ${M.cyan(this.formatBytes(t.memoryUsage.rss))}`),console.log(""),console.log(M.bold(" Event Loop:")),console.log(` Active Handles: ${t.activeHandles}`),console.log(` Active Requests: ${t.activeRequests}`);}catch(t){this.renderer.showStatus(t.message,"error");}switch(console.log(""),await select({message:"What next?",choices:[{name:"Refresh runtime metrics",value:"refresh"},{name:"View pool metrics",value:"pools"},{name:"View health check",value:"health"},{name:M.gray("\u2190 Back to metrics menu"),value:"menu"},{name:M.gray("\u2190 Back to main menu"),value:"back"}]})){case "refresh":return this.showRuntimeMetrics();case "pools":return this.showPoolMetrics();case "health":return this.showHealthMetrics();case "menu":return this.show();default:return await this.cleanup(),{type:"back"}}}printPoolMetrics(e,t){if(console.log(M.bold(" Pool Status:")),console.log(` Active Pools: ${M.cyan(String(e.pools.pools.total))} / ${e.pools.pools.maxPools}`),console.log(` Total Connections: ${t.totalConnections}`),console.log(` Idle Connections: ${M.green(String(t.idleConnections))}`),console.log(` Waiting Requests: ${t.waitingRequests>0?M.yellow(String(t.waitingRequests)):String(t.waitingRequests)}`),console.log(""),console.log(M.bold(" Shared Database:")),e.pools.shared.initialized){let a=e.pools.shared.connections;console.log(` Status: ${M.green("Initialized")}`),console.log(` Connections: ${a.total} total, ${a.idle} idle, ${a.waiting} waiting`);}else console.log(` Status: ${M.dim("Not initialized")}`);let r=e.pools.pools.tenants;if(r.length>0&&r.length<=15){console.log(""),console.log(M.bold(" Active Tenants:"));for(let a of r){let o=a.connections.waiting>0?M.yellow:s=>s;console.log(` ${M.cyan(a.tenantId)}: ${a.connections.total} conn, ${a.connections.idle} idle, ${o(String(a.connections.waiting))} waiting`);}}else r.length>15&&(console.log(""),console.log(M.dim(` (${r.length} active tenants)`)));console.log(""),console.log(M.dim(` Collected at ${e.collectedAt}`));}printHealthMetrics(e,t,r){if(!e.health){this.printPoolMetrics(e,t);return}console.log(M.bold(" Health Status:"));let a=e.health.healthy?M.green("\u2713"):M.red("\u2717");console.log(` Overall: ${a} ${e.health.healthy?"Healthy":"Unhealthy"}`),console.log(` Pools: ${M.green(String(t.healthyPools))} healthy, ${M.yellow(String(t.degradedPools))} degraded, ${M.red(String(t.unhealthyPools))} unhealthy`),console.log(` Shared DB: ${this.getStatusColor(e.health.sharedDb)(e.health.sharedDb)}`),e.health.sharedDbResponseTimeMs!==void 0&&console.log(` Shared DB Latency: ${e.health.sharedDbResponseTimeMs}ms`),console.log(` Check Duration: ${r}ms`);let o=e.health.pools;if(o.length>0&&o.length<=15){console.log(""),console.log(M.bold(" Pool Health:"));for(let s of o){let i=this.getStatusIcon(s.status),c=s.responseTimeMs?M.dim(` (${s.responseTimeMs}ms)`):"";console.log(` ${i} ${M.cyan(s.tenantId)}${c}`),s.error&&console.log(` ${M.red(s.error)}`);}}else o.length>15&&(console.log(""),console.log(M.dim(` (${o.length} pools checked)`)));console.log(""),console.log(M.dim(` Collected at ${e.collectedAt}`));}getStatusIcon(e){switch(e){case "ok":return M.green("\u25CF");case "degraded":return M.yellow("\u25D0");case "unhealthy":return M.red("\u25CB")}}getStatusColor(e){switch(e){case "ok":return M.green;case "degraded":return M.yellow;case "unhealthy":return M.red}}formatBytes(e){return e<1024?`${e} B`:e<1024*1024?`${(e/1024).toFixed(1)} KB`:e<1024*1024*1024?`${(e/(1024*1024)).toFixed(1)} MB`:`${(e/(1024*1024*1024)).toFixed(1)} GB`}formatUptime(e){if(e<60)return `${Math.floor(e)}s`;if(e<3600)return `${Math.floor(e/60)}m ${Math.floor(e%60)}s`;let t=Math.floor(e/3600),r=Math.floor(e%3600/60);return `${t}h ${r}m`}};var un=class{constructor(e){this.configPath=e;this.renderer=new V;}renderer;ctx=null;statuses=[];sharedStatus=null;hasSharedMigrations=false;async start(){if(this.renderer.clearScreen(),Fr(),this.ctx=await this.loadMenuContext(),!this.ctx){await this.renderer.pressEnterToContinue();return}await this.refreshStatuses(),await this.loop();}async loop(){for(;;){let e=await this.showMainMenu();e.type==="exit"&&(console.log(M.cyan(`
|
|
1743
1743
|
Goodbye!
|
|
1744
|
-
`)),process.exit(0)),e.type==="refresh"&&await this.refreshStatuses();}}async showMainMenu(){let e=this.renderer.getStatusSummary(this.statuses),t=[{name:`Migration Status ${
|
|
1744
|
+
`)),process.exit(0)),e.type==="refresh"&&await this.refreshStatuses();}}async showMainMenu(){let e=this.renderer.getStatusSummary(this.statuses),t=[{name:`Migration Status ${M.gray(`(${M.green(e.upToDate)} ok, ${M.yellow(e.behind)} pending)`)}`,value:"status"},{name:`Migrate Tenants ${e.totalPending>0?M.yellow(`(${e.totalPending} pending)`):M.dim("(all up to date)")}`,value:"migrate"}];if(this.hasSharedMigrations&&this.sharedStatus){let a=this.sharedStatus.pendingCount;t.push({name:`Migrate Shared Schema ${a>0?M.yellow(`(${a} pending)`):M.dim("(up to date)")}`,value:"migrate-shared"});}t.push({name:"Seed Tenants",value:"seed"},new Separator,{name:"Create Tenant",value:"create"},{name:"Clone Tenant",value:"clone"},{name:"Drop Tenant",value:"drop"},new Separator,{name:"Generate Migration",value:"generate"}),this.hasSharedMigrations&&t.push({name:"Generate Shared Migration",value:"generate-shared"}),t.push(new Separator,{name:"Schema Lint",value:"lint"},{name:"Metrics Dashboard",value:"metrics"},{name:"Refresh",value:"refresh"},new Separator,{name:"Exit",value:"exit"});let r=await select({message:"drizzle-multitenant - Main Menu",choices:t});return this.handleChoice(r)}async handleChoice(e){if(!this.ctx)return {type:"exit"};switch(e){case "status":{let r=await new De(this.ctx,this.renderer).show(this.statuses);return this.handleScreenAction(r)}case "migrate":{let r=await new ge(this.ctx,this.renderer).show(this.statuses);return this.handleScreenAction(r)}case "seed":{let r=await new ve(this.ctx,this.renderer).show(this.statuses);return this.handleScreenAction(r)}case "create":{let r=await new fe(this.ctx,this.renderer).showCreate();return this.handleScreenAction(r)}case "clone":{let r=await new fe(this.ctx,this.renderer).showClone(this.statuses);return this.handleScreenAction(r)}case "drop":{let r=await new fe(this.ctx,this.renderer).showDrop(this.statuses);return this.handleScreenAction(r)}case "generate":return await new Te(this.ctx,this.renderer).show(),{type:"back"};case "migrate-shared":return await this.migrateShared(),await this.refreshStatuses(),{type:"refresh"};case "generate-shared":return await new Te(this.ctx,this.renderer).showShared(),{type:"back"};case "lint":return await new ze(this.ctx,this.renderer).show(),{type:"back"};case "metrics":return await new Fe(this.ctx,this.renderer).show(),{type:"back"};case "refresh":return await this.refreshStatuses(),{type:"refresh"};case "exit":return {type:"exit"};default:return {type:"back"}}}async migrateShared(){if(!this.ctx||!this.sharedStatus)return;if(this.sharedStatus.pendingCount===0){console.log(M.green(`
|
|
1745
1745
|
Shared schema is already up to date.
|
|
1746
|
-
`)),await this.renderer.pressEnterToContinue();return}console.log(
|
|
1746
|
+
`)),await this.renderer.pressEnterToContinue();return}console.log(M.cyan(`
|
|
1747
1747
|
Pending shared migrations: ${this.sharedStatus.pendingCount}
|
|
1748
|
-
`));for(let t of this.sharedStatus.pendingMigrations)console.log(
|
|
1749
|
-
Error: ${r.error}`));}catch(t){e.fail("Migration failed"),console.log(
|
|
1750
|
-
${t.message}`));}console.log(""),await this.renderer.pressEnterToContinue();}async handleScreenAction(e){if(!this.ctx)return {type:"exit"};if(e.type==="navigate")switch(e.screen){case "migrate":{let r=await new ge(this.ctx,this.renderer).show(this.statuses);return this.handleScreenAction(r)}case "migrate-single":{let t=e.params?.tenantId;if(t){let a=await new ge(this.ctx,this.renderer).migrateSingle(t);return await this.refreshStatuses(),a}return {type:"back"}}default:return {type:"back"}}return e.type==="refresh"&&await this.refreshStatuses(),e}async loadMenuContext(){let e=st("Loading configuration...").start();try{let{config:t,migrationsFolder:r,migrationsTable:a,tenantDiscovery:o,sharedMigrationsFolder:s,
|
|
1751
|
-
Add migrations.tenantDiscovery to your config:`)),console.log(
|
|
1752
|
-
${t.message}`)),null}}async refreshStatuses(){if(!this.ctx)return;let e=st("Fetching status...").start();try{let t=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,sharedMigrationsFolder:this.ctx.sharedMigrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},tenantDiscovery:this.ctx.tenantDiscovery});this.statuses=await t.getStatus(),this.hasSharedMigrations&&(this.sharedStatus=await t.getSharedStatus()),e.stop();}catch(t){e.fail("Failed to fetch status"),console.log(
|
|
1753
|
-
${t.message}`)),this.statuses=[],this.sharedStatus=null;}}};async function pt(n){await new un(n).start();}process.on("SIGINT",()=>{console.log(
|
|
1748
|
+
`));for(let t of this.sharedStatus.pendingMigrations)console.log(M.dim(` - ${t}`));console.log("");let e=st("Migrating shared schema...").start();try{let r=await I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,sharedMigrationsFolder:this.ctx.sharedMigrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},...this.ctx.sharedMigrationsTable&&{sharedMigrationsTable:this.ctx.sharedMigrationsTable},...this.ctx.sharedTableFormat&&this.ctx.sharedTableFormat!=="auto"&&{sharedTableFormat:this.ctx.sharedTableFormat},...this.ctx.sharedDefaultFormat&&{sharedDefaultFormat:this.ctx.sharedDefaultFormat},tenantDiscovery:this.ctx.tenantDiscovery}).migrateShared({onProgress:(a,o)=>{a==="migrating"&&o&&(e.text=`Applying: ${o}`);}});if(r.success){e.succeed(`Applied ${r.appliedMigrations.length} migration(s)`),console.log("");for(let a of r.appliedMigrations)console.log(M.green(` \u2713 ${a}`));}else e.fail("Migration failed"),console.log(M.red(`
|
|
1749
|
+
Error: ${r.error}`));}catch(t){e.fail("Migration failed"),console.log(M.red(`
|
|
1750
|
+
${t.message}`));}console.log(""),await this.renderer.pressEnterToContinue();}async handleScreenAction(e){if(!this.ctx)return {type:"exit"};if(e.type==="navigate")switch(e.screen){case "migrate":{let r=await new ge(this.ctx,this.renderer).show(this.statuses);return this.handleScreenAction(r)}case "migrate-single":{let t=e.params?.tenantId;if(t){let a=await new ge(this.ctx,this.renderer).migrateSingle(t);return await this.refreshStatuses(),a}return {type:"back"}}default:return {type:"back"}}return e.type==="refresh"&&await this.refreshStatuses(),e}async loadMenuContext(){let e=st("Loading configuration...").start();try{let{config:t,migrationsFolder:r,migrationsTable:a,tenantDiscovery:o,sharedMigrationsFolder:s,sharedMigrationsTable:i,sharedTableFormat:c,sharedDefaultFormat:l,sharedConfigSource:m,drizzleKitConfigFile:u}=await k(this.configPath);if(!o)return e.fail("No tenant discovery function found in config"),console.log(M.dim(`
|
|
1751
|
+
Add migrations.tenantDiscovery to your config:`)),console.log(M.dim(" migrations: {")),console.log(M.dim(" tenantDiscovery: async () => ['tenant-1', 'tenant-2'],")),console.log(M.dim(" }")),null;let p=O(r),h;if(s)try{h=O(s),this.hasSharedMigrations=!0;}catch{this.hasSharedMigrations=!1;}return e.succeed("Configuration loaded"),h&&m?console.log(M.dim(` \u2514\u2500 Shared schema: ${s} ${M.cyan(`(from ${m})`)}`)):u&&!s&&console.log(M.dim(` \u2514\u2500 Found ${u} (no shared migrations folder configured)`)),{config:t,migrationsFolder:p,migrationsTable:a,tenantDiscovery:o,sharedMigrationsFolder:h,sharedMigrationsTable:i,sharedTableFormat:c,sharedDefaultFormat:l,sharedConfigSource:m??void 0,drizzleKitConfigFile:u??void 0}}catch(t){return e.fail("Failed to load configuration"),console.log(M.red(`
|
|
1752
|
+
${t.message}`)),null}}async refreshStatuses(){if(!this.ctx)return;let e=st("Fetching status...").start();try{let t=I(this.ctx.config,{migrationsFolder:this.ctx.migrationsFolder,sharedMigrationsFolder:this.ctx.sharedMigrationsFolder,...this.ctx.migrationsTable&&{migrationsTable:this.ctx.migrationsTable},...this.ctx.sharedMigrationsTable&&{sharedMigrationsTable:this.ctx.sharedMigrationsTable},...this.ctx.sharedTableFormat&&this.ctx.sharedTableFormat!=="auto"&&{sharedTableFormat:this.ctx.sharedTableFormat},...this.ctx.sharedDefaultFormat&&{sharedDefaultFormat:this.ctx.sharedDefaultFormat},tenantDiscovery:this.ctx.tenantDiscovery});this.statuses=await t.getStatus(),this.hasSharedMigrations&&(this.sharedStatus=await t.getSharedStatus()),e.stop();}catch(t){e.fail("Failed to fetch status"),console.log(M.red(`
|
|
1753
|
+
${t.message}`)),this.statuses=[],this.sharedStatus=null;}}};async function pt(n){await new un(n).start();}process.on("SIGINT",()=>{console.log(M.cyan(`
|
|
1754
1754
|
|
|
1755
1755
|
Goodbye!
|
|
1756
|
-
`)),process.exit(0);});process.on("uncaughtException",n=>{n.name==="ExitPromptError"&&(console.log(
|
|
1756
|
+
`)),process.exit(0);});process.on("uncaughtException",n=>{n.name==="ExitPromptError"&&(console.log(M.cyan(`
|
|
1757
1757
|
|
|
1758
1758
|
Goodbye!
|
|
1759
|
-
`)),process.exit(0)),console.error(
|
|
1759
|
+
`)),process.exit(0)),console.error(M.red(`
|
|
1760
1760
|
Unexpected error:`),n.message),process.exit(1);});var hn=new Command("interactive").alias("i").description("Launch interactive TUI mode").option("-c, --config <path>","Path to config file").addHelpText("after",`
|
|
1761
1761
|
Examples:
|
|
1762
1762
|
$ drizzle-multitenant interactive
|
|
@@ -1786,14 +1786,14 @@ Seed File Format:
|
|
|
1786
1786
|
{ name: 'user', permissions: ['read'] },
|
|
1787
1787
|
]);
|
|
1788
1788
|
};
|
|
1789
|
-
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config),c=O(o);H(`Using migrations folder: ${c}`),r.text="Loading seed file...";let l=resolve(process.cwd(),n.file),m=pathToFileURL(l).href,u;try{let
|
|
1790
|
-
${
|
|
1789
|
+
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config),c=O(o);H(`Using migrations folder: ${c}`),r.text="Loading seed file...";let l=resolve(process.cwd(),n.file),m=pathToFileURL(l).href,u;try{let y=await import(m);if(u=y.seed||y.default,typeof u!="function")throw new Error('Seed file must export a "seed" function or default export')}catch(y){r.fail("Failed to load seed file");let w=y;w.message.includes("Cannot find module")?(d(`
|
|
1790
|
+
${C(`Seed file not found: ${l}`)}`),d(f(`
|
|
1791
1791
|
Make sure the file exists and has the correct format:`)),d(f(" export const seed: SeedFunction = async (db, tenantId) => { ... };"))):d(`
|
|
1792
|
-
${
|
|
1793
|
-
`));let
|
|
1794
|
-
`));let g=I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:p})
|
|
1795
|
-
`+T("Summary:")),d(` Total: ${b.total}`),d(` Succeeded: ${E(b.succeeded.toString())}`),b.failed>0&&d(` Failed: ${v(b.failed.toString())}`),b.skipped>0&&d(` Skipped: ${
|
|
1796
|
-
`+T("Failed tenants:"));for(let
|
|
1792
|
+
${C(w.message)}`),process.exit(1);}let p,h;if(n.tenant)p=async()=>[n.tenant],h=[n.tenant];else if(n.tenants){let y=n.tenants.split(",").map(w=>w.trim());p=async()=>y,h=y;}else if(n.all){if(!i)throw Q.noTenantDiscovery();p=i,r.text="Discovering tenants...",h=await i();}else if(r.stop(),me()&&i){d(J(`No tenants specified. Fetching available tenants...
|
|
1793
|
+
`));let y=await i();if(y.length===0){d(C("No tenants found."));return}let w=await checkbox({message:"Select tenants to seed:",choices:y.map(D=>({name:D,value:D})),pageSize:15});if(w.length===0){d(C("No tenants selected. Aborting."));return}p=async()=>w,h=w;}else throw Q.noTenantSpecified();if(h.length===0){r.stop(),d(C("No tenants found."));return}r.text=`Found ${h.length} tenant${h.length>1?"s":""}`,r.succeed(),d(J(`Seeding ${h.length} tenant${h.length>1?"s":""}...
|
|
1794
|
+
`));let g=I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:p}),$=parseInt(n.concurrency||"10",10),S=ae({total:h.length});S.start();let b=await g.seedAll(u,{concurrency:$,onProgress:(y,w)=>{w==="completed"?(S.increment({tenant:y,status:"success"}),H(`Completed: ${y}`)):w==="failed"&&(S.increment({tenant:y,status:"error"}),H(`Failed: ${y}`));},onError:(y,w)=>(H(`Error on ${y}: ${w.message}`),"continue")});S.stop();let x=Date.now()-e;if(t.jsonMode){let y={results:b.details.map(w=>({tenantId:w.tenantId,schema:w.schemaName,success:w.success,durationMs:w.durationMs,error:w.error})),summary:{total:b.total,succeeded:b.succeeded,failed:b.failed,skipped:b.skipped,durationMs:x}};N(y),process.exit(b.failed>0?1:0);}if(d(`
|
|
1795
|
+
`+T("Summary:")),d(` Total: ${b.total}`),d(` Succeeded: ${E(b.succeeded.toString())}`),b.failed>0&&d(` Failed: ${v(b.failed.toString())}`),b.skipped>0&&d(` Skipped: ${C(b.skipped.toString())}`),d(` Duration: ${f(ei(x))}`),b.failed>0){d(`
|
|
1796
|
+
`+T("Failed tenants:"));for(let y of b.details.filter(w=>!w.success))d(` ${v(y.tenantId)}: ${f(y.error||"Unknown error")}`);d(`
|
|
1797
1797
|
`+f("Run with --verbose to see more details."));}b.failed>0&&process.exit(1);}catch(a){r.fail(a.message),j(a);}});function ei(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}var fn=new Command("seed-shared").description("Seed the shared schema (public) with initial data").requiredOption("-f, --file <path>","Path to shared seed file (TypeScript or JavaScript)").option("-c, --config <path>","Path to config file").addHelpText("after",`
|
|
1798
1798
|
Examples:
|
|
1799
1799
|
$ drizzle-multitenant seed-shared --file=./seeds/shared/plans.ts
|
|
@@ -1810,11 +1810,11 @@ Seed File Format:
|
|
|
1810
1810
|
]).onConflictDoNothing();
|
|
1811
1811
|
};
|
|
1812
1812
|
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config),c=O(o);H(`Using migrations folder: ${c}`),a.schemas?.shared||(r.fail("Shared schema not configured"),d(`
|
|
1813
|
-
${
|
|
1814
|
-
Add a shared schema to your config file:`)),d(f(" schemas: {")),d(f(" tenant: tenantSchema,")),d(f(" shared: sharedSchema, // <-- Add this")),d(f(" }")),process.exit(1)),r.text="Loading seed file...";let l=resolve(process.cwd(),n.file),m=pathToFileURL(l).href,u;try{let
|
|
1815
|
-
${
|
|
1813
|
+
${C("The shared schema (schemas.shared) is not configured.")}`),d(f(`
|
|
1814
|
+
Add a shared schema to your config file:`)),d(f(" schemas: {")),d(f(" tenant: tenantSchema,")),d(f(" shared: sharedSchema, // <-- Add this")),d(f(" }")),process.exit(1)),r.text="Loading seed file...";let l=resolve(process.cwd(),n.file),m=pathToFileURL(l).href,u;try{let $=await import(m);if(u=$.seed||$.default,typeof u!="function")throw new Error('Seed file must export a "seed" function or default export')}catch($){r.fail("Failed to load seed file");let S=$;S.message.includes("Cannot find module")?(d(`
|
|
1815
|
+
${C(`Seed file not found: ${l}`)}`),d(f(`
|
|
1816
1816
|
Make sure the file exists and has the correct format:`)),d(f(" export const seed: SharedSeedFunction = async (db) => { ... };"))):d(`
|
|
1817
|
-
${
|
|
1817
|
+
${C(S.message)}`),process.exit(1);}r.text="Seeding shared schema...";let h=await I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:i||(()=>Promise.resolve([]))}).seedShared(u),g=Date.now()-e;if(h.success?r.succeed("Shared schema seeded successfully"):r.fail("Failed to seed shared schema"),t.jsonMode){let $={result:{schema:h.schemaName,success:h.success,durationMs:h.durationMs,error:h.error}};N($),process.exit(h.success?0:1);}d(`
|
|
1818
1818
|
`+T("Summary:")),d(` Schema: ${h.schemaName}`),d(` Status: ${h.success?E("success"):v("failed")}`),d(` Duration: ${f(ai(g))}`),h.error&&d(`
|
|
1819
1819
|
${v("Error:")} ${h.error}`),h.success||process.exit(1);}catch(a){r.fail(a.message),j(a);}});function ai(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}var yn=new Command("seed-all").description("Seed shared schema first, then all tenants").requiredOption("--shared-file <path>","Path to shared schema seed file").requiredOption("--tenant-file <path>","Path to tenant seed file").option("-c, --config <path>","Path to config file").option("--concurrency <number>","Number of concurrent tenant seed operations","10").addHelpText("after",`
|
|
1820
1820
|
Examples:
|
|
@@ -1842,28 +1842,28 @@ Seed File Formats:
|
|
|
1842
1842
|
await db.insert(roles).values([...]);
|
|
1843
1843
|
};
|
|
1844
1844
|
`).action(async n=>{let e=Date.now(),t=L(),r=R("Loading configuration...");try{r.start();let{config:a,migrationsFolder:o,migrationsTable:s,tenantDiscovery:i}=await k(n.config);i||(r.fail("Tenant discovery not configured"),d(`
|
|
1845
|
-
${
|
|
1846
|
-
${
|
|
1847
|
-
${
|
|
1848
|
-
${
|
|
1849
|
-
${
|
|
1850
|
-
${
|
|
1851
|
-
[1/2] Seeding shared schema...`));let x=R("Seeding shared schema...");x.start();let
|
|
1852
|
-
[2/2] Seeding ${
|
|
1853
|
-
`+T("Summary:")),d(T(" Shared:")),d(` Schema: ${
|
|
1854
|
-
Tenants:`)),d(` Total: ${D.total}`),d(` Succeeded: ${E(D.succeeded.toString())}`),D.failed>0&&d(` Failed: ${v(D.failed.toString())}`),D.skipped>0&&d(` Skipped: ${
|
|
1845
|
+
${C("The tenantDiscovery function is not configured in your config file.")}`),process.exit(1));let c=O(o);H(`Using migrations folder: ${c}`),a.schemas?.shared||(r.fail("Shared schema not configured"),d(`
|
|
1846
|
+
${C("The shared schema (schemas.shared) is not configured.")}`),process.exit(1)),r.text="Loading shared seed file...";let l=resolve(process.cwd(),n.sharedFile),m=pathToFileURL(l).href,u;try{let z=await import(m);if(u=z.seed||z.default,typeof u!="function")throw new Error('Shared seed file must export a "seed" function or default export')}catch(z){r.fail("Failed to load shared seed file");let B=z;B.message.includes("Cannot find module")?d(`
|
|
1847
|
+
${C(`Shared seed file not found: ${l}`)}`):d(`
|
|
1848
|
+
${C(B.message)}`),process.exit(1);}r.text="Loading tenant seed file...";let p=resolve(process.cwd(),n.tenantFile),h=pathToFileURL(p).href,g;try{let z=await import(h);if(g=z.seed||z.default,typeof g!="function")throw new Error('Tenant seed file must export a "seed" function or default export')}catch(z){r.fail("Failed to load tenant seed file");let B=z;B.message.includes("Cannot find module")?d(`
|
|
1849
|
+
${C(`Tenant seed file not found: ${p}`)}`):d(`
|
|
1850
|
+
${C(B.message)}`),process.exit(1);}r.text="Discovering tenants...";let $=await i();if($.length===0){r.stop(),d(C("No tenants found."));return}r.text=`Found ${$.length} tenant${$.length>1?"s":""}`,r.succeed();let S=I(a,{migrationsFolder:c,...s&&{migrationsTable:s},tenantDiscovery:i}),b=parseInt(n.concurrency||"10",10);d(T(`
|
|
1851
|
+
[1/2] Seeding shared schema...`));let x=R("Seeding shared schema...");x.start();let y=await S.seedShared(u);y.success?x.succeed(`Shared schema seeded in ${y.durationMs}ms`):(x.fail(`Failed to seed shared schema: ${y.error}`),t.jsonMode||process.exit(1)),d(T(`
|
|
1852
|
+
[2/2] Seeding ${$.length} tenant${$.length>1?"s":""}...`));let w=ae({total:$.length});w.start();let D=await S.seedAll(g,{concurrency:b,onProgress:(z,B)=>{B==="completed"?(w.increment({tenant:z,status:"success"}),H(`Completed: ${z}`)):B==="failed"&&(w.increment({tenant:z,status:"error"}),H(`Failed: ${z}`));},onError:(z,B)=>(H(`Error on ${z}: ${B.message}`),"continue")});w.stop();let W=Date.now()-e;if(t.jsonMode){let z={shared:{schema:y.schemaName,success:y.success,durationMs:y.durationMs,error:y.error},tenants:{results:D.details.map(B=>({tenantId:B.tenantId,schema:B.schemaName,success:B.success,durationMs:B.durationMs,error:B.error})),summary:{total:D.total,succeeded:D.succeeded,failed:D.failed,skipped:D.skipped}},totalDurationMs:W};N(z),process.exit(y.success&&D.failed===0?0:1);}if(d(`
|
|
1853
|
+
`+T("Summary:")),d(T(" Shared:")),d(` Schema: ${y.schemaName}`),d(` Status: ${y.success?E("success"):v("failed")}`),d(` Duration: ${f(Qr(y.durationMs))}`),d(T(`
|
|
1854
|
+
Tenants:`)),d(` Total: ${D.total}`),d(` Succeeded: ${E(D.succeeded.toString())}`),D.failed>0&&d(` Failed: ${v(D.failed.toString())}`),D.skipped>0&&d(` Skipped: ${C(D.skipped.toString())}`),d(T(`
|
|
1855
1855
|
Total:`)),d(` Duration: ${f(Qr(W))}`),D.failed>0){d(`
|
|
1856
|
-
`+T("Failed tenants:"));for(let z of D.details.filter(B=>!B.success))d(` ${v(z.tenantId)}: ${f(z.error||"Unknown error")}`);}(!
|
|
1856
|
+
`+T("Failed tenants:"));for(let z of D.details.filter(B=>!B.success))d(` ${v(z.tenantId)}: ${f(z.error||"Unknown error")}`);}(!y.success||D.failed>0)&&process.exit(1);}catch(a){r.fail(a.message),j(a);}});function Qr(n){if(n<1e3)return `${n}ms`;if(n<6e4)return `${(n/1e3).toFixed(1)}s`;let e=Math.floor(n/6e4),t=Math.round(n%6e4/1e3);return `${e}m ${t}s`}var Sn=new Command("doctor").description("Diagnose configuration and environment issues").option("-c, --config <path>","Path to config file").addHelpText("after",`
|
|
1857
1857
|
Examples:
|
|
1858
1858
|
$ drizzle-multitenant doctor
|
|
1859
1859
|
$ drizzle-multitenant doctor --json
|
|
1860
1860
|
$ drizzle-multitenant doctor --config ./custom.config.ts
|
|
1861
|
-
`).action(async n=>{let e=L(),t=R("Running diagnostics..."),r=[],a=[],o=Date.now();try{t.start(),t.text="Checking configuration...";let s=await di(n.config);if(r.push(s.check),s.recommendation&&a.push(s.recommendation),!s.config){t.fail("Configuration check failed"),e.jsonMode&&N({healthy:!1,checks:r,recommendations:a,durationMs:Date.now()-o});return}let{config:i,migrationsFolder:c,tenantDiscovery:l,sharedMigrationsFolder:m}=s.config;t.text="Checking database connection...";let u=await ui(i.connection.url);r.push(u.check),u.recommendation&&a.push(u.recommendation),t.text="Checking tenant discovery...";let p=await pi(l);r.push(p.check),p.recommendation&&a.push(p.recommendation),t.text="Checking migrations folder...";let h=Xr(c,"tenant");r.push(h.check),h.recommendation&&a.push(h.recommendation),t.text="Checking shared migrations folder...";let g=Xr(m,"shared");r.push(g.check),g.recommendation&&a.push(g.recommendation),t.text="Checking schema isolation...";let
|
|
1861
|
+
`).action(async n=>{let e=L(),t=R("Running diagnostics..."),r=[],a=[],o=Date.now();try{t.start(),t.text="Checking configuration...";let s=await di(n.config);if(r.push(s.check),s.recommendation&&a.push(s.recommendation),!s.config){t.fail("Configuration check failed"),e.jsonMode&&N({healthy:!1,checks:r,recommendations:a,durationMs:Date.now()-o});return}let{config:i,migrationsFolder:c,tenantDiscovery:l,sharedMigrationsFolder:m}=s.config;t.text="Checking database connection...";let u=await ui(i.connection.url);r.push(u.check),u.recommendation&&a.push(u.recommendation),t.text="Checking tenant discovery...";let p=await pi(l);r.push(p.check),p.recommendation&&a.push(p.recommendation),t.text="Checking migrations folder...";let h=Xr(c,"tenant");r.push(h.check),h.recommendation&&a.push(h.recommendation),t.text="Checking shared migrations folder...";let g=Xr(m,"shared");r.push(g.check),g.recommendation&&a.push(g.recommendation),t.text="Checking schema isolation...";let $=hi(i.isolation);r.push($.check),t.text="Checking pool configuration...";let S=gi(i.isolation,p.tenantCount);r.push(S.check),S.recommendation&&a.push(S.recommendation),t.succeed("Diagnostics complete");let b=r.some(y=>y.status==="warn"),x=r.some(y=>y.status==="error");if(e.jsonMode){let y={healthy:!x,checks:r,recommendations:a,poolConfig:{maxPools:i.isolation.maxPools??50,poolTtlMs:i.isolation.poolTtlMs??36e5},durationMs:Date.now()-o};u.dbInfo&&(y.database=u.dbInfo),p.tenantCount!==void 0&&(y.tenantCount=p.tenantCount),N(y);return}d(`
|
|
1862
1862
|
`+T(G("\u{1F50D} drizzle-multitenant Doctor"))+`
|
|
1863
|
-
`);for(let
|
|
1863
|
+
`);for(let y of r){let w=fi(y.status),D=yi(y.status);d(`${w} ${D(y.name)}: ${y.message}`),y.details&&d(f(` ${y.details}`));}a.length>0&&(d(`
|
|
1864
1864
|
`+T(Z("\u26A0 Recommendations:"))+`
|
|
1865
|
-
`),a.forEach((
|
|
1866
|
-
`+T("\u{1F4CA} Summary:")),u.dbInfo&&d(` Database: PostgreSQL ${u.dbInfo.version} (${u.dbInfo.latencyMs}ms latency)`),p.tenantCount!==void 0&&d(` Tenants: ${p.tenantCount} discovered`),d(` Pool: max=${i.isolation.maxPools??50}, ttl=${ea(i.isolation.poolTtlMs??36e5)}`),d(` Isolation: ${i.isolation.strategy}-based`),d(""),x?(d(v(T("\u2717 Some checks failed. Please review the issues above."))),process.exit(1)):b?d(
|
|
1865
|
+
`),a.forEach((y,w)=>{d(` ${w+1}. ${y.message}`),y.action&&d(f(` \u2192 ${y.action}`));})),d(`
|
|
1866
|
+
`+T("\u{1F4CA} Summary:")),u.dbInfo&&d(` Database: PostgreSQL ${u.dbInfo.version} (${u.dbInfo.latencyMs}ms latency)`),p.tenantCount!==void 0&&d(` Tenants: ${p.tenantCount} discovered`),d(` Pool: max=${i.isolation.maxPools??50}, ttl=${ea(i.isolation.poolTtlMs??36e5)}`),d(` Isolation: ${i.isolation.strategy}-based`),d(""),x?(d(v(T("\u2717 Some checks failed. Please review the issues above."))),process.exit(1)):b?d(C(T("\u26A0 All checks passed with warnings."))):d(E(T("\u2713 All checks passed!")));}catch(s){t.fail(s.message),j(s);}});async function di(n){try{let e=await k(n);return {check:{name:"Configuration",status:"ok",message:"Configuration file found and valid",details:n??"tenant.config.ts"},config:e}}catch(e){return {check:{name:"Configuration",status:"error",message:e.message},recommendation:{priority:"high",message:"Configuration file not found or invalid",action:"Run `npx drizzle-multitenant init` to create a configuration file"}}}}async function ui(n){let e=new Pool({connectionString:n}),t=Date.now();try{let r=await e.query("SELECT version()"),a=Date.now()-t,o=r.rows[0].version.match(/PostgreSQL (\d+\.\d+)/),s=o?o[1]:"unknown";return await e.end(),{check:{name:"Database Connection",status:"ok",message:`PostgreSQL ${s} connected`,details:`Response time: ${a}ms`},dbInfo:{version:s,latencyMs:a}}}catch(r){return await e.end().catch(()=>{}),{check:{name:"Database Connection",status:"error",message:r.message},recommendation:{priority:"high",message:"Cannot connect to database",action:"Verify DATABASE_URL is correct and PostgreSQL is running"}}}}async function pi(n){if(!n)return {check:{name:"Tenant Discovery",status:"warn",message:"Not configured",details:"migrations.tenantDiscovery is not defined in config"},recommendation:{priority:"medium",message:"Tenant discovery is not configured",action:"Add tenantDiscovery function to your config for automatic tenant detection"}};try{let e=await n();return e.length===0?{check:{name:"Tenant Discovery",status:"warn",message:"No tenants found"},recommendation:{priority:"low",message:"No tenants discovered",action:"Create a tenant using `npx drizzle-multitenant tenant:create --id=<tenant-id>`"},tenantCount:0}:{check:{name:"Tenant Discovery",status:"ok",message:`Found ${e.length} tenant(s)`},tenantCount:e.length}}catch(e){return {check:{name:"Tenant Discovery",status:"error",message:e.message},recommendation:{priority:"high",message:"Tenant discovery function failed",action:"Check your tenantDiscovery implementation for errors"}}}}function Xr(n,e){let t=e==="tenant"?"Migrations Folder":"Shared Migrations Folder";if(!n)return e==="shared"?{check:{name:t,status:"warn",message:"Not configured",details:"sharedMigrationsFolder is not defined in config"},recommendation:{priority:"low",message:"Shared migrations folder not configured",action:"Add sharedFolder to your migrations config to enable shared schema migrations"}}:{check:{name:t,status:"warn",message:"Not configured"},recommendation:{priority:"medium",message:"Migrations folder not configured",action:"Add migrations.folder or migrations.tenantFolder to your config"}};let r=resolve(process.cwd(),n);if(!existsSync(r))return {check:{name:t,status:"warn",message:"Folder does not exist",details:r},recommendation:{priority:"medium",message:`${e==="tenant"?"Migrations":"Shared migrations"} folder not found`,action:`Create the folder: mkdir -p ${n}`},folderInfo:{exists:false,path:r,fileCount:0}};try{let a=readdirSync(r).filter(o=>o.endsWith(".sql")&&statSync(resolve(r,o)).isFile());return {check:{name:t,status:"ok",message:`${a.length} migration file(s)`,details:n},folderInfo:{exists:!0,path:r,fileCount:a.length}}}catch(a){return {check:{name:t,status:"error",message:a.message}}}}function hi(n){if(n.strategy!=="schema")return {check:{name:"Schema Isolation",status:"warn",message:`Strategy "${n.strategy}" is not yet supported`,details:'Only "schema" strategy is currently supported'}};try{let e=n.schemaNameTemplate("test-tenant");if(!e||typeof e!="string")throw new Error("schemaNameTemplate must return a string");return {check:{name:"Schema Isolation",status:"ok",message:"schema-based isolation",details:`Template: test-tenant \u2192 ${e}`}}}catch(e){return {check:{name:"Schema Isolation",status:"error",message:e.message}}}}function gi(n,e){let t=n.maxPools??50,r=n.poolTtlMs??36e5,a=`max=${t}, ttl=${ea(r)}`;if(e!==void 0&&e>t*.8){let o=Math.ceil(e*1.5);return {check:{name:"Pool Configuration",status:"warn",message:"Pool limit may be insufficient",details:a},recommendation:{priority:"medium",message:`Current maxPools (${t}) is close to tenant count (${e})`,action:`Consider increasing maxPools to ${o} to avoid LRU evictions`}}}return {check:{name:"Pool Configuration",status:"ok",message:"Configuration looks good",details:a}}}function fi(n){switch(n){case "ok":return Y("\u2713");case "warn":return Z("\u26A0");case "error":return te("\u2717")}}function yi(n){switch(n){case "ok":return e=>e;case "warn":return Z;case "error":return te}}function ea(n){return n<1e3?`${n}ms`:n<6e4?`${Math.round(n/1e3)}s`:n<36e5?`${Math.round(n/6e4)}min`:`${Math.round(n/36e5)}h`}function bn(n){let{tableName:e,tableNamePascal:t,tableNameCamel:r,type:a}=n,o=Si(n),s=bi(n),i=Ti(n),c=xi(n),l=wi(n);return `/**
|
|
1867
1867
|
* ${t} schema
|
|
1868
1868
|
* Type: ${a}
|
|
1869
1869
|
*
|
|
@@ -2139,7 +2139,7 @@ CREATE TRIGGER update_${e}_updated_at
|
|
|
2139
2139
|
-- 1. Idempotency: Use IF NOT EXISTS / IF EXISTS where possible
|
|
2140
2140
|
-- 2. Transactions: This will run in a transaction
|
|
2141
2141
|
-- 3. Rollback: Plan how to undo these changes if needed
|
|
2142
|
-
`}function wn(n){let e=[/^create[-_](.+)$/i,/^add[-_](.+?)[-_]?(?:table)?$/i,/^(?:add|create)[-_](?:index|fk|constraint)[-_](?:to|on)[-_](.+)$/i,/^(.+?)[-_](?:table|schema)$/i];for(let t of e){let r=n.match(t);if(r?.[1])return r[1].toLowerCase().replace(/[-\s]+/g,"_").replace(/s$/,"")}}function $n(n){let e=n.toLowerCase();return e.includes("create")||e.includes("table")?"create-table":e.includes("index")||e.includes("idx")?"add-index":e.includes("fk")||e.includes("foreign")||e.includes("reference")?"add-foreign-key":e.includes("add")||e.includes("column")?"add-column":"blank"}function Dn(n){let e=n.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/[-\s]+/g,"_").toLowerCase(),t=e.split("_").map(a=>a.charAt(0).toUpperCase()+a.slice(1)).join(""),r=t.charAt(0).toLowerCase()+t.slice(1);return {snake:e,pascal:t,camel:r}}var Oe={schemaDir:"src/db/schema",seedDir:"drizzle/seeds",tenantMigrationsDir:"drizzle/tenant-migrations",sharedMigrationsDir:"drizzle/shared-migrations"};async function vn(n){let{name:e,type:t,outputDir:r,includeExample:a=true,includeTimestamps:o=true,includeSoftDelete:s=false,useUuid:i=true}=n;try{let c=Dn(e),l={tableName:c.snake,tableNamePascal:c.pascal,tableNameCamel:c.camel,type:t,includeTimestamps:o,includeSoftDelete:s,useUuid:i,includeExample:a},m=bn(l),u=r||join(Oe.schemaDir,t),p=`${c.camel}.ts`,h=resolve(process.cwd(),u,p);return await mkdir(dirname(h),{recursive:!0}),existsSync(h)?{success:!1,filePath:h,fileName:p,kind:"schema",type:t,error:`File already exists: ${h}`}:(await writeFile(h,m,"utf-8"),{success:!0,filePath:h,fileName:p,kind:"schema",type:t})}catch(c){return {success:false,filePath:"",fileName:"",kind:"schema",type:t,error:c instanceof Error?c.message:String(c)}}}async function _n(n){let{name:e,type:t,outputDir:r,tableName:a}=n;try{let o=Dn(e),s={seedName:o.camel,type:t,...a!==void 0&&{tableName:a}},i=Tn(s),c=r||join(Oe.seedDir,t),l=`${o.camel}.ts`,m=resolve(process.cwd(),c,l);return await mkdir(dirname(m),{recursive:!0}),existsSync(m)?{success:!1,filePath:m,fileName:l,kind:"seed",type:t,error:`File already exists: ${m}`}:(await writeFile(m,i,"utf-8"),{success:!0,filePath:m,fileName:l,kind:"seed",type:t})}catch(o){return {success:false,filePath:"",fileName:"",kind:"seed",type:t,error:o instanceof Error?o.message:String(o)}}}async function Pn(n){let{name:e,type:t,outputDir:r,template:a}=n;try{let o=t==="shared"?Oe.sharedMigrationsDir:Oe.tenantMigrationsDir,s=r||o,i=resolve(process.cwd(),s);await mkdir(i,{recursive:!0});let l=(existsSync(i)?await readdir(i):[]).filter(
|
|
2142
|
+
`}function wn(n){let e=[/^create[-_](.+)$/i,/^add[-_](.+?)[-_]?(?:table)?$/i,/^(?:add|create)[-_](?:index|fk|constraint)[-_](?:to|on)[-_](.+)$/i,/^(.+?)[-_](?:table|schema)$/i];for(let t of e){let r=n.match(t);if(r?.[1])return r[1].toLowerCase().replace(/[-\s]+/g,"_").replace(/s$/,"")}}function $n(n){let e=n.toLowerCase();return e.includes("create")||e.includes("table")?"create-table":e.includes("index")||e.includes("idx")?"add-index":e.includes("fk")||e.includes("foreign")||e.includes("reference")?"add-foreign-key":e.includes("add")||e.includes("column")?"add-column":"blank"}function Dn(n){let e=n.replace(/([a-z])([A-Z])/g,"$1_$2").replace(/[-\s]+/g,"_").toLowerCase(),t=e.split("_").map(a=>a.charAt(0).toUpperCase()+a.slice(1)).join(""),r=t.charAt(0).toLowerCase()+t.slice(1);return {snake:e,pascal:t,camel:r}}var Oe={schemaDir:"src/db/schema",seedDir:"drizzle/seeds",tenantMigrationsDir:"drizzle/tenant-migrations",sharedMigrationsDir:"drizzle/shared-migrations"};async function vn(n){let{name:e,type:t,outputDir:r,includeExample:a=true,includeTimestamps:o=true,includeSoftDelete:s=false,useUuid:i=true}=n;try{let c=Dn(e),l={tableName:c.snake,tableNamePascal:c.pascal,tableNameCamel:c.camel,type:t,includeTimestamps:o,includeSoftDelete:s,useUuid:i,includeExample:a},m=bn(l),u=r||join(Oe.schemaDir,t),p=`${c.camel}.ts`,h=resolve(process.cwd(),u,p);return await mkdir(dirname(h),{recursive:!0}),existsSync(h)?{success:!1,filePath:h,fileName:p,kind:"schema",type:t,error:`File already exists: ${h}`}:(await writeFile(h,m,"utf-8"),{success:!0,filePath:h,fileName:p,kind:"schema",type:t})}catch(c){return {success:false,filePath:"",fileName:"",kind:"schema",type:t,error:c instanceof Error?c.message:String(c)}}}async function _n(n){let{name:e,type:t,outputDir:r,tableName:a}=n;try{let o=Dn(e),s={seedName:o.camel,type:t,...a!==void 0&&{tableName:a}},i=Tn(s),c=r||join(Oe.seedDir,t),l=`${o.camel}.ts`,m=resolve(process.cwd(),c,l);return await mkdir(dirname(m),{recursive:!0}),existsSync(m)?{success:!1,filePath:m,fileName:l,kind:"seed",type:t,error:`File already exists: ${m}`}:(await writeFile(m,i,"utf-8"),{success:!0,filePath:m,fileName:l,kind:"seed",type:t})}catch(o){return {success:false,filePath:"",fileName:"",kind:"seed",type:t,error:o instanceof Error?o.message:String(o)}}}async function Pn(n){let{name:e,type:t,outputDir:r,template:a}=n;try{let o=t==="shared"?Oe.sharedMigrationsDir:Oe.tenantMigrationsDir,s=r||o,i=resolve(process.cwd(),s);await mkdir(i,{recursive:!0});let l=(existsSync(i)?await readdir(i):[]).filter(y=>y.endsWith(".sql")),m=0;for(let y of l){let w=y.match(/^(\d+)_/);w?.[1]&&(m=Math.max(m,parseInt(w[1],10)));}let u=(m+1).toString().padStart(4,"0"),p=e.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,""),h=`${u}_${p}.sql`,g=join(i,h),$=a||$n(e),S=wn(e),b={migrationName:e,type:t,template:$,...S!==void 0&&{tableName:S}},x=xn(b);return existsSync(g)?{success:!1,filePath:g,fileName:h,kind:"migration",type:t,error:`File already exists: ${g}`}:(await writeFile(g,x,"utf-8"),{success:!0,filePath:g,fileName:h,kind:"migration",type:t})}catch(o){return {success:false,filePath:"",fileName:"",kind:"migration",type:t,error:o instanceof Error?o.message:String(o)}}}function zn(){return [{value:"create-table",label:"Create Table",description:"Template for creating a new table with common columns"},{value:"add-column",label:"Add Column",description:"Template for adding columns to an existing table"},{value:"add-index",label:"Add Index",description:"Template for creating indexes"},{value:"add-foreign-key",label:"Add Foreign Key",description:"Template for adding foreign key constraints"},{value:"blank",label:"Blank",description:"Empty migration with basic comments"}]}var kn=new Command("scaffold:schema").description("Generate a new Drizzle schema file").argument("<name>","Schema/table name (e.g., orders, user-profiles)").option("-t, --type <type>","Schema type: tenant or shared","tenant").option("-o, --output <path>","Output directory").option("--no-timestamps","Do not include createdAt/updatedAt columns").option("--soft-delete","Include deletedAt column for soft delete").option("--no-uuid","Use serial instead of UUID for primary key").option("--no-example","Do not include example columns").option("-i, --interactive","Run in interactive mode").addHelpText("after",`
|
|
2143
2143
|
Examples:
|
|
2144
2144
|
$ drizzle-multitenant scaffold:schema orders --type=tenant
|
|
2145
2145
|
$ drizzle-multitenant scaffold:schema plans --type=shared
|
|
@@ -2283,13 +2283,13 @@ JSON File Format:
|
|
|
2283
2283
|
|
|
2284
2284
|
You can generate this file using:
|
|
2285
2285
|
$ drizzle-multitenant export > schemas.json
|
|
2286
|
-
`).action(async(n,e)=>{let t=L(),r=R("Importing schemas...");try{let a=resolve(process.cwd(),n);t.jsonMode||(d(`Reading schema file: ${G(relative(process.cwd(),a))}`),r.start());let o=await Un(a);if(!o.tables||!Array.isArray(o.tables))throw new Error("Invalid schema file: missing tables array");if(o.tables.length===0)throw new Error("Schema file contains no tables");let s={outputDir:resolve(process.cwd(),e.output??"./src/db/schema"),overwrite:e.overwrite,generateTenant:e.tenant!==!1,generateShared:e.shared!==!1,includeZod:e.includeZod,includeTypes:e.types!==!1,dryRun:e.dryRun},c=await qn().import(o,s);if(t.jsonMode||r.stop(),t.jsonMode)N({success:c.success,dryRun:e.dryRun,filesCreated:c.filesCreated,filesSkipped:c.filesSkipped,errors:c.errors,summary:{created:c.filesCreated.length,skipped:c.filesSkipped.length,errors:c.errors.length}});else {if(d(""),e.dryRun&&(d(T(Z("Dry run - no files were written"))),d("")),c.filesCreated.length>0){d(T(Y(`${e.dryRun?"Would create":"Created"} ${c.filesCreated.length} file(s):`)));for(let l of c.filesCreated)d(` ${Y("+")} ${relative(process.cwd(),l)}`);d("");}if(c.filesSkipped.length>0){d(T(Z(`Skipped ${c.filesSkipped.length} existing file(s):`)));for(let l of c.filesSkipped)d(` ${Z("-")} ${relative(process.cwd(),l)}`);d(""),d(f("Use --overwrite to replace existing files")),d("");}if(c.errors.length>0){d(T(v(`Errors (${c.errors.length}):`)));for(let l of c.errors)d(` ${relative(process.cwd(),l.file)}: ${l.error}`);d("");}c.success?E("Import completed successfully"):v("Import completed with errors"),c.filesCreated.length>0&&!e.dryRun&&(d(""),d(T("Next steps:")),d(" 1. Review the generated schema files"),d(" 2. Uncomment and adjust foreign key references"),d(" 3. Add relations if needed"),d(" 4. Generate migrations: npx drizzle-multitenant generate"));}c.success||process.exit(1);}catch(a){t.jsonMode||r.fail(a.message),j(a);}});process.on("SIGINT",()=>{console.log(
|
|
2286
|
+
`).action(async(n,e)=>{let t=L(),r=R("Importing schemas...");try{let a=resolve(process.cwd(),n);t.jsonMode||(d(`Reading schema file: ${G(relative(process.cwd(),a))}`),r.start());let o=await Un(a);if(!o.tables||!Array.isArray(o.tables))throw new Error("Invalid schema file: missing tables array");if(o.tables.length===0)throw new Error("Schema file contains no tables");let s={outputDir:resolve(process.cwd(),e.output??"./src/db/schema"),overwrite:e.overwrite,generateTenant:e.tenant!==!1,generateShared:e.shared!==!1,includeZod:e.includeZod,includeTypes:e.types!==!1,dryRun:e.dryRun},c=await qn().import(o,s);if(t.jsonMode||r.stop(),t.jsonMode)N({success:c.success,dryRun:e.dryRun,filesCreated:c.filesCreated,filesSkipped:c.filesSkipped,errors:c.errors,summary:{created:c.filesCreated.length,skipped:c.filesSkipped.length,errors:c.errors.length}});else {if(d(""),e.dryRun&&(d(T(Z("Dry run - no files were written"))),d("")),c.filesCreated.length>0){d(T(Y(`${e.dryRun?"Would create":"Created"} ${c.filesCreated.length} file(s):`)));for(let l of c.filesCreated)d(` ${Y("+")} ${relative(process.cwd(),l)}`);d("");}if(c.filesSkipped.length>0){d(T(Z(`Skipped ${c.filesSkipped.length} existing file(s):`)));for(let l of c.filesSkipped)d(` ${Z("-")} ${relative(process.cwd(),l)}`);d(""),d(f("Use --overwrite to replace existing files")),d("");}if(c.errors.length>0){d(T(v(`Errors (${c.errors.length}):`)));for(let l of c.errors)d(` ${relative(process.cwd(),l.file)}: ${l.error}`);d("");}c.success?E("Import completed successfully"):v("Import completed with errors"),c.filesCreated.length>0&&!e.dryRun&&(d(""),d(T("Next steps:")),d(" 1. Review the generated schema files"),d(" 2. Uncomment and adjust foreign key references"),d(" 3. Add relations if needed"),d(" 4. Generate migrations: npx drizzle-multitenant generate"));}c.success||process.exit(1);}catch(a){t.jsonMode||r.fail(a.message),j(a);}});process.on("SIGINT",()=>{console.log(M.cyan(`
|
|
2287
2287
|
|
|
2288
2288
|
Goodbye!
|
|
2289
|
-
`)),process.exit(0);});process.on("uncaughtException",n=>{n.name==="ExitPromptError"&&(console.log(
|
|
2289
|
+
`)),process.exit(0);});process.on("uncaughtException",n=>{n.name==="ExitPromptError"&&(console.log(M.cyan(`
|
|
2290
2290
|
|
|
2291
2291
|
Goodbye!
|
|
2292
|
-
`)),process.exit(0)),console.error(
|
|
2292
|
+
`)),process.exit(0)),console.error(M.red(`
|
|
2293
2293
|
Unexpected error:`),n.message),process.exit(1);});var U=new Command;U.name("drizzle-multitenant").description("Multi-tenancy toolkit for Drizzle ORM").version("1.1.0").option("--json","Output as JSON (machine-readable)").option("-v, --verbose","Show verbose output").option("-q, --quiet","Only show errors").option("--no-color","Disable colored output").hook("preAction",n=>{let e=n.opts();yr({json:e.json,verbose:e.verbose,quiet:e.quiet,noColor:e.color===false});});U.addHelpText("after",`
|
|
2294
2294
|
Examples:
|
|
2295
2295
|
$ drizzle-multitenant # Launch interactive mode
|