drizzle-multitenant 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +14 -0
- package/LICENSE +21 -0
- package/README.md +262 -0
- package/bin/drizzle-multitenant.js +2 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +866 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-DBerWr50.d.ts +76 -0
- package/dist/cross-schema/index.d.ts +262 -0
- package/dist/cross-schema/index.js +219 -0
- package/dist/cross-schema/index.js.map +1 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +935 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/express.d.ts +93 -0
- package/dist/integrations/express.js +110 -0
- package/dist/integrations/express.js.map +1 -0
- package/dist/integrations/fastify.d.ts +92 -0
- package/dist/integrations/fastify.js +236 -0
- package/dist/integrations/fastify.js.map +1 -0
- package/dist/integrations/hono.d.ts +2 -0
- package/dist/integrations/hono.js +3 -0
- package/dist/integrations/hono.js.map +1 -0
- package/dist/integrations/nestjs/index.d.ts +386 -0
- package/dist/integrations/nestjs/index.js +9828 -0
- package/dist/integrations/nestjs/index.js.map +1 -0
- package/dist/migrator/index.d.ts +208 -0
- package/dist/migrator/index.js +390 -0
- package/dist/migrator/index.js.map +1 -0
- package/dist/types-DKVaTaIb.d.ts +130 -0
- package/package.json +102 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { C as Config } from '../types-DKVaTaIb.js';
|
|
2
|
+
import 'pg';
|
|
3
|
+
import 'drizzle-orm/node-postgres';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Migration file metadata
|
|
7
|
+
*/
|
|
8
|
+
interface MigrationFile {
|
|
9
|
+
/** Migration file name */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Full file path */
|
|
12
|
+
path: string;
|
|
13
|
+
/** SQL content */
|
|
14
|
+
sql: string;
|
|
15
|
+
/** Timestamp extracted from filename */
|
|
16
|
+
timestamp: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Migration status for a tenant
|
|
20
|
+
*/
|
|
21
|
+
interface TenantMigrationStatus {
|
|
22
|
+
tenantId: string;
|
|
23
|
+
schemaName: string;
|
|
24
|
+
appliedCount: number;
|
|
25
|
+
pendingCount: number;
|
|
26
|
+
pendingMigrations: string[];
|
|
27
|
+
status: 'ok' | 'behind' | 'error';
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Migration result for a single tenant
|
|
32
|
+
*/
|
|
33
|
+
interface TenantMigrationResult {
|
|
34
|
+
tenantId: string;
|
|
35
|
+
schemaName: string;
|
|
36
|
+
success: boolean;
|
|
37
|
+
appliedMigrations: string[];
|
|
38
|
+
error?: string;
|
|
39
|
+
durationMs: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Aggregate migration results
|
|
43
|
+
*/
|
|
44
|
+
interface MigrationResults {
|
|
45
|
+
total: number;
|
|
46
|
+
succeeded: number;
|
|
47
|
+
failed: number;
|
|
48
|
+
skipped: number;
|
|
49
|
+
details: TenantMigrationResult[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Progress callback for migrations
|
|
53
|
+
*/
|
|
54
|
+
type MigrationProgressCallback = (tenantId: string, status: 'starting' | 'migrating' | 'completed' | 'failed' | 'skipped', migrationName?: string) => void;
|
|
55
|
+
/**
|
|
56
|
+
* Error handler for migrations
|
|
57
|
+
*/
|
|
58
|
+
type MigrationErrorHandler = (tenantId: string, error: Error) => 'continue' | 'abort';
|
|
59
|
+
/**
|
|
60
|
+
* Migration hooks
|
|
61
|
+
*/
|
|
62
|
+
interface MigrationHooks {
|
|
63
|
+
/** Called before migrating a tenant */
|
|
64
|
+
beforeTenant?: (tenantId: string) => void | Promise<void>;
|
|
65
|
+
/** Called after migrating a tenant */
|
|
66
|
+
afterTenant?: (tenantId: string, result: TenantMigrationResult) => void | Promise<void>;
|
|
67
|
+
/** Called before applying a migration */
|
|
68
|
+
beforeMigration?: (tenantId: string, migrationName: string) => void | Promise<void>;
|
|
69
|
+
/** Called after applying a migration */
|
|
70
|
+
afterMigration?: (tenantId: string, migrationName: string, durationMs: number) => void | Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Migrator configuration
|
|
74
|
+
*/
|
|
75
|
+
interface MigratorConfig {
|
|
76
|
+
/** Path to tenant migrations folder */
|
|
77
|
+
migrationsFolder: string;
|
|
78
|
+
/** Table name for tracking migrations */
|
|
79
|
+
migrationsTable?: string;
|
|
80
|
+
/** Function to discover tenant IDs */
|
|
81
|
+
tenantDiscovery: () => Promise<string[]>;
|
|
82
|
+
/** Migration hooks */
|
|
83
|
+
hooks?: MigrationHooks;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Migrate options
|
|
87
|
+
*/
|
|
88
|
+
interface MigrateOptions {
|
|
89
|
+
/** Number of concurrent migrations */
|
|
90
|
+
concurrency?: number;
|
|
91
|
+
/** Progress callback */
|
|
92
|
+
onProgress?: MigrationProgressCallback;
|
|
93
|
+
/** Error handler */
|
|
94
|
+
onError?: MigrationErrorHandler;
|
|
95
|
+
/** Dry run mode */
|
|
96
|
+
dryRun?: boolean;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Tenant creation options
|
|
100
|
+
*/
|
|
101
|
+
interface CreateTenantOptions {
|
|
102
|
+
/** Apply all migrations after creating schema */
|
|
103
|
+
migrate?: boolean;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Tenant drop options
|
|
107
|
+
*/
|
|
108
|
+
interface DropTenantOptions {
|
|
109
|
+
/** Skip confirmation (force drop) */
|
|
110
|
+
force?: boolean;
|
|
111
|
+
/** Cascade drop */
|
|
112
|
+
cascade?: boolean;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Applied migration record
|
|
116
|
+
*/
|
|
117
|
+
interface AppliedMigration {
|
|
118
|
+
id: number;
|
|
119
|
+
name: string;
|
|
120
|
+
appliedAt: Date;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parallel migration engine for multi-tenant applications
|
|
125
|
+
*/
|
|
126
|
+
declare class Migrator<TTenantSchema extends Record<string, unknown>, TSharedSchema extends Record<string, unknown>> {
|
|
127
|
+
private readonly tenantConfig;
|
|
128
|
+
private readonly migratorConfig;
|
|
129
|
+
private readonly migrationsTable;
|
|
130
|
+
constructor(tenantConfig: Config<TTenantSchema, TSharedSchema>, migratorConfig: MigratorConfig);
|
|
131
|
+
/**
|
|
132
|
+
* Migrate all tenants in parallel
|
|
133
|
+
*/
|
|
134
|
+
migrateAll(options?: MigrateOptions): Promise<MigrationResults>;
|
|
135
|
+
/**
|
|
136
|
+
* Migrate a single tenant
|
|
137
|
+
*/
|
|
138
|
+
migrateTenant(tenantId: string, migrations?: MigrationFile[], options?: {
|
|
139
|
+
dryRun?: boolean;
|
|
140
|
+
onProgress?: MigrateOptions['onProgress'];
|
|
141
|
+
}): Promise<TenantMigrationResult>;
|
|
142
|
+
/**
|
|
143
|
+
* Migrate specific tenants
|
|
144
|
+
*/
|
|
145
|
+
migrateTenants(tenantIds: string[], options?: MigrateOptions): Promise<MigrationResults>;
|
|
146
|
+
/**
|
|
147
|
+
* Get migration status for all tenants
|
|
148
|
+
*/
|
|
149
|
+
getStatus(): Promise<TenantMigrationStatus[]>;
|
|
150
|
+
/**
|
|
151
|
+
* Get migration status for a specific tenant
|
|
152
|
+
*/
|
|
153
|
+
getTenantStatus(tenantId: string, migrations?: MigrationFile[]): Promise<TenantMigrationStatus>;
|
|
154
|
+
/**
|
|
155
|
+
* Create a new tenant schema and optionally apply migrations
|
|
156
|
+
*/
|
|
157
|
+
createTenant(tenantId: string, options?: CreateTenantOptions): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Drop a tenant schema
|
|
160
|
+
*/
|
|
161
|
+
dropTenant(tenantId: string, options?: DropTenantOptions): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Check if a tenant schema exists
|
|
164
|
+
*/
|
|
165
|
+
tenantExists(tenantId: string): Promise<boolean>;
|
|
166
|
+
/**
|
|
167
|
+
* Load migration files from the migrations folder
|
|
168
|
+
*/
|
|
169
|
+
private loadMigrations;
|
|
170
|
+
/**
|
|
171
|
+
* Create a pool for a specific schema
|
|
172
|
+
*/
|
|
173
|
+
private createPool;
|
|
174
|
+
/**
|
|
175
|
+
* Ensure migrations table exists
|
|
176
|
+
*/
|
|
177
|
+
private ensureMigrationsTable;
|
|
178
|
+
/**
|
|
179
|
+
* Check if migrations table exists
|
|
180
|
+
*/
|
|
181
|
+
private migrationsTableExists;
|
|
182
|
+
/**
|
|
183
|
+
* Get applied migrations for a schema
|
|
184
|
+
*/
|
|
185
|
+
private getAppliedMigrations;
|
|
186
|
+
/**
|
|
187
|
+
* Apply a migration to a schema
|
|
188
|
+
*/
|
|
189
|
+
private applyMigration;
|
|
190
|
+
/**
|
|
191
|
+
* Create a skipped result
|
|
192
|
+
*/
|
|
193
|
+
private createSkippedResult;
|
|
194
|
+
/**
|
|
195
|
+
* Create an error result
|
|
196
|
+
*/
|
|
197
|
+
private createErrorResult;
|
|
198
|
+
/**
|
|
199
|
+
* Aggregate migration results
|
|
200
|
+
*/
|
|
201
|
+
private aggregateResults;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Create a migrator instance
|
|
205
|
+
*/
|
|
206
|
+
declare function createMigrator<TTenantSchema extends Record<string, unknown>, TSharedSchema extends Record<string, unknown>>(tenantConfig: Config<TTenantSchema, TSharedSchema>, migratorConfig: MigratorConfig): Migrator<TTenantSchema, TSharedSchema>;
|
|
207
|
+
|
|
208
|
+
export { type AppliedMigration, type CreateTenantOptions, type DropTenantOptions, type MigrateOptions, type MigrationErrorHandler, type MigrationFile, type MigrationHooks, type MigrationProgressCallback, type MigrationResults, Migrator, type MigratorConfig, type TenantMigrationResult, type TenantMigrationStatus, createMigrator };
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { readdir, readFile } from 'fs/promises';
|
|
2
|
+
import { join, basename } from 'path';
|
|
3
|
+
import { Pool } from 'pg';
|
|
4
|
+
|
|
5
|
+
// src/migrator/migrator.ts
|
|
6
|
+
var DEFAULT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
7
|
+
var Migrator = class {
|
|
8
|
+
constructor(tenantConfig, migratorConfig) {
|
|
9
|
+
this.tenantConfig = tenantConfig;
|
|
10
|
+
this.migratorConfig = migratorConfig;
|
|
11
|
+
this.migrationsTable = migratorConfig.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE;
|
|
12
|
+
}
|
|
13
|
+
migrationsTable;
|
|
14
|
+
/**
|
|
15
|
+
* Migrate all tenants in parallel
|
|
16
|
+
*/
|
|
17
|
+
async migrateAll(options = {}) {
|
|
18
|
+
const {
|
|
19
|
+
concurrency = 10,
|
|
20
|
+
onProgress,
|
|
21
|
+
onError,
|
|
22
|
+
dryRun = false
|
|
23
|
+
} = options;
|
|
24
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
25
|
+
const migrations = await this.loadMigrations();
|
|
26
|
+
const results = [];
|
|
27
|
+
let aborted = false;
|
|
28
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
29
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
30
|
+
const batchResults = await Promise.all(
|
|
31
|
+
batch.map(async (tenantId) => {
|
|
32
|
+
if (aborted) {
|
|
33
|
+
return this.createSkippedResult(tenantId);
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
onProgress?.(tenantId, "starting");
|
|
37
|
+
const result = await this.migrateTenant(tenantId, migrations, { dryRun, onProgress });
|
|
38
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
39
|
+
return result;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
onProgress?.(tenantId, "failed");
|
|
42
|
+
const action = onError?.(tenantId, error);
|
|
43
|
+
if (action === "abort") {
|
|
44
|
+
aborted = true;
|
|
45
|
+
}
|
|
46
|
+
return this.createErrorResult(tenantId, error);
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
results.push(...batchResults);
|
|
51
|
+
}
|
|
52
|
+
if (aborted) {
|
|
53
|
+
const remaining = tenantIds.slice(results.length);
|
|
54
|
+
for (const tenantId of remaining) {
|
|
55
|
+
results.push(this.createSkippedResult(tenantId));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return this.aggregateResults(results);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Migrate a single tenant
|
|
62
|
+
*/
|
|
63
|
+
async migrateTenant(tenantId, migrations, options = {}) {
|
|
64
|
+
const startTime = Date.now();
|
|
65
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
66
|
+
const appliedMigrations = [];
|
|
67
|
+
const pool = await this.createPool(schemaName);
|
|
68
|
+
try {
|
|
69
|
+
await this.migratorConfig.hooks?.beforeTenant?.(tenantId);
|
|
70
|
+
await this.ensureMigrationsTable(pool, schemaName);
|
|
71
|
+
const allMigrations = migrations ?? await this.loadMigrations();
|
|
72
|
+
const applied = await this.getAppliedMigrations(pool, schemaName);
|
|
73
|
+
const appliedSet = new Set(applied.map((m) => m.name));
|
|
74
|
+
const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
return {
|
|
77
|
+
tenantId,
|
|
78
|
+
schemaName,
|
|
79
|
+
success: true,
|
|
80
|
+
appliedMigrations: pending.map((m) => m.name),
|
|
81
|
+
durationMs: Date.now() - startTime
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
for (const migration of pending) {
|
|
85
|
+
const migrationStart = Date.now();
|
|
86
|
+
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
87
|
+
await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
88
|
+
await this.applyMigration(pool, schemaName, migration);
|
|
89
|
+
await this.migratorConfig.hooks?.afterMigration?.(
|
|
90
|
+
tenantId,
|
|
91
|
+
migration.name,
|
|
92
|
+
Date.now() - migrationStart
|
|
93
|
+
);
|
|
94
|
+
appliedMigrations.push(migration.name);
|
|
95
|
+
}
|
|
96
|
+
const result = {
|
|
97
|
+
tenantId,
|
|
98
|
+
schemaName,
|
|
99
|
+
success: true,
|
|
100
|
+
appliedMigrations,
|
|
101
|
+
durationMs: Date.now() - startTime
|
|
102
|
+
};
|
|
103
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
104
|
+
return result;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
const result = {
|
|
107
|
+
tenantId,
|
|
108
|
+
schemaName,
|
|
109
|
+
success: false,
|
|
110
|
+
appliedMigrations,
|
|
111
|
+
error: error.message,
|
|
112
|
+
durationMs: Date.now() - startTime
|
|
113
|
+
};
|
|
114
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
115
|
+
return result;
|
|
116
|
+
} finally {
|
|
117
|
+
await pool.end();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Migrate specific tenants
|
|
122
|
+
*/
|
|
123
|
+
async migrateTenants(tenantIds, options = {}) {
|
|
124
|
+
const migrations = await this.loadMigrations();
|
|
125
|
+
const results = [];
|
|
126
|
+
const { concurrency = 10, onProgress, onError } = options;
|
|
127
|
+
for (let i = 0; i < tenantIds.length; i += concurrency) {
|
|
128
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
129
|
+
const batchResults = await Promise.all(
|
|
130
|
+
batch.map(async (tenantId) => {
|
|
131
|
+
try {
|
|
132
|
+
onProgress?.(tenantId, "starting");
|
|
133
|
+
const result = await this.migrateTenant(tenantId, migrations, { dryRun: options.dryRun ?? false, onProgress });
|
|
134
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
135
|
+
return result;
|
|
136
|
+
} catch (error) {
|
|
137
|
+
onProgress?.(tenantId, "failed");
|
|
138
|
+
onError?.(tenantId, error);
|
|
139
|
+
return this.createErrorResult(tenantId, error);
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
results.push(...batchResults);
|
|
144
|
+
}
|
|
145
|
+
return this.aggregateResults(results);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Get migration status for all tenants
|
|
149
|
+
*/
|
|
150
|
+
async getStatus() {
|
|
151
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
152
|
+
const migrations = await this.loadMigrations();
|
|
153
|
+
const statuses = [];
|
|
154
|
+
for (const tenantId of tenantIds) {
|
|
155
|
+
statuses.push(await this.getTenantStatus(tenantId, migrations));
|
|
156
|
+
}
|
|
157
|
+
return statuses;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get migration status for a specific tenant
|
|
161
|
+
*/
|
|
162
|
+
async getTenantStatus(tenantId, migrations) {
|
|
163
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
164
|
+
const pool = await this.createPool(schemaName);
|
|
165
|
+
try {
|
|
166
|
+
const allMigrations = migrations ?? await this.loadMigrations();
|
|
167
|
+
const tableExists = await this.migrationsTableExists(pool, schemaName);
|
|
168
|
+
if (!tableExists) {
|
|
169
|
+
return {
|
|
170
|
+
tenantId,
|
|
171
|
+
schemaName,
|
|
172
|
+
appliedCount: 0,
|
|
173
|
+
pendingCount: allMigrations.length,
|
|
174
|
+
pendingMigrations: allMigrations.map((m) => m.name),
|
|
175
|
+
status: allMigrations.length > 0 ? "behind" : "ok"
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const applied = await this.getAppliedMigrations(pool, schemaName);
|
|
179
|
+
const appliedSet = new Set(applied.map((m) => m.name));
|
|
180
|
+
const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
|
|
181
|
+
return {
|
|
182
|
+
tenantId,
|
|
183
|
+
schemaName,
|
|
184
|
+
appliedCount: applied.length,
|
|
185
|
+
pendingCount: pending.length,
|
|
186
|
+
pendingMigrations: pending.map((m) => m.name),
|
|
187
|
+
status: pending.length > 0 ? "behind" : "ok"
|
|
188
|
+
};
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return {
|
|
191
|
+
tenantId,
|
|
192
|
+
schemaName,
|
|
193
|
+
appliedCount: 0,
|
|
194
|
+
pendingCount: 0,
|
|
195
|
+
pendingMigrations: [],
|
|
196
|
+
status: "error",
|
|
197
|
+
error: error.message
|
|
198
|
+
};
|
|
199
|
+
} finally {
|
|
200
|
+
await pool.end();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Create a new tenant schema and optionally apply migrations
|
|
205
|
+
*/
|
|
206
|
+
async createTenant(tenantId, options = {}) {
|
|
207
|
+
const { migrate = true } = options;
|
|
208
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
209
|
+
const pool = new Pool({
|
|
210
|
+
connectionString: this.tenantConfig.connection.url,
|
|
211
|
+
...this.tenantConfig.connection.poolConfig
|
|
212
|
+
});
|
|
213
|
+
try {
|
|
214
|
+
await pool.query(`CREATE SCHEMA IF NOT EXISTS "${schemaName}"`);
|
|
215
|
+
if (migrate) {
|
|
216
|
+
await this.migrateTenant(tenantId);
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
await pool.end();
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Drop a tenant schema
|
|
224
|
+
*/
|
|
225
|
+
async dropTenant(tenantId, options = {}) {
|
|
226
|
+
const { cascade = true } = options;
|
|
227
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
228
|
+
const pool = new Pool({
|
|
229
|
+
connectionString: this.tenantConfig.connection.url,
|
|
230
|
+
...this.tenantConfig.connection.poolConfig
|
|
231
|
+
});
|
|
232
|
+
try {
|
|
233
|
+
const cascadeSql = cascade ? "CASCADE" : "RESTRICT";
|
|
234
|
+
await pool.query(`DROP SCHEMA IF EXISTS "${schemaName}" ${cascadeSql}`);
|
|
235
|
+
} finally {
|
|
236
|
+
await pool.end();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Check if a tenant schema exists
|
|
241
|
+
*/
|
|
242
|
+
async tenantExists(tenantId) {
|
|
243
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
244
|
+
const pool = new Pool({
|
|
245
|
+
connectionString: this.tenantConfig.connection.url,
|
|
246
|
+
...this.tenantConfig.connection.poolConfig
|
|
247
|
+
});
|
|
248
|
+
try {
|
|
249
|
+
const result = await pool.query(
|
|
250
|
+
`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`,
|
|
251
|
+
[schemaName]
|
|
252
|
+
);
|
|
253
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
254
|
+
} finally {
|
|
255
|
+
await pool.end();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Load migration files from the migrations folder
|
|
260
|
+
*/
|
|
261
|
+
async loadMigrations() {
|
|
262
|
+
const files = await readdir(this.migratorConfig.migrationsFolder);
|
|
263
|
+
const migrations = [];
|
|
264
|
+
for (const file of files) {
|
|
265
|
+
if (!file.endsWith(".sql")) continue;
|
|
266
|
+
const filePath = join(this.migratorConfig.migrationsFolder, file);
|
|
267
|
+
const content = await readFile(filePath, "utf-8");
|
|
268
|
+
const match = file.match(/^(\d+)_/);
|
|
269
|
+
const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;
|
|
270
|
+
migrations.push({
|
|
271
|
+
name: basename(file, ".sql"),
|
|
272
|
+
path: filePath,
|
|
273
|
+
sql: content,
|
|
274
|
+
timestamp
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return migrations.sort((a, b) => a.timestamp - b.timestamp);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Create a pool for a specific schema
|
|
281
|
+
*/
|
|
282
|
+
async createPool(schemaName) {
|
|
283
|
+
return new Pool({
|
|
284
|
+
connectionString: this.tenantConfig.connection.url,
|
|
285
|
+
...this.tenantConfig.connection.poolConfig,
|
|
286
|
+
options: `-c search_path="${schemaName}",public`
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* Ensure migrations table exists
|
|
291
|
+
*/
|
|
292
|
+
async ensureMigrationsTable(pool, schemaName) {
|
|
293
|
+
await pool.query(`
|
|
294
|
+
CREATE TABLE IF NOT EXISTS "${schemaName}"."${this.migrationsTable}" (
|
|
295
|
+
id SERIAL PRIMARY KEY,
|
|
296
|
+
name VARCHAR(255) NOT NULL UNIQUE,
|
|
297
|
+
applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
298
|
+
)
|
|
299
|
+
`);
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Check if migrations table exists
|
|
303
|
+
*/
|
|
304
|
+
async migrationsTableExists(pool, schemaName) {
|
|
305
|
+
const result = await pool.query(
|
|
306
|
+
`SELECT 1 FROM information_schema.tables
|
|
307
|
+
WHERE table_schema = $1 AND table_name = $2`,
|
|
308
|
+
[schemaName, this.migrationsTable]
|
|
309
|
+
);
|
|
310
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get applied migrations for a schema
|
|
314
|
+
*/
|
|
315
|
+
async getAppliedMigrations(pool, schemaName) {
|
|
316
|
+
const result = await pool.query(
|
|
317
|
+
`SELECT id, name, applied_at FROM "${schemaName}"."${this.migrationsTable}" ORDER BY id`
|
|
318
|
+
);
|
|
319
|
+
return result.rows.map((row) => ({
|
|
320
|
+
id: row.id,
|
|
321
|
+
name: row.name,
|
|
322
|
+
appliedAt: row.applied_at
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Apply a migration to a schema
|
|
327
|
+
*/
|
|
328
|
+
async applyMigration(pool, schemaName, migration) {
|
|
329
|
+
const client = await pool.connect();
|
|
330
|
+
try {
|
|
331
|
+
await client.query("BEGIN");
|
|
332
|
+
await client.query(migration.sql);
|
|
333
|
+
await client.query(
|
|
334
|
+
`INSERT INTO "${schemaName}"."${this.migrationsTable}" (name) VALUES ($1)`,
|
|
335
|
+
[migration.name]
|
|
336
|
+
);
|
|
337
|
+
await client.query("COMMIT");
|
|
338
|
+
} catch (error) {
|
|
339
|
+
await client.query("ROLLBACK");
|
|
340
|
+
throw error;
|
|
341
|
+
} finally {
|
|
342
|
+
client.release();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Create a skipped result
|
|
347
|
+
*/
|
|
348
|
+
createSkippedResult(tenantId) {
|
|
349
|
+
return {
|
|
350
|
+
tenantId,
|
|
351
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
352
|
+
success: false,
|
|
353
|
+
appliedMigrations: [],
|
|
354
|
+
error: "Skipped due to abort",
|
|
355
|
+
durationMs: 0
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Create an error result
|
|
360
|
+
*/
|
|
361
|
+
createErrorResult(tenantId, error) {
|
|
362
|
+
return {
|
|
363
|
+
tenantId,
|
|
364
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
365
|
+
success: false,
|
|
366
|
+
appliedMigrations: [],
|
|
367
|
+
error: error.message,
|
|
368
|
+
durationMs: 0
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Aggregate migration results
|
|
373
|
+
*/
|
|
374
|
+
aggregateResults(results) {
|
|
375
|
+
return {
|
|
376
|
+
total: results.length,
|
|
377
|
+
succeeded: results.filter((r) => r.success).length,
|
|
378
|
+
failed: results.filter((r) => !r.success && r.error !== "Skipped due to abort").length,
|
|
379
|
+
skipped: results.filter((r) => r.error === "Skipped due to abort").length,
|
|
380
|
+
details: results
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
function createMigrator(tenantConfig, migratorConfig) {
|
|
385
|
+
return new Migrator(tenantConfig, migratorConfig);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export { Migrator, createMigrator };
|
|
389
|
+
//# sourceMappingURL=index.js.map
|
|
390
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/migrator/migrator.ts"],"names":[],"mappings":";;;;;AAgBA,IAAM,wBAAA,GAA2B,sBAAA;AAK1B,IAAM,WAAN,MAGL;AAAA,EAGA,WAAA,CACmB,cACA,cAAA,EACjB;AAFiB,IAAA,IAAA,CAAA,YAAA,GAAA,YAAA;AACA,IAAA,IAAA,CAAA,cAAA,GAAA,cAAA;AAEjB,IAAA,IAAA,CAAK,eAAA,GAAkB,eAAe,eAAA,IAAmB,wBAAA;AAAA,EAC3D;AAAA,EAPiB,eAAA;AAAA;AAAA;AAAA;AAAA,EAYjB,MAAM,UAAA,CAAW,OAAA,GAA0B,EAAC,EAA8B;AACxE,IAAA,MAAM;AAAA,MACJ,WAAA,GAAc,EAAA;AAAA,MACd,UAAA;AAAA,MACA,OAAA;AAAA,MACA,MAAA,GAAS;AAAA,KACX,GAAI,OAAA;AAEJ,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,cAAA,CAAe,eAAA,EAAgB;AAC5D,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,cAAA,EAAe;AAE7C,IAAA,MAAM,UAAmC,EAAC;AAC1C,IAAA,IAAI,OAAA,GAAU,KAAA;AAGd,IAAA,KAAA,IAAS,CAAA,GAAI,GAAG,CAAA,GAAI,SAAA,CAAU,UAAU,CAAC,OAAA,EAAS,KAAK,WAAA,EAAa;AAClE,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,KAAA,CAAM,CAAA,EAAG,IAAI,WAAW,CAAA;AAEhD,MAAA,MAAM,YAAA,GAAe,MAAM,OAAA,CAAQ,GAAA;AAAA,QACjC,KAAA,CAAM,GAAA,CAAI,OAAO,QAAA,KAAa;AAC5B,UAAA,IAAI,OAAA,EAAS;AACX,YAAA,OAAO,IAAA,CAAK,oBAAoB,QAAQ,CAAA;AAAA,UAC1C;AAEA,UAAA,IAAI;AACF,YAAA,UAAA,GAAa,UAAU,UAAU,CAAA;AACjC,YAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,aAAA,CAAc,UAAU,UAAA,EAAY,EAAE,MAAA,EAAQ,UAAA,EAAY,CAAA;AACpF,YAAA,UAAA,GAAa,QAAA,EAAU,MAAA,CAAO,OAAA,GAAU,WAAA,GAAc,QAAQ,CAAA;AAC9D,YAAA,OAAO,MAAA;AAAA,UACT,SAAS,KAAA,EAAO;AACd,YAAA,UAAA,GAAa,UAAU,QAAQ,CAAA;AAC/B,YAAA,MAAM,MAAA,GAAS,OAAA,GAAU,QAAA,EAAU,KAAc,CAAA;AACjD,YAAA,IAAI,WAAW,OAAA,EAAS;AACtB,cAAA,OAAA,GAAU,IAAA;AAAA,YACZ;AACA,YAAA,OAAO,IAAA,CAAK,iBAAA,CAAkB,QAAA,EAAU,KAAc,CAAA;AAAA,UACxD;AAAA,QACF,CAAC;AAAA,OACH;AAEA,MAAA,OAAA,CAAQ,IAAA,CAAK,GAAG,YAAY,CAAA;AAAA,IAC9B;AAGA,IAAA,IAAI,OAAA,EAAS;AACX,MAAA,MAAM,SAAA,GAAY,SAAA,CAAU,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA;AAChD,MAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,QAAA,OAAA,CAAQ,IAAA,CAAK,IAAA,CAAK,mBAAA,CAAoB,QAAQ,CAAC,CAAA;AAAA,MACjD;AAAA,IACF;AAEA,IAAA,OAAO,IAAA,CAAK,iBAAiB,OAAO,CAAA;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAA,CACJ,QAAA,EACA,UAAA,EACA,OAAA,GAA2E,EAAC,EAC5C;AAChC,IAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAC3B,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAC1E,IAAA,MAAM,oBAA8B,EAAC;AAErC,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA;AAE7C,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,cAAA,CAAe,KAAA,EAAO,YAAA,GAAe,QAAQ,CAAA;AAGxD,MAAA,MAAM,IAAA,CAAK,qBAAA,CAAsB,IAAA,EAAM,UAAU,CAAA;AAGjD,MAAA,MAAM,aAAA,GAAgB,UAAA,IAAc,MAAM,IAAA,CAAK,cAAA,EAAe;AAG9D,MAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,oBAAA,CAAqB,MAAM,UAAU,CAAA;AAChE,MAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA;AAGrD,MAAA,MAAM,OAAA,GAAU,aAAA,CAAc,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,UAAA,CAAW,GAAA,CAAI,CAAA,CAAE,IAAI,CAAC,CAAA;AAEnE,MAAA,IAAI,QAAQ,MAAA,EAAQ;AAClB,QAAA,OAAO;AAAA,UACL,QAAA;AAAA,UACA,UAAA;AAAA,UACA,OAAA,EAAS,IAAA;AAAA,UACT,mBAAmB,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,UAC5C,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,SAC3B;AAAA,MACF;AAGA,MAAA,KAAA,MAAW,aAAa,OAAA,EAAS;AAC/B,QAAA,MAAM,cAAA,GAAiB,KAAK,GAAA,EAAI;AAChC,QAAA,OAAA,CAAQ,UAAA,GAAa,QAAA,EAAU,WAAA,EAAa,SAAA,CAAU,IAAI,CAAA;AAE1D,QAAA,MAAM,KAAK,cAAA,CAAe,KAAA,EAAO,eAAA,GAAkB,QAAA,EAAU,UAAU,IAAI,CAAA;AAC3E,QAAA,MAAM,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM,UAAA,EAAY,SAAS,CAAA;AACrD,QAAA,MAAM,IAAA,CAAK,eAAe,KAAA,EAAO,cAAA;AAAA,UAC/B,QAAA;AAAA,UACA,SAAA,CAAU,IAAA;AAAA,UACV,IAAA,CAAK,KAAI,GAAI;AAAA,SACf;AAEA,QAAA,iBAAA,CAAkB,IAAA,CAAK,UAAU,IAAI,CAAA;AAAA,MACvC;AAEA,MAAA,MAAM,MAAA,GAAgC;AAAA,QACpC,QAAA;AAAA,QACA,UAAA;AAAA,QACA,OAAA,EAAS,IAAA;AAAA,QACT,iBAAA;AAAA,QACA,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC3B;AAEA,MAAA,MAAM,IAAA,CAAK,cAAA,CAAe,KAAA,EAAO,WAAA,GAAc,UAAU,MAAM,CAAA;AAE/D,MAAA,OAAO,MAAA;AAAA,IACT,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,MAAA,GAAgC;AAAA,QACpC,QAAA;AAAA,QACA,UAAA;AAAA,QACA,OAAA,EAAS,KAAA;AAAA,QACT,iBAAA;AAAA,QACA,OAAQ,KAAA,CAAgB,OAAA;AAAA,QACxB,UAAA,EAAY,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,OAC3B;AAEA,MAAA,MAAM,IAAA,CAAK,cAAA,CAAe,KAAA,EAAO,WAAA,GAAc,UAAU,MAAM,CAAA;AAE/D,MAAA,OAAO,MAAA;AAAA,IACT,CAAA,SAAE;AACA,MAAA,MAAM,KAAK,GAAA,EAAI;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,cAAA,CAAe,SAAA,EAAqB,OAAA,GAA0B,EAAC,EAA8B;AACjG,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,cAAA,EAAe;AAC7C,IAAA,MAAM,UAAmC,EAAC;AAE1C,IAAA,MAAM,EAAE,WAAA,GAAc,EAAA,EAAI,UAAA,EAAY,SAAQ,GAAI,OAAA;AAElD,IAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,SAAA,CAAU,MAAA,EAAQ,KAAK,WAAA,EAAa;AACtD,MAAA,MAAM,KAAA,GAAQ,SAAA,CAAU,KAAA,CAAM,CAAA,EAAG,IAAI,WAAW,CAAA;AAEhD,MAAA,MAAM,YAAA,GAAe,MAAM,OAAA,CAAQ,GAAA;AAAA,QACjC,KAAA,CAAM,GAAA,CAAI,OAAO,QAAA,KAAa;AAC5B,UAAA,IAAI;AACF,YAAA,UAAA,GAAa,UAAU,UAAU,CAAA;AACjC,YAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,aAAA,CAAc,QAAA,EAAU,UAAA,EAAY,EAAE,MAAA,EAAQ,OAAA,CAAQ,MAAA,IAAU,KAAA,EAAO,UAAA,EAAY,CAAA;AAC7G,YAAA,UAAA,GAAa,QAAA,EAAU,MAAA,CAAO,OAAA,GAAU,WAAA,GAAc,QAAQ,CAAA;AAC9D,YAAA,OAAO,MAAA;AAAA,UACT,SAAS,KAAA,EAAO;AACd,YAAA,UAAA,GAAa,UAAU,QAAQ,CAAA;AAC/B,YAAA,OAAA,GAAU,UAAU,KAAc,CAAA;AAClC,YAAA,OAAO,IAAA,CAAK,iBAAA,CAAkB,QAAA,EAAU,KAAc,CAAA;AAAA,UACxD;AAAA,QACF,CAAC;AAAA,OACH;AAEA,MAAA,OAAA,CAAQ,IAAA,CAAK,GAAG,YAAY,CAAA;AAAA,IAC9B;AAEA,IAAA,OAAO,IAAA,CAAK,iBAAiB,OAAO,CAAA;AAAA,EACtC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,SAAA,GAA8C;AAClD,IAAA,MAAM,SAAA,GAAY,MAAM,IAAA,CAAK,cAAA,CAAe,eAAA,EAAgB;AAC5D,IAAA,MAAM,UAAA,GAAa,MAAM,IAAA,CAAK,cAAA,EAAe;AAC7C,IAAA,MAAM,WAAoC,EAAC;AAE3C,IAAA,KAAA,MAAW,YAAY,SAAA,EAAW;AAChC,MAAA,QAAA,CAAS,KAAK,MAAM,IAAA,CAAK,eAAA,CAAgB,QAAA,EAAU,UAAU,CAAC,CAAA;AAAA,IAChE;AAEA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,eAAA,CAAgB,QAAA,EAAkB,UAAA,EAA8D;AACpG,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAC1E,IAAA,MAAM,IAAA,GAAO,MAAM,IAAA,CAAK,UAAA,CAAW,UAAU,CAAA;AAE7C,IAAA,IAAI;AACF,MAAA,MAAM,aAAA,GAAgB,UAAA,IAAc,MAAM,IAAA,CAAK,cAAA,EAAe;AAG9D,MAAA,MAAM,WAAA,GAAc,MAAM,IAAA,CAAK,qBAAA,CAAsB,MAAM,UAAU,CAAA;AACrE,MAAA,IAAI,CAAC,WAAA,EAAa;AAChB,QAAA,OAAO;AAAA,UACL,QAAA;AAAA,UACA,UAAA;AAAA,UACA,YAAA,EAAc,CAAA;AAAA,UACd,cAAc,aAAA,CAAc,MAAA;AAAA,UAC5B,mBAAmB,aAAA,CAAc,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,UAClD,MAAA,EAAQ,aAAA,CAAc,MAAA,GAAS,CAAA,GAAI,QAAA,GAAW;AAAA,SAChD;AAAA,MACF;AAEA,MAAA,MAAM,OAAA,GAAU,MAAM,IAAA,CAAK,oBAAA,CAAqB,MAAM,UAAU,CAAA;AAChE,MAAA,MAAM,UAAA,GAAa,IAAI,GAAA,CAAI,OAAA,CAAQ,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC,CAAA;AACrD,MAAA,MAAM,OAAA,GAAU,aAAA,CAAc,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,UAAA,CAAW,GAAA,CAAI,CAAA,CAAE,IAAI,CAAC,CAAA;AAEnE,MAAA,OAAO;AAAA,QACL,QAAA;AAAA,QACA,UAAA;AAAA,QACA,cAAc,OAAA,CAAQ,MAAA;AAAA,QACtB,cAAc,OAAA,CAAQ,MAAA;AAAA,QACtB,mBAAmB,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,IAAI,CAAA;AAAA,QAC5C,MAAA,EAAQ,OAAA,CAAQ,MAAA,GAAS,CAAA,GAAI,QAAA,GAAW;AAAA,OAC1C;AAAA,IACF,SAAS,KAAA,EAAO;AACd,MAAA,OAAO;AAAA,QACL,QAAA;AAAA,QACA,UAAA;AAAA,QACA,YAAA,EAAc,CAAA;AAAA,QACd,YAAA,EAAc,CAAA;AAAA,QACd,mBAAmB,EAAC;AAAA,QACpB,MAAA,EAAQ,OAAA;AAAA,QACR,OAAQ,KAAA,CAAgB;AAAA,OAC1B;AAAA,IACF,CAAA,SAAE;AACA,MAAA,MAAM,KAAK,GAAA,EAAI;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAA,CAAa,QAAA,EAAkB,OAAA,GAA+B,EAAC,EAAkB;AACrF,IAAA,MAAM,EAAE,OAAA,GAAU,IAAA,EAAK,GAAI,OAAA;AAC3B,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAE1E,IAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK;AAAA,MACpB,gBAAA,EAAkB,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW,GAAA;AAAA,MAC/C,GAAG,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW;AAAA,KACjC,CAAA;AAED,IAAA,IAAI;AAEF,MAAA,MAAM,IAAA,CAAK,KAAA,CAAM,CAAA,6BAAA,EAAgC,UAAU,CAAA,CAAA,CAAG,CAAA;AAE9D,MAAA,IAAI,OAAA,EAAS;AAEX,QAAA,MAAM,IAAA,CAAK,cAAc,QAAQ,CAAA;AAAA,MACnC;AAAA,IACF,CAAA,SAAE;AACA,MAAA,MAAM,KAAK,GAAA,EAAI;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,UAAA,CAAW,QAAA,EAAkB,OAAA,GAA6B,EAAC,EAAkB;AACjF,IAAA,MAAM,EAAE,OAAA,GAAU,IAAA,EAAK,GAAI,OAAA;AAC3B,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAE1E,IAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK;AAAA,MACpB,gBAAA,EAAkB,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW,GAAA;AAAA,MAC/C,GAAG,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW;AAAA,KACjC,CAAA;AAED,IAAA,IAAI;AACF,MAAA,MAAM,UAAA,GAAa,UAAU,SAAA,GAAY,UAAA;AACzC,MAAA,MAAM,KAAK,KAAA,CAAM,CAAA,uBAAA,EAA0B,UAAU,CAAA,EAAA,EAAK,UAAU,CAAA,CAAE,CAAA;AAAA,IACxE,CAAA,SAAE;AACA,MAAA,MAAM,KAAK,GAAA,EAAI;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,QAAA,EAAoC;AACrD,IAAA,MAAM,UAAA,GAAa,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAE1E,IAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK;AAAA,MACpB,gBAAA,EAAkB,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW,GAAA;AAAA,MAC/C,GAAG,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW;AAAA,KACjC,CAAA;AAED,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA;AAAA,QACxB,CAAA,gEAAA,CAAA;AAAA,QACA,CAAC,UAAU;AAAA,OACb;AACA,MAAA,OAAO,MAAA,CAAO,QAAA,KAAa,IAAA,IAAQ,MAAA,CAAO,QAAA,GAAW,CAAA;AAAA,IACvD,CAAA,SAAE;AACA,MAAA,MAAM,KAAK,GAAA,EAAI;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cAAA,GAA2C;AACvD,IAAA,MAAM,KAAA,GAAQ,MAAM,OAAA,CAAQ,IAAA,CAAK,eAAe,gBAAgB,CAAA;AAEhE,IAAA,MAAM,aAA8B,EAAC;AAErC,IAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,MAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,MAAM,CAAA,EAAG;AAE5B,MAAA,MAAM,QAAA,GAAW,IAAA,CAAK,IAAA,CAAK,cAAA,CAAe,kBAAkB,IAAI,CAAA;AAChE,MAAA,MAAM,OAAA,GAAU,MAAM,QAAA,CAAS,QAAA,EAAU,OAAO,CAAA;AAGhD,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAClC,MAAA,MAAM,SAAA,GAAY,QAAQ,CAAC,CAAA,GAAI,SAAS,KAAA,CAAM,CAAC,CAAA,EAAG,EAAE,CAAA,GAAI,CAAA;AAExD,MAAA,UAAA,CAAW,IAAA,CAAK;AAAA,QACd,IAAA,EAAM,QAAA,CAAS,IAAA,EAAM,MAAM,CAAA;AAAA,QAC3B,IAAA,EAAM,QAAA;AAAA,QACN,GAAA,EAAK,OAAA;AAAA,QACL;AAAA,OACD,CAAA;AAAA,IACH;AAGA,IAAA,OAAO,UAAA,CAAW,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,SAAA,GAAY,EAAE,SAAS,CAAA;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,WAAW,UAAA,EAAmC;AAC1D,IAAA,OAAO,IAAI,IAAA,CAAK;AAAA,MACd,gBAAA,EAAkB,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW,GAAA;AAAA,MAC/C,GAAG,IAAA,CAAK,YAAA,CAAa,UAAA,CAAW,UAAA;AAAA,MAChC,OAAA,EAAS,mBAAmB,UAAU,CAAA,QAAA;AAAA,KACvC,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,qBAAA,CAAsB,IAAA,EAAY,UAAA,EAAmC;AACjF,IAAA,MAAM,KAAK,KAAA,CAAM;AAAA,kCAAA,EACe,UAAU,CAAA,GAAA,EAAM,IAAA,CAAK,eAAe,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAA,CAKnE,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,qBAAA,CAAsB,IAAA,EAAY,UAAA,EAAsC;AACpF,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA;AAAA,MACxB,CAAA;AAAA,kDAAA,CAAA;AAAA,MAEA,CAAC,UAAA,EAAY,IAAA,CAAK,eAAe;AAAA,KACnC;AACA,IAAA,OAAO,MAAA,CAAO,QAAA,KAAa,IAAA,IAAQ,MAAA,CAAO,QAAA,GAAW,CAAA;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAAA,CAAqB,IAAA,EAAY,UAAA,EAAiD;AAC9F,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,KAAA;AAAA,MACxB,CAAA,kCAAA,EAAqC,UAAU,CAAA,GAAA,EAAM,IAAA,CAAK,eAAe,CAAA,aAAA;AAAA,KAC3E;AACA,IAAA,OAAO,MAAA,CAAO,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,MAC/B,IAAI,GAAA,CAAI,EAAA;AAAA,MACR,MAAM,GAAA,CAAI,IAAA;AAAA,MACV,WAAW,GAAA,CAAI;AAAA,KACjB,CAAE,CAAA;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cAAA,CAAe,IAAA,EAAY,UAAA,EAAoB,SAAA,EAAyC;AACpG,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,OAAA,EAAQ;AAElC,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,CAAO,MAAM,OAAO,CAAA;AAG1B,MAAA,MAAM,MAAA,CAAO,KAAA,CAAM,SAAA,CAAU,GAAG,CAAA;AAGhC,MAAA,MAAM,MAAA,CAAO,KAAA;AAAA,QACX,CAAA,aAAA,EAAgB,UAAU,CAAA,GAAA,EAAM,IAAA,CAAK,eAAe,CAAA,oBAAA,CAAA;AAAA,QACpD,CAAC,UAAU,IAAI;AAAA,OACjB;AAEA,MAAA,MAAM,MAAA,CAAO,MAAM,QAAQ,CAAA;AAAA,IAC7B,SAAS,KAAA,EAAO;AACd,MAAA,MAAM,MAAA,CAAO,MAAM,UAAU,CAAA;AAC7B,MAAA,MAAM,KAAA;AAAA,IACR,CAAA,SAAE;AACA,MAAA,MAAA,CAAO,OAAA,EAAQ;AAAA,IACjB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,oBAAoB,QAAA,EAAyC;AACnE,IAAA,OAAO;AAAA,MACL,QAAA;AAAA,MACA,UAAA,EAAY,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAAA,MACnE,OAAA,EAAS,KAAA;AAAA,MACT,mBAAmB,EAAC;AAAA,MACpB,KAAA,EAAO,sBAAA;AAAA,MACP,UAAA,EAAY;AAAA,KACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAA,CAAkB,UAAkB,KAAA,EAAqC;AAC/E,IAAA,OAAO;AAAA,MACL,QAAA;AAAA,MACA,UAAA,EAAY,IAAA,CAAK,YAAA,CAAa,SAAA,CAAU,mBAAmB,QAAQ,CAAA;AAAA,MACnE,OAAA,EAAS,KAAA;AAAA,MACT,mBAAmB,EAAC;AAAA,MACpB,OAAO,KAAA,CAAM,OAAA;AAAA,MACb,UAAA,EAAY;AAAA,KACd;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,iBAAiB,OAAA,EAAoD;AAC3E,IAAA,OAAO;AAAA,MACL,OAAO,OAAA,CAAQ,MAAA;AAAA,MACf,WAAW,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,OAAO,CAAA,CAAE,MAAA;AAAA,MAC5C,MAAA,EAAQ,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,CAAA,CAAE,OAAA,IAAW,CAAA,CAAE,KAAA,KAAU,sBAAsB,CAAA,CAAE,MAAA;AAAA,MAChF,OAAA,EAAS,QAAQ,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,KAAA,KAAU,sBAAsB,CAAA,CAAE,MAAA;AAAA,MACnE,OAAA,EAAS;AAAA,KACX;AAAA,EACF;AACF;AAKO,SAAS,cAAA,CAId,cACA,cAAA,EACwC;AACxC,EAAA,OAAO,IAAI,QAAA,CAAS,YAAA,EAAc,cAAc,CAAA;AAClD","file":"index.js","sourcesContent":["import { readdir, readFile } from 'node:fs/promises';\nimport { join, basename } from 'node:path';\nimport { Pool } from 'pg';\nimport type { Config } from '../types.js';\nimport type {\n MigratorConfig,\n MigrationFile,\n MigrateOptions,\n TenantMigrationResult,\n MigrationResults,\n TenantMigrationStatus,\n AppliedMigration,\n CreateTenantOptions,\n DropTenantOptions,\n} from './types.js';\n\nconst DEFAULT_MIGRATIONS_TABLE = '__drizzle_migrations';\n\n/**\n * Parallel migration engine for multi-tenant applications\n */\nexport class Migrator<\n TTenantSchema extends Record<string, unknown>,\n TSharedSchema extends Record<string, unknown>,\n> {\n private readonly migrationsTable: string;\n\n constructor(\n private readonly tenantConfig: Config<TTenantSchema, TSharedSchema>,\n private readonly migratorConfig: MigratorConfig\n ) {\n this.migrationsTable = migratorConfig.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE;\n }\n\n /**\n * Migrate all tenants in parallel\n */\n async migrateAll(options: MigrateOptions = {}): Promise<MigrationResults> {\n const {\n concurrency = 10,\n onProgress,\n onError,\n dryRun = false,\n } = options;\n\n const tenantIds = await this.migratorConfig.tenantDiscovery();\n const migrations = await this.loadMigrations();\n\n const results: TenantMigrationResult[] = [];\n let aborted = false;\n\n // Process tenants in batches\n for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {\n const batch = tenantIds.slice(i, i + concurrency);\n\n const batchResults = await Promise.all(\n batch.map(async (tenantId) => {\n if (aborted) {\n return this.createSkippedResult(tenantId);\n }\n\n try {\n onProgress?.(tenantId, 'starting');\n const result = await this.migrateTenant(tenantId, migrations, { dryRun, onProgress });\n onProgress?.(tenantId, result.success ? 'completed' : 'failed');\n return result;\n } catch (error) {\n onProgress?.(tenantId, 'failed');\n const action = onError?.(tenantId, error as Error);\n if (action === 'abort') {\n aborted = true;\n }\n return this.createErrorResult(tenantId, error as Error);\n }\n })\n );\n\n results.push(...batchResults);\n }\n\n // Mark remaining tenants as skipped if aborted\n if (aborted) {\n const remaining = tenantIds.slice(results.length);\n for (const tenantId of remaining) {\n results.push(this.createSkippedResult(tenantId));\n }\n }\n\n return this.aggregateResults(results);\n }\n\n /**\n * Migrate a single tenant\n */\n async migrateTenant(\n tenantId: string,\n migrations?: MigrationFile[],\n options: { dryRun?: boolean; onProgress?: MigrateOptions['onProgress'] } = {}\n ): Promise<TenantMigrationResult> {\n const startTime = Date.now();\n const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);\n const appliedMigrations: string[] = [];\n\n const pool = await this.createPool(schemaName);\n\n try {\n await this.migratorConfig.hooks?.beforeTenant?.(tenantId);\n\n // Ensure migrations table exists\n await this.ensureMigrationsTable(pool, schemaName);\n\n // Load migrations if not provided\n const allMigrations = migrations ?? await this.loadMigrations();\n\n // Get applied migrations\n const applied = await this.getAppliedMigrations(pool, schemaName);\n const appliedSet = new Set(applied.map((m) => m.name));\n\n // Filter pending migrations\n const pending = allMigrations.filter((m) => !appliedSet.has(m.name));\n\n if (options.dryRun) {\n return {\n tenantId,\n schemaName,\n success: true,\n appliedMigrations: pending.map((m) => m.name),\n durationMs: Date.now() - startTime,\n };\n }\n\n // Apply pending migrations\n for (const migration of pending) {\n const migrationStart = Date.now();\n options.onProgress?.(tenantId, 'migrating', migration.name);\n\n await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);\n await this.applyMigration(pool, schemaName, migration);\n await this.migratorConfig.hooks?.afterMigration?.(\n tenantId,\n migration.name,\n Date.now() - migrationStart\n );\n\n appliedMigrations.push(migration.name);\n }\n\n const result: TenantMigrationResult = {\n tenantId,\n schemaName,\n success: true,\n appliedMigrations,\n durationMs: Date.now() - startTime,\n };\n\n await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);\n\n return result;\n } catch (error) {\n const result: TenantMigrationResult = {\n tenantId,\n schemaName,\n success: false,\n appliedMigrations,\n error: (error as Error).message,\n durationMs: Date.now() - startTime,\n };\n\n await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);\n\n return result;\n } finally {\n await pool.end();\n }\n }\n\n /**\n * Migrate specific tenants\n */\n async migrateTenants(tenantIds: string[], options: MigrateOptions = {}): Promise<MigrationResults> {\n const migrations = await this.loadMigrations();\n const results: TenantMigrationResult[] = [];\n\n const { concurrency = 10, onProgress, onError } = options;\n\n for (let i = 0; i < tenantIds.length; i += concurrency) {\n const batch = tenantIds.slice(i, i + concurrency);\n\n const batchResults = await Promise.all(\n batch.map(async (tenantId) => {\n try {\n onProgress?.(tenantId, 'starting');\n const result = await this.migrateTenant(tenantId, migrations, { dryRun: options.dryRun ?? false, onProgress });\n onProgress?.(tenantId, result.success ? 'completed' : 'failed');\n return result;\n } catch (error) {\n onProgress?.(tenantId, 'failed');\n onError?.(tenantId, error as Error);\n return this.createErrorResult(tenantId, error as Error);\n }\n })\n );\n\n results.push(...batchResults);\n }\n\n return this.aggregateResults(results);\n }\n\n /**\n * Get migration status for all tenants\n */\n async getStatus(): Promise<TenantMigrationStatus[]> {\n const tenantIds = await this.migratorConfig.tenantDiscovery();\n const migrations = await this.loadMigrations();\n const statuses: TenantMigrationStatus[] = [];\n\n for (const tenantId of tenantIds) {\n statuses.push(await this.getTenantStatus(tenantId, migrations));\n }\n\n return statuses;\n }\n\n /**\n * Get migration status for a specific tenant\n */\n async getTenantStatus(tenantId: string, migrations?: MigrationFile[]): Promise<TenantMigrationStatus> {\n const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);\n const pool = await this.createPool(schemaName);\n\n try {\n const allMigrations = migrations ?? await this.loadMigrations();\n\n // Check if migrations table exists\n const tableExists = await this.migrationsTableExists(pool, schemaName);\n if (!tableExists) {\n return {\n tenantId,\n schemaName,\n appliedCount: 0,\n pendingCount: allMigrations.length,\n pendingMigrations: allMigrations.map((m) => m.name),\n status: allMigrations.length > 0 ? 'behind' : 'ok',\n };\n }\n\n const applied = await this.getAppliedMigrations(pool, schemaName);\n const appliedSet = new Set(applied.map((m) => m.name));\n const pending = allMigrations.filter((m) => !appliedSet.has(m.name));\n\n return {\n tenantId,\n schemaName,\n appliedCount: applied.length,\n pendingCount: pending.length,\n pendingMigrations: pending.map((m) => m.name),\n status: pending.length > 0 ? 'behind' : 'ok',\n };\n } catch (error) {\n return {\n tenantId,\n schemaName,\n appliedCount: 0,\n pendingCount: 0,\n pendingMigrations: [],\n status: 'error',\n error: (error as Error).message,\n };\n } finally {\n await pool.end();\n }\n }\n\n /**\n * Create a new tenant schema and optionally apply migrations\n */\n async createTenant(tenantId: string, options: CreateTenantOptions = {}): Promise<void> {\n const { migrate = true } = options;\n const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);\n\n const pool = new Pool({\n connectionString: this.tenantConfig.connection.url,\n ...this.tenantConfig.connection.poolConfig,\n });\n\n try {\n // Create schema\n await pool.query(`CREATE SCHEMA IF NOT EXISTS \"${schemaName}\"`);\n\n if (migrate) {\n // Apply all migrations\n await this.migrateTenant(tenantId);\n }\n } finally {\n await pool.end();\n }\n }\n\n /**\n * Drop a tenant schema\n */\n async dropTenant(tenantId: string, options: DropTenantOptions = {}): Promise<void> {\n const { cascade = true } = options;\n const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);\n\n const pool = new Pool({\n connectionString: this.tenantConfig.connection.url,\n ...this.tenantConfig.connection.poolConfig,\n });\n\n try {\n const cascadeSql = cascade ? 'CASCADE' : 'RESTRICT';\n await pool.query(`DROP SCHEMA IF EXISTS \"${schemaName}\" ${cascadeSql}`);\n } finally {\n await pool.end();\n }\n }\n\n /**\n * Check if a tenant schema exists\n */\n async tenantExists(tenantId: string): Promise<boolean> {\n const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);\n\n const pool = new Pool({\n connectionString: this.tenantConfig.connection.url,\n ...this.tenantConfig.connection.poolConfig,\n });\n\n try {\n const result = await pool.query(\n `SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`,\n [schemaName]\n );\n return result.rowCount !== null && result.rowCount > 0;\n } finally {\n await pool.end();\n }\n }\n\n /**\n * Load migration files from the migrations folder\n */\n private async loadMigrations(): Promise<MigrationFile[]> {\n const files = await readdir(this.migratorConfig.migrationsFolder);\n\n const migrations: MigrationFile[] = [];\n\n for (const file of files) {\n if (!file.endsWith('.sql')) continue;\n\n const filePath = join(this.migratorConfig.migrationsFolder, file);\n const content = await readFile(filePath, 'utf-8');\n\n // Extract timestamp from filename (e.g., 0001_migration_name.sql)\n const match = file.match(/^(\\d+)_/);\n const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;\n\n migrations.push({\n name: basename(file, '.sql'),\n path: filePath,\n sql: content,\n timestamp,\n });\n }\n\n // Sort by timestamp\n return migrations.sort((a, b) => a.timestamp - b.timestamp);\n }\n\n /**\n * Create a pool for a specific schema\n */\n private async createPool(schemaName: string): Promise<Pool> {\n return new Pool({\n connectionString: this.tenantConfig.connection.url,\n ...this.tenantConfig.connection.poolConfig,\n options: `-c search_path=\"${schemaName}\",public`,\n });\n }\n\n /**\n * Ensure migrations table exists\n */\n private async ensureMigrationsTable(pool: Pool, schemaName: string): Promise<void> {\n await pool.query(`\n CREATE TABLE IF NOT EXISTS \"${schemaName}\".\"${this.migrationsTable}\" (\n id SERIAL PRIMARY KEY,\n name VARCHAR(255) NOT NULL UNIQUE,\n applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP\n )\n `);\n }\n\n /**\n * Check if migrations table exists\n */\n private async migrationsTableExists(pool: Pool, schemaName: string): Promise<boolean> {\n const result = await pool.query(\n `SELECT 1 FROM information_schema.tables\n WHERE table_schema = $1 AND table_name = $2`,\n [schemaName, this.migrationsTable]\n );\n return result.rowCount !== null && result.rowCount > 0;\n }\n\n /**\n * Get applied migrations for a schema\n */\n private async getAppliedMigrations(pool: Pool, schemaName: string): Promise<AppliedMigration[]> {\n const result = await pool.query<{ id: number; name: string; applied_at: Date }>(\n `SELECT id, name, applied_at FROM \"${schemaName}\".\"${this.migrationsTable}\" ORDER BY id`\n );\n return result.rows.map((row) => ({\n id: row.id,\n name: row.name,\n appliedAt: row.applied_at,\n }));\n }\n\n /**\n * Apply a migration to a schema\n */\n private async applyMigration(pool: Pool, schemaName: string, migration: MigrationFile): Promise<void> {\n const client = await pool.connect();\n\n try {\n await client.query('BEGIN');\n\n // Execute migration SQL\n await client.query(migration.sql);\n\n // Record migration\n await client.query(\n `INSERT INTO \"${schemaName}\".\"${this.migrationsTable}\" (name) VALUES ($1)`,\n [migration.name]\n );\n\n await client.query('COMMIT');\n } catch (error) {\n await client.query('ROLLBACK');\n throw error;\n } finally {\n client.release();\n }\n }\n\n /**\n * Create a skipped result\n */\n private createSkippedResult(tenantId: string): TenantMigrationResult {\n return {\n tenantId,\n schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),\n success: false,\n appliedMigrations: [],\n error: 'Skipped due to abort',\n durationMs: 0,\n };\n }\n\n /**\n * Create an error result\n */\n private createErrorResult(tenantId: string, error: Error): TenantMigrationResult {\n return {\n tenantId,\n schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),\n success: false,\n appliedMigrations: [],\n error: error.message,\n durationMs: 0,\n };\n }\n\n /**\n * Aggregate migration results\n */\n private aggregateResults(results: TenantMigrationResult[]): MigrationResults {\n return {\n total: results.length,\n succeeded: results.filter((r) => r.success).length,\n failed: results.filter((r) => !r.success && r.error !== 'Skipped due to abort').length,\n skipped: results.filter((r) => r.error === 'Skipped due to abort').length,\n details: results,\n };\n }\n}\n\n/**\n * Create a migrator instance\n */\nexport function createMigrator<\n TTenantSchema extends Record<string, unknown>,\n TSharedSchema extends Record<string, unknown>,\n>(\n tenantConfig: Config<TTenantSchema, TSharedSchema>,\n migratorConfig: MigratorConfig\n): Migrator<TTenantSchema, TSharedSchema> {\n return new Migrator(tenantConfig, migratorConfig);\n}\n"]}
|