drizzle-multitenant 1.0.3 → 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 +121 -8
- package/dist/cli/index.js +1352 -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/roadmap.md +105 -7
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 (
|
|
143
|
+
} catch (error2) {
|
|
48
144
|
onProgress?.(tenantId, "failed");
|
|
49
|
-
const action = onError?.(tenantId,
|
|
145
|
+
const action = onError?.(tenantId, error2);
|
|
50
146
|
if (action === "abort") {
|
|
51
147
|
aborted = true;
|
|
52
148
|
}
|
|
53
|
-
return this.createErrorResult(tenantId,
|
|
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.
|
|
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.
|
|
81
|
-
const pending = allMigrations.filter(
|
|
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 (
|
|
213
|
+
} catch (error2) {
|
|
113
214
|
const result = {
|
|
114
215
|
tenantId,
|
|
115
216
|
schemaName,
|
|
116
217
|
success: false,
|
|
117
218
|
appliedMigrations,
|
|
118
|
-
error:
|
|
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 (
|
|
244
|
+
} catch (error2) {
|
|
144
245
|
onProgress?.(tenantId, "failed");
|
|
145
|
-
onError?.(tenantId,
|
|
146
|
-
return this.createErrorResult(tenantId,
|
|
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
|
|
186
|
-
const
|
|
187
|
-
const
|
|
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 (
|
|
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:
|
|
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}"."${
|
|
414
|
+
CREATE TABLE IF NOT EXISTS "${schemaName}"."${format.tableName}" (
|
|
302
415
|
id SERIAL PRIMARY KEY,
|
|
303
|
-
|
|
304
|
-
|
|
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,
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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}"."${
|
|
342
|
-
[
|
|
491
|
+
`INSERT INTO "${schemaName}"."${format.tableName}" ("${identifier}", "${timestamp}") VALUES ($1, $2)`,
|
|
492
|
+
[identifierValue, timestampValue]
|
|
343
493
|
);
|
|
344
494
|
await client.query("COMMIT");
|
|
345
|
-
} catch (
|
|
495
|
+
} catch (error2) {
|
|
346
496
|
await client.query("ROLLBACK");
|
|
347
|
-
throw
|
|
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,
|
|
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:
|
|
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
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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 ?
|
|
463
|
-
const statusText = result.success ?
|
|
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
|
|
627
|
+
return chalk.green("\nAll tenants are up to date.");
|
|
482
628
|
}
|
|
483
|
-
const lines = [
|
|
629
|
+
const lines = [chalk.yellow("\nPending migrations:")];
|
|
484
630
|
for (const [migration, count] of pendingMap.entries()) {
|
|
485
631
|
lines.push(
|
|
486
|
-
` ${
|
|
632
|
+
` ${chalk.dim("-")} ${migration} ${chalk.dim(`(${count} tenant${count > 1 ? "s" : ""})`)}`
|
|
487
633
|
);
|
|
488
634
|
}
|
|
489
635
|
lines.push(
|
|
490
|
-
|
|
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
|
|
643
|
+
return chalk.green("\u2713");
|
|
498
644
|
case "behind":
|
|
499
|
-
return
|
|
645
|
+
return chalk.yellow("\u26A0");
|
|
500
646
|
case "error":
|
|
501
|
-
return
|
|
647
|
+
return chalk.red("\u2717");
|
|
502
648
|
}
|
|
503
649
|
}
|
|
504
650
|
function getStatusText(status) {
|
|
505
651
|
switch (status) {
|
|
506
652
|
case "ok":
|
|
507
|
-
return
|
|
653
|
+
return chalk.green("OK");
|
|
508
654
|
case "behind":
|
|
509
|
-
return
|
|
655
|
+
return chalk.yellow("Behind");
|
|
510
656
|
case "error":
|
|
511
|
-
return
|
|
657
|
+
return chalk.red("Error");
|
|
512
658
|
}
|
|
513
659
|
}
|
|
514
660
|
var CONFIG_FILE_NAMES = [
|
|
@@ -556,6 +702,7 @@ async function loadConfig(configPath) {
|
|
|
556
702
|
return {
|
|
557
703
|
config: exported,
|
|
558
704
|
migrationsFolder: exported.migrations?.tenantFolder,
|
|
705
|
+
migrationsTable: exported.migrations?.migrationsTable,
|
|
559
706
|
tenantDiscovery: exported.migrations?.tenantDiscovery
|
|
560
707
|
};
|
|
561
708
|
}
|
|
@@ -581,120 +728,465 @@ function resolveMigrationsFolder(folder) {
|
|
|
581
728
|
}
|
|
582
729
|
return resolved;
|
|
583
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
|
+
}
|
|
584
970
|
|
|
585
971
|
// src/cli/commands/migrate.ts
|
|
586
|
-
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").
|
|
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();
|
|
587
983
|
const spinner = createSpinner("Loading configuration...");
|
|
588
984
|
try {
|
|
589
985
|
spinner.start();
|
|
590
|
-
const { config, migrationsFolder, tenantDiscovery } = await loadConfig(options.config);
|
|
986
|
+
const { config, migrationsFolder, migrationsTable, tenantDiscovery } = await loadConfig(options.config);
|
|
591
987
|
const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
|
|
988
|
+
debug(`Using migrations folder: ${folder}`);
|
|
592
989
|
let discoveryFn;
|
|
990
|
+
let tenantIds;
|
|
593
991
|
if (options.tenant) {
|
|
594
992
|
discoveryFn = async () => [options.tenant];
|
|
993
|
+
tenantIds = [options.tenant];
|
|
595
994
|
} else if (options.tenants) {
|
|
596
|
-
const
|
|
597
|
-
discoveryFn = async () =>
|
|
995
|
+
const ids = options.tenants.split(",").map((id) => id.trim());
|
|
996
|
+
discoveryFn = async () => ids;
|
|
997
|
+
tenantIds = ids;
|
|
598
998
|
} else if (options.all) {
|
|
599
999
|
if (!tenantDiscovery) {
|
|
600
|
-
throw
|
|
601
|
-
"No tenant discovery function configured. Add migrations.tenantDiscovery to your config."
|
|
602
|
-
);
|
|
1000
|
+
throw CLIErrors.noTenantDiscovery();
|
|
603
1001
|
}
|
|
604
1002
|
discoveryFn = tenantDiscovery;
|
|
1003
|
+
spinner.text = "Discovering tenants...";
|
|
1004
|
+
tenantIds = await tenantDiscovery();
|
|
605
1005
|
} else {
|
|
606
1006
|
spinner.stop();
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
+
}
|
|
613
1028
|
}
|
|
614
|
-
spinner.text = "Discovering tenants...";
|
|
615
|
-
const migrator = createMigrator(config, {
|
|
616
|
-
migrationsFolder: folder,
|
|
617
|
-
tenantDiscovery: discoveryFn
|
|
618
|
-
});
|
|
619
|
-
const tenantIds = await discoveryFn();
|
|
620
1029
|
if (tenantIds.length === 0) {
|
|
621
1030
|
spinner.stop();
|
|
622
|
-
|
|
1031
|
+
log(warning("No tenants found."));
|
|
623
1032
|
return;
|
|
624
1033
|
}
|
|
625
1034
|
spinner.text = `Found ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}`;
|
|
626
1035
|
spinner.succeed();
|
|
627
1036
|
if (options.dryRun) {
|
|
628
|
-
|
|
1037
|
+
log(info(bold("\nDry run mode - no changes will be made\n")));
|
|
629
1038
|
}
|
|
630
|
-
|
|
1039
|
+
log(info(`Migrating ${tenantIds.length} tenant${tenantIds.length > 1 ? "s" : ""}...
|
|
631
1040
|
`));
|
|
632
|
-
const
|
|
633
|
-
|
|
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();
|
|
634
1049
|
const results = await migrator.migrateAll({
|
|
635
1050
|
concurrency,
|
|
636
|
-
dryRun: options.dryRun,
|
|
1051
|
+
dryRun: !!options.dryRun,
|
|
637
1052
|
onProgress: (tenantId, status, migrationName) => {
|
|
638
1053
|
if (status === "completed") {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
console.log(`${dim(progress)} ${success(tenantId)}`);
|
|
1054
|
+
progressBar.increment({ tenant: tenantId, status: "success" });
|
|
1055
|
+
debug(`Completed: ${tenantId}`);
|
|
642
1056
|
} else if (status === "failed") {
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
console.log(`${dim(progress)} ${error(tenantId)}`);
|
|
1057
|
+
progressBar.increment({ tenant: tenantId, status: "error" });
|
|
1058
|
+
debug(`Failed: ${tenantId}`);
|
|
646
1059
|
} else if (status === "migrating" && migrationName) {
|
|
1060
|
+
debug(`${tenantId}: Applying ${migrationName}`);
|
|
647
1061
|
}
|
|
648
1062
|
},
|
|
649
1063
|
onError: (tenantId, err) => {
|
|
650
|
-
|
|
1064
|
+
debug(`Error on ${tenantId}: ${err.message}`);
|
|
651
1065
|
return "continue";
|
|
652
1066
|
}
|
|
653
1067
|
});
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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())}`);
|
|
659
1098
|
if (results.failed > 0) {
|
|
660
|
-
|
|
1099
|
+
log(` Failed: ${error(results.failed.toString())}`);
|
|
661
1100
|
}
|
|
662
1101
|
if (results.skipped > 0) {
|
|
663
|
-
|
|
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."));
|
|
664
1114
|
}
|
|
665
1115
|
if (results.failed > 0) {
|
|
666
1116
|
process.exit(1);
|
|
667
1117
|
}
|
|
668
1118
|
} catch (err) {
|
|
669
1119
|
spinner.fail(err.message);
|
|
670
|
-
|
|
1120
|
+
handleError(err);
|
|
671
1121
|
}
|
|
672
1122
|
});
|
|
673
|
-
|
|
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();
|
|
674
1142
|
const spinner = createSpinner("Loading configuration...");
|
|
675
1143
|
try {
|
|
676
1144
|
spinner.start();
|
|
677
|
-
const { config, migrationsFolder, tenantDiscovery } = await loadConfig(options.config);
|
|
1145
|
+
const { config, migrationsFolder, migrationsTable, tenantDiscovery } = await loadConfig(options.config);
|
|
678
1146
|
if (!tenantDiscovery) {
|
|
679
|
-
throw
|
|
680
|
-
"No tenant discovery function configured. Add migrations.tenantDiscovery to your config."
|
|
681
|
-
);
|
|
1147
|
+
throw CLIErrors.noTenantDiscovery();
|
|
682
1148
|
}
|
|
683
1149
|
const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
|
|
1150
|
+
debug(`Using migrations folder: ${folder}`);
|
|
684
1151
|
spinner.text = "Discovering tenants...";
|
|
685
1152
|
const migrator = createMigrator(config, {
|
|
686
1153
|
migrationsFolder: folder,
|
|
1154
|
+
...migrationsTable && { migrationsTable },
|
|
687
1155
|
tenantDiscovery
|
|
688
1156
|
});
|
|
689
1157
|
spinner.text = "Fetching migration status...";
|
|
690
1158
|
const statuses = await migrator.getStatus();
|
|
691
1159
|
spinner.succeed(`Found ${statuses.length} tenant${statuses.length > 1 ? "s" : ""}`);
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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));
|
|
695
1187
|
} catch (err) {
|
|
696
1188
|
spinner.fail(err.message);
|
|
697
|
-
|
|
1189
|
+
handleError(err);
|
|
698
1190
|
}
|
|
699
1191
|
});
|
|
700
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) => {
|
|
@@ -721,7 +1213,7 @@ var generateCommand = new Command("generate").description("Generate a new migrat
|
|
|
721
1213
|
let maxSequence = 0;
|
|
722
1214
|
for (const file of sqlFiles) {
|
|
723
1215
|
const match = file.match(/^(\d+)_/);
|
|
724
|
-
if (match) {
|
|
1216
|
+
if (match?.[1]) {
|
|
725
1217
|
const seq = parseInt(match[1], 10);
|
|
726
1218
|
if (seq > maxSequence) {
|
|
727
1219
|
maxSequence = seq;
|
|
@@ -752,10 +1244,11 @@ var tenantCreateCommand = new Command("tenant:create").description("Create a new
|
|
|
752
1244
|
const spinner = createSpinner("Loading configuration...");
|
|
753
1245
|
try {
|
|
754
1246
|
spinner.start();
|
|
755
|
-
const { config, migrationsFolder } = await loadConfig(options.config);
|
|
1247
|
+
const { config, migrationsFolder, migrationsTable } = await loadConfig(options.config);
|
|
756
1248
|
const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
|
|
757
1249
|
const migrator = createMigrator(config, {
|
|
758
1250
|
migrationsFolder: folder,
|
|
1251
|
+
...migrationsTable && { migrationsTable },
|
|
759
1252
|
tenantDiscovery: async () => []
|
|
760
1253
|
});
|
|
761
1254
|
const schemaName = config.isolation.schemaNameTemplate(options.id);
|
|
@@ -800,10 +1293,11 @@ var tenantDropCommand = new Command("tenant:drop").description("Drop a tenant sc
|
|
|
800
1293
|
const spinner = createSpinner("Loading configuration...");
|
|
801
1294
|
try {
|
|
802
1295
|
spinner.start();
|
|
803
|
-
const { config, migrationsFolder } = await loadConfig(options.config);
|
|
1296
|
+
const { config, migrationsFolder, migrationsTable } = await loadConfig(options.config);
|
|
804
1297
|
const folder = options.migrationsFolder ? resolveMigrationsFolder(options.migrationsFolder) : resolveMigrationsFolder(migrationsFolder);
|
|
805
1298
|
const migrator = createMigrator(config, {
|
|
806
1299
|
migrationsFolder: folder,
|
|
1300
|
+
...migrationsTable && { migrationsTable },
|
|
807
1301
|
tenantDiscovery: async () => []
|
|
808
1302
|
});
|
|
809
1303
|
const schemaName = config.isolation.schemaNameTemplate(options.id);
|
|
@@ -852,15 +1346,732 @@ async function askConfirmation(question, expected) {
|
|
|
852
1346
|
});
|
|
853
1347
|
});
|
|
854
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
|
+
}
|
|
855
2043
|
|
|
856
2044
|
// src/cli/index.ts
|
|
857
2045
|
var program = new Command();
|
|
858
|
-
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
|
+
`);
|
|
859
2067
|
program.addCommand(migrateCommand);
|
|
860
2068
|
program.addCommand(statusCommand);
|
|
861
2069
|
program.addCommand(generateCommand);
|
|
862
2070
|
program.addCommand(tenantCreateCommand);
|
|
863
2071
|
program.addCommand(tenantDropCommand);
|
|
2072
|
+
program.addCommand(convertFormatCommand);
|
|
2073
|
+
program.addCommand(initCommand);
|
|
2074
|
+
program.addCommand(completionCommand);
|
|
864
2075
|
program.parse();
|
|
865
2076
|
//# sourceMappingURL=index.js.map
|
|
866
2077
|
//# sourceMappingURL=index.js.map
|