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/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.ensureMigrationsTable(pool, schemaName);
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.name));
407
- const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
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 applied = await this.getAppliedMigrations(pool, schemaName);
512
- const appliedSet = new Set(applied.map((m) => m.name));
513
- const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
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}"."${this.migrationsTable}" (
739
+ CREATE TABLE IF NOT EXISTS "${schemaName}"."${format.tableName}" (
628
740
  id SERIAL PRIMARY KEY,
629
- name VARCHAR(255) NOT NULL UNIQUE,
630
- applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
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, name, applied_at FROM "${schemaName}"."${this.migrationsTable}" ORDER BY 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
- id: row.id,
654
- name: row.name,
655
- appliedAt: row.applied_at
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}"."${this.migrationsTable}" (name) VALUES ($1)`,
668
- [migration.name]
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) {