drizzle-multitenant 1.0.8 → 1.0.9
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 +36 -372
- 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/cli/index.js
CHANGED
|
@@ -370,6 +370,372 @@ var Migrator = class {
|
|
|
370
370
|
await pool.end();
|
|
371
371
|
}
|
|
372
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* Mark migrations as applied without executing SQL
|
|
375
|
+
* Useful for syncing tracking state with already-applied migrations
|
|
376
|
+
*/
|
|
377
|
+
async markAsApplied(tenantId, options = {}) {
|
|
378
|
+
const startTime = Date.now();
|
|
379
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
380
|
+
const markedMigrations = [];
|
|
381
|
+
const pool = await this.createPool(schemaName);
|
|
382
|
+
try {
|
|
383
|
+
await this.migratorConfig.hooks?.beforeTenant?.(tenantId);
|
|
384
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
385
|
+
await this.ensureMigrationsTable(pool, schemaName, format);
|
|
386
|
+
const allMigrations = await this.loadMigrations();
|
|
387
|
+
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
388
|
+
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
389
|
+
const pending = allMigrations.filter(
|
|
390
|
+
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
391
|
+
);
|
|
392
|
+
for (const migration of pending) {
|
|
393
|
+
const migrationStart = Date.now();
|
|
394
|
+
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
395
|
+
await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
396
|
+
await this.recordMigration(pool, schemaName, migration, format);
|
|
397
|
+
await this.migratorConfig.hooks?.afterMigration?.(
|
|
398
|
+
tenantId,
|
|
399
|
+
migration.name,
|
|
400
|
+
Date.now() - migrationStart
|
|
401
|
+
);
|
|
402
|
+
markedMigrations.push(migration.name);
|
|
403
|
+
}
|
|
404
|
+
const result = {
|
|
405
|
+
tenantId,
|
|
406
|
+
schemaName,
|
|
407
|
+
success: true,
|
|
408
|
+
appliedMigrations: markedMigrations,
|
|
409
|
+
durationMs: Date.now() - startTime,
|
|
410
|
+
format: format.format
|
|
411
|
+
};
|
|
412
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
413
|
+
return result;
|
|
414
|
+
} catch (error2) {
|
|
415
|
+
const result = {
|
|
416
|
+
tenantId,
|
|
417
|
+
schemaName,
|
|
418
|
+
success: false,
|
|
419
|
+
appliedMigrations: markedMigrations,
|
|
420
|
+
error: error2.message,
|
|
421
|
+
durationMs: Date.now() - startTime
|
|
422
|
+
};
|
|
423
|
+
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
424
|
+
return result;
|
|
425
|
+
} finally {
|
|
426
|
+
await pool.end();
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Mark migrations as applied for all tenants without executing SQL
|
|
431
|
+
* Useful for syncing tracking state with already-applied migrations
|
|
432
|
+
*/
|
|
433
|
+
async markAllAsApplied(options = {}) {
|
|
434
|
+
const {
|
|
435
|
+
concurrency = 10,
|
|
436
|
+
onProgress,
|
|
437
|
+
onError
|
|
438
|
+
} = options;
|
|
439
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
440
|
+
const results = [];
|
|
441
|
+
let aborted = false;
|
|
442
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
443
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
444
|
+
const batchResults = await Promise.all(
|
|
445
|
+
batch.map(async (tenantId) => {
|
|
446
|
+
if (aborted) {
|
|
447
|
+
return this.createSkippedResult(tenantId);
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
onProgress?.(tenantId, "starting");
|
|
451
|
+
const result = await this.markAsApplied(tenantId, { onProgress });
|
|
452
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
453
|
+
return result;
|
|
454
|
+
} catch (error2) {
|
|
455
|
+
onProgress?.(tenantId, "failed");
|
|
456
|
+
const action = onError?.(tenantId, error2);
|
|
457
|
+
if (action === "abort") {
|
|
458
|
+
aborted = true;
|
|
459
|
+
}
|
|
460
|
+
return this.createErrorResult(tenantId, error2);
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
results.push(...batchResults);
|
|
465
|
+
}
|
|
466
|
+
if (aborted) {
|
|
467
|
+
const remaining = tenantIds.slice(results.length);
|
|
468
|
+
for (const tenantId of remaining) {
|
|
469
|
+
results.push(this.createSkippedResult(tenantId));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return this.aggregateResults(results);
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Get sync status for all tenants
|
|
476
|
+
* Detects divergences between migrations on disk and tracking in database
|
|
477
|
+
*/
|
|
478
|
+
async getSyncStatus() {
|
|
479
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
480
|
+
const migrations = await this.loadMigrations();
|
|
481
|
+
const statuses = [];
|
|
482
|
+
for (const tenantId of tenantIds) {
|
|
483
|
+
statuses.push(await this.getTenantSyncStatus(tenantId, migrations));
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
total: statuses.length,
|
|
487
|
+
inSync: statuses.filter((s) => s.inSync && !s.error).length,
|
|
488
|
+
outOfSync: statuses.filter((s) => !s.inSync && !s.error).length,
|
|
489
|
+
error: statuses.filter((s) => !!s.error).length,
|
|
490
|
+
details: statuses
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Get sync status for a specific tenant
|
|
495
|
+
*/
|
|
496
|
+
async getTenantSyncStatus(tenantId, migrations) {
|
|
497
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
498
|
+
const pool = await this.createPool(schemaName);
|
|
499
|
+
try {
|
|
500
|
+
const allMigrations = migrations ?? await this.loadMigrations();
|
|
501
|
+
const migrationNames = new Set(allMigrations.map((m) => m.name));
|
|
502
|
+
const migrationHashes = new Set(allMigrations.map((m) => m.hash));
|
|
503
|
+
const tableExists = await this.migrationsTableExists(pool, schemaName);
|
|
504
|
+
if (!tableExists) {
|
|
505
|
+
return {
|
|
506
|
+
tenantId,
|
|
507
|
+
schemaName,
|
|
508
|
+
missing: allMigrations.map((m) => m.name),
|
|
509
|
+
orphans: [],
|
|
510
|
+
inSync: allMigrations.length === 0,
|
|
511
|
+
format: null
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
515
|
+
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
516
|
+
const appliedIdentifiers = new Set(applied.map((m) => m.identifier));
|
|
517
|
+
const missing = allMigrations.filter((m) => !this.isMigrationApplied(m, appliedIdentifiers, format)).map((m) => m.name);
|
|
518
|
+
const orphans = applied.filter((m) => {
|
|
519
|
+
if (format.columns.identifier === "name") {
|
|
520
|
+
return !migrationNames.has(m.identifier);
|
|
521
|
+
}
|
|
522
|
+
return !migrationHashes.has(m.identifier) && !migrationNames.has(m.identifier);
|
|
523
|
+
}).map((m) => m.identifier);
|
|
524
|
+
return {
|
|
525
|
+
tenantId,
|
|
526
|
+
schemaName,
|
|
527
|
+
missing,
|
|
528
|
+
orphans,
|
|
529
|
+
inSync: missing.length === 0 && orphans.length === 0,
|
|
530
|
+
format: format.format
|
|
531
|
+
};
|
|
532
|
+
} catch (error2) {
|
|
533
|
+
return {
|
|
534
|
+
tenantId,
|
|
535
|
+
schemaName,
|
|
536
|
+
missing: [],
|
|
537
|
+
orphans: [],
|
|
538
|
+
inSync: false,
|
|
539
|
+
format: null,
|
|
540
|
+
error: error2.message
|
|
541
|
+
};
|
|
542
|
+
} finally {
|
|
543
|
+
await pool.end();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Mark missing migrations as applied for a tenant
|
|
548
|
+
*/
|
|
549
|
+
async markMissing(tenantId) {
|
|
550
|
+
const startTime = Date.now();
|
|
551
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
552
|
+
const markedMigrations = [];
|
|
553
|
+
const pool = await this.createPool(schemaName);
|
|
554
|
+
try {
|
|
555
|
+
const syncStatus = await this.getTenantSyncStatus(tenantId);
|
|
556
|
+
if (syncStatus.error) {
|
|
557
|
+
return {
|
|
558
|
+
tenantId,
|
|
559
|
+
schemaName,
|
|
560
|
+
success: false,
|
|
561
|
+
markedMigrations: [],
|
|
562
|
+
removedOrphans: [],
|
|
563
|
+
error: syncStatus.error,
|
|
564
|
+
durationMs: Date.now() - startTime
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (syncStatus.missing.length === 0) {
|
|
568
|
+
return {
|
|
569
|
+
tenantId,
|
|
570
|
+
schemaName,
|
|
571
|
+
success: true,
|
|
572
|
+
markedMigrations: [],
|
|
573
|
+
removedOrphans: [],
|
|
574
|
+
durationMs: Date.now() - startTime
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
578
|
+
await this.ensureMigrationsTable(pool, schemaName, format);
|
|
579
|
+
const allMigrations = await this.loadMigrations();
|
|
580
|
+
const missingSet = new Set(syncStatus.missing);
|
|
581
|
+
for (const migration of allMigrations) {
|
|
582
|
+
if (missingSet.has(migration.name)) {
|
|
583
|
+
await this.recordMigration(pool, schemaName, migration, format);
|
|
584
|
+
markedMigrations.push(migration.name);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
tenantId,
|
|
589
|
+
schemaName,
|
|
590
|
+
success: true,
|
|
591
|
+
markedMigrations,
|
|
592
|
+
removedOrphans: [],
|
|
593
|
+
durationMs: Date.now() - startTime
|
|
594
|
+
};
|
|
595
|
+
} catch (error2) {
|
|
596
|
+
return {
|
|
597
|
+
tenantId,
|
|
598
|
+
schemaName,
|
|
599
|
+
success: false,
|
|
600
|
+
markedMigrations,
|
|
601
|
+
removedOrphans: [],
|
|
602
|
+
error: error2.message,
|
|
603
|
+
durationMs: Date.now() - startTime
|
|
604
|
+
};
|
|
605
|
+
} finally {
|
|
606
|
+
await pool.end();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Mark missing migrations as applied for all tenants
|
|
611
|
+
*/
|
|
612
|
+
async markAllMissing(options = {}) {
|
|
613
|
+
const { concurrency = 10, onProgress, onError } = options;
|
|
614
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
615
|
+
const results = [];
|
|
616
|
+
let aborted = false;
|
|
617
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
618
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
619
|
+
const batchResults = await Promise.all(
|
|
620
|
+
batch.map(async (tenantId) => {
|
|
621
|
+
if (aborted) {
|
|
622
|
+
return this.createSkippedSyncResult(tenantId);
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
onProgress?.(tenantId, "starting");
|
|
626
|
+
const result = await this.markMissing(tenantId);
|
|
627
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
628
|
+
return result;
|
|
629
|
+
} catch (error2) {
|
|
630
|
+
onProgress?.(tenantId, "failed");
|
|
631
|
+
const action = onError?.(tenantId, error2);
|
|
632
|
+
if (action === "abort") {
|
|
633
|
+
aborted = true;
|
|
634
|
+
}
|
|
635
|
+
return this.createErrorSyncResult(tenantId, error2);
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
);
|
|
639
|
+
results.push(...batchResults);
|
|
640
|
+
}
|
|
641
|
+
return this.aggregateSyncResults(results);
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Remove orphan migration records for a tenant
|
|
645
|
+
*/
|
|
646
|
+
async cleanOrphans(tenantId) {
|
|
647
|
+
const startTime = Date.now();
|
|
648
|
+
const schemaName = this.tenantConfig.isolation.schemaNameTemplate(tenantId);
|
|
649
|
+
const removedOrphans = [];
|
|
650
|
+
const pool = await this.createPool(schemaName);
|
|
651
|
+
try {
|
|
652
|
+
const syncStatus = await this.getTenantSyncStatus(tenantId);
|
|
653
|
+
if (syncStatus.error) {
|
|
654
|
+
return {
|
|
655
|
+
tenantId,
|
|
656
|
+
schemaName,
|
|
657
|
+
success: false,
|
|
658
|
+
markedMigrations: [],
|
|
659
|
+
removedOrphans: [],
|
|
660
|
+
error: syncStatus.error,
|
|
661
|
+
durationMs: Date.now() - startTime
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (syncStatus.orphans.length === 0) {
|
|
665
|
+
return {
|
|
666
|
+
tenantId,
|
|
667
|
+
schemaName,
|
|
668
|
+
success: true,
|
|
669
|
+
markedMigrations: [],
|
|
670
|
+
removedOrphans: [],
|
|
671
|
+
durationMs: Date.now() - startTime
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
675
|
+
const identifierColumn = format.columns.identifier;
|
|
676
|
+
for (const orphan of syncStatus.orphans) {
|
|
677
|
+
await pool.query(
|
|
678
|
+
`DELETE FROM "${schemaName}"."${format.tableName}" WHERE "${identifierColumn}" = $1`,
|
|
679
|
+
[orphan]
|
|
680
|
+
);
|
|
681
|
+
removedOrphans.push(orphan);
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
tenantId,
|
|
685
|
+
schemaName,
|
|
686
|
+
success: true,
|
|
687
|
+
markedMigrations: [],
|
|
688
|
+
removedOrphans,
|
|
689
|
+
durationMs: Date.now() - startTime
|
|
690
|
+
};
|
|
691
|
+
} catch (error2) {
|
|
692
|
+
return {
|
|
693
|
+
tenantId,
|
|
694
|
+
schemaName,
|
|
695
|
+
success: false,
|
|
696
|
+
markedMigrations: [],
|
|
697
|
+
removedOrphans,
|
|
698
|
+
error: error2.message,
|
|
699
|
+
durationMs: Date.now() - startTime
|
|
700
|
+
};
|
|
701
|
+
} finally {
|
|
702
|
+
await pool.end();
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Remove orphan migration records for all tenants
|
|
707
|
+
*/
|
|
708
|
+
async cleanAllOrphans(options = {}) {
|
|
709
|
+
const { concurrency = 10, onProgress, onError } = options;
|
|
710
|
+
const tenantIds = await this.migratorConfig.tenantDiscovery();
|
|
711
|
+
const results = [];
|
|
712
|
+
let aborted = false;
|
|
713
|
+
for (let i = 0; i < tenantIds.length && !aborted; i += concurrency) {
|
|
714
|
+
const batch = tenantIds.slice(i, i + concurrency);
|
|
715
|
+
const batchResults = await Promise.all(
|
|
716
|
+
batch.map(async (tenantId) => {
|
|
717
|
+
if (aborted) {
|
|
718
|
+
return this.createSkippedSyncResult(tenantId);
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
onProgress?.(tenantId, "starting");
|
|
722
|
+
const result = await this.cleanOrphans(tenantId);
|
|
723
|
+
onProgress?.(tenantId, result.success ? "completed" : "failed");
|
|
724
|
+
return result;
|
|
725
|
+
} catch (error2) {
|
|
726
|
+
onProgress?.(tenantId, "failed");
|
|
727
|
+
const action = onError?.(tenantId, error2);
|
|
728
|
+
if (action === "abort") {
|
|
729
|
+
aborted = true;
|
|
730
|
+
}
|
|
731
|
+
return this.createErrorSyncResult(tenantId, error2);
|
|
732
|
+
}
|
|
733
|
+
})
|
|
734
|
+
);
|
|
735
|
+
results.push(...batchResults);
|
|
736
|
+
}
|
|
737
|
+
return this.aggregateSyncResults(results);
|
|
738
|
+
}
|
|
373
739
|
/**
|
|
374
740
|
* Load migration files from the migrations folder
|
|
375
741
|
*/
|
|
@@ -499,6 +865,19 @@ var Migrator = class {
|
|
|
499
865
|
client.release();
|
|
500
866
|
}
|
|
501
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* Record a migration as applied without executing SQL
|
|
870
|
+
* Used by markAsApplied to sync tracking state
|
|
871
|
+
*/
|
|
872
|
+
async recordMigration(pool, schemaName, migration, format) {
|
|
873
|
+
const { identifier, timestamp, timestampType } = format.columns;
|
|
874
|
+
const identifierValue = identifier === "name" ? migration.name : migration.hash;
|
|
875
|
+
const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
|
|
876
|
+
await pool.query(
|
|
877
|
+
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
878
|
+
[identifierValue, timestampValue]
|
|
879
|
+
);
|
|
880
|
+
}
|
|
502
881
|
/**
|
|
503
882
|
* Create a skipped result
|
|
504
883
|
*/
|
|
@@ -537,6 +916,45 @@ var Migrator = class {
|
|
|
537
916
|
details: results
|
|
538
917
|
};
|
|
539
918
|
}
|
|
919
|
+
/**
|
|
920
|
+
* Create a skipped sync result
|
|
921
|
+
*/
|
|
922
|
+
createSkippedSyncResult(tenantId) {
|
|
923
|
+
return {
|
|
924
|
+
tenantId,
|
|
925
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
926
|
+
success: false,
|
|
927
|
+
markedMigrations: [],
|
|
928
|
+
removedOrphans: [],
|
|
929
|
+
error: "Skipped due to abort",
|
|
930
|
+
durationMs: 0
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Create an error sync result
|
|
935
|
+
*/
|
|
936
|
+
createErrorSyncResult(tenantId, error2) {
|
|
937
|
+
return {
|
|
938
|
+
tenantId,
|
|
939
|
+
schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
|
|
940
|
+
success: false,
|
|
941
|
+
markedMigrations: [],
|
|
942
|
+
removedOrphans: [],
|
|
943
|
+
error: error2.message,
|
|
944
|
+
durationMs: 0
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Aggregate sync results
|
|
949
|
+
*/
|
|
950
|
+
aggregateSyncResults(results) {
|
|
951
|
+
return {
|
|
952
|
+
total: results.length,
|
|
953
|
+
succeeded: results.filter((r) => r.success).length,
|
|
954
|
+
failed: results.filter((r) => !r.success).length,
|
|
955
|
+
details: results
|
|
956
|
+
};
|
|
957
|
+
}
|
|
540
958
|
};
|
|
541
959
|
function createMigrator(tenantConfig, migratorConfig) {
|
|
542
960
|
return new Migrator(tenantConfig, migratorConfig);
|
|
@@ -969,7 +1387,7 @@ function handleError(err) {
|
|
|
969
1387
|
}
|
|
970
1388
|
|
|
971
1389
|
// src/cli/commands/migrate.ts
|
|
972
|
-
var migrateCommand = new Command("migrate").description("Apply pending migrations to tenant schemas").option("-c, --config <path>", "Path to config file").option("-a, --all", "Migrate all tenants").option("-t, --tenant <id>", "Migrate a specific tenant").option("--tenants <ids>", "Migrate specific tenants (comma-separated)").option("--concurrency <number>", "Number of concurrent migrations", "10").option("--dry-run", "Show what would be applied without executing").option("--migrations-folder <path>", "Path to migrations folder").addHelpText("after", `
|
|
1390
|
+
var migrateCommand = new Command("migrate").description("Apply pending migrations to tenant schemas").option("-c, --config <path>", "Path to config file").option("-a, --all", "Migrate all tenants").option("-t, --tenant <id>", "Migrate a specific tenant").option("--tenants <ids>", "Migrate specific tenants (comma-separated)").option("--concurrency <number>", "Number of concurrent migrations", "10").option("--dry-run", "Show what would be applied without executing").option("--mark-applied", "Mark migrations as applied without executing SQL").option("--migrations-folder <path>", "Path to migrations folder").addHelpText("after", `
|
|
973
1391
|
Examples:
|
|
974
1392
|
$ drizzle-multitenant migrate --all
|
|
975
1393
|
$ drizzle-multitenant migrate --tenant=my-tenant
|
|
@@ -977,6 +1395,7 @@ Examples:
|
|
|
977
1395
|
$ drizzle-multitenant migrate --all --dry-run
|
|
978
1396
|
$ drizzle-multitenant migrate --all --concurrency=5
|
|
979
1397
|
$ drizzle-multitenant migrate --all --json
|
|
1398
|
+
$ drizzle-multitenant migrate --all --mark-applied
|
|
980
1399
|
`).action(async (options) => {
|
|
981
1400
|
const startTime = Date.now();
|
|
982
1401
|
const ctx = getOutputContext();
|
|
@@ -1036,7 +1455,11 @@ Examples:
|
|
|
1036
1455
|
if (options.dryRun) {
|
|
1037
1456
|
log(info(bold("\nDry run mode - no changes will be made\n")));
|
|
1038
1457
|
}
|
|
1039
|
-
|
|
1458
|
+
if (options.markApplied) {
|
|
1459
|
+
log(info(bold("\nMark-applied mode - migrations will be recorded without executing SQL\n")));
|
|
1460
|
+
}
|
|
1461
|
+
const actionLabel = options.markApplied ? "Marking" : "Migrating";
|
|
1462
|
+
log(info(`${actionLabel} ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}...
|
|
1040
1463
|
`));
|
|
1041
1464
|
const migrator = createMigrator(config, {
|
|
1042
1465
|
migrationsFolder: folder,
|
|
@@ -1046,9 +1469,8 @@ Examples:
|
|
|
1046
1469
|
const concurrency = parseInt(options.concurrency || "10", 10);
|
|
1047
1470
|
const progressBar = createProgressBar({ total: tenantIds.length });
|
|
1048
1471
|
progressBar.start();
|
|
1049
|
-
const
|
|
1472
|
+
const migrateOptions = {
|
|
1050
1473
|
concurrency,
|
|
1051
|
-
dryRun: !!options.dryRun,
|
|
1052
1474
|
onProgress: (tenantId, status, migrationName) => {
|
|
1053
1475
|
if (status === "completed") {
|
|
1054
1476
|
progressBar.increment({ tenant: tenantId, status: "success" });
|
|
@@ -1057,14 +1479,16 @@ Examples:
|
|
|
1057
1479
|
progressBar.increment({ tenant: tenantId, status: "error" });
|
|
1058
1480
|
debug(`Failed: ${tenantId}`);
|
|
1059
1481
|
} else if (status === "migrating" && migrationName) {
|
|
1060
|
-
|
|
1482
|
+
const actionVerb = options.markApplied ? "Marking" : "Applying";
|
|
1483
|
+
debug(`${tenantId}: ${actionVerb} ${migrationName}`);
|
|
1061
1484
|
}
|
|
1062
1485
|
},
|
|
1063
1486
|
onError: (tenantId, err) => {
|
|
1064
1487
|
debug(`Error on ${tenantId}: ${err.message}`);
|
|
1065
1488
|
return "continue";
|
|
1066
1489
|
}
|
|
1067
|
-
}
|
|
1490
|
+
};
|
|
1491
|
+
const results = options.markApplied ? await migrator.markAllAsApplied(migrateOptions) : await migrator.migrateAll({ ...migrateOptions, dryRun: !!options.dryRun });
|
|
1068
1492
|
progressBar.stop();
|
|
1069
1493
|
const totalDuration = Date.now() - startTime;
|
|
1070
1494
|
if (ctx.jsonMode) {
|
|
@@ -1189,6 +1613,261 @@ Examples:
|
|
|
1189
1613
|
handleError(err);
|
|
1190
1614
|
}
|
|
1191
1615
|
});
|
|
1616
|
+
var syncCommand = new Command("sync").description("Detect and fix divergences between migrations on disk and tracking in database").option("-c, --config <path>", "Path to config file").option("-s, --status", "Show sync status without making changes").option("--mark-missing", "Mark missing migrations as applied").option("--clean-orphans", "Remove orphan records from tracking table").option("--concurrency <number>", "Number of concurrent operations", "10").option("--migrations-folder <path>", "Path to migrations folder").addHelpText("after", `
|
|
1617
|
+
Examples:
|
|
1618
|
+
$ drizzle-multitenant sync --status
|
|
1619
|
+
$ drizzle-multitenant sync --mark-missing
|
|
1620
|
+
$ drizzle-multitenant sync --clean-orphans
|
|
1621
|
+
$ drizzle-multitenant sync --mark-missing --clean-orphans
|
|
1622
|
+
$ drizzle-multitenant sync --status --json
|
|
1623
|
+
`).action(async (options) => {
|
|
1624
|
+
const startTime = Date.now();
|
|
1625
|
+
const ctx = getOutputContext();
|
|
1626
|
+
const spinner = createSpinner("Loading configuration...");
|
|
1627
|
+
try {
|
|
1628
|
+
spinner.start();
|
|
1629
|
+
const { config, migrationsFolder, migrationsTable, tenantDiscovery } = await loadConfig(options.config);
|
|
1630
|
+
if (!tenantDiscovery) {
|
|
1631
|
+
throw CLIErrors.noTenantDiscovery();
|
|
1632
|
+
}
|
|
1633
|
+
const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
|
|
1634
|
+
debug(`Using migrations folder: ${folder}`);
|
|
1635
|
+
const migrator = createMigrator(config, {
|
|
1636
|
+
migrationsFolder: folder,
|
|
1637
|
+
...migrationsTable && { migrationsTable },
|
|
1638
|
+
tenantDiscovery
|
|
1639
|
+
});
|
|
1640
|
+
const showStatus = options.status || !options.markMissing && !options.cleanOrphans;
|
|
1641
|
+
if (showStatus) {
|
|
1642
|
+
spinner.text = "Fetching sync status...";
|
|
1643
|
+
const syncStatus = await migrator.getSyncStatus();
|
|
1644
|
+
spinner.succeed(`Found ${syncStatus.total} tenant${syncStatus.total > 1 ? "s" : ""}`);
|
|
1645
|
+
if (ctx.jsonMode) {
|
|
1646
|
+
const jsonOutput = {
|
|
1647
|
+
tenants: syncStatus.details.map((s) => ({
|
|
1648
|
+
id: s.tenantId,
|
|
1649
|
+
schema: s.schemaName,
|
|
1650
|
+
format: s.format,
|
|
1651
|
+
inSync: s.inSync,
|
|
1652
|
+
missing: s.missing,
|
|
1653
|
+
orphans: s.orphans,
|
|
1654
|
+
error: s.error
|
|
1655
|
+
})),
|
|
1656
|
+
summary: {
|
|
1657
|
+
total: syncStatus.total,
|
|
1658
|
+
inSync: syncStatus.inSync,
|
|
1659
|
+
outOfSync: syncStatus.outOfSync,
|
|
1660
|
+
error: syncStatus.error
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
outputJson(jsonOutput);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
log("\n" + bold("Sync Status:"));
|
|
1667
|
+
log(createSyncStatusTable(syncStatus.details));
|
|
1668
|
+
log(createSyncSummary(syncStatus));
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const concurrency = parseInt(options.concurrency || "10", 10);
|
|
1672
|
+
if (options.markMissing) {
|
|
1673
|
+
spinner.text = "Marking missing migrations...";
|
|
1674
|
+
spinner.succeed();
|
|
1675
|
+
const tenantIds = await tenantDiscovery();
|
|
1676
|
+
log(info(`
|
|
1677
|
+
Marking missing migrations for ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}...
|
|
1678
|
+
`));
|
|
1679
|
+
const progressBar = createProgressBar({ total: tenantIds.length });
|
|
1680
|
+
progressBar.start();
|
|
1681
|
+
const results = await migrator.markAllMissing({
|
|
1682
|
+
concurrency,
|
|
1683
|
+
onProgress: (tenantId, status) => {
|
|
1684
|
+
if (status === "completed") {
|
|
1685
|
+
progressBar.increment({ tenant: tenantId, status: "success" });
|
|
1686
|
+
} else if (status === "failed") {
|
|
1687
|
+
progressBar.increment({ tenant: tenantId, status: "error" });
|
|
1688
|
+
}
|
|
1689
|
+
},
|
|
1690
|
+
onError: (tenantId, err) => {
|
|
1691
|
+
debug(`Error on ${tenantId}: ${err.message}`);
|
|
1692
|
+
return "continue";
|
|
1693
|
+
}
|
|
1694
|
+
});
|
|
1695
|
+
progressBar.stop();
|
|
1696
|
+
const totalDuration = Date.now() - startTime;
|
|
1697
|
+
if (ctx.jsonMode) {
|
|
1698
|
+
outputJson({
|
|
1699
|
+
action: "mark-missing",
|
|
1700
|
+
results: results.details.map((r) => ({
|
|
1701
|
+
tenantId: r.tenantId,
|
|
1702
|
+
schema: r.schemaName,
|
|
1703
|
+
success: r.success,
|
|
1704
|
+
markedMigrations: r.markedMigrations,
|
|
1705
|
+
durationMs: r.durationMs,
|
|
1706
|
+
error: r.error
|
|
1707
|
+
})),
|
|
1708
|
+
summary: {
|
|
1709
|
+
total: results.total,
|
|
1710
|
+
succeeded: results.succeeded,
|
|
1711
|
+
failed: results.failed,
|
|
1712
|
+
durationMs: totalDuration
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
process.exit(results.failed > 0 ? 1 : 0);
|
|
1716
|
+
}
|
|
1717
|
+
log("\n" + bold("Results:"));
|
|
1718
|
+
log(createSyncResultsTable(results.details, "mark-missing"));
|
|
1719
|
+
log("\n" + bold("Summary:"));
|
|
1720
|
+
log(` Total: ${results.total}`);
|
|
1721
|
+
log(` Succeeded: ${success(results.succeeded.toString())}`);
|
|
1722
|
+
if (results.failed > 0) {
|
|
1723
|
+
log(` Failed: ${error(results.failed.toString())}`);
|
|
1724
|
+
}
|
|
1725
|
+
log(` Duration: ${dim(formatDuration2(totalDuration))}`);
|
|
1726
|
+
}
|
|
1727
|
+
if (options.cleanOrphans) {
|
|
1728
|
+
if (options.markMissing) {
|
|
1729
|
+
log("\n");
|
|
1730
|
+
}
|
|
1731
|
+
spinner.text = "Cleaning orphan records...";
|
|
1732
|
+
spinner.succeed();
|
|
1733
|
+
const tenantIds = await tenantDiscovery();
|
|
1734
|
+
log(info(`
|
|
1735
|
+
Cleaning orphan records for ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}...
|
|
1736
|
+
`));
|
|
1737
|
+
const progressBar = createProgressBar({ total: tenantIds.length });
|
|
1738
|
+
progressBar.start();
|
|
1739
|
+
const results = await migrator.cleanAllOrphans({
|
|
1740
|
+
concurrency,
|
|
1741
|
+
onProgress: (tenantId, status) => {
|
|
1742
|
+
if (status === "completed") {
|
|
1743
|
+
progressBar.increment({ tenant: tenantId, status: "success" });
|
|
1744
|
+
} else if (status === "failed") {
|
|
1745
|
+
progressBar.increment({ tenant: tenantId, status: "error" });
|
|
1746
|
+
}
|
|
1747
|
+
},
|
|
1748
|
+
onError: (tenantId, err) => {
|
|
1749
|
+
debug(`Error on ${tenantId}: ${err.message}`);
|
|
1750
|
+
return "continue";
|
|
1751
|
+
}
|
|
1752
|
+
});
|
|
1753
|
+
progressBar.stop();
|
|
1754
|
+
const totalDuration = Date.now() - startTime;
|
|
1755
|
+
if (ctx.jsonMode) {
|
|
1756
|
+
outputJson({
|
|
1757
|
+
action: "clean-orphans",
|
|
1758
|
+
results: results.details.map((r) => ({
|
|
1759
|
+
tenantId: r.tenantId,
|
|
1760
|
+
schema: r.schemaName,
|
|
1761
|
+
success: r.success,
|
|
1762
|
+
removedOrphans: r.removedOrphans,
|
|
1763
|
+
durationMs: r.durationMs,
|
|
1764
|
+
error: r.error
|
|
1765
|
+
})),
|
|
1766
|
+
summary: {
|
|
1767
|
+
total: results.total,
|
|
1768
|
+
succeeded: results.succeeded,
|
|
1769
|
+
failed: results.failed,
|
|
1770
|
+
durationMs: totalDuration
|
|
1771
|
+
}
|
|
1772
|
+
});
|
|
1773
|
+
process.exit(results.failed > 0 ? 1 : 0);
|
|
1774
|
+
}
|
|
1775
|
+
log("\n" + bold("Results:"));
|
|
1776
|
+
log(createSyncResultsTable(results.details, "clean-orphans"));
|
|
1777
|
+
log("\n" + bold("Summary:"));
|
|
1778
|
+
log(` Total: ${results.total}`);
|
|
1779
|
+
log(` Succeeded: ${success(results.succeeded.toString())}`);
|
|
1780
|
+
if (results.failed > 0) {
|
|
1781
|
+
log(` Failed: ${error(results.failed.toString())}`);
|
|
1782
|
+
}
|
|
1783
|
+
log(` Duration: ${dim(formatDuration2(totalDuration))}`);
|
|
1784
|
+
}
|
|
1785
|
+
} catch (err) {
|
|
1786
|
+
spinner.fail(err.message);
|
|
1787
|
+
handleError(err);
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
function formatDuration2(ms) {
|
|
1791
|
+
if (ms < 1e3) {
|
|
1792
|
+
return `${ms}ms`;
|
|
1793
|
+
}
|
|
1794
|
+
if (ms < 6e4) {
|
|
1795
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1796
|
+
}
|
|
1797
|
+
const mins = Math.floor(ms / 6e4);
|
|
1798
|
+
const secs = Math.round(ms % 6e4 / 1e3);
|
|
1799
|
+
return `${mins}m ${secs}s`;
|
|
1800
|
+
}
|
|
1801
|
+
function createSyncStatusTable(details) {
|
|
1802
|
+
if (details.length === 0) {
|
|
1803
|
+
return " No tenants found.\n";
|
|
1804
|
+
}
|
|
1805
|
+
const lines = [];
|
|
1806
|
+
for (const detail of details) {
|
|
1807
|
+
if (detail.error) {
|
|
1808
|
+
lines.push(` ${error(detail.tenantId)}: ${dim(detail.error)}`);
|
|
1809
|
+
} else if (detail.inSync) {
|
|
1810
|
+
lines.push(` ${success(detail.tenantId)}: ${dim("In sync")}`);
|
|
1811
|
+
} else {
|
|
1812
|
+
const issues = [];
|
|
1813
|
+
if (detail.missing.length > 0) {
|
|
1814
|
+
issues.push(`${detail.missing.length} missing`);
|
|
1815
|
+
}
|
|
1816
|
+
if (detail.orphans.length > 0) {
|
|
1817
|
+
issues.push(`${detail.orphans.length} orphan${detail.orphans.length > 1 ? "s" : ""}`);
|
|
1818
|
+
}
|
|
1819
|
+
lines.push(` ${warning(detail.tenantId)}: ${issues.join(", ")}`);
|
|
1820
|
+
if (detail.missing.length > 0) {
|
|
1821
|
+
lines.push(` ${dim("Missing:")} ${detail.missing.slice(0, 3).join(", ")}${detail.missing.length > 3 ? `, +${detail.missing.length - 3} more` : ""}`);
|
|
1822
|
+
}
|
|
1823
|
+
if (detail.orphans.length > 0) {
|
|
1824
|
+
lines.push(` ${dim("Orphans:")} ${detail.orphans.slice(0, 3).join(", ")}${detail.orphans.length > 3 ? `, +${detail.orphans.length - 3} more` : ""}`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return lines.join("\n") + "\n";
|
|
1829
|
+
}
|
|
1830
|
+
function createSyncSummary(status) {
|
|
1831
|
+
const lines = [];
|
|
1832
|
+
lines.push("\n" + bold("Summary:"));
|
|
1833
|
+
lines.push(` Total: ${status.total}`);
|
|
1834
|
+
lines.push(` In Sync: ${success(status.inSync.toString())}`);
|
|
1835
|
+
if (status.outOfSync > 0) {
|
|
1836
|
+
lines.push(` Out of Sync: ${warning(status.outOfSync.toString())}`);
|
|
1837
|
+
}
|
|
1838
|
+
if (status.error > 0) {
|
|
1839
|
+
lines.push(` Errors: ${error(status.error.toString())}`);
|
|
1840
|
+
}
|
|
1841
|
+
if (status.outOfSync > 0) {
|
|
1842
|
+
lines.push("\n" + dim("Run with --mark-missing to mark missing migrations as applied."));
|
|
1843
|
+
lines.push(dim("Run with --clean-orphans to remove orphan records."));
|
|
1844
|
+
}
|
|
1845
|
+
return lines.join("\n");
|
|
1846
|
+
}
|
|
1847
|
+
function createSyncResultsTable(details, action) {
|
|
1848
|
+
if (details.length === 0) {
|
|
1849
|
+
return " No tenants processed.\n";
|
|
1850
|
+
}
|
|
1851
|
+
const lines = [];
|
|
1852
|
+
for (const detail of details) {
|
|
1853
|
+
if (detail.error) {
|
|
1854
|
+
lines.push(` ${error(detail.tenantId)}: ${dim(detail.error)}`);
|
|
1855
|
+
} else if (action === "mark-missing") {
|
|
1856
|
+
if (detail.markedMigrations.length > 0) {
|
|
1857
|
+
lines.push(` ${success(detail.tenantId)}: Marked ${detail.markedMigrations.length} migration${detail.markedMigrations.length > 1 ? "s" : ""}`);
|
|
1858
|
+
} else {
|
|
1859
|
+
lines.push(` ${dim(detail.tenantId)}: Nothing to mark`);
|
|
1860
|
+
}
|
|
1861
|
+
} else {
|
|
1862
|
+
if (detail.removedOrphans.length > 0) {
|
|
1863
|
+
lines.push(` ${success(detail.tenantId)}: Removed ${detail.removedOrphans.length} orphan${detail.removedOrphans.length > 1 ? "s" : ""}`);
|
|
1864
|
+
} else {
|
|
1865
|
+
lines.push(` ${dim(detail.tenantId)}: No orphans found`);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return lines.join("\n") + "\n";
|
|
1870
|
+
}
|
|
1192
1871
|
var generateCommand = 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 (options) => {
|
|
1193
1872
|
const spinner = createSpinner("Loading configuration...");
|
|
1194
1873
|
try {
|
|
@@ -2066,6 +2745,7 @@ Documentation:
|
|
|
2066
2745
|
`);
|
|
2067
2746
|
program.addCommand(migrateCommand);
|
|
2068
2747
|
program.addCommand(statusCommand);
|
|
2748
|
+
program.addCommand(syncCommand);
|
|
2069
2749
|
program.addCommand(generateCommand);
|
|
2070
2750
|
program.addCommand(tenantCreateCommand);
|
|
2071
2751
|
program.addCommand(tenantDropCommand);
|