drizzle-multitenant 1.0.4 → 1.0.6

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/cli/index.js CHANGED
@@ -1,15 +1,111 @@
1
1
  #!/usr/bin/env node
2
2
  import { Command } from 'commander';
3
+ import { checkbox, confirm, select, input } from '@inquirer/prompts';
3
4
  import { mkdir, readdir, writeFile, readFile } from 'fs/promises';
4
5
  import { resolve, join, extname, basename } from 'path';
6
+ import { createHash } from 'crypto';
5
7
  import { Pool } from 'pg';
6
8
  import ora from 'ora';
7
- import chalk2 from 'chalk';
8
9
  import Table from 'cli-table3';
10
+ import chalk from 'chalk';
9
11
  import { pathToFileURL } from 'url';
10
- import { existsSync } from 'fs';
12
+ import { existsSync, writeFileSync, mkdirSync } from 'fs';
13
+ import cliProgress from 'cli-progress';
11
14
  import { createInterface } from 'readline';
12
15
 
16
+ // src/migrator/table-format.ts
17
+ async function detectTableFormat(pool, schemaName, tableName) {
18
+ const tableExists = await pool.query(
19
+ `SELECT EXISTS (
20
+ SELECT 1 FROM information_schema.tables
21
+ WHERE table_schema = $1 AND table_name = $2
22
+ ) as exists`,
23
+ [schemaName, tableName]
24
+ );
25
+ if (!tableExists.rows[0]?.exists) {
26
+ return null;
27
+ }
28
+ const columnsResult = await pool.query(
29
+ `SELECT column_name, data_type
30
+ FROM information_schema.columns
31
+ WHERE table_schema = $1 AND table_name = $2`,
32
+ [schemaName, tableName]
33
+ );
34
+ const columnMap = new Map(
35
+ columnsResult.rows.map((r) => [r.column_name, r.data_type])
36
+ );
37
+ if (columnMap.has("name")) {
38
+ return {
39
+ format: "name",
40
+ tableName,
41
+ columns: {
42
+ identifier: "name",
43
+ timestamp: columnMap.has("applied_at") ? "applied_at" : "created_at",
44
+ timestampType: "timestamp"
45
+ }
46
+ };
47
+ }
48
+ if (columnMap.has("hash")) {
49
+ const createdAtType = columnMap.get("created_at");
50
+ if (createdAtType === "bigint") {
51
+ return {
52
+ format: "drizzle-kit",
53
+ tableName,
54
+ columns: {
55
+ identifier: "hash",
56
+ timestamp: "created_at",
57
+ timestampType: "bigint"
58
+ }
59
+ };
60
+ }
61
+ return {
62
+ format: "hash",
63
+ tableName,
64
+ columns: {
65
+ identifier: "hash",
66
+ timestamp: "created_at",
67
+ timestampType: "timestamp"
68
+ }
69
+ };
70
+ }
71
+ return null;
72
+ }
73
+ function getFormatConfig(format, tableName = "__drizzle_migrations") {
74
+ switch (format) {
75
+ case "name":
76
+ return {
77
+ format: "name",
78
+ tableName,
79
+ columns: {
80
+ identifier: "name",
81
+ timestamp: "applied_at",
82
+ timestampType: "timestamp"
83
+ }
84
+ };
85
+ case "hash":
86
+ return {
87
+ format: "hash",
88
+ tableName,
89
+ columns: {
90
+ identifier: "hash",
91
+ timestamp: "created_at",
92
+ timestampType: "timestamp"
93
+ }
94
+ };
95
+ case "drizzle-kit":
96
+ return {
97
+ format: "drizzle-kit",
98
+ tableName,
99
+ columns: {
100
+ identifier: "hash",
101
+ timestamp: "created_at",
102
+ timestampType: "bigint"
103
+ }
104
+ };
105
+ }
106
+ }
107
+
108
+ // src/migrator/migrator.ts
13
109
  var DEFAULT_MIGRATIONS_TABLE = "__drizzle_migrations";
14
110
  var Migrator = class {
15
111
  constructor(tenantConfig, migratorConfig) {
@@ -44,13 +140,13 @@ var Migrator = class {
44
140
  const result = await this.migrateTenant(tenantId, migrations, { dryRun, onProgress });
45
141
  onProgress?.(tenantId, result.success ? "completed" : "failed");
46
142
  return result;
47
- } catch (error6) {
143
+ } catch (error2) {
48
144
  onProgress?.(tenantId, "failed");
49
- const action = onError?.(tenantId, error6);
145
+ const action = onError?.(tenantId, error2);
50
146
  if (action === "abort") {
51
147
  aborted = true;
52
148
  }
53
- return this.createErrorResult(tenantId, error6);
149
+ return this.createErrorResult(tenantId, error2);
54
150
  }
55
151
  })
56
152
  );
@@ -74,25 +170,29 @@ var Migrator = class {
74
170
  const pool = await this.createPool(schemaName);
75
171
  try {
76
172
  await this.migratorConfig.hooks?.beforeTenant?.(tenantId);
77
- await this.ensureMigrationsTable(pool, schemaName);
173
+ const format = await this.getOrDetectFormat(pool, schemaName);
174
+ await this.ensureMigrationsTable(pool, schemaName, format);
78
175
  const allMigrations = migrations ?? await this.loadMigrations();
79
- const applied = await this.getAppliedMigrations(pool, schemaName);
80
- const appliedSet = new Set(applied.map((m) => m.name));
81
- const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
176
+ const applied = await this.getAppliedMigrations(pool, schemaName, format);
177
+ const appliedSet = new Set(applied.map((m) => m.identifier));
178
+ const pending = allMigrations.filter(
179
+ (m) => !this.isMigrationApplied(m, appliedSet, format)
180
+ );
82
181
  if (options.dryRun) {
83
182
  return {
84
183
  tenantId,
85
184
  schemaName,
86
185
  success: true,
87
186
  appliedMigrations: pending.map((m) => m.name),
88
- durationMs: Date.now() - startTime
187
+ durationMs: Date.now() - startTime,
188
+ format: format.format
89
189
  };
90
190
  }
91
191
  for (const migration of pending) {
92
192
  const migrationStart = Date.now();
93
193
  options.onProgress?.(tenantId, "migrating", migration.name);
94
194
  await this.migratorConfig.hooks?.beforeMigration?.(tenantId, migration.name);
95
- await this.applyMigration(pool, schemaName, migration);
195
+ await this.applyMigration(pool, schemaName, migration, format);
96
196
  await this.migratorConfig.hooks?.afterMigration?.(
97
197
  tenantId,
98
198
  migration.name,
@@ -105,17 +205,18 @@ var Migrator = class {
105
205
  schemaName,
106
206
  success: true,
107
207
  appliedMigrations,
108
- durationMs: Date.now() - startTime
208
+ durationMs: Date.now() - startTime,
209
+ format: format.format
109
210
  };
110
211
  await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
111
212
  return result;
112
- } catch (error6) {
213
+ } catch (error2) {
113
214
  const result = {
114
215
  tenantId,
115
216
  schemaName,
116
217
  success: false,
117
218
  appliedMigrations,
118
- error: error6.message,
219
+ error: error2.message,
119
220
  durationMs: Date.now() - startTime
120
221
  };
121
222
  await this.migratorConfig.hooks?.afterTenant?.(tenantId, result);
@@ -140,10 +241,10 @@ var Migrator = class {
140
241
  const result = await this.migrateTenant(tenantId, migrations, { dryRun: options.dryRun ?? false, onProgress });
141
242
  onProgress?.(tenantId, result.success ? "completed" : "failed");
142
243
  return result;
143
- } catch (error6) {
244
+ } catch (error2) {
144
245
  onProgress?.(tenantId, "failed");
145
- onError?.(tenantId, error6);
146
- return this.createErrorResult(tenantId, error6);
246
+ onError?.(tenantId, error2);
247
+ return this.createErrorResult(tenantId, error2);
147
248
  }
148
249
  })
149
250
  );
@@ -179,21 +280,27 @@ var Migrator = class {
179
280
  appliedCount: 0,
180
281
  pendingCount: allMigrations.length,
181
282
  pendingMigrations: allMigrations.map((m) => m.name),
182
- status: allMigrations.length > 0 ? "behind" : "ok"
283
+ status: allMigrations.length > 0 ? "behind" : "ok",
284
+ format: null
285
+ // New tenant, no table yet
183
286
  };
184
287
  }
185
- const applied = await this.getAppliedMigrations(pool, schemaName);
186
- const appliedSet = new Set(applied.map((m) => m.name));
187
- const pending = allMigrations.filter((m) => !appliedSet.has(m.name));
288
+ const format = await this.getOrDetectFormat(pool, schemaName);
289
+ const applied = await this.getAppliedMigrations(pool, schemaName, format);
290
+ const appliedSet = new Set(applied.map((m) => m.identifier));
291
+ const pending = allMigrations.filter(
292
+ (m) => !this.isMigrationApplied(m, appliedSet, format)
293
+ );
188
294
  return {
189
295
  tenantId,
190
296
  schemaName,
191
297
  appliedCount: applied.length,
192
298
  pendingCount: pending.length,
193
299
  pendingMigrations: pending.map((m) => m.name),
194
- status: pending.length > 0 ? "behind" : "ok"
300
+ status: pending.length > 0 ? "behind" : "ok",
301
+ format: format.format
195
302
  };
