drizzle-multitenant 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +94 -339
- package/dist/cli/index.js +686 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/{context-DBerWr50.d.ts → context-DoHx79MS.d.ts} +1 -1
- package/dist/cross-schema/index.d.ts +152 -1
- package/dist/cross-schema/index.js +208 -1
- package/dist/cross-schema/index.js.map +1 -1
- package/dist/index.d.ts +62 -5
- package/dist/index.js +1181 -50
- package/dist/index.js.map +1 -1
- package/dist/integrations/express.d.ts +3 -3
- package/dist/integrations/fastify.d.ts +3 -3
- package/dist/integrations/nestjs/index.d.ts +1 -1
- package/dist/integrations/nestjs/index.js +484 -3
- package/dist/integrations/nestjs/index.js.map +1 -1
- package/dist/migrator/index.d.ts +116 -1
- package/dist/migrator/index.js +418 -0
- package/dist/migrator/index.js.map +1 -1
- package/dist/types-B5eSRLFW.d.ts +235 -0
- package/package.json +9 -3
- package/dist/types-DKVaTaIb.d.ts +0 -130
package/dist/migrator/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as Config } from '../types-
|
|
1
|
+
import { C as Config } from '../types-B5eSRLFW.js';
|
|
2
2
|
import { Pool } from 'pg';
|
|
3
3
|
import 'drizzle-orm/node-postgres';
|
|
4
4
|
|
|
@@ -191,6 +191,67 @@ interface AppliedMigration {
|
|
|
191
191
|
hash?: string;
|
|
192
192
|
appliedAt: Date;
|
|
193
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* Sync status for a single tenant
|
|
196
|
+
*/
|
|
197
|
+
interface TenantSyncStatus {
|
|
198
|
+
tenantId: string;
|
|
199
|
+
schemaName: string;
|
|
200
|
+
/** Migrations in disk but not tracked in database */
|
|
201
|
+
missing: string[];
|
|
202
|
+
/** Migrations tracked in database but not found in disk */
|
|
203
|
+
orphans: string[];
|
|
204
|
+
/** Whether the tenant is in sync */
|
|
205
|
+
inSync: boolean;
|
|
206
|
+
/** Table format used */
|
|
207
|
+
format: TableFormat | null;
|
|
208
|
+
/** Error if any */
|
|
209
|
+
error?: string;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Aggregate sync status
|
|
213
|
+
*/
|
|
214
|
+
interface SyncStatus {
|
|
215
|
+
total: number;
|
|
216
|
+
inSync: number;
|
|
217
|
+
outOfSync: number;
|
|
218
|
+
error: number;
|
|
219
|
+
details: TenantSyncStatus[];
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Sync result for a single tenant
|
|
223
|
+
*/
|
|
224
|
+
interface TenantSyncResult {
|
|
225
|
+
tenantId: string;
|
|
226
|
+
schemaName: string;
|
|
227
|
+
success: boolean;
|
|
228
|
+
/** Migrations that were marked as applied */
|
|
229
|
+
markedMigrations: string[];
|
|
230
|
+
/** Orphan records that were removed */
|
|
231
|
+
removedOrphans: string[];
|
|
232
|
+
error?: string;
|
|
233
|
+
durationMs: number;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Aggregate sync results
|
|
237
|
+
*/
|
|
238
|
+
interface SyncResults {
|
|
239
|
+
total: number;
|
|
240
|
+
succeeded: number;
|
|
241
|
+
failed: number;
|
|
242
|
+
details: TenantSyncResult[];
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Options for sync operations
|
|
246
|
+
*/
|
|
247
|
+
interface SyncOptions {
|
|
248
|
+
/** Number of concurrent operations */
|
|
249
|
+
concurrency?: number;
|
|
250
|
+
/** Progress callback */
|
|
251
|
+
onProgress?: (tenantId: string, status: 'starting' | 'syncing' | 'completed' | 'failed') => void;
|
|
252
|
+
/** Error handler */
|
|
253
|
+
onError?: MigrationErrorHandler;
|
|
254
|
+
}
|
|
194
255
|
|
|
195
256
|
/**
|
|
196
257
|
* Parallel migration engine for multi-tenant applications
|
|
@@ -235,6 +296,43 @@ declare class Migrator<TTenantSchema extends Record<string, unknown>, TSharedSch
|
|
|
235
296
|
* Check if a tenant schema exists
|
|
236
297
|
*/
|
|
237
298
|
tenantExists(tenantId: string): Promise<boolean>;
|
|
299
|
+
/**
|
|
300
|
+
* Mark migrations as applied without executing SQL
|
|
301
|
+
* Useful for syncing tracking state with already-applied migrations
|
|
302
|
+
*/
|
|
303
|
+
markAsApplied(tenantId: string, options?: {
|
|
304
|
+
onProgress?: MigrateOptions['onProgress'];
|
|
305
|
+
}): Promise<TenantMigrationResult>;
|
|
306
|
+
/**
|
|
307
|
+
* Mark migrations as applied for all tenants without executing SQL
|
|
308
|
+
* Useful for syncing tracking state with already-applied migrations
|
|
309
|
+
*/
|
|
310
|
+
markAllAsApplied(options?: MigrateOptions): Promise<MigrationResults>;
|
|
311
|
+
/**
|
|
312
|
+
* Get sync status for all tenants
|
|
313
|
+
* Detects divergences between migrations on disk and tracking in database
|
|
314
|
+
*/
|
|
315
|
+
getSyncStatus(): Promise<SyncStatus>;
|
|
316
|
+
/**
|
|
317
|
+
* Get sync status for a specific tenant
|
|
318
|
+
*/
|
|
319
|
+
getTenantSyncStatus(tenantId: string, migrations?: MigrationFile[]): Promise<TenantSyncStatus>;
|
|
320
|
+
/**
|
|
321
|
+
* Mark missing migrations as applied for a tenant
|
|
322
|
+
*/
|
|
323
|
+
markMissing(tenantId: string): Promise<TenantSyncResult>;
|
|
324
|
+
/**
|
|
325
|
+
* Mark missing migrations as applied for all tenants
|
|
326
|
+
*/
|
|
327
|
+
markAllMissing(options?: SyncOptions): Promise<SyncResults>;
|
|
328
|
+
/**
|
|
329
|
+
* Remove orphan migration records for a tenant
|
|
330
|
+
*/
|
|
331
|
+
cleanOrphans(tenantId: string): Promise<TenantSyncResult>;
|
|
332
|
+
/**
|
|
333
|
+
* Remove orphan migration records for all tenants
|
|
334
|
+
*/
|
|
335
|
+
cleanAllOrphans(options?: SyncOptions): Promise<SyncResults>;
|
|
238
336
|
/**
|
|
239
337
|
* Load migration files from the migrations folder
|
|
240
338
|
*/
|
|
@@ -268,6 +366,11 @@ declare class Migrator<TTenantSchema extends Record<string, unknown>, TSharedSch
|
|
|
268
366
|
* Apply a migration to a schema
|
|
269
367
|
*/
|
|
270
368
|
private applyMigration;
|
|
369
|
+
/**
|
|
370
|
+
* Record a migration as applied without executing SQL
|
|
371
|
+
* Used by markAsApplied to sync tracking state
|
|
372
|
+
*/
|
|
373
|
+
private recordMigration;
|
|
271
374
|
/**
|
|
272
375
|
* Create a skipped result
|
|
273
376
|
*/
|
|
@@ -280,6 +383,18 @@ declare class Migrator<TTenantSchema extends Record<string, unknown>, TSharedSch
|
|
|
280
383
|
* Aggregate migration results
|
|
281
384
|
*/
|
|
282
385
|
private aggregateResults;
|
|
386
|
+
/**
|
|
387
|
+
* Create a skipped sync result
|
|
388
|
+
*/
|
|
389
|
+
private createSkippedSyncResult;
|
|
390
|
+
/**
|
|
391
|
+
* Create an error sync result
|
|
392
|
+
*/
|
|
393
|
+
private createErrorSyncResult;
|
|
394
|
+
/**
|
|
395
|
+
* Aggregate sync results
|
|
396
|
+
*/
|
|
397
|
+
private aggregateSyncResults;
|
|
283
398
|
}
|
|
284
399
|
/**
|
|
285
400
|
* Create a migrator instance
|
package/dist/migrator/index.js
CHANGED
|
@@ -380,6 +380,372 @@ var Migrator = class {
|
|
|
380
380
|
await pool.end();
|
|
381
381
|
}
|
|
382
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Mark migrations as applied without executing SQL
|
|
385
|
+
* Useful for syncing tracking state with already-applied migrations
|
|
386
|
+
*/
|
|
387
|
+
async markAsApplied(tenantId, options = {}) {
|
|
388
|
+
const startTime = Date.now();
|
|
389
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
390
|
+
const markedMigrations = [];
|
|
391
|
+
const pool = await this.createPool(schemaName);
|
|
392
|
+
try {
|
|
393
|
+
await this.migratorConfig.hooks?.beforeTenant?.(tenantId);
|
|
394
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
395
|
+
await this.ensureMigrationsTable(pool, schemaName, format);
|
|
396
|
+
const allMigrations = await this.loadMigrations();
|
|
397
|
+
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
398
|
+
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
399
|
+
const pending = allMigrations.filter(
|
|
400
|
+
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
401
|
+
);
|
|
402
|
+
for (const migration of pending) {
|
|
403
|
+
const migrationStart = Date.now();
|
|
404
|
+
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
405
|
+
await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
406
|
+
await this.recordMigration(pool, schemaName, migration, format);
|
|
407
|
+
await this.migratorConfig.hooks?.afterMigration?.(
|
|
408
|
+
tenantId,
|
|
409
|
+
migration.name,
|
|
410
|
+
Date.now() - migrationStart
|
|
411
|
+
);
|
|
412
|
+
markedMigrations.push(migration.name);
|
|
413
|
+
}
|
|
414
|
+
const result = {
|
|
415
|
+
tenantId,
|
|
416
|
+
schemaName,
|
|
417
|
+
success: true,
|
|
418
|
+
appliedMigrations: markedMigrations,
|
|
419
|
+
durationMs: Date.now() - startTime,
|
|
420
|
+
format: format.format
|
|
421
|
+
};
|
|
422
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
423
|
+
return result;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
const result = {
|
|
426
|
+
tenantId,
|
|
427
|
+
schemaName,
|
|
428
|
+
success: false,
|
|
429
|
+
appliedMigrations: markedMigrations,
|
|
430
|
+
error: error.message,
|
|
431
|
+
durationMs: Date.now() - startTime
|
|
432
|
+
};
|
|
433
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
434
|
+
return result;
|
|
435
|
+
} finally {
|
|
436
|
+
await pool.end();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Mark migrations as applied for all tenants without executing SQL
|
|
441
|
+
* Useful for syncing tracking state with already-applied migrations
|
|
442
|
+
*/
|
|
443
|
+
async markAllAsApplied(options = {}) {
|
|
444
|
+
const {
|
|
445
|
+
concurrency = 10,
|
|
446
|
+
onProgress,
|
|
447
|
+
onError
|
|
448
|
+
} = options;
|
|
449
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
450
|
+
const results = [];
|
|
451
|
+
let aborted = false;
|
|
452
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
453
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
454
|
+
const batchResults = await Promise.all(
|
|
455
|
+
batch.map(async (tenantId) => {
|
|
456
|
+
if (aborted) {
|
|
457
|
+
return this.createSkippedResult(tenantId);
|
|
458
|
+
}
|
|
459
|
+
try {
|
|
460
|
+
onProgress?.(tenantId, "starting");
|
|
461
|
+
const result = await this.markAsApplied(tenantId, { onProgress });
|
|
462
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
463
|
+
return result;
|
|
464
|
+
} catch (error) {
|
|
465
|
+
onProgress?.(tenantId, "failed");
|
|
466
|
+
const action = onError?.(tenantId, error);
|
|
467
|
+
if (action === "abort") {
|
|
468
|
+
aborted = true;
|
|
469
|
+
}
|
|
470
|
+
return this.createErrorResult(tenantId, error);
|
|
471
|
+
}
|
|
472
|
+
})
|
|
473
|
+
);
|
|
474
|
+
results.push(...batchResults);
|
|
475
|
+
}
|
|
476
|
+
if (aborted) {
|
|
477
|
+
const remaining = tenantIds.slice(results.length);
|
|
478
|
+
for (const tenantId of remaining) {
|
|
479
|
+
results.push(this.createSkippedResult(tenantId));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return this.aggregateResults(results);
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Get sync status for all tenants
|
|
486
|
+
* Detects divergences between migrations on disk and tracking in database
|
|
487
|
+
*/
|
|
488
|
+
async getSyncStatus() {
|
|
489
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
490
|
+
const migrations = await this.loadMigrations();
|
|
491
|
+
const statuses = [];
|
|
492
|
+
for (const tenantId of tenantIds) {
|
|
493
|
+
statuses.push(await this.getTenantSyncStatus(tenantId, migrations));
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
total: statuses.length,
|
|
497
|
+
inSync: statuses.filter((s) => s.inSync && !s.error).length,
|
|
498
|
+
outOfSync: statuses.filter((s) => !s.inSync && !s.error).length,
|
|
499
|
+
error: statuses.filter((s) => !!s.error).length,
|
|
500
|
+
details: statuses
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Get sync status for a specific tenant
|
|
505
|
+
*/
|
|
506
|
+
async getTenantSyncStatus(tenantId, migrations) {
|
|
507
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
508
|
+
const pool = await this.createPool(schemaName);
|
|
509
|
+
try {
|
|
510
|
+
const allMigrations = migrations ?? await this.loadMigrations();
|
|
511
|
+
const migrationNames = new Set(allMigrations.map((m) => m.name));
|
|
512
|
+
const migrationHashes = new Set(allMigrations.map((m) => m.hash));
|
|
513
|
+
const tableExists = await this.migrationsTableExists(pool, schemaName);
|
|
514
|
+
if (!tableExists) {
|
|
515
|
+
return {
|
|
516
|
+
tenantId,
|
|
517
|
+
schemaName,
|
|
518
|
+
missing: allMigrations.map((m) => m.name),
|
|
519
|
+
orphans: [],
|
|
520
|
+
inSync: allMigrations.length === 0,
|
|
521
|
+
format: null
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
525
|
+
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
526
|
+
const appliedIdentifiers = new Set(applied.map((m) => m.identifier));
|
|
527
|
+
const missing = allMigrations.filter((m) => !this.isMigrationApplied(m, appliedIdentifiers, format)).map((m) => m.name);
|
|
528
|
+
const orphans = applied.filter((m) => {
|
|
529
|
+
if (format.columns.identifier === "name") {
|
|
530
|
+
return !migrationNames.has(m.identifier);
|
|
531
|
+
}
|
|
532
|
+
return !migrationHashes.has(m.identifier) && !migrationNames.has(m.identifier);
|
|
533
|
+
}).map((m) => m.identifier);
|
|
534
|
+
return {
|
|
535
|
+
tenantId,
|
|
536
|
+
schemaName,
|
|
537
|
+
missing,
|
|
538
|
+
orphans,
|
|
539
|
+
inSync: missing.length === 0 && orphans.length === 0,
|
|
540
|
+
format: format.format
|
|
541
|
+
};
|
|
542
|
+
} catch (error) {
|
|
543
|
+
return {
|
|
544
|
+
tenantId,
|
|
545
|
+
schemaName,
|
|
546
|
+
missing: [],
|
|
547
|
+
orphans: [],
|
|
548
|
+
inSync: false,
|
|
549
|
+
format: null,
|
|
550
|
+
error: error.message
|
|
551
|
+
};
|
|
552
|
+
} finally {
|
|
553
|
+
await pool.end();
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Mark missing migrations as applied for a tenant
|
|
558
|
+
*/
|
|
559
|
+
async markMissing(tenantId) {
|
|
560
|
+
const startTime = Date.now();
|
|
561
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
562
|
+
const markedMigrations = [];
|
|
563
|
+
const pool = await this.createPool(schemaName);
|
|
564
|
+
try {
|
|
565
|
+
const syncStatus = await this.getTenantSyncStatus(tenantId);
|
|
566
|
+
if (syncStatus.error) {
|
|
567
|
+
return {
|
|
568
|
+
tenantId,
|
|
569
|
+
schemaName,
|
|
570
|
+
success: false,
|
|
571
|
+
markedMigrations: [],
|
|
572
|
+
removedOrphans: [],
|
|
573
|
+
error: syncStatus.error,
|
|
574
|
+
durationMs: Date.now() - startTime
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (syncStatus.missing.length === 0) {
|
|
578
|
+
return {
|
|
579
|
+
tenantId,
|
|
580
|
+
schemaName,
|
|
581
|
+
success: true,
|
|
582
|
+
markedMigrations: [],
|
|
583
|
+
removedOrphans: [],
|
|
584
|
+
durationMs: Date.now() - startTime
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
588
|
+
await this.ensureMigrationsTable(pool, schemaName, format);
|
|
589
|
+
const allMigrations = await this.loadMigrations();
|
|
590
|
+
const missingSet = new Set(syncStatus.missing);
|
|
591
|
+
for (const migration of allMigrations) {
|
|
592
|
+
if (missingSet.has(migration.name)) {
|
|
593
|
+
await this.recordMigration(pool, schemaName, migration, format);
|
|
594
|
+
markedMigrations.push(migration.name);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
tenantId,
|
|
599
|
+
schemaName,
|
|
600
|
+
success: true,
|
|
601
|
+
markedMigrations,
|
|
602
|
+
removedOrphans: [],
|
|
603
|
+
durationMs: Date.now() - startTime
|
|
604
|
+
};
|
|
605
|
+
} catch (error) {
|
|
606
|
+
return {
|
|
607
|
+
tenantId,
|
|
608
|
+
schemaName,
|
|
609
|
+
success: false,
|
|
610
|
+
markedMigrations,
|
|
611
|
+
removedOrphans: [],
|
|
612
|
+
error: error.message,
|
|
613
|
+
durationMs: Date.now() - startTime
|
|
614
|
+
};
|
|
615
|
+
} finally {
|
|
616
|
+
await pool.end();
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Mark missing migrations as applied for all tenants
|
|
621
|
+
*/
|
|
622
|
+
async markAllMissing(options = {}) {
|
|
623
|
+
const { concurrency = 10, onProgress, onError } = options;
|
|
624
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
625
|
+
const results = [];
|
|
626
|
+
let aborted = false;
|
|
627
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
628
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
629
|
+
const batchResults = await Promise.all(
|
|
630
|
+
batch.map(async (tenantId) => {
|
|
631
|
+
if (aborted) {
|
|
632
|
+
return this.createSkippedSyncResult(tenantId);
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
onProgress?.(tenantId, "starting");
|
|
636
|
+
const result = await this.markMissing(tenantId);
|
|
637
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
638
|
+
return result;
|
|
639
|
+
} catch (error) {
|
|
640
|
+
onProgress?.(tenantId, "failed");
|
|
641
|
+
const action = onError?.(tenantId, error);
|
|
642
|
+
if (action === "abort") {
|
|
643
|
+
aborted = true;
|
|
644
|
+
}
|
|
645
|
+
return this.createErrorSyncResult(tenantId, error);
|
|
646
|
+
}
|
|
647
|
+
})
|
|
648
|
+
);
|
|
649
|
+
results.push(...batchResults);
|
|
650
|
+
}
|
|
651
|
+
return this.aggregateSyncResults(results);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Remove orphan migration records for a tenant
|
|
655
|
+
*/
|
|
656
|
+
async cleanOrphans(tenantId) {
|
|
657
|
+
const startTime = Date.now();
|
|
658
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
659
|
+
const removedOrphans = [];
|
|
660
|
+
const pool = await this.createPool(schemaName);
|
|
661
|
+
try {
|
|
662
|
+
const syncStatus = await this.getTenantSyncStatus(tenantId);
|
|
663
|
+
if (syncStatus.error) {
|
|
664
|
+
return {
|
|
665
|
+
tenantId,
|
|
666
|
+
schemaName,
|
|
667
|
+
success: false,
|
|
668
|
+
markedMigrations: [],
|
|
669
|
+
removedOrphans: [],
|
|
670
|
+
error: syncStatus.error,
|
|
671
|
+
durationMs: Date.now() - startTime
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (syncStatus.orphans.length === 0) {
|
|
675
|
+
return {
|
|
676
|
+
tenantId,
|
|
677
|
+
schemaName,
|
|
678
|
+
success: true,
|
|
679
|
+
markedMigrations: [],
|
|
680
|
+
removedOrphans: [],
|
|
681
|
+
durationMs: Date.now() - startTime
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
685
|
+
const identifierColumn = format.columns.identifier;
|
|
686
|
+
for (const orphan of syncStatus.orphans) {
|
|
687
|
+
await pool.query(
|
|
688
|
+
`DELETE FROM "${schemaName}"."${format.tableName}" WHERE "${identifierColumn}" = $1`,
|
|
689
|
+
[orphan]
|
|
690
|
+
);
|
|
691
|
+
removedOrphans.push(orphan);
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
tenantId,
|
|
695
|
+
schemaName,
|
|
696
|
+
success: true,
|
|
697
|
+
markedMigrations: [],
|
|
698
|
+
removedOrphans,
|
|
699
|
+
durationMs: Date.now() - startTime
|
|
700
|
+
};
|
|
701
|
+
} catch (error) {
|
|
702
|
+
return {
|
|
703
|
+
tenantId,
|
|
704
|
+
schemaName,
|
|
705
|
+
success: false,
|
|
706
|
+
markedMigrations: [],
|
|
707
|
+
removedOrphans,
|
|
708
|
+
error: error.message,
|
|
709
|
+
durationMs: Date.now() - startTime
|
|
710
|
+
};
|
|
711
|
+
} finally {
|
|
712
|
+
await pool.end();
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Remove orphan migration records for all tenants
|
|
717
|
+
*/
|
|
718
|
+
async cleanAllOrphans(options = {}) {
|
|
719
|
+
const { concurrency = 10, onProgress, onError } = options;
|
|
720
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
721
|
+
const results = [];
|
|
722
|
+
let aborted = false;
|
|
723
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
724
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
725
|
+
const batchResults = await Promise.all(
|
|
726
|
+
batch.map(async (tenantId) => {
|
|
727
|
+
if (aborted) {
|
|
728
|
+
return this.createSkippedSyncResult(tenantId);
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
onProgress?.(tenantId, "starting");
|
|
732
|
+
const result = await this.cleanOrphans(tenantId);
|
|
733
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
734
|
+
return result;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
onProgress?.(tenantId, "failed");
|
|
737
|
+
const action = onError?.(tenantId, error);
|
|
738
|
+
if (action === "abort") {
|
|
739
|
+
aborted = true;
|
|
740
|
+
}
|
|
741
|
+
return this.createErrorSyncResult(tenantId, error);
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
);
|
|
745
|
+
results.push(...batchResults);
|
|
746
|
+
}
|
|
747
|
+
return this.aggregateSyncResults(results);
|
|
748
|
+
}
|
|
383
749
|
/**
|
|
384
750
|
* Load migration files from the migrations folder
|
|
385
751
|
*/
|
|
@@ -509,6 +875,19 @@ var Migrator = class {
|
|
|
509
875
|
client.release();
|
|
510
876
|
}
|
|
511
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Record a migration as applied without executing SQL
|
|
880
|
+
* Used by markAsApplied to sync tracking state
|
|
881
|
+
*/
|
|
882
|
+
async recordMigration(pool, schemaName, migration, format) {
|
|
883
|
+
const { identifier, timestamp, timestampType } = format.columns;
|
|
884
|
+
const identifierValue = identifier === "name" ? migration.name : migration.hash;
|
|
885
|
+
const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
|
|
886
|
+
await pool.query(
|
|
887
|
+
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
888
|
+
[identifierValue, timestampValue]
|
|
889
|
+
);
|
|
890
|
+
}
|
|
512
891
|
/**
|
|
513
892
|
* Create a skipped result
|
|
514
893
|
*/
|
|
@@ -547,6 +926,45 @@ var Migrator = class {
|
|
|
547
926
|
details: results
|
|
548
927
|
};
|
|
549
928
|
}
|
|
929
|
+
/**
|
|
930
|
+
* Create a skipped sync result
|
|
931
|
+
*/
|
|
932
|
+
createSkippedSyncResult(tenantId) {
|
|
933
|
+
return {
|
|
934
|
+
tenantId,
|
|
935
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
936
|
+
success: false,
|
|
937
|
+
markedMigrations: [],
|
|
938
|
+
removedOrphans: [],
|
|
939
|
+
error: "Skipped due to abort",
|
|
940
|
+
durationMs: 0
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Create an error sync result
|
|
945
|
+
*/
|
|
946
|
+
createErrorSyncResult(tenantId, error) {
|
|
947
|
+
return {
|
|
948
|
+
tenantId,
|
|
949
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
950
|
+
success: false,
|
|
951
|
+
markedMigrations: [],
|
|
952
|
+
removedOrphans: [],
|
|
953
|
+
error: error.message,
|
|
954
|
+
durationMs: 0
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Aggregate sync results
|
|
959
|
+
*/
|
|
960
|
+
aggregateSyncResults(results) {
|
|
961
|
+
return {
|
|
962
|
+
total: results.length,
|
|
963
|
+
succeeded: results.filter((r) => r.success).length,
|
|
964
|
+
failed: results.filter((r) => !r.success).length,
|
|
965
|
+
details: results
|
|
966
|
+
};
|
|
967
|
+
}
|
|
550
968
|
};
|
|
551
969
|
function createMigrator(tenantConfig, migratorConfig) {
|
|
552
970
|
return new Migrator(tenantConfig, migratorConfig);
|