drizzle-multitenant 1.0.4 → 1.0.5
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 +3 -1
- package/README.md +112 -8
- package/dist/cli/index.js +1347 -141
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +178 -29
- package/dist/index.js.map +1 -1
- package/dist/migrator/index.d.ts +85 -4
- package/dist/migrator/index.js +197 -30
- package/dist/migrator/index.js.map +1 -1
- package/package.json +4 -1
- package/proposals/drizzle-kit-compatibility.md +499 -0
- package/proposals/improvements-from-primesys.md +385 -0
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { LRUCache } from 'lru-cache';
|
|
|
4
4
|
import { AsyncLocalStorage } from 'async_hooks';
|
|
5
5
|
import { readdir, readFile } from 'fs/promises';
|
|
6
6
|
import { join, basename } from 'path';
|
|
7
|
+
import { createHash } from 'crypto';
|
|
7
8
|
import { sql, getTableName } from 'drizzle-orm';
|
|
8
9
|
|
|
9
10
|
// src/config.ts
|
|
@@ -336,6 +337,100 @@ function createTenantContext(manager) {
|
|
|
336
337
|
isInTenantContext
|
|
337
338
|
};
|
|
338
339
|
}
|
|
340
|
+
|
|
341
|
+
// src/migrator/table-format.ts
|
|
342
|
+
async function detectTableFormat(pool, schemaName, tableName) {
|
|
343
|
+
const tableExists = await pool.query(
|
|
344
|
+
`SELECT EXISTS (
|
|
345
|
+
SELECT 1 FROM information_schema.tables
|
|
346
|
+
WHERE table_schema = $1 AND table_name = $2
|
|
347
|
+
) as exists`,
|
|
348
|
+
[schemaName, tableName]
|
|
349
|
+
);
|
|
350
|
+
if (!tableExists.rows[0]?.exists) {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
const columnsResult = await pool.query(
|
|
354
|
+
`SELECT column_name, data_type
|
|
355
|
+
FROM information_schema.columns
|
|
356
|
+
WHERE table_schema = $1 AND table_name = $2`,
|
|
357
|
+
[schemaName, tableName]
|
|
358
|
+
);
|
|
359
|
+
const columnMap = new Map(
|
|
360
|
+
columnsResult.rows.map((r) => [r.column_name, r.data_type])
|
|
361
|
+
);
|
|
362
|
+
if (columnMap.has("name")) {
|
|
363
|
+
return {
|
|
364
|
+
format: "name",
|
|
365
|
+
tableName,
|
|
366
|
+
columns: {
|
|
367
|
+
identifier: "name",
|
|
368
|
+
timestamp: columnMap.has("applied_at") ? "applied_at" : "created_at",
|
|
369
|
+
timestampType: "timestamp"
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (columnMap.has("hash")) {
|
|
374
|
+
const createdAtType = columnMap.get("created_at");
|
|
375
|
+
if (createdAtType === "bigint") {
|
|
376
|
+
return {
|
|
377
|
+
format: "drizzle-kit",
|
|
378
|
+
tableName,
|
|
379
|
+
columns: {
|
|
380
|
+
identifier: "hash",
|
|
381
|
+
timestamp: "created_at",
|
|
382
|
+
timestampType: "bigint"
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return {
|
|
387
|
+
format: "hash",
|
|
388
|
+
tableName,
|
|
389
|
+
columns: {
|
|
390
|
+
identifier: "hash",
|
|
391
|
+
timestamp: "created_at",
|
|
392
|
+
timestampType: "timestamp"
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
function getFormatConfig(format, tableName = "__drizzle_migrations") {
|
|
399
|
+
switch (format) {
|
|
400
|
+
case "name":
|
|
401
|
+
return {
|
|
402
|
+
format: "name",
|
|
403
|
+
tableName,
|
|
404
|
+
columns: {
|
|
405
|
+
identifier: "name",
|
|
406
|
+
timestamp: "applied_at",
|
|
407
|
+
timestampType: "timestamp"
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
case "hash":
|
|
411
|
+
return {
|
|
412
|
+
format: "hash",
|
|
413
|
+
tableName,
|
|
414
|
+
columns: {
|
|
415
|
+
identifier: "hash",
|
|
416
|
+
timestamp: "created_at",
|
|
417
|
+
timestampType: "timestamp"
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
case "drizzle-kit":
|
|
421
|
+
return {
|
|
422
|
+
format: "drizzle-kit",
|
|
423
|
+
tableName,
|
|
424
|
+
columns: {
|
|
425
|
+
identifier: "hash",
|
|
426
|
+
timestamp: "created_at",
|
|
427
|
+
timestampType: "bigint"
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// src/migrator/migrator.ts
|
|
339
434
|
var DEFAULT_MIGRATIONS_TABLE = "__drizzle_migrations";
|
|
340
435
|
var Migrator = class {
|
|
341
436
|
constructor(tenantConfig, migratorConfig) {
|
|
@@ -400,25 +495,29 @@ var Migrator = class {
|
|
|
400
495
|
const pool = await this.createPool(schemaName);
|
|
401
496
|
try {
|
|
402
497
|
await this.migratorConfig.hooks?.beforeTenant?.(tenantId);
|
|
403
|
-
await this.
|
|
498
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
499
|
+
await this.ensureMigrationsTable(pool, schemaName, format);
|
|
404
500
|
const allMigrations = migrations ?? await this.loadMigrations();
|
|
405
|
-
const applied = await this.getAppliedMigrations(pool, schemaName);
|
|
406
|
-
const appliedSet = new Set(applied.map((m) => m.
|
|
407
|
-
const pending = allMigrations.filter(
|
|
501
|
+
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
502
|
+
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
503
|
+
const pending = allMigrations.filter(
|
|
504
|
+
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
505
|
+
);
|
|
408
506
|
if (options.dryRun) {
|
|
409
507
|
return {
|
|
410
508
|
tenantId,
|
|
411
509
|
schemaName,
|
|
412
510
|
success: true,
|
|
413
511
|
appliedMigrations: pending.map((m) => m.name),
|
|
414
|
-
durationMs: Date.now() - startTime
|
|
512
|
+
durationMs: Date.now() - startTime,
|
|
513
|
+
format: format.format
|
|
415
514
|
};
|
|
416
515
|
}
|
|
417
516
|
for (const migration of pending) {
|
|
418
517
|
const migrationStart = Date.now();
|
|
419
518
|
options.onProgress?.(tenantId, "migrating", migration.name);
|
|
420
519
|
await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);
|
|
421
|
-
await this.applyMigration(pool, schemaName, migration);
|
|
520
|
+
await this.applyMigration(pool, schemaName, migration, format);
|
|
422
521
|
await this.migratorConfig.hooks?.afterMigration?.(
|
|
423
522
|
tenantId,
|
|
424
523
|
migration.name,
|
|
@@ -431,7 +530,8 @@ var Migrator = class {
|
|
|
431
530
|
schemaName,
|
|
432
531
|
success: true,
|
|
433
532
|
appliedMigrations,
|
|
434
|
-
durationMs: Date.now() - startTime
|
|
533
|
+
durationMs: Date.now() - startTime,
|
|
534
|
+
format: format.format
|
|
435
535
|
};
|
|
436
536
|
await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
|
|
437
537
|
return result;
|
|
@@ -505,19 +605,25 @@ var Migrator = class {
|
|
|
505
605
|
appliedCount: 0,
|
|
506
606
|
pendingCount: allMigrations.length,
|
|
507
607
|
pendingMigrations: allMigrations.map((m) => m.name),
|
|
508
|
-
status: allMigrations.length > 0 ? "behind" : "ok"
|
|
608
|
+
status: allMigrations.length > 0 ? "behind" : "ok",
|
|
609
|
+
format: null
|
|
610
|
+
// New tenant, no table yet
|
|
509
611
|
};
|
|
510
612
|
}
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
const
|
|
613
|
+
const format = await this.getOrDetectFormat(pool, schemaName);
|
|
614
|
+
const applied = await this.getAppliedMigrations(pool, schemaName, format);
|
|
615
|
+
const appliedSet = new Set(applied.map((m) => m.identifier));
|
|
616
|
+
const pending = allMigrations.filter(
|
|
617
|
+
(m) => !this.isMigrationApplied(m, appliedSet, format)
|
|
618
|
+
);
|
|
514
619
|
return {
|
|
515
620
|
tenantId,
|
|
516
621
|
schemaName,
|
|
517
622
|
appliedCount: applied.length,
|
|
518
623
|
pendingCount: pending.length,
|
|
519
624
|
pendingMigrations: pending.map((m) => m.name),
|
|
520
|
-
status: pending.length > 0 ? "behind" : "ok"
|
|
625
|
+
status: pending.length > 0 ? "behind" : "ok",
|
|
626
|
+
format: format.format
|
|
521
627
|
};
|
|
522
628
|
} catch (error) {
|
|
523
629
|
return {
|
|
@@ -527,7 +633,8 @@ var Migrator = class {
|
|
|
527
633
|
pendingCount: 0,
|
|
528
634
|
pendingMigrations: [],
|
|
529
635
|
status: "error",
|
|
530
|
-
error: error.message
|
|
636
|
+
error: error.message,
|
|
637
|
+
format: null
|
|
531
638
|
};
|
|
532
639
|
} finally {
|
|
533
640
|
await pool.end();
|
|
@@ -600,11 +707,13 @@ var Migrator = class {
|
|
|
600
707
|
const content = await readFile(filePath, "utf-8");
|
|
601
708
|
const match = file.match(/^(\d+)_/);
|
|
602
709
|
const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;
|
|
710
|
+
const hash = createHash("sha256").update(content).digest("hex");
|
|
603
711
|
migrations.push({
|
|
604
712
|
name: basename(file, ".sql"),
|
|
605
713
|
path: filePath,
|
|
606
714
|
sql: content,
|
|
607
|
-
timestamp
|
|
715
|
+
timestamp,
|
|
716
|
+
hash
|
|
608
717
|
});
|
|
609
718
|
}
|
|
610
719
|
return migrations.sort((a, b) => a.timestamp - b.timestamp);
|
|
@@ -620,14 +729,17 @@ var Migrator = class {
|
|
|
620
729
|
});
|
|
621
730
|
}
|
|
622
731
|
/**
|
|
623
|
-
* Ensure migrations table exists
|
|
732
|
+
* Ensure migrations table exists with the correct format
|
|
624
733
|
*/
|
|
625
|
-
async ensureMigrationsTable(pool, schemaName) {
|
|
734
|
+
async ensureMigrationsTable(pool, schemaName, format) {
|
|
735
|
+
const { identifier, timestamp, timestampType } = format.columns;
|
|
736
|
+
const identifierCol = identifier === "name" ? "name VARCHAR(255) NOT NULL UNIQUE" : "hash TEXT NOT NULL";
|
|
737
|
+
const timestampCol = timestampType === "bigint" ? `${timestamp} BIGINT NOT NULL` : `${timestamp} TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP`;
|
|
626
738
|
await pool.query(`
|
|
627
|
-
CREATE TABLE IF NOT EXISTS "${schemaName}"."${
|
|
739
|
+
CREATE TABLE IF NOT EXISTS "${schemaName}"."${format.tableName}" (
|
|
628
740
|
id SERIAL PRIMARY KEY,
|
|
629
|
-
|
|
630
|
-
|
|
741
|
+
${identifierCol},
|
|
742
|
+
${timestampCol}
|
|
631
743
|
)
|
|
632
744
|
`);
|
|
633
745
|
}
|
|
@@ -645,27 +757,64 @@ var Migrator = class {
|
|
|
645
757
|
/**
|
|
646
758
|
* Get applied migrations for a schema
|
|
647
759
|
*/
|
|
648
|
-
async getAppliedMigrations(pool, schemaName) {
|
|
760
|
+
async getAppliedMigrations(pool, schemaName, format) {
|
|
761
|
+
const identifierColumn = format.columns.identifier;
|
|
762
|
+
const timestampColumn = format.columns.timestamp;
|
|
649
763
|
const result = await pool.query(
|
|
650
|
-
`SELECT id,
|
|
764
|
+
`SELECT id, "${identifierColumn}" as identifier, "${timestampColumn}" as applied_at
|
|
765
|
+
FROM "${schemaName}"."${format.tableName}"
|
|
766
|
+
ORDER BY id`
|
|
651
767
|
);
|
|
652
|
-
return result.rows.map((row) =>
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
768
|
+
return result.rows.map((row) => {
|
|
769
|
+
const appliedAt = format.columns.timestampType === "bigint" ? new Date(Number(row.applied_at)) : new Date(row.applied_at);
|
|
770
|
+
return {
|
|
771
|
+
id: row.id,
|
|
772
|
+
identifier: row.identifier,
|
|
773
|
+
// Set name or hash based on format
|
|
774
|
+
...format.columns.identifier === "name" ? { name: row.identifier } : { hash: row.identifier },
|
|
775
|
+
appliedAt
|
|
776
|
+
};
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Check if a migration has been applied
|
|
781
|
+
*/
|
|
782
|
+
isMigrationApplied(migration, appliedIdentifiers, format) {
|
|
783
|
+
if (format.columns.identifier === "name") {
|
|
784
|
+
return appliedIdentifiers.has(migration.name);
|
|
785
|
+
}
|
|
786
|
+
return appliedIdentifiers.has(migration.hash) || appliedIdentifiers.has(migration.name);
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Get or detect the format for a schema
|
|
790
|
+
* Returns the configured format or auto-detects from existing table
|
|
791
|
+
*/
|
|
792
|
+
async getOrDetectFormat(pool, schemaName) {
|
|
793
|
+
const configuredFormat = this.migratorConfig.tableFormat ?? "auto";
|
|
794
|
+
if (configuredFormat !== "auto") {
|
|
795
|
+
return getFormatConfig(configuredFormat, this.migrationsTable);
|
|
796
|
+
}
|
|
797
|
+
const detected = await detectTableFormat(pool, schemaName, this.migrationsTable);
|
|
798
|
+
if (detected) {
|
|
799
|
+
return detected;
|
|
800
|
+
}
|
|
801
|
+
const defaultFormat = this.migratorConfig.defaultFormat ?? "name";
|
|
802
|
+
return getFormatConfig(defaultFormat, this.migrationsTable);
|
|
657
803
|
}
|
|
658
804
|
/**
|
|
659
805
|
* Apply a migration to a schema
|
|
660
806
|
*/
|
|
661
|
-
async applyMigration(pool, schemaName, migration) {
|
|
807
|
+
async applyMigration(pool, schemaName, migration, format) {
|
|
662
808
|
const client = await pool.connect();
|
|
663
809
|
try {
|
|
664
810
|
await client.query("BEGIN");
|
|
665
811
|
await client.query(migration.sql);
|
|
812
|
+
const { identifier, timestamp, timestampType } = format.columns;
|
|
813
|
+
const identifierValue = identifier === "name" ? migration.name : migration.hash;
|
|
814
|
+
const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
|
|
666
815
|
await client.query(
|
|
667
|
-
`INSERT INTO "${schemaName}"."${
|
|
668
|
-
[
|
|
816
|
+
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
817
|
+
[identifierValue, timestampValue]
|
|
669
818
|
);
|
|
670
819
|
await client.query("COMMIT");
|
|
671
820
|
} catch (error) {
|