196
- } catch (error6) {
303
+ } catch (error2) {
197
304
  return {
198
305
  tenantId,
199
306
  schemaName,
@@ -201,7 +308,8 @@ var Migrator = class {
201
308
  pendingCount: 0,
202
309
  pendingMigrations: [],
203
310
  status: "error",
204
- error: error6.message
311
+ error: error2.message,
312
+ format: null
205
313
  };
206
314
  } finally {
207
315
  await pool.end();
@@ -274,11 +382,13 @@ var Migrator = class {
274
382
  const content = await readFile(filePath, "utf-8");
275
383
  const match = file.match(/^(\d+)_/);
276
384
  const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;
385
+ const hash = createHash("sha256").update(content).digest("hex");
277
386
  migrations.push({
278
387
  name: basename(file, ".sql"),
279
388
  path: filePath,
280
389
  sql: content,
281
- timestamp
390
+ timestamp,
391
+ hash
282
392
  });
283
393
  }
284
394
  return migrations.sort((a, b) => a.timestamp - b.timestamp);
@@ -294,14 +404,17 @@ var Migrator = class {
294
404
  });
295
405
  }
296
406
  /**
297
- * Ensure migrations table exists
407
+ * Ensure migrations table exists with the correct format
298
408
  */
299
- async ensureMigrationsTable(pool, schemaName) {
409
+ async ensureMigrationsTable(pool, schemaName, format) {
410
+ const { identifier, timestamp, timestampType } = format.columns;
411
+ const identifierCol = identifier === "name" ? "name VARCHAR(255) NOT NULL UNIQUE" : "hash TEXT NOT NULL";
412
+ const timestampCol = timestampType === "bigint" ? `${timestamp} BIGINT NOT NULL` : `${timestamp} TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP`;
300
413
  await pool.query(`
301
- CREATE TABLE IF NOT EXISTS "${schemaName}"."${this.migrationsTable}" (
414
+ CREATE TABLE IF NOT EXISTS "${schemaName}"."${format.tableName}" (
302
415
  id SERIAL PRIMARY KEY,
303
- name VARCHAR(255) NOT NULL UNIQUE,
304
- applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
416
+ ${identifierCol},
417
+ ${timestampCol}
305
418
  )
306
419
  `);
307
420
  }
@@ -319,32 +432,69 @@ var Migrator = class {
319
432
  /**
320
433
  * Get applied migrations for a schema
321
434
  */
322
- async getAppliedMigrations(pool, schemaName) {
435
+ async getAppliedMigrations(pool, schemaName, format) {
436
+ const identifierColumn = format.columns.identifier;
437
+ const timestampColumn = format.columns.timestamp;
323
438
  const result = await pool.query(
324
- `SELECT id, name, applied_at FROM "${schemaName}"."${this.migrationsTable}" ORDER BY id`
439
+ `SELECT id, "${identifierColumn}" as identifier, "${timestampColumn}" as applied_at
440
+ FROM "${schemaName}"."${format.tableName}"
441
+ ORDER BY id`
325
442
  );
326
- return result.rows.map((row) => ({
327
- id: row.id,
328
- name: row.name,
329
- appliedAt: row.applied_at
330
- }));
443
+ return result.rows.map((row) => {
444
+ const appliedAt = format.columns.timestampType === "bigint" ? new Date(Number(row.applied_at)) : new Date(row.applied_at);
445
+ return {
446
+ id: row.id,
447
+ identifier: row.identifier,
448
+ // Set name or hash based on format
449
+ ...format.columns.identifier === "name" ? { name: row.identifier } : { hash: row.identifier },
450
+ appliedAt
451
+ };
452
+ });
453
+ }
454
+ /**
455
+ * Check if a migration has been applied
456
+ */
457
+ isMigrationApplied(migration, appliedIdentifiers, format) {
458
+ if (format.columns.identifier === "name") {
459
+ return appliedIdentifiers.has(migration.name);
460
+ }
461
+ return appliedIdentifiers.has(migration.hash) || appliedIdentifiers.has(migration.name);
462
+ }
463
+ /**
464
+ * Get or detect the format for a schema
465
+ * Returns the configured format or auto-detects from existing table
466
+ */
467
+ async getOrDetectFormat(pool, schemaName) {
468
+ const configuredFormat = this.migratorConfig.tableFormat ?? "auto";
469
+ if (configuredFormat !== "auto") {
470
+ return getFormatConfig(configuredFormat, this.migrationsTable);
471
+ }
472
+ const detected = await detectTableFormat(pool, schemaName, this.migrationsTable);
473
+ if (detected) {
474
+ return detected;
475
+ }
476
+ const defaultFormat = this.migratorConfig.defaultFormat ?? "name";
477
+ return getFormatConfig(defaultFormat, this.migrationsTable);
331
478
  }
332
479
  /**
333
480
  * Apply a migration to a schema
334
481
  */
335
- async applyMigration(pool, schemaName, migration) {
482
+ async applyMigration(pool, schemaName, migration, format) {
336
483
  const client = await pool.connect();
337
484
  try {
338
485
  await client.query("BEGIN");
339
486
  await client.query(migration.sql);
487
+ const { identifier, timestamp, timestampType } = format.columns;
488
+ const identifierValue = identifier === "name" ? migration.name : migration.hash;
489
+ const timestampValue = timestampType === "bigint" ? Date.now() : /* @__PURE__ */ new Date();
340
490
  await client.query(
341
- `INSERT INTO "${schemaName}"."${this.migrationsTable}" (name) VALUES ($1)`,
342
- [migration.name]
491
+ `INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
492
+ [identifierValue, timestampValue]
343
493
  );
344
494
  await client.query("COMMIT");
345
- } catch (error6) {
495
+ } catch (error2) {
346
496
  await client.query("ROLLBACK");
347
- throw error6;
497
+ throw error2;
348
498
  } finally {
349
499
  client.release();
350
500
  }
@@ -365,13 +515,13 @@ var Migrator = class {
365
515
  /**
366
516
  * Create an error result
367
517
  */
368
- createErrorResult(tenantId, error6) {
518
+ createErrorResult(tenantId, error2) {
369
519
  return {
370
520
  tenantId,
371
521
  schemaName: this.tenantConfig.isolation.schemaNameTemplate(tenantId),
372
522
  success: false,
373
523
  appliedMigrations: [],
374
- error: error6.message,
524
+ error: error2.message,
375
525
  durationMs: 0
376
526
  };
377
527
  }
@@ -397,35 +547,28 @@ function createSpinner(text) {
397
547
  color: "cyan"
398
548
  });
399
549
  }
400
- function success(message) {
401
- return chalk2.green("\u2713 ") + message;
402
- }
403
- function error(message) {
404
- return chalk2.red("\u2717 ") + message;
405
- }
406
- function warning(message) {
407
- return chalk2.yellow("\u26A0 ") + message;
408
- }
409
- function info(message) {
410
- return chalk2.blue("\u2139 ") + message;
411
- }
412
- function dim(message) {
413
- return chalk2.dim(message);
414
- }
415
- function bold(message) {
416
- return chalk2.bold(message);
417
- }
418
- function red(message) {
419
- return chalk2.red(message);
550
+ function getFormatText(format) {
551
+ if (format === null) {
552
+ return chalk.dim("(new)");
553
+ }
554
+ switch (format) {
555
+ case "name":
556
+ return chalk.blue("name");
557
+ case "hash":
558
+ return chalk.magenta("hash");
559
+ case "drizzle-kit":
560
+ return chalk.cyan("drizzle-kit");
561
+ }
420
562
  }
421
563
  function createStatusTable(statuses) {
422
564
  const table = new Table({
423
565
  head: [
424
- chalk2.cyan("Tenant"),
425
- chalk2.cyan("Schema"),
426
- chalk2.cyan("Applied"),
427
- chalk2.cyan("Pending"),
428
- chalk2.cyan("Status")
566
+ chalk.cyan("Tenant"),
567
+ chalk.cyan("Schema"),
568
+ chalk.cyan("Format"),
569
+ chalk.cyan("Applied"),
570
+ chalk.cyan("Pending"),
571
+ chalk.cyan("Status")
429
572
  ],
430
573
  style: {
431
574
  head: [],
@@ -437,9 +580,10 @@ function createStatusTable(statuses) {
437
580
  const statusText = getStatusText(status.status);
438
581
  table.push([
439
582
  status.tenantId,
440
- chalk2.dim(status.schemaName),
441
- chalk2.green(status.appliedCount.toString()),
442
- status.pendingCount > 0 ? chalk2.yellow(status.pendingCount.toString()) : chalk2.dim("0"),
583
+ chalk.dim(status.schemaName),
584
+ getFormatText(status.format),
585
+ chalk.green(status.appliedCount.toString()),
586
+ status.pendingCount > 0 ? chalk.yellow(status.pendingCount.toString()) : chalk.dim("0"),
443
587
  `${statusIcon} ${statusText}`
444
588
  ]);
445
589
  }
@@ -448,10 +592,11 @@ function createStatusTable(statuses) {
448
592
  function createResultsTable(results) {
449
593
  const table = new Table({
450
594
  head: [
451
- chalk2.cyan("Tenant"),
452
- chalk2.cyan("Migrations"),
453
- chalk2.cyan("Duration"),
454
- chalk2.cyan("Status")
595
+ chalk.cyan("Tenant"),
596
+ chalk.cyan("Format"),
597
+ chalk.cyan("Migrations"),
598
+ chalk.cyan("Duration"),
599
+ chalk.cyan("Status")
455
600
  ],
456
601
  style: {
457
602
  head: [],
@@ -459,10 +604,11 @@ function createResultsTable(results) {
459
604
  }
460
605
  });
461
606
  for (const result of results) {
462
- const statusIcon = result.success ? chalk2.green("\u2713") : chalk2.red("\u2717");
463
- const statusText = result.success ? chalk2.green("OK") : chalk2.red(result.error ?? "Failed");
607
+ const statusIcon = result.success ? chalk.green("\u2713") : chalk.red("\u2717");
608
+ const statusText = result.success ? chalk.green("OK") : chalk.red(result.error ?? "Failed");
464
609
  table.push([
465
610
  result.tenantId,
611
+ getFormatText(result.format ?? null),
466
612
  result.appliedMigrations.length.toString(),
467
613
  `${result.durationMs}ms`,
468
614
  `${statusIcon} ${statusText}`
@@ -478,37 +624,37 @@ function createPendingSummary(statuses) {
478
624
  }
479
625
  }
480
626
  if (pendingMap.size === 0) {
481
- return chalk2.green("\nAll tenants are up to date.");
627
+ return chalk.green("\nAll tenants are up to date.");
482
628
  }
483
- const lines = [chalk2.yellow("\nPending migrations:")];
629
+ const lines = [chalk.yellow("\nPending migrations:")];
484
630
  for (const [migration, count] of pendingMap.entries()) {
485
631
  lines.push(
486
- ` ${chalk2.dim("-")} ${migration} ${chalk2.dim(`(${count} tenant${count > 1 ? "s" : ""})`)}`
632
+ ` ${chalk.dim("-")} ${migration} ${chalk.dim(`(${count} tenant${count > 1 ? "s" : ""})`)}`
487
633
  );
488
634
  }
489
635
  lines.push(
490
- chalk2.dim("\nRun 'drizzle-multitenant migrate --all' to apply pending migrations.")
636
+ chalk.dim("\nRun 'drizzle-multitenant migrate --all' to apply pending migrations.")
491
637
  );
492
638
  return lines.join("\n");
493
639
  }
494
640
  function getStatusIcon(status) {
495
641
  switch (status) {
496
642
  case "ok":
497
- return chalk2.green("\u2713");
643
+ return chalk.green("\u2713");
498
644
  case "behind":
499
- return chalk2.yellow("\u26A0");
645
+ return chalk.yellow("\u26A0");
500
646
  case "error":
501
- return chalk2.red("\u2717");
647
+ return chalk.red("\u2717");
502
648
  }
503
649
  }
504
650
  function getStatusText(status) {
505
651
  switch (status) {
506
652
  case "ok":
507
- return chalk2.green("OK");
653
+ return chalk.green("OK");
508
654
  case "behind":
509
- return chalk2.yellow("Behind");
655
+ return chalk.yellow("Behind");
510
656
  case "error":
511
- return chalk2.red("Error");
657
+ return chalk.red("Error");
512
658
  }
513
659
  }
514
660
  var CONFIG_FILE_NAMES = [
@@ -582,122 +728,465 @@ function resolveMigrationsFolder(folder) {
582
728
  }
583
729
  return resolved;
584
730
  }
731
+ var globalContext = {
732
+ isInteractive: process.stdout.isTTY ?? false,
733
+ jsonMode: false,
734
+ verbose: false,
735
+ quiet: false,
736
+ noColor: false
737
+ };
738
+ function initOutputContext(options) {
739
+ globalContext = {
740
+ isInteractive: process.stdout.isTTY ?? false,
741
+ jsonMode: options.json ?? false,
742
+ verbose: options.verbose ?? false,
743
+ quiet: options.quiet ?? false,
744
+ noColor: options.noColor ?? !process.stdout.isTTY
745
+ };
746
+ if (globalContext.noColor) {
747
+ chalk.level = 0;
748
+ }
749
+ }
750
+ function getOutputContext() {
751
+ return globalContext;
752
+ }
753
+ function shouldShowInteractive() {
754
+ return globalContext.isInteractive && !globalContext.jsonMode && !globalContext.quiet;
755
+ }
756
+ function shouldShowLog() {
757
+ return !globalContext.jsonMode && !globalContext.quiet;
758
+ }
759
+ function shouldShowVerbose() {
760
+ return globalContext.verbose && !globalContext.jsonMode;
761
+ }
762
+ function log(message) {
763
+ if (shouldShowLog()) {
764
+ console.log(message);
765
+ }
766
+ }
767
+ function debug(message) {
768
+ if (shouldShowVerbose()) {
769
+ console.log(chalk.dim(`[debug] ${message}`));
770
+ }
771
+ }
772
+ function outputJson(data) {
773
+ if (globalContext.jsonMode) {
774
+ console.log(JSON.stringify(data, null, 2));
775
+ }
776
+ }
777
+ function success(message) {
778
+ return chalk.green("\u2713 ") + message;
779
+ }
780
+ function error(message) {
781
+ return chalk.red("\u2717 ") + message;
782
+ }
783
+ function warning(message) {
784
+ return chalk.yellow("\u26A0 ") + message;
785
+ }
786
+ function info(message) {
787
+ return chalk.blue("\u2139 ") + message;
788
+ }
789
+ function dim(message) {
790
+ return chalk.dim(message);
791
+ }
792
+ function bold(message) {
793
+ return chalk.bold(message);
794
+ }
795
+ function cyan(message) {
796
+ return chalk.cyan(message);
797
+ }
798
+ function green(message) {
799
+ return chalk.green(message);
800
+ }
801
+ function red(message) {
802
+ return chalk.red(message);
803
+ }
804
+ function createProgressBar(options) {
805
+ const { total } = options;
806
+ if (!shouldShowInteractive()) {
807
+ return {
808
+ start: () => {
809
+ },
810
+ update: () => {
811
+ },
812
+ increment: () => {
813
+ },
814
+ stop: () => {
815
+ }
816
+ };
817
+ }
818
+ const format = options.format || `${chalk.cyan("Migrating")} ${chalk.cyan("{bar}")} ${chalk.yellow("{percentage}%")} | {value}/{total} | {tenant} | ${dim("{eta}s")}`;
819
+ const bar = new cliProgress.SingleBar(
820
+ {
821
+ format,
822
+ barCompleteChar: "\u2588",
823
+ barIncompleteChar: "\u2591",
824
+ hideCursor: true,
825
+ clearOnComplete: false,
826
+ stopOnComplete: true,
827
+ etaBuffer: 10
828
+ },
829
+ cliProgress.Presets.shades_classic
830
+ );
831
+ let currentValue = 0;
832
+ return {
833
+ start() {
834
+ bar.start(total, 0, { tenant: "starting...", status: "" });
835
+ },
836
+ update(current, payload) {
837
+ currentValue = current;
838
+ const statusIcon = payload?.status === "success" ? green("\u2713") : payload?.status === "error" ? red("\u2717") : "";
839
+ bar.update(current, {
840
+ tenant: payload?.tenant ? `${statusIcon} ${payload.tenant}` : ""
841
+ });
842
+ },
843
+ increment(payload) {
844
+ currentValue++;
845
+ const statusIcon = payload?.status === "success" ? green("\u2713") : payload?.status === "error" ? red("\u2717") : "";
846
+ bar.update(currentValue, {
847
+ tenant: payload?.tenant ? `${statusIcon} ${payload.tenant}` : ""
848
+ });
849
+ },
850
+ stop() {
851
+ bar.stop();
852
+ }
853
+ };
854
+ }
855
+
856
+ // src/cli/utils/errors.ts
857
+ var CLIError = class extends Error {
858
+ constructor(message, suggestion, example, docs) {
859
+ super(message);
860
+ this.suggestion = suggestion;
861
+ this.example = example;
862
+ this.docs = docs;
863
+ this.name = "CLIError";
864
+ }
865
+ /**
866
+ * Format the error for display
867
+ */
868
+ format() {
869
+ const lines = [error(this.message)];
870
+ if (this.suggestion) {
871
+ lines.push("");
872
+ lines.push(dim(" Suggestion: ") + this.suggestion);
873
+ }
874
+ if (this.example) {
875
+ lines.push("");
876
+ lines.push(dim(" Example:"));
877
+ lines.push(cyan(" " + this.example));
878
+ }
879
+ if (this.docs) {
880
+ lines.push("");
881
+ lines.push(dim(" Docs: ") + this.docs);
882
+ }
883
+ return lines.join("\n");
884
+ }
885
+ /**
886
+ * Format as JSON for machine-readable output
887
+ */
888
+ toJSON() {
889
+ return {
890
+ error: this.message,
891
+ suggestion: this.suggestion,
892
+ example: this.example,
893
+ docs: this.docs
894
+ };
895
+ }
896
+ };
897
+ var CLIErrors = {
898
+ configNotFound: (searchPaths) => new CLIError(
899
+ "Configuration file not found",
900
+ "Create a tenant.config.ts file or use --config <path>",
901
+ `export default defineConfig({
902
+ connection: process.env.DATABASE_URL,
903
+ isolation: { type: 'schema', schemaNameTemplate: (id) => \`tenant_\${id}\` },
904
+ schemas: {
905
+ tenant: { ... },
906
+ },
907
+ })`,
908
+ searchPaths ? `Searched: ${searchPaths.join(", ")}` : void 0
909
+ ),
910
+ noTenantDiscovery: () => new CLIError(
911
+ "No tenant discovery function configured",
912
+ "Add a tenantDiscovery function to your config migrations settings",
913
+ `migrations: {
914
+ tenantDiscovery: async () => {
915
+ const tenants = await db.select().from(tenantsTable);
916
+ return tenants.map(t => t.id);
917
+ },
918
+ }`
919
+ ),
920
+ noTenantSpecified: () => new CLIError(
921
+ "No tenant specified",
922
+ "Use --all, --tenant <id>, or --tenants <ids> to specify which tenants to migrate",
923
+ `npx drizzle-multitenant migrate --all
924
+ npx drizzle-multitenant migrate --tenant=my-tenant
925
+ npx drizzle-multitenant migrate --tenants=tenant-1,tenant-2`
926
+ ),
927
+ tenantNotFound: (tenantId) => new CLIError(
928
+ `Tenant '${tenantId}' not found`,
929
+ "Check if the tenant exists in your database",
930
+ "npx drizzle-multitenant status"
931
+ ),
932
+ migrationsFolderNotFound: (path) => new CLIError(
933
+ `Migrations folder not found: ${path}`,
934
+ "Create the migrations folder or specify a different path with --migrations-folder",
935
+ `mkdir -p ${path}
936
+ npx drizzle-multitenant generate --name initial`
937
+ ),
938
+ invalidFormat: (format, validFormats) => new CLIError(
939
+ `Invalid format: '${format}'`,
940
+ `Valid formats are: ${validFormats.join(", ")}`,
941
+ `npx drizzle-multitenant convert-format --to=${validFormats[0]}`
942
+ ),
943
+ connectionFailed: (reason) => new CLIError(
944
+ `Database connection failed: ${reason}`,
945
+ "Check your DATABASE_URL and ensure the database is running",
946
+ 'export DATABASE_URL="postgresql://user:pass@localhost:5432/mydb"'
947
+ ),
948
+ migrationFailed: (tenantId, reason) => new CLIError(
949
+ `Migration failed for tenant '${tenantId}': ${reason}`,
950
+ "Check the migration SQL for syntax errors or constraint violations"
951
+ )
952
+ };
953
+ function handleError(err) {
954
+ const ctx = getOutputContext();
955
+ if (ctx.jsonMode) {
956
+ if (err instanceof CLIError) {
957
+ console.log(JSON.stringify(err.toJSON(), null, 2));
958
+ } else {
959
+ console.log(JSON.stringify({ error: err.message }, null, 2));
960
+ }
961
+ process.exit(1);
962
+ }
963
+ if (err instanceof CLIError) {
964
+ console.error(err.format());
965
+ } else {
966
+ console.error(error(err.message));
967
+ }
968
+ process.exit(1);
969
+ }
585
970
 
586
971
  // src/cli/commands/migrate.ts
587
- 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").action(async (options) => {
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", `
973
+ Examples:
974
+ $ drizzle-multitenant migrate --all
975
+ $ drizzle-multitenant migrate --tenant=my-tenant
976
+ $ drizzle-multitenant migrate --tenants=tenant-1,tenant-2
977
+ $ drizzle-multitenant migrate --all --dry-run
978
+ $ drizzle-multitenant migrate --all --concurrency=5
979
+ $ drizzle-multitenant migrate --all --json
980
+ `).action(async (options) => {
981
+ const startTime = Date.now();
982
+ const ctx = getOutputContext();
588
983
  const spinner = createSpinner("Loading configuration...");
589
984
  try {
590
985
  spinner.start();
591
986
  const { config, migrationsFolder, migrationsTable, tenantDiscovery } = await loadConfig(options.config);
592
987
  const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
988
+ debug(`Using migrations folder: ${folder}`);
593
989
  let discoveryFn;
990
+ let tenantIds;
594
991
  if (options.tenant) {
595
992
  discoveryFn = async () => [options.tenant];
993
+ tenantIds = [options.tenant];
596
994
  } else if (options.tenants) {
597
- const tenantIds2 = options.tenants.split(",").map((id) => id.trim());
598
- discoveryFn = async () => tenantIds2;
995
+ const ids = options.tenants.split(",").map((id) => id.trim());
996
+ discoveryFn = async () => ids;
997
+ tenantIds = ids;
599
998
  } else if (options.all) {
600
999
  if (!tenantDiscovery) {
601
- throw new Error(
602
- "No tenant discovery function configured. Add migrations.tenantDiscovery to your config."
603
- );
1000
+ throw CLIErrors.noTenantDiscovery();
604
1001
  }
605
1002
  discoveryFn = tenantDiscovery;
1003
+ spinner.text = "Discovering tenants...";
1004
+ tenantIds = await tenantDiscovery();
606
1005
  } else {
607
1006
  spinner.stop();
608
- console.log(error("Please specify --all, --tenant, or --tenants"));
609
- console.log(dim("\nExamples:"));
610
- console.log(dim(" npx drizzle-multitenant migrate --all"));
611
- console.log(dim(" npx drizzle-multitenant migrate --tenant=tenant-uuid"));
612
- console.log(dim(" npx drizzle-multitenant migrate --tenants=tenant-1,tenant-2"));
613
- process.exit(1);
1007
+ if (shouldShowInteractive() && tenantDiscovery) {
1008
+ log(info("No tenants specified. Fetching available tenants...\n"));
1009
+ const availableTenants = await tenantDiscovery();
1010
+ if (availableTenants.length === 0) {
1011
+ log(warning("No tenants found."));
1012
+ return;
1013
+ }
1014
+ const selectedTenants = await checkbox({
1015
+ message: "Select tenants to migrate:",
1016
+ choices: availableTenants.map((id) => ({ name: id, value: id })),
1017
+ pageSize: 15
1018
+ });
1019
+ if (selectedTenants.length === 0) {
1020
+ log(warning("No tenants selected. Aborting."));
1021
+ return;
1022
+ }
1023
+ discoveryFn = async () => selectedTenants;
1024
+ tenantIds = selectedTenants;
1025
+ } else {
1026
+ throw CLIErrors.noTenantSpecified();
1027
+ }
614
1028
  }
615
- spinner.text = "Discovering tenants...";
616
- const migrator = createMigrator(config, {
617
- migrationsFolder: folder,
618
- migrationsTable,
619
- tenantDiscovery: discoveryFn
620
- });
621
- const tenantIds = await discoveryFn();
622
1029
  if (tenantIds.length === 0) {
623
1030
  spinner.stop();
624
- console.log(warning("No tenants found."));
1031
+ log(warning("No tenants found."));
625
1032
  return;
626
1033
  }
627
1034
  spinner.text = `Found ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}`;
628
1035
  spinner.succeed();
629
1036
  if (options.dryRun) {
630
- console.log(info(bold("\nDry run mode - no changes will be made\n")));
1037
+ log(info(bold("\nDry run mode - no changes will be made\n")));
631
1038
  }
632
- console.log(info(`Migrating ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}...
1039
+ log(info(`Migrating ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}...
633
1040
  `));
634
- const concurrency = parseInt(options.concurrency, 10);
635
- let completed = 0;
1041
+ const migrator = createMigrator(config, {
1042
+ migrationsFolder: folder,
1043
+ ...migrationsTable && { migrationsTable },
1044
+ tenantDiscovery: discoveryFn
1045
+ });
1046
+ const concurrency = parseInt(options.concurrency || "10", 10);
1047
+ const progressBar = createProgressBar({ total: tenantIds.length });
1048
+ progressBar.start();
636
1049
  const results = await migrator.migrateAll({
637
1050
  concurrency,
638
- dryRun: options.dryRun,
1051
+ dryRun: !!options.dryRun,
639
1052
  onProgress: (tenantId, status, migrationName) => {
640
1053
  if (status === "completed") {
641
- completed++;
642
- const progress = `[${completed}/${tenantIds.length}]`;
643
- console.log(`${dim(progress)} ${success(tenantId)}`);
1054
+ progressBar.increment({ tenant: tenantId, status: "success" });
1055
+ debug(`Completed: ${tenantId}`);
644
1056
  } else if (status === "failed") {
645
- completed++;
646
- const progress = `[${completed}/${tenantIds.length}]`;
647
- console.log(`${dim(progress)} ${error(tenantId)}`);
1057
+ progressBar.increment({ tenant: tenantId, status: "error" });
1058
+ debug(`Failed: ${tenantId}`);
648
1059
  } else if (status === "migrating" && migrationName) {
1060
+ debug(`${tenantId}: Applying ${migrationName}`);
649
1061
  }
650
1062
  },
651
1063
  onError: (tenantId, err) => {
652
- console.log(error(`${tenantId}: ${err.message}`));
1064
+ debug(`Error on ${tenantId}: ${err.message}`);
653
1065
  return "continue";
654
1066
  }
655
1067
  });
656
- console.log("\n" + bold("Results:"));
657
- console.log(createResultsTable(results.details));
658
- console.log("\n" + bold("Summary:"));
659
- console.log(` Total: ${results.total}`);
660
- console.log(` Succeeded: ${success(results.succeeded.toString())}`);
1068
+ progressBar.stop();
1069
+ const totalDuration = Date.now() - startTime;
1070
+ if (ctx.jsonMode) {
1071
+ const jsonOutput = {
1072
+ results: results.details.map((r) => ({
1073
+ tenantId: r.tenantId,
1074
+ schema: r.schemaName,
1075
+ success: r.success,
1076
+ appliedMigrations: r.appliedMigrations,
1077
+ durationMs: r.durationMs,
1078
+ format: r.format,
1079
+ error: r.error
1080
+ })),
1081
+ summary: {
1082
+ total: results.total,
1083
+ succeeded: results.succeeded,
1084
+ failed: results.failed,
1085
+ skipped: results.skipped,
1086
+ durationMs: totalDuration,
1087
+ averageMs: results.total > 0 ? Math.round(totalDuration / results.total) : void 0
1088
+ }
1089
+ };
1090
+ outputJson(jsonOutput);
1091
+ process.exit(results.failed > 0 ? 1 : 0);
1092
+ }
1093
+ log("\n" + bold("Results:"));
1094
+ log(createResultsTable(results.details));
1095
+ log("\n" + bold("Summary:"));
1096
+ log(` Total: ${results.total}`);
1097
+ log(` Succeeded: ${success(results.succeeded.toString())}`);
661
1098
  if (results.failed > 0) {
662
- console.log(` Failed: ${error(results.failed.toString())}`);
1099
+ log(` Failed: ${error(results.failed.toString())}`);
663
1100
  }
664
1101
  if (results.skipped > 0) {
665
- console.log(` Skipped: ${warning(results.skipped.toString())}`);
1102
+ log(` Skipped: ${warning(results.skipped.toString())}`);
1103
+ }
1104
+ log(` Duration: ${dim(formatDuration(totalDuration))}`);
1105
+ if (results.total > 0) {
1106
+ log(` Average: ${dim(formatDuration(Math.round(totalDuration / results.total)) + "/tenant")}`);
1107
+ }
1108
+ if (results.failed > 0) {
1109
+ log("\n" + bold("Failed tenants:"));
1110
+ for (const detail of results.details.filter((d) => !d.success)) {
1111
+ log(` ${error(detail.tenantId)}: ${dim(detail.error || "Unknown error")}`);
1112
+ }
1113
+ log("\n" + dim("Run with --verbose to see more details."));
666
1114
  }
667
1115
  if (results.failed > 0) {
668
1116
  process.exit(1);
669
1117
  }
670
1118
  } catch (err) {
671
1119
  spinner.fail(err.message);
672
- process.exit(1);
1120
+ handleError(err);
673
1121
  }
674
1122
  });
675
- var statusCommand = new Command("status").description("Show migration status for all tenants").option("-c, --config <path>", "Path to config file").option("--migrations-folder <path>", "Path to migrations folder").action(async (options) => {
1123
+ function formatDuration(ms) {
1124
+ if (ms < 1e3) {
1125
+ return `${ms}ms`;
1126
+ }
1127
+ if (ms < 6e4) {
1128
+ return `${(ms / 1e3).toFixed(1)}s`;
1129
+ }
1130
+ const mins = Math.floor(ms / 6e4);
1131
+ const secs = Math.round(ms % 6e4 / 1e3);
1132
+ return `${mins}m ${secs}s`;
1133
+ }
1134
+ var statusCommand = new Command("status").description("Show migration status for all tenants").option("-c, --config <path>", "Path to config file").option("--migrations-folder <path>", "Path to migrations folder").addHelpText("after", `
1135
+ Examples:
1136
+ $ drizzle-multitenant status
1137
+ $ drizzle-multitenant status --json
1138
+ $ drizzle-multitenant status --json | jq '.tenants[] | select(.pending > 0)'
1139
+ $ drizzle-multitenant status --verbose
1140
+ `).action(async (options) => {
1141
+ const ctx = getOutputContext();
676
1142
  const spinner = createSpinner("Loading configuration...");
677
1143
  try {
678
1144
  spinner.start();
679
1145
  const { config, migrationsFolder, migrationsTable, tenantDiscovery } = await loadConfig(options.config);
680
1146
  if (!tenantDiscovery) {
681
- throw new Error(
682
- "No tenant discovery function configured. Add migrations.tenantDiscovery to your config."
683
- );
1147
+ throw CLIErrors.noTenantDiscovery();
684
1148
  }
685
1149
  const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
1150
+ debug(`Using migrations folder: ${folder}`);
686
1151
  spinner.text = "Discovering tenants...";
687
1152
  const migrator = createMigrator(config, {
688
1153
  migrationsFolder: folder,
689
- migrationsTable,
1154
+ ...migrationsTable && { migrationsTable },
690
1155
  tenantDiscovery
691
1156
  });
692
1157
  spinner.text = "Fetching migration status...";
693
1158
  const statuses = await migrator.getStatus();
694
1159
  spinner.succeed(`Found ${statuses.length} tenant${statuses.length > 1 ? "s" : ""}`);
695
- console.log("\n" + bold("Migration Status:"));
696
- console.log(createStatusTable(statuses));
697
- console.log(createPendingSummary(statuses));
1160
+ const summary = {
1161
+ total: statuses.length,
1162
+ upToDate: statuses.filter((s) => s.status === "ok").length,
1163
+ behind: statuses.filter((s) => s.status === "behind").length,
1164
+ error: statuses.filter((s) => s.status === "error").length
1165
+ };
1166
+ debug(`Summary: ${summary.upToDate} up-to-date, ${summary.behind} behind, ${summary.error} errors`);
1167
+ if (ctx.jsonMode) {
1168
+ const jsonOutput = {
1169
+ tenants: statuses.map((s) => ({
1170
+ id: s.tenantId,
1171
+ schema: s.schemaName,
1172
+ format: s.format,
1173
+ applied: s.appliedCount,
1174
+ pending: s.pendingCount,
1175
+ status: s.status,
1176
+ pendingMigrations: s.pendingMigrations,
1177
+ error: s.error
1178
+ })),
1179
+ summary
1180
+ };
1181
+ outputJson(jsonOutput);
1182
+ return;
1183
+ }
1184
+ log("\n" + bold("Migration Status:"));
1185
+ log(createStatusTable(statuses));
1186
+ log(createPendingSummary(statuses));
698
1187
  } catch (err) {
699
1188
  spinner.fail(err.message);
700
- process.exit(1);
1189
+ handleError(err);
701
1190
  }
702
1191
  });
703
1192
  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) => {
@@ -724,7 +1213,7 @@ var generateCommand = new Command("generate").description("Generate a new migrat
724
1213
  let maxSequence = 0;
725
1214
  for (const file of sqlFiles) {
726
1215
  const match = file.match(/^(\d+)_/);
727
- if (match) {
1216
+ if (match?.[1]) {
728
1217
  const seq = parseInt(match[1], 10);
729
1218
  if (seq > maxSequence) {
730
1219
  maxSequence = seq;
@@ -759,7 +1248,7 @@ var tenantCreateCommand = new Command("tenant:create").description("Create a new
759
1248
  const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
760
1249
  const migrator = createMigrator(config, {
761
1250
  migrationsFolder: folder,
762
- migrationsTable,
1251
+ ...migrationsTable && { migrationsTable },
763
1252
  tenantDiscovery: async () => []
764
1253
  });
765
1254
  const schemaName = config.isolation.schemaNameTemplate(options.id);
@@ -808,7 +1297,7 @@ var tenantDropCommand = new Command("tenant:drop").description("Drop a tenant sc
808
1297
  const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
809
1298
  const migrator = createMigrator(config, {
810
1299
  migrationsFolder: folder,
811
- migrationsTable,
1300
+ ...migrationsTable && { migrationsTable },
812
1301
  tenantDiscovery: async () => []
813
1302
  });
814
1303
  const schemaName = config.isolation.schemaNameTemplate(options.id);
@@ -857,15 +1346,732 @@ async function askConfirmation(question, expected) {
857
1346
  });
858
1347
  });
859
1348
  }
1349
+ async function loadMigrationFiles(folder) {
1350
+ const files = await readdir(folder);
1351
+ const migrations = [];
1352
+ for (const file of files) {
1353
+ if (!file.endsWith(".sql")) continue;
1354
+ const filePath = join(folder, file);
1355
+ const content = await readFile(filePath, "utf-8");
1356
+ const hash = createHash("sha256").update(content).digest("hex");
1357
+ migrations.push({
1358
+ name: basename(file, ".sql"),
1359
+ hash
1360
+ });
1361
+ }
1362
+ return migrations;
1363
+ }
1364
+ async function convertTenantFormat(pool, schemaName, tableName, migrations, currentFormat, targetFormat, dryRun) {
1365
+ const hashToName = new Map(migrations.map((m) => [m.hash, m.name]));
1366
+ const nameToHash = new Map(migrations.map((m) => [m.name, m.hash]));
1367
+ const client = await pool.connect();
1368
+ try {
1369
+ const identifierCol = currentFormat.columns.identifier;
1370
+ const current = await client.query(
1371
+ `SELECT id, "${identifierCol}" as identifier FROM "${schemaName}"."${tableName}" ORDER BY id`
1372
+ );
1373
+ if (dryRun) {
1374
+ const conversions = [];
1375
+ for (const row of current.rows) {
1376
+ if (targetFormat === "name" && currentFormat.columns.identifier === "hash") {
1377
+ const name = hashToName.get(row.identifier);
1378
+ if (name) {
1379
+ conversions.push(` ${dim(row.identifier.slice(0, 8))}... -> ${name}`);
1380
+ } else {
1381
+ conversions.push(` ${warning(row.identifier.slice(0, 8))}... -> ${error("unknown")}`);
1382
+ }
1383
+ } else if (targetFormat !== "name" && currentFormat.columns.identifier === "name") {
1384
+ const hash = nameToHash.get(row.identifier);
1385
+ if (hash) {
1386
+ conversions.push(` ${row.identifier} -> ${dim(hash.slice(0, 16))}...`);
1387
+ } else {
1388
+ conversions.push(` ${row.identifier} -> ${error("unknown")}`);
1389
+ }
1390
+ }
1391
+ }
1392
+ return {
1393
+ success: true,
1394
+ message: conversions.length > 0 ? conversions.join("\n") : " No conversions needed"
1395
+ };
1396
+ }
1397
+ await client.query("BEGIN");
1398
+ try {
1399
+ if (targetFormat === "name" && currentFormat.columns.identifier === "hash") {
1400
+ await client.query(`
1401
+ ALTER TABLE "${schemaName}"."${tableName}"
1402
+ ADD COLUMN IF NOT EXISTS name VARCHAR(255)
1403
+ `);
1404
+ let converted = 0;
1405
+ for (const row of current.rows) {
1406
+ const name = hashToName.get(row.identifier);
1407
+ if (name) {
1408
+ await client.query(
1409
+ `UPDATE "${schemaName}"."${tableName}" SET name = $1 WHERE id = $2`,
1410
+ [name, row.id]
1411
+ );
1412
+ converted++;
1413
+ }
1414
+ }
1415
+ await client.query(`
1416
+ ALTER TABLE "${schemaName}"."${tableName}"
1417
+ ALTER COLUMN name SET NOT NULL
1418
+ `);
1419
+ await client.query(`
1420
+ DO $$
1421
+ BEGIN
1422
+ IF NOT EXISTS (
1423
+ SELECT 1 FROM pg_constraint
1424
+ WHERE conname = '${tableName}_name_unique'
1425
+ ) THEN
1426
+ ALTER TABLE "${schemaName}"."${tableName}"
1427
+ ADD CONSTRAINT ${tableName}_name_unique UNIQUE (name);
1428
+ END IF;
1429
+ END $$;
1430
+ `);
1431
+ if (currentFormat.columns.timestamp === "created_at") {
1432
+ await client.query(`
1433
+ ALTER TABLE "${schemaName}"."${tableName}"
1434
+ RENAME COLUMN created_at TO applied_at
1435
+ `);
1436
+ if (currentFormat.columns.timestampType === "bigint") {
1437
+ await client.query(`
1438
+ ALTER TABLE "${schemaName}"."${tableName}"
1439
+ ALTER COLUMN applied_at TYPE TIMESTAMP WITH TIME ZONE
1440
+ USING to_timestamp(applied_at / 1000.0)
1441
+ `);
1442
+ }
1443
+ }
1444
+ await client.query("COMMIT");
1445
+ return { success: true, message: `Converted ${converted} records to name format` };
1446
+ } else if (targetFormat !== "name" && currentFormat.columns.identifier === "name") {
1447
+ await client.query(`
1448
+ ALTER TABLE "${schemaName}"."${tableName}"
1449
+ ADD COLUMN IF NOT EXISTS hash TEXT
1450
+ `);
1451
+ let converted = 0;
1452
+ for (const row of current.rows) {
1453
+ const hash = nameToHash.get(row.identifier);
1454
+ if (hash) {
1455
+ await client.query(
1456
+ `UPDATE "${schemaName}"."${tableName}" SET hash = $1 WHERE id = $2`,
1457
+ [hash, row.id]
1458
+ );
1459
+ converted++;
1460
+ }
1461
+ }
1462
+ await client.query(`
1463
+ ALTER TABLE "${schemaName}"."${tableName}"
1464
+ ALTER COLUMN hash SET NOT NULL
1465
+ `);
1466
+ if (currentFormat.columns.timestamp === "applied_at") {
1467
+ await client.query(`
1468
+ ALTER TABLE "${schemaName}"."${tableName}"
1469
+ RENAME COLUMN applied_at TO created_at
1470
+ `);
1471
+ if (targetFormat === "drizzle-kit") {
1472
+ await client.query(`
1473
+ ALTER TABLE "${schemaName}"."${tableName}"
1474
+ ALTER COLUMN created_at TYPE BIGINT
1475
+ USING (EXTRACT(EPOCH FROM created_at) * 1000)::BIGINT
1476
+ `);
1477
+ }
1478
+ }
1479
+ await client.query("COMMIT");
1480
+ return { success: true, message: `Converted ${converted} records to ${targetFormat} format` };
1481
+ }
1482
+ await client.query("COMMIT");
1483
+ return { success: true, message: "No conversion needed" };
1484
+ } catch (err) {
1485
+ await client.query("ROLLBACK");
1486
+ throw err;
1487
+ }
1488
+ } finally {
1489
+ client.release();
1490
+ }
1491
+ }
1492
+ var convertFormatCommand = new Command("convert-format").description("Convert migration table format between name/hash/drizzle-kit").requiredOption("--to <format>", "Target format: name, hash, or drizzle-kit").option("-c, --config <path>", "Path to config file").option("-t, --tenant <id>", "Convert a specific tenant only").option("--dry-run", "Preview changes without applying").option("--migrations-folder <path>", "Path to migrations folder").action(async (options) => {
1493
+ const spinner = createSpinner("Loading configuration...");
1494
+ try {
1495
+ const targetFormat = options.to;
1496
+ if (!["name", "hash", "drizzle-kit"].includes(targetFormat)) {
1497
+ throw new Error(`Invalid format: ${options.to}. Use: name, hash, or drizzle-kit`);
1498
+ }
1499
+ spinner.start();
1500
+ const { config, migrationsFolder, migrationsTable, tenantDiscovery } = await loadConfig(options.config);
1501
+ const tableName = migrationsTable ?? "__drizzle_migrations";
1502
+ const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
1503
+ spinner.text = "Loading migration files...";
1504
+ const migrations = await loadMigrationFiles(folder);
1505
+ spinner.text = "Discovering tenants...";
1506
+ let tenantIds;
1507
+ if (options.tenant) {
1508
+ tenantIds = [options.tenant];
1509
+ } else {
1510
+ if (!tenantDiscovery) {
1511
+ throw new Error(
1512
+ "No tenant discovery function configured. Add migrations.tenantDiscovery to your config."
1513
+ );
1514
+ }
1515
+ tenantIds = await tenantDiscovery();
1516
+ }
1517
+ spinner.succeed(`Found ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}`);
1518
+ if (options.dryRun) {
1519
+ console.log(warning(bold("\nDry run mode - no changes will be made\n")));
1520
+ }
1521
+ console.log(bold(`
1522
+ Converting to ${targetFormat} format:
1523
+ `));
1524
+ let successCount = 0;
1525
+ let skipCount = 0;
1526
+ let failCount = 0;
1527
+ for (const tenantId of tenantIds) {
1528
+ const schemaName = config.isolation.schemaNameTemplate(tenantId);
1529
+ const pool = new Pool({
1530
+ connectionString: config.connection.url,
1531
+ ...config.connection.poolConfig
1532
+ });
1533
+ try {
1534
+ const currentFormat = await detectTableFormat(pool, schemaName, tableName);
1535
+ if (!currentFormat) {
1536
+ console.log(`${dim(tenantId)}: ${dim("No migrations table found, skipping")}`);
1537
+ skipCount++;
1538
+ continue;
1539
+ }
1540
+ if (currentFormat.format === targetFormat) {
1541
+ console.log(`${dim(tenantId)}: ${dim(`Already using ${targetFormat} format`)}`);
1542
+ skipCount++;
1543
+ continue;
1544
+ }
1545
+ console.log(`${bold(tenantId)}: ${currentFormat.format} -> ${targetFormat}`);
1546
+ const result = await convertTenantFormat(
1547
+ pool,
1548
+ schemaName,
1549
+ tableName,
1550
+ migrations,
1551
+ currentFormat,
1552
+ targetFormat,
1553
+ options.dryRun
1554
+ );
1555
+ if (result.success) {
1556
+ if (options.dryRun) {
1557
+ console.log(result.message);
1558
+ } else {
1559
+ console.log(` ${success(result.message)}`);
1560
+ }
1561
+ successCount++;
1562
+ } else {
1563
+ console.log(` ${error(result.message)}`);
1564
+ failCount++;
1565
+ }
1566
+ } catch (err) {
1567
+ console.log(` ${error(err.message)}`);
1568
+ failCount++;
1569
+ } finally {
1570
+ await pool.end();
1571
+ }
1572
+ }
1573
+ console.log(bold("\nSummary:"));
1574
+ console.log(` Converted: ${success(successCount.toString())}`);
1575
+ console.log(` Skipped: ${dim(skipCount.toString())}`);
1576
+ if (failCount > 0) {
1577
+ console.log(` Failed: ${error(failCount.toString())}`);
1578
+ process.exit(1);
1579
+ }
1580
+ } catch (err) {
1581
+ spinner.fail(err.message);
1582
+ process.exit(1);
1583
+ }
1584
+ });
1585
+ var initCommand = new Command("init").description("Initialize a new drizzle-multitenant configuration").option("--force", "Overwrite existing configuration").addHelpText("after", `
1586
+ Examples:
1587
+ $ drizzle-multitenant init
1588
+ $ drizzle-multitenant init --force
1589
+ `).action(async (options) => {
1590
+ try {
1591
+ if (!shouldShowInteractive()) {
1592
+ log(warning("Interactive mode required. Please run in a terminal."));
1593
+ process.exit(1);
1594
+ }
1595
+ log(bold("\n\u{1F680} drizzle-multitenant Setup Wizard\n"));
1596
+ const configFiles = [
1597
+ "tenant.config.ts",
1598
+ "tenant.config.js",
1599
+ "drizzle-multitenant.config.ts",
1600
+ "drizzle-multitenant.config.js"
1601
+ ];
1602
+ const existingConfig = configFiles.find((f) => existsSync(join(process.cwd(), f)));
1603
+ if (existingConfig && !options.force) {
1604
+ log(warning(`Configuration file already exists: ${existingConfig}`));
1605
+ const overwrite = await confirm({
1606
+ message: "Do you want to overwrite it?",
1607
+ default: false
1608
+ });
1609
+ if (!overwrite) {
1610
+ log(info("Setup cancelled."));
1611
+ return;
1612
+ }
1613
+ }
1614
+ const isolationType = await select({
1615
+ message: "Which isolation strategy do you want to use?",
1616
+ choices: [
1617
+ {
1618
+ name: "Schema-based isolation (recommended)",
1619
+ value: "schema",
1620
+ description: "Each tenant has its own PostgreSQL schema"
1621
+ },
1622
+ {
1623
+ name: "Row-level security (RLS)",
1624
+ value: "rls",
1625
+ description: "Shared tables with tenant_id column and RLS policies"
1626
+ }
1627
+ ]
1628
+ });
1629
+ const dbEnvVar = await input({
1630
+ message: "Environment variable for database connection:",
1631
+ default: "DATABASE_URL"
1632
+ });
1633
+ const migrationsFolder = await input({
1634
+ message: "Migrations folder path:",
1635
+ default: "./drizzle/tenant-migrations"
1636
+ });
1637
+ let schemaTemplate = "tenant_${id}";
1638
+ if (isolationType === "schema") {
1639
+ schemaTemplate = await input({
1640
+ message: "Schema name template (use ${id} for tenant ID):",
1641
+ default: "tenant_${id}"
1642
+ });
1643
+ }
1644
+ const useTypeScript = await confirm({
1645
+ message: "Use TypeScript for configuration?",
1646
+ default: true
1647
+ });
1648
+ const configContent = generateConfigContent({
1649
+ isolationType,
1650
+ dbEnvVar,
1651
+ migrationsFolder,
1652
+ schemaTemplate,
1653
+ useTypeScript
1654
+ });
1655
+ const configFileName = useTypeScript ? "tenant.config.ts" : "tenant.config.js";
1656
+ const configPath = join(process.cwd(), configFileName);
1657
+ writeFileSync(configPath, configContent);
1658
+ log(success(`Created ${configFileName}`));
1659
+ const fullMigrationsPath = join(process.cwd(), migrationsFolder);
1660
+ if (!existsSync(fullMigrationsPath)) {
1661
+ mkdirSync(fullMigrationsPath, { recursive: true });
1662
+ log(success(`Created migrations folder: ${migrationsFolder}`));
1663
+ }
1664
+ const gitkeepPath = join(fullMigrationsPath, ".gitkeep");
1665
+ if (!existsSync(gitkeepPath)) {
1666
+ writeFileSync(gitkeepPath, "");
1667
+ }
1668
+ log("\n" + bold("\u2728 Setup complete!\n"));
1669
+ log("Next steps:\n");
1670
+ log(dim("1. Update your schema definitions in the config file"));
1671
+ log(dim("2. Set up tenant discovery function"));
1672
+ log(dim("3. Generate your first migration:"));
1673
+ log(cyan(` npx drizzle-multitenant generate --name initial`));
1674
+ log(dim("4. Create a tenant:"));
1675
+ log(cyan(` npx drizzle-multitenant tenant:create --id my-first-tenant`));
1676
+ log(dim("5. Check status:"));
1677
+ log(cyan(` npx drizzle-multitenant status`));
1678
+ log("");
1679
+ } catch (err) {
1680
+ handleError(err);
1681
+ }
1682
+ });
1683
+ function generateConfigContent(options) {
1684
+ const { isolationType, dbEnvVar, migrationsFolder, schemaTemplate, useTypeScript } = options;
1685
+ if (useTypeScript) {
1686
+ return `import { defineConfig } from 'drizzle-multitenant';
1687
+ import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
1688
+
1689
+ // Example tenant schema - customize this for your needs
1690
+ const users = pgTable('users', {
1691
+ id: uuid('id').primaryKey().defaultRandom(),
1692
+ email: text('email').notNull().unique(),
1693
+ name: text('name'),
1694
+ createdAt: timestamp('created_at').defaultNow().notNull(),
1695
+ });
1696
+
1697
+ export default defineConfig({
1698
+ // Database connection
1699
+ connection: process.env.${dbEnvVar}!,
1700
+
1701
+ // Isolation strategy
1702
+ isolation: {
1703
+ type: '${isolationType}',
1704
+ ${isolationType === "schema" ? `schemaNameTemplate: (id) => \`${schemaTemplate.replace("${id}", "${id}")}\`,` : ""}
1705
+ },
1706
+
1707
+ // Schema definitions
1708
+ schemas: {
1709
+ tenant: {
1710
+ users,
1711
+ // Add more tables here...
1712
+ },
1713
+ },
1714
+
1715
+ // Migration settings
1716
+ migrations: {
1717
+ folder: '${migrationsFolder}',
1718
+ table: '__drizzle_migrations',
1719
+
1720
+ // Tenant discovery function - customize this!
1721
+ // This should return an array of tenant IDs from your database
1722
+ tenantDiscovery: async () => {
1723
+ // Example: Query your tenants table
1724
+ // const tenants = await db.select().from(tenantsTable);
1725
+ // return tenants.map(t => t.id);
1726
+
1727
+ // For now, return empty array - update this!
1728
+ console.warn('\u26A0\uFE0F tenantDiscovery not configured - returning empty array');
1729
+ return [];
1730
+ },
1731
+ },
1732
+ });
1733
+ `;
1734
+ }
1735
+ return `// @ts-check
1736
+ const { defineConfig } = require('drizzle-multitenant');
1737
+ const { pgTable, text, timestamp, uuid } = require('drizzle-orm/pg-core');
1738
+
1739
+ // Example tenant schema - customize this for your needs
1740
+ const users = pgTable('users', {
1741
+ id: uuid('id').primaryKey().defaultRandom(),
1742
+ email: text('email').notNull().unique(),
1743
+ name: text('name'),
1744
+ createdAt: timestamp('created_at').defaultNow().notNull(),
1745
+ });
1746
+
1747
+ module.exports = defineConfig({
1748
+ // Database connection
1749
+ connection: process.env.${dbEnvVar},
1750
+
1751
+ // Isolation strategy
1752
+ isolation: {
1753
+ type: '${isolationType}',
1754
+ ${isolationType === "schema" ? `schemaNameTemplate: (id) => \`${schemaTemplate.replace("${id}", "${id}")}\`,` : ""}
1755
+ },
1756
+
1757
+ // Schema definitions
1758
+ schemas: {
1759
+ tenant: {
1760
+ users,
1761
+ // Add more tables here...
1762
+ },
1763
+ },
1764
+
1765
+ // Migration settings
1766
+ migrations: {
1767
+ folder: '${migrationsFolder}',
1768
+ table: '__drizzle_migrations',
1769
+
1770
+ // Tenant discovery function - customize this!
1771
+ tenantDiscovery: async () => {
1772
+ // Example: Query your tenants table
1773
+ // For now, return empty array - update this!
1774
+ console.warn('\u26A0\uFE0F tenantDiscovery not configured - returning empty array');
1775
+ return [];
1776
+ },
1777
+ },
1778
+ });
1779
+ `;
1780
+ }
1781
+ var completionCommand = new Command("completion").description("Generate shell completion scripts").argument("<shell>", "Shell type: bash, zsh, or fish").addHelpText("after", `
1782
+ Examples:
1783
+ $ drizzle-multitenant completion bash >> ~/.bashrc
1784
+ $ drizzle-multitenant completion zsh >> ~/.zshrc
1785
+ $ drizzle-multitenant completion fish > ~/.config/fish/completions/drizzle-multitenant.fish
1786
+
1787
+ After adding the completion script, restart your shell or run:
1788
+ $ source ~/.bashrc # for bash
1789
+ $ source ~/.zshrc # for zsh
1790
+ `).action((shell) => {
1791
+ const shellLower = shell.toLowerCase();
1792
+ switch (shellLower) {
1793
+ case "bash":
1794
+ console.log(generateBashCompletion());
1795
+ break;
1796
+ case "zsh":
1797
+ console.log(generateZshCompletion());
1798
+ break;
1799
+ case "fish":
1800
+ console.log(generateFishCompletion());
1801
+ break;
1802
+ default:
1803
+ log(warning(`Unknown shell: ${shell}`));
1804
+ log(info("Supported shells: bash, zsh, fish"));
1805
+ process.exit(1);
1806
+ }
1807
+ });
1808
+ function generateBashCompletion() {
1809
+ return `# drizzle-multitenant bash completion
1810
+ # Add this to ~/.bashrc or ~/.bash_completion
1811
+
1812
+ _drizzle_multitenant() {
1813
+ local cur prev opts commands
1814
+ COMPREPLY=()
1815
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1816
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
1817
+
1818
+ commands="migrate status generate tenant:create tenant:drop convert-format init completion"
1819
+
1820
+ global_opts="--json --verbose --quiet --no-color --help --version"
1821
+
1822
+ migrate_opts="-c --config -a --all -t --tenant --tenants --concurrency --dry-run --migrations-folder"
1823
+ status_opts="-c --config --migrations-folder"
1824
+ generate_opts="-n --name -c --config --type --migrations-folder"
1825
+ tenant_create_opts="--id -c --config --migrations-folder --no-migrate"
1826
+ tenant_drop_opts="--id -c --config --migrations-folder -f --force --no-cascade"
1827
+ convert_opts="--to -c --config -t --tenant --dry-run --migrations-folder"
1828
+
1829
+ case "\${COMP_WORDS[1]}" in
1830
+ migrate)
1831
+ COMPREPLY=( $(compgen -W "\${migrate_opts} \${global_opts}" -- \${cur}) )
1832
+ return 0
1833
+ ;;
1834
+ status)
1835
+ COMPREPLY=( $(compgen -W "\${status_opts} \${global_opts}" -- \${cur}) )
1836
+ return 0
1837
+ ;;
1838
+ generate)
1839
+ COMPREPLY=( $(compgen -W "\${generate_opts} \${global_opts}" -- \${cur}) )
1840
+ return 0
1841
+ ;;
1842
+ tenant:create)
1843
+ COMPREPLY=( $(compgen -W "\${tenant_create_opts} \${global_opts}" -- \${cur}) )
1844
+ return 0
1845
+ ;;
1846
+ tenant:drop)
1847
+ COMPREPLY=( $(compgen -W "\${tenant_drop_opts} \${global_opts}" -- \${cur}) )
1848
+ return 0
1849
+ ;;
1850
+ convert-format)
1851
+ COMPREPLY=( $(compgen -W "\${convert_opts} \${global_opts}" -- \${cur}) )
1852
+ return 0
1853
+ ;;
1854
+ completion)
1855
+ COMPREPLY=( $(compgen -W "bash zsh fish" -- \${cur}) )
1856
+ return 0
1857
+ ;;
1858
+ esac
1859
+
1860
+ if [[ \${cur} == -* ]] ; then
1861
+ COMPREPLY=( $(compgen -W "\${global_opts}" -- \${cur}) )
1862
+ return 0
1863
+ fi
1864
+
1865
+ COMPREPLY=( $(compgen -W "\${commands}" -- \${cur}) )
1866
+ return 0
1867
+ }
1868
+
1869
+ complete -F _drizzle_multitenant drizzle-multitenant
1870
+ complete -F _drizzle_multitenant npx drizzle-multitenant
1871
+ `;
1872
+ }
1873
+ function generateZshCompletion() {
1874
+ return `#compdef drizzle-multitenant
1875
+ # drizzle-multitenant zsh completion
1876
+ # Add this to ~/.zshrc or place in a file in your $fpath
1877
+
1878
+ _drizzle_multitenant() {
1879
+ local -a commands
1880
+ local -a global_opts
1881
+
1882
+ commands=(
1883
+ 'migrate:Apply pending migrations to tenant schemas'
1884
+ 'status:Show migration status for all tenants'
1885
+ 'generate:Generate a new migration file'
1886
+ 'tenant\\:create:Create a new tenant schema'
1887
+ 'tenant\\:drop:Drop a tenant schema (DESTRUCTIVE)'
1888
+ 'convert-format:Convert migration table format'
1889
+ 'init:Initialize a new configuration'
1890
+ 'completion:Generate shell completion scripts'
1891
+ )
1892
+
1893
+ global_opts=(
1894
+ '--json[Output as JSON]'
1895
+ '(-v --verbose)'{-v,--verbose}'[Show verbose output]'
1896
+ '(-q --quiet)'{-q,--quiet}'[Only show errors]'
1897
+ '--no-color[Disable colored output]'
1898
+ '(-h --help)'{-h,--help}'[Show help]'
1899
+ '(-V --version)'{-V,--version}'[Show version]'
1900
+ )
1901
+
1902
+ _arguments -C \\
1903
+ "\${global_opts[@]}" \\
1904
+ '1: :->command' \\
1905
+ '*:: :->args'
1906
+
1907
+ case $state in
1908
+ command)
1909
+ _describe -t commands 'command' commands
1910
+ ;;
1911
+ args)
1912
+ case $words[1] in
1913
+ migrate)
1914
+ _arguments \\
1915
+ '(-c --config)'{-c,--config}'[Path to config file]:file:_files' \\
1916
+ '(-a --all)'{-a,--all}'[Migrate all tenants]' \\
1917
+ '(-t --tenant)'{-t,--tenant}'[Migrate specific tenant]:tenant id:' \\
1918
+ '--tenants[Migrate specific tenants (comma-separated)]:tenant ids:' \\
1919
+ '--concurrency[Number of concurrent migrations]:number:' \\
1920
+ '--dry-run[Show what would be applied]' \\
1921
+ '--migrations-folder[Path to migrations folder]:folder:_directories' \\
1922
+ "\${global_opts[@]}"
1923
+ ;;
1924
+ status)
1925
+ _arguments \\
1926
+ '(-c --config)'{-c,--config}'[Path to config file]:file:_files' \\
1927
+ '--migrations-folder[Path to migrations folder]:folder:_directories' \\
1928
+ "\${global_opts[@]}"
1929
+ ;;
1930
+ generate)
1931
+ _arguments \\
1932
+ '(-n --name)'{-n,--name}'[Migration name]:name:' \\
1933
+ '(-c --config)'{-c,--config}'[Path to config file]:file:_files' \\
1934
+ '--type[Migration type]:type:(tenant shared)' \\
1935
+ '--migrations-folder[Path to migrations folder]:folder:_directories' \\
1936
+ "\${global_opts[@]}"
1937
+ ;;
1938
+ tenant:create)
1939
+ _arguments \\
1940
+ '--id[Tenant ID]:tenant id:' \\
1941
+ '(-c --config)'{-c,--config}'[Path to config file]:file:_files' \\
1942
+ '--migrations-folder[Path to migrations folder]:folder:_directories' \\
1943
+ '--no-migrate[Skip running migrations]' \\
1944
+ "\${global_opts[@]}"
1945
+ ;;
1946
+ tenant:drop)
1947
+ _arguments \\
1948
+ '--id[Tenant ID]:tenant id:' \\
1949
+ '(-c --config)'{-c,--config}'[Path to config file]:file:_files' \\
1950
+ '--migrations-folder[Path to migrations folder]:folder:_directories' \\
1951
+ '(-f --force)'{-f,--force}'[Skip confirmation]' \\
1952
+ '--no-cascade[Use RESTRICT instead of CASCADE]' \\
1953
+ "\${global_opts[@]}"
1954
+ ;;
1955
+ convert-format)
1956
+ _arguments \\
1957
+ '--to[Target format]:format:(name hash drizzle-kit)' \\
1958
+ '(-c --config)'{-c,--config}'[Path to config file]:file:_files' \\
1959
+ '(-t --tenant)'{-t,--tenant}'[Convert specific tenant]:tenant id:' \\
1960
+ '--dry-run[Show what would be converted]' \\
1961
+ '--migrations-folder[Path to migrations folder]:folder:_directories' \\
1962
+ "\${global_opts[@]}"
1963
+ ;;
1964
+ completion)
1965
+ _arguments '1:shell:(bash zsh fish)'
1966
+ ;;
1967
+ esac
1968
+ ;;
1969
+ esac
1970
+ }
1971
+
1972
+ _drizzle_multitenant "$@"
1973
+ `;
1974
+ }
1975
+ function generateFishCompletion() {
1976
+ return `# drizzle-multitenant fish completion
1977
+ # Save to ~/.config/fish/completions/drizzle-multitenant.fish
1978
+
1979
+ # Disable file completion by default
1980
+ complete -c drizzle-multitenant -f
1981
+
1982
+ # Commands
1983
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "migrate" -d "Apply pending migrations"
1984
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "status" -d "Show migration status"
1985
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "generate" -d "Generate new migration"
1986
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "tenant:create" -d "Create tenant schema"
1987
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "tenant:drop" -d "Drop tenant schema"
1988
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "convert-format" -d "Convert migration format"
1989
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "init" -d "Initialize configuration"
1990
+ complete -c drizzle-multitenant -n "__fish_use_subcommand" -a "completion" -d "Generate completions"
1991
+
1992
+ # Global options
1993
+ complete -c drizzle-multitenant -l json -d "Output as JSON"
1994
+ complete -c drizzle-multitenant -s v -l verbose -d "Verbose output"
1995
+ complete -c drizzle-multitenant -s q -l quiet -d "Only show errors"
1996
+ complete -c drizzle-multitenant -l no-color -d "Disable colors"
1997
+ complete -c drizzle-multitenant -s h -l help -d "Show help"
1998
+ complete -c drizzle-multitenant -s V -l version -d "Show version"
1999
+
2000
+ # migrate options
2001
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -s c -l config -r -d "Config file"
2002
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -s a -l all -d "Migrate all tenants"
2003
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -s t -l tenant -r -d "Specific tenant"
2004
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -l tenants -r -d "Multiple tenants"
2005
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -l concurrency -r -d "Concurrent migrations"
2006
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -l dry-run -d "Dry run mode"
2007
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from migrate" -l migrations-folder -r -d "Migrations folder"
2008
+
2009
+ # status options
2010
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from status" -s c -l config -r -d "Config file"
2011
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from status" -l migrations-folder -r -d "Migrations folder"
2012
+
2013
+ # generate options
2014
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from generate" -s n -l name -r -d "Migration name"
2015
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from generate" -s c -l config -r -d "Config file"
2016
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from generate" -l type -r -a "tenant shared" -d "Migration type"
2017
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from generate" -l migrations-folder -r -d "Migrations folder"
2018
+
2019
+ # tenant:create options
2020
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:create" -l id -r -d "Tenant ID"
2021
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:create" -s c -l config -r -d "Config file"
2022
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:create" -l migrations-folder -r -d "Migrations folder"
2023
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:create" -l no-migrate -d "Skip migrations"
2024
+
2025
+ # tenant:drop options
2026
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:drop" -l id -r -d "Tenant ID"
2027
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:drop" -s c -l config -r -d "Config file"
2028
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:drop" -l migrations-folder -r -d "Migrations folder"
2029
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:drop" -s f -l force -d "Skip confirmation"
2030
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from tenant:drop" -l no-cascade -d "Use RESTRICT"
2031
+
2032
+ # convert-format options
2033
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from convert-format" -l to -r -a "name hash drizzle-kit" -d "Target format"
2034
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from convert-format" -s c -l config -r -d "Config file"
2035
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from convert-format" -s t -l tenant -r -d "Specific tenant"
2036
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from convert-format" -l dry-run -d "Dry run mode"
2037
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from convert-format" -l migrations-folder -r -d "Migrations folder"
2038
+
2039
+ # completion options
2040
+ complete -c drizzle-multitenant -n "__fish_seen_subcommand_from completion" -a "bash zsh fish" -d "Shell type"
2041
+ `;
2042
+ }
860
2043
 
861
2044
  // src/cli/index.ts
862
2045
  var program = new Command();
863
- program.name("drizzle-multitenant").description("Multi-tenancy toolkit for Drizzle ORM").version("0.3.0");
2046
+ program.name("drizzle-multitenant").description("Multi-tenancy toolkit for Drizzle ORM").version("0.3.0").option("--json", "Output as JSON (machine-readable)").option("-v, --verbose", "Show verbose output").option("-q, --quiet", "Only show errors").option("--no-color", "Disable colored output").hook("preAction", (thisCommand) => {
2047
+ const opts = thisCommand.opts();
2048
+ initOutputContext({
2049
+ json: opts.json,
2050
+ verbose: opts.verbose,
2051
+ quiet: opts.quiet,
2052
+ noColor: opts.color === false
2053
+ });
2054
+ });
2055
+ program.addHelpText("after", `
2056
+ Examples:
2057
+ $ drizzle-multitenant status
2058
+ $ drizzle-multitenant migrate --all
2059
+ $ drizzle-multitenant migrate --tenant=my-tenant --dry-run
2060
+ $ drizzle-multitenant generate --name add-users-table
2061
+ $ drizzle-multitenant tenant:create --id new-tenant
2062
+ $ drizzle-multitenant status --json | jq '.summary'
2063
+
2064
+ Documentation:
2065
+ https://github.com/your-repo/drizzle-multitenant
2066
+ `);
864
2067
  program.addCommand(migrateCommand);
865
2068
  program.addCommand(statusCommand);
866
2069
  program.addCommand(generateCommand);
867
2070
  program.addCommand(tenantCreateCommand);
868
2071
  program.addCommand(tenantDropCommand);
2072
+ program.addCommand(convertFormatCommand);
2073
+ program.addCommand(initCommand);
2074
+ program.addCommand(completionCommand);
869
2075
  program.parse();
870
2076
  //# sourceMappingURL=index.js.map
871
2077
  //# sourceMappingURL=index.js.map