drizzle-multitenant 1.0.5 → 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.
@@ -12,7 +12,9 @@
12
12
  "Bash(ls:*)",
13
13
  "Bash(npm run test:*)",
14
14
  "Bash(node ./dist/cli/index.js:*)",
15
- "Bash(npx tsc:*)"
15
+ "Bash(npx tsc:*)",
16
+ "Bash(git filter-branch:*)",
17
+ "Bash(git add:*)"
16
18
  ]
17
19
  }
18
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drizzle-multitenant",
3
- "version": "1.0.5",
3
+ "version": "1.0.6",
4
4
  "description": "Multi-tenancy toolkit for Drizzle ORM with schema isolation, tenant context, and parallel migrations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,499 +0,0 @@
1
- # Proposal: Drizzle-Kit Native Compatibility
2
-
3
- > **Status**: Proposal
4
- > **Priority**: P0 (Critical for Adoption)
5
- > **Effort**: 6-8 hours
6
- > **Date**: 2024-12-24
7
-
8
- ## Problem Statement
9
-
10
- `drizzle-multitenant` currently cannot work with databases that have existing migrations applied via:
11
- 1. **drizzle-kit migrate** (standard Drizzle ORM workflow)
12
- 2. **Custom migration scripts** with different table structures
13
-
14
- When running `npx drizzle-multitenant status`, users see errors like:
15
-
16
- ```
17
- column "name" does not exist
18
- ```
19
-
20
- This happens because `drizzle-multitenant` expects a specific table structure that differs from what `drizzle-kit` and other tools create.
21
-
22
- ## Migration Table Formats
23
-
24
- ### 1. drizzle-multitenant (current)
25
-
26
- ```sql
27
- CREATE TABLE "__drizzle_migrations" (
28
- id SERIAL PRIMARY KEY,
29
- name VARCHAR(255) NOT NULL UNIQUE,
30
- applied_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
31
- );
32
- ```
33
-
34
- **Tracking method**: Filename without extension (e.g., `0001_initial_schema`)
35
-
36
- ### 2. drizzle-kit (standard Drizzle ORM)
37
-
38
- ```sql
39
- CREATE TABLE "__drizzle_migrations" (
40
- id SERIAL PRIMARY KEY,
41
- hash TEXT NOT NULL,
42
- created_at BIGINT NOT NULL
43
- );
44
- ```
45
-
46
- **Tracking method**: SHA-256 hash of migration file content
47
-
48
- ### 3. Custom Scripts (e.g., PrimeSys-v2 legacy)
49
-
50
- ```sql
51
- CREATE TABLE "__drizzle_tenant_migrations" (
52
- id SERIAL PRIMARY KEY,
53
- hash TEXT NOT NULL,
54
- created_at BIGINT NOT NULL
55
- );
56
- ```
57
-
58
- **Tracking method**: SHA-256 hash (same as drizzle-kit, different table name)
59
-
60
- ## Proposed Solution
61
-
62
- ### 1. Auto-Detection of Table Format
63
-
64
- The migrator should automatically detect the existing table structure and adapt:
65
-
66
- ```typescript
67
- // src/migrator/table-format.ts
68
- export type TableFormat = 'name' | 'hash' | 'drizzle-kit';
69
-
70
- export interface DetectedFormat {
71
- format: TableFormat;
72
- tableName: string;
73
- columns: {
74
- identifier: 'name' | 'hash';
75
- timestamp: 'applied_at' | 'created_at';
76
- timestampType: 'timestamp' | 'bigint';
77
- };
78
- }
79
-
80
- export async function detectTableFormat(
81
- pool: Pool,
82
- schemaName: string,
83
- tableName: string
84
- ): Promise<DetectedFormat | null> {
85
- // Check if table exists
86
- const tableExists = await pool.query(`
87
- SELECT 1 FROM information_schema.tables
88
- WHERE table_schema = $1 AND table_name = $2
89
- `, [schemaName, tableName]);
90
-
91
- if (tableExists.rowCount === 0) return null;
92
-
93
- // Check columns
94
- const columns = await pool.query(`
95
- SELECT column_name, data_type
96
- FROM information_schema.columns
97
- WHERE table_schema = $1 AND table_name = $2
98
- `, [schemaName, tableName]);
99
-
100
- const columnMap = new Map(columns.rows.map(r => [r.column_name, r.data_type]));
101
-
102
- if (columnMap.has('name')) {
103
- return {
104
- format: 'name',
105
- tableName,
106
- columns: {
107
- identifier: 'name',
108
- timestamp: 'applied_at',
109
- timestampType: 'timestamp',
110
- },
111
- };
112
- }
113
-
114
- if (columnMap.has('hash')) {
115
- const timestampType = columnMap.get('created_at');
116
- return {
117
- format: timestampType === 'bigint' ? 'drizzle-kit' : 'hash',
118
- tableName,
119
- columns: {
120
- identifier: 'hash',
121
- timestamp: 'created_at',
122
- timestampType: timestampType === 'bigint' ? 'bigint' : 'timestamp',
123
- },
124
- };
125
- }
126
-
127
- return null;
128
- }
129
- ```
130
-
131
- ### 2. Format-Aware Migration Tracking
132
-
133
- Update the `Migrator` class to work with any detected format:
134
-
135
- ```typescript
136
- // src/migrator/migrator.ts
137
-
138
- interface MigrationIdentifier {
139
- name: string; // Filename without extension
140
- hash: string; // SHA-256 of content
141
- }
142
-
143
- private async getAppliedMigrations(
144
- pool: Pool,
145
- schemaName: string,
146
- format: DetectedFormat
147
- ): Promise<AppliedMigration[]> {
148
- const identifierColumn = format.columns.identifier;
149
- const timestampColumn = format.columns.timestamp;
150
-
151
- const result = await pool.query(
152
- `SELECT id, ${identifierColumn} as identifier, ${timestampColumn} as applied_at
153
- FROM "${schemaName}"."${format.tableName}"
154
- ORDER BY id`
155
- );
156
-
157
- return result.rows.map((row) => ({
158
- id: row.id,
159
- identifier: row.identifier,
160
- appliedAt: format.columns.timestampType === 'bigint'
161
- ? new Date(parseInt(row.applied_at))
162
- : row.applied_at,
163
- }));
164
- }
165
-
166
- private isMigrationApplied(
167
- migration: MigrationFile,
168
- appliedIdentifiers: Set<string>,
169
- format: DetectedFormat
170
- ): boolean {
171
- if (format.columns.identifier === 'name') {
172
- return appliedIdentifiers.has(migration.name);
173
- }
174
- // Hash-based: check both hash AND name for backwards compatibility
175
- return appliedIdentifiers.has(migration.hash) ||
176
- appliedIdentifiers.has(migration.name);
177
- }
178
-
179
- private async recordMigration(
180
- pool: Pool,
181
- schemaName: string,
182
- migration: MigrationFile,
183
- format: DetectedFormat
184
- ): Promise<void> {
185
- const identifierColumn = format.columns.identifier;
186
- const timestampColumn = format.columns.timestamp;
187
- const identifier = format.columns.identifier === 'name'
188
- ? migration.name
189
- : migration.hash;
190
- const timestamp = format.columns.timestampType === 'bigint'
191
- ? Date.now()
192
- : new Date();
193
-
194
- await pool.query(
195
- `INSERT INTO "${schemaName}"."${format.tableName}"
196
- (${identifierColumn}, ${timestampColumn}) VALUES ($1, $2)`,
197
- [identifier, timestamp]
198
- );
199
- }
200
- ```
201
-
202
- ### 3. Configuration Options
203
-
204
- Allow explicit format configuration with sensible defaults:
205
-
206
- ```typescript
207
- // src/migrator/types.ts
208
- export interface MigratorConfig {
209
- migrationsFolder: string;
210
- tenantDiscovery: () => Promise<string[]>;
211
-
212
- /**
213
- * Migration table name
214
- * @default "__drizzle_migrations"
215
- */
216
- migrationsTable?: string;
217
-
218
- /**
219
- * Table format for tracking migrations
220
- * - "auto": Auto-detect existing format, use "name" for new tables
221
- * - "name": Use filename (drizzle-multitenant native)
222
- * - "hash": Use SHA-256 hash (drizzle-kit compatible)
223
- * - "drizzle-kit": Exact drizzle-kit format (hash + bigint timestamp)
224
- * @default "auto"
225
- */
226
- tableFormat?: 'auto' | 'name' | 'hash' | 'drizzle-kit';
227
-
228
- /**
229
- * When using "auto" format and no table exists, which format to create
230
- * @default "name"
231
- */
232
- defaultFormat?: 'name' | 'hash' | 'drizzle-kit';
233
-
234
- hooks?: MigratorHooks;
235
- }
236
- ```
237
-
238
- ### 4. Migration File Enhancement
239
-
240
- Add hash computation to migration files:
241
-
242
- ```typescript
243
- // src/migrator/migrator.ts
244
- import { createHash } from 'node:crypto';
245
-
246
- private async loadMigrations(): Promise<MigrationFile[]> {
247
- const files = await readdir(this.migratorConfig.migrationsFolder);
248
- const migrations: MigrationFile[] = [];
249
-
250
- for (const file of files) {
251
- if (!file.endsWith('.sql')) continue;
252
-
253
- const filePath = join(this.migratorConfig.migrationsFolder, file);
254
- const content = await readFile(filePath, 'utf-8');
255
-
256
- const match = file.match(/^(\d+)_/);
257
- const timestamp = match?.[1] ? parseInt(match[1], 10) : 0;
258
-
259
- migrations.push({
260
- name: basename(file, '.sql'),
261
- path: filePath,
262
- sql: content,
263
- timestamp,
264
- hash: createHash('sha256').update(content).digest('hex'), // NEW
265
- });
266
- }
267
-
268
- return migrations.sort((a, b) => a.timestamp - b.timestamp);
269
- }
270
- ```
271
-
272
- ### 5. CLI Updates
273
-
274
- Update status command to show format information:
275
-
276
- ```bash
277
- $ npx drizzle-multitenant status -c tenant.config.ts
278
-
279
- Migration Status:
280
- ┌──────────────────────────────────────┬──────────────────────┬────────┬─────────┬─────────┬──────────────┐
281
- │ Tenant │ Schema │ Format │ Applied │ Pending │ Status │
282
- ├──────────────────────────────────────┼──────────────────────┼────────┼─────────┼─────────┼──────────────┤
283
- │ abc-123 │ empresa_abc_123 │ hash │ 45 │ 3 │ ✓ Behind │
284
- │ def-456 │ empresa_def_456 │ name │ 48 │ 0 │ ✓ Up to date │
285
- │ ghi-789 │ empresa_ghi_789 │ (new) │ 0 │ 48 │ ○ New tenant │
286
- └──────────────────────────────────────┴──────────────────────┴────────┴─────────┴─────────┴──────────────┘
287
- ```
288
-
289
- ### 6. New CLI Command: `convert-format`
290
-
291
- For users who want to standardize on a single format:
292
-
293
- ```bash
294
- # Preview conversion
295
- npx drizzle-multitenant convert-format --to=name --dry-run
296
-
297
- # Convert all tenants from hash to name format
298
- npx drizzle-multitenant convert-format --to=name
299
-
300
- # Convert specific tenant
301
- npx drizzle-multitenant convert-format --to=name --tenant=abc-123
302
- ```
303
-
304
- Implementation:
305
-
306
- ```typescript
307
- // src/cli/commands/convert-format.ts
308
- async function convertFormat(
309
- pool: Pool,
310
- schemaName: string,
311
- tableName: string,
312
- migrations: MigrationFile[],
313
- targetFormat: 'name' | 'hash'
314
- ): Promise<void> {
315
- const migrationMap = new Map(
316
- migrations.map(m => [m.hash, m.name])
317
- );
318
-
319
- // Read current records
320
- const current = await pool.query(
321
- `SELECT id, hash FROM "${schemaName}"."${tableName}" ORDER BY id`
322
- );
323
-
324
- await pool.query('BEGIN');
325
-
326
- try {
327
- // Add name column if converting to name format
328
- if (targetFormat === 'name') {
329
- await pool.query(`
330
- ALTER TABLE "${schemaName}"."${tableName}"
331
- ADD COLUMN IF NOT EXISTS name VARCHAR(255)
332
- `);
333
-
334
- // Populate name from hash using migration files
335
- for (const row of current.rows) {
336
- const name = migrationMap.get(row.hash);
337
- if (name) {
338
- await pool.query(
339
- `UPDATE "${schemaName}"."${tableName}"
340
- SET name = $1 WHERE id = $2`,
341
- [name, row.id]
342
- );
343
- }
344
- }
345
-
346
- // Make name NOT NULL and add unique constraint
347
- await pool.query(`
348
- ALTER TABLE "${schemaName}"."${tableName}"
349
- ALTER COLUMN name SET NOT NULL,
350
- ADD CONSTRAINT ${tableName}_name_unique UNIQUE (name)
351
- `);
352
-
353
- // Optionally drop hash column
354
- // await pool.query(`ALTER TABLE ... DROP COLUMN hash`);
355
- }
356
-
357
- await pool.query('COMMIT');
358
- } catch (error) {
359
- await pool.query('ROLLBACK');
360
- throw error;
361
- }
362
- }
363
- ```
364
-
365
- ## Implementation Plan
366
-
367
- ### Phase 1: Detection & Read Compatibility (2 hours)
368
-
369
- 1. Implement `detectTableFormat()` function
370
- 2. Update `getAppliedMigrations()` to handle all formats
371
- 3. Update `isMigrationApplied()` for hash-based comparison
372
- 4. Update `getTenantStatus()` to show format in status
373
-
374
- ### Phase 2: Write Compatibility (2 hours)
375
-
376
- 1. Add hash computation to `loadMigrations()`
377
- 2. Update `recordMigration()` to use detected format
378
- 3. Update `ensureMigrationsTable()` to create correct format
379
- 4. Add `tableFormat` config option
380
-
381
- ### Phase 3: CLI Updates (2 hours)
382
-
383
- 1. Update `status` command output
384
- 2. Add `convert-format` command
385
- 3. Update `--dry-run` to show format info
386
- 4. Add format info to `migrate` command output
387
-
388
- ### Phase 4: Testing & Documentation (2 hours)
389
-
390
- 1. Unit tests for format detection
391
- 2. Integration tests with all formats
392
- 3. Update README with format documentation
393
- 4. Add migration guide for existing users
394
-
395
- ## Backwards Compatibility
396
-
397
- - **Existing drizzle-multitenant users**: No changes needed, default behavior preserved
398
- - **drizzle-kit users**: Works automatically with `tableFormat: "auto"` (default)
399
- - **Custom script users**: Set `tableFormat: "hash"` or let auto-detection handle it
400
-
401
- ## Configuration Examples
402
-
403
- ### New Project (drizzle-multitenant native)
404
-
405
- ```typescript
406
- export default {
407
- ...config,
408
- migrations: {
409
- tenantFolder: "./drizzle/tenant",
410
- tenantDiscovery: discoverTenants,
411
- // Uses default: tableFormat: "auto", defaultFormat: "name"
412
- },
413
- };
414
- ```
415
-
416
- ### Migrating from drizzle-kit
417
-
418
- ```typescript
419
- export default {
420
- ...config,
421
- migrations: {
422
- tenantFolder: "./drizzle/tenant",
423
- tenantDiscovery: discoverTenants,
424
- migrationsTable: "__drizzle_migrations",
425
- tableFormat: "auto", // Auto-detects drizzle-kit format
426
- },
427
- };
428
- ```
429
-
430
- ### Explicit drizzle-kit Compatibility
431
-
432
- ```typescript
433
- export default {
434
- ...config,
435
- migrations: {
436
- tenantFolder: "./drizzle/tenant",
437
- tenantDiscovery: discoverTenants,
438
- migrationsTable: "__drizzle_migrations",
439
- tableFormat: "drizzle-kit", // Forces drizzle-kit format
440
- },
441
- };
442
- ```
443
-
444
- ### Legacy Custom Script
445
-
446
- ```typescript
447
- export default {
448
- ...config,
449
- migrations: {
450
- tenantFolder: "./drizzle/tenant",
451
- tenantDiscovery: discoverTenants,
452
- migrationsTable: "__drizzle_tenant_migrations", // Custom table name
453
- tableFormat: "hash", // Uses hash-based tracking
454
- },
455
- };
456
- ```
457
-
458
- ## Success Criteria
459
-
460
- 1. `npx drizzle-multitenant status` works with existing drizzle-kit databases
461
- 2. `npx drizzle-multitenant migrate` applies new migrations to drizzle-kit databases
462
- 3. No data loss or duplicate migrations during format transition
463
- 4. Clear error messages when format detection fails
464
- 5. Documentation covers all migration scenarios
465
-
466
- ## Related
467
-
468
- - [Improvements from PrimeSys](./improvements-from-primesys.md) - Section 3
469
- - [drizzle-multitenant Roadmap](../roadmap.md)
470
- - [Drizzle ORM Migrations Docs](https://orm.drizzle.team/docs/migrations)
471
- - [Drizzle Kit Migrate](https://orm.drizzle.team/docs/drizzle-kit-migrate)
472
-
473
- ## Appendix: Table Structure Reference
474
-
475
- ### drizzle-kit Internal Structure
476
-
477
- Based on Drizzle ORM source code, `drizzle-kit migrate` creates:
478
-
479
- ```sql
480
- CREATE TABLE IF NOT EXISTS "__drizzle_migrations" (
481
- id SERIAL PRIMARY KEY,
482
- hash TEXT NOT NULL,
483
- created_at BIGINT NOT NULL -- Unix timestamp in milliseconds
484
- );
485
- ```
486
-
487
- The hash is computed as SHA-256 of the migration file content, allowing drizzle-kit to detect if a migration file was modified after being applied.
488
-
489
- ### Why Hash-Based Tracking?
490
-
491
- - **Content verification**: Detects if migration files were modified
492
- - **Idempotency**: Same content = same hash, prevents accidental re-runs
493
- - **drizzle-kit compatibility**: Matches drizzle-kit's internal behavior
494
-
495
- ### Why Name-Based Tracking?
496
-
497
- - **Human readable**: Easy to see which migrations are applied
498
- - **Debug friendly**: `SELECT * FROM __drizzle_migrations` shows migration names
499
- - **Simpler**: No hash computation needed