drizzle-multitenant 1.0.8 → 1.0.10

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Mateus Florez
3
+ Copyright (c) 2024 Mateus Flores
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,30 +1,36 @@
1
- # drizzle-multitenant
1
+ <p align="center">
2
+ <img src="./assets/banner.svg" alt="drizzle-multitenant" width="500" />
3
+ </p>
2
4
 
3
- [![npm version](https://img.shields.io/npm/v/drizzle-multitenant.svg)](https://www.npmjs.com/package/drizzle-multitenant)
4
- [![GitHub](https://img.shields.io/github/stars/mateusflorez/drizzle-multitenant?style=social)](https://github.com/mateusflorez/drizzle-multitenant)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ <p align="center">
6
+ <strong>Multi-tenancy toolkit for Drizzle ORM</strong>
7
+ </p>
6
8
 
7
- Multi-tenancy toolkit for Drizzle ORM with schema isolation, tenant context, and parallel migrations.
9
+ <p align="center">
10
+ Schema isolation, tenant context propagation, and parallel migrations for PostgreSQL
11
+ </p>
8
12
 
9
- ```
10
- ┌─────────────────────────────────────────────────────────────────────┐
11
- ╔═══════════════════════════════════════════════════════════════╗ │
12
- ║ drizzle-multitenant ║ │
13
- ║ ━━━━━━━━━━━━━━━━━━━━ ║ │
14
- │ ║ ║ │
15
- │ ║ Schema isolation • Context propagation • Migrations ║ │
16
- │ ╚═══════════════════════════════════════════════════════════════╝ │
17
- └─────────────────────────────────────────────────────────────────────┘
18
- ```
13
+ <p align="center">
14
+ <a href="https://www.npmjs.com/package/drizzle-multitenant"><img src="https://img.shields.io/npm/v/drizzle-multitenant.svg?style=flat-square&color=4A9A98" alt="npm version"></a>
15
+ <a href="https://www.npmjs.com/package/drizzle-multitenant"><img src="https://img.shields.io/npm/dm/drizzle-multitenant.svg?style=flat-square&color=3D5A80" alt="npm downloads"></a>
16
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-6AADAB.svg?style=flat-square" alt="License"></a>
17
+ <a href="https://mateusflorez.github.io/drizzle-multitenant/"><img src="https://img.shields.io/badge/docs-online-2B3E5C.svg?style=flat-square" alt="Documentation"></a>
18
+ </p>
19
+
20
+ <br />
19
21
 
20
22
  ## Features
21
23
 
22
- - **Schema Isolation** - Automatic PostgreSQL schema-per-tenant with LRU pool management
23
- - **Context Propagation** - AsyncLocalStorage-based tenant context across your entire stack
24
- - **Parallel Migrations** - Apply migrations to all tenants concurrently with progress tracking
25
- - **Cross-Schema Queries** - Type-safe queries joining tenant and shared schemas
26
- - **Framework Integrations** - Express, Fastify, NestJS, and Hono middleware/plugins
27
- - **CLI Tools** - Generate migrations, manage tenants, check status
24
+ | Feature | Description |
25
+ |---------|-------------|
26
+ | **Schema Isolation** | PostgreSQL schema-per-tenant with automatic LRU pool management |
27
+ | **Context Propagation** | AsyncLocalStorage-based tenant context across your entire stack |
28
+ | **Parallel Migrations** | Apply migrations to all tenants concurrently with progress tracking |
29
+ | **Cross-Schema Queries** | Type-safe joins between tenant and shared tables |
30
+ | **Connection Retry** | Automatic retry with exponential backoff for transient failures |
31
+ | **Framework Support** | First-class Express, Fastify, and NestJS integrations |
32
+
33
+ <br />
28
34
 
29
35
  ## Installation
30
36
 
@@ -32,6 +38,8 @@ Multi-tenancy toolkit for Drizzle ORM with schema isolation, tenant context, and
32
38
  npm install drizzle-multitenant drizzle-orm pg
33
39
  ```
34
40
 
41
+ <br />
42
+
35
43
  ## Quick Start
36
44
 
37
45
  ### 1. Define your configuration
@@ -40,94 +48,89 @@ npm install drizzle-multitenant drizzle-orm pg
40
48
  // tenant.config.ts
41
49
  import { defineConfig } from 'drizzle-multitenant';
42
50
  import * as tenantSchema from './schemas/tenant';
43
- import * as sharedSchema from './schemas/shared';
44
51
 
45
52
  export default defineConfig({
46
- connection: {
47
- url: process.env.DATABASE_URL!,
48
- },
53
+ connection: { url: process.env.DATABASE_URL! },
49
54
  isolation: {
50
55
  strategy: 'schema',
51
- schemaNameTemplate: (tenantId) => `tenant_${tenantId}`,
52
- maxPools: 50,
53
- poolTtlMs: 60 * 60 * 1000,
54
- },
55
- schemas: {
56
- tenant: tenantSchema,
57
- shared: sharedSchema,
58
- },
59
- // Optional: CLI migrations config
60
- migrations: {
61
- tenantFolder: './drizzle/tenant',
62
- migrationsTable: '__drizzle_migrations', // Custom table name
63
- tenantDiscovery: async () => {
64
- // Return list of tenant IDs for migrations
65
- return ['tenant-1', 'tenant-2'];
66
- },
56
+ schemaNameTemplate: (id) => `tenant_${id}`,
67
57
  },
58
+ schemas: { tenant: tenantSchema },
68
59
  });
69
60
  ```
70
61
 
71
62
  ### 2. Create the tenant manager
72
63
 
73
64
  ```typescript
65
+ // app.ts
74
66
  import { createTenantManager } from 'drizzle-multitenant';
75
67
  import config from './tenant.config';
76
68
 
77
69
  const tenants = createTenantManager(config);
78
70
 
79
- // Get typed DB for a tenant
80
- const db = tenants.getDb('tenant-123');
71
+ // Type-safe database for each tenant
72
+ const db = tenants.getDb('acme');
81
73
  const users = await db.select().from(schema.users);
82
-
83
- // Get shared DB (public schema)
84
- const shared = tenants.getSharedDb();
85
- const plans = await shared.select().from(sharedSchema.plans);
86
74
  ```
87
75
 
88
- ### 3. Use context propagation
89
-
90
- ```typescript
91
- import { createTenantContext } from 'drizzle-multitenant';
76
+ <br />
92
77
 
93
- const ctx = createTenantContext(tenants);
78
+ ## CLI Commands
94
79
 
95
- // Run code within tenant context
96
- await ctx.runWithTenant({ tenantId: 'tenant-123' }, async () => {
97
- const db = ctx.getTenantDb();
98
- // All queries automatically scoped to tenant
99
- const users = await db.select().from(schema.users);
100
- });
80
+ ```bash
81
+ npx drizzle-multitenant init # Interactive setup wizard
82
+ npx drizzle-multitenant generate --name=users # Generate migration
83
+ npx drizzle-multitenant migrate --all # Apply to all tenants
84
+ npx drizzle-multitenant status # Check migration status
85
+ npx drizzle-multitenant tenant:create --id=acme # Create new tenant
101
86
  ```
102
87
 
88
+ <br />
89
+
103
90
  ## Framework Integrations
104
91
 
105
- ### Express
92
+ <details>
93
+ <summary><strong>Express</strong></summary>
106
94
 
107
95
  ```typescript
108
96
  import { createExpressMiddleware } from 'drizzle-multitenant/express';
109
97
 
110
- const middleware = createExpressMiddleware({
98
+ app.use(createExpressMiddleware({
111
99
  manager: tenants,
112
100
  extractTenantId: (req) => req.headers['x-tenant-id'] as string,
113
- validateTenant: async (id) => checkTenantExists(id),
114
- });
101
+ }));
115
102
 
116
- app.use('/api/:tenantId/*', middleware);
103
+ app.get('/users', async (req, res) => {
104
+ const db = req.tenantContext.db;
105
+ const users = await db.select().from(schema.users);
106
+ res.json(users);
107
+ });
117
108
  ```
118
109
 
119
- ### Fastify
110
+ </details>
111
+
112
+ <details>
113
+ <summary><strong>Fastify</strong></summary>
120
114
 
121
115
  ```typescript
122
116
  import { fastifyTenantPlugin } from 'drizzle-multitenant/fastify';
123
117
 
124
- await fastify.register(fastifyTenantPlugin, {
118
+ fastify.register(fastifyTenantPlugin, {
125
119
  manager: tenants,
126
120
  extractTenantId: (req) => req.headers['x-tenant-id'] as string,
127
121
  });
122
+
123
+ fastify.get('/users', async (req, reply) => {
124
+ const db = req.tenantContext.db;
125
+ const users = await db.select().from(schema.users);
126
+ return users;
127
+ });
128
128
  ```
129
129
 
130
- ### NestJS
130
+ </details>
131
+
132
+ <details>
133
+ <summary><strong>NestJS</strong></summary>
131
134
 
132
135
  ```typescript
133
136
  import { TenantModule, InjectTenantDb } from 'drizzle-multitenant/nestjs';
@@ -135,9 +138,8 @@ import { TenantModule, InjectTenantDb } from 'drizzle-multitenant/nestjs';
135
138
  @Module({
136
139
  imports: [
137
140
  TenantModule.forRoot({
138
- config: tenantConfig,
141
+ config,
139
142
  extractTenantId: (req) => req.headers['x-tenant-id'],
140
- isGlobal: true,
141
143
  }),
142
144
  ],
143
145
  })
@@ -145,264 +147,39 @@ export class AppModule {}
145
147
 
146
148
  @Injectable({ scope: Scope.REQUEST })
147
149
  export class UserService {
148
- constructor(@InjectTenantDb() private readonly db: TenantDb) {}
150
+ constructor(@InjectTenantDb() private db: TenantDb) {}
149
151
 
150
- async findAll() {
152
+ findAll() {
151
153
  return this.db.select().from(users);
152
154
  }
153
155
  }
154
156
  ```
155
157
 
156
- #### Singleton Services (Cron Jobs, Event Handlers)
157
-
158
- Use `TenantDbFactory` when you need to access tenant databases from singleton services:
159
-
160
- ```typescript
161
- import { TenantDbFactory, InjectTenantDbFactory } from 'drizzle-multitenant/nestjs';
162
-
163
- @Injectable() // Singleton - no scope needed
164
- export class ReportService {
165
- constructor(@InjectTenantDbFactory() private dbFactory: TenantDbFactory) {}
166
-
167
- async generateReport(tenantId: string) {
168
- const db = this.dbFactory.getDb(tenantId);
169
- return db.select().from(reports);
170
- }
171
- }
172
-
173
- // Cron job example
174
- @Injectable()
175
- export class DailyReportCron {
176
- constructor(@InjectTenantDbFactory() private dbFactory: TenantDbFactory) {}
177
-
178
- @Cron('0 8 * * *')
179
- async run() {
180
- const tenants = await this.getTenantIds();
181
- for (const tenantId of tenants) {
182
- const db = this.dbFactory.getDb(tenantId);
183
- await this.processReports(db);
184
- }
185
- }
186
- }
187
- ```
188
-
189
- #### Debugging
190
-
191
- The injected `TenantDb` provides debug utilities:
192
-
193
- ```typescript
194
- // Console output shows useful info
195
- console.log(tenantDb);
196
- // [TenantDb] tenant=123 schema=empresa_123
197
-
198
- // Access debug information
199
- console.log(tenantDb.__debug);
200
- // { tenantId: '123', schemaName: 'empresa_123', isProxy: true, poolCount: 5 }
201
-
202
- // Quick access
203
- console.log(tenantDb.__tenantId); // '123'
204
-
205
- // In tests
206
- expect(tenantDb.__tenantId).toBe('expected-tenant');
207
- ```
208
-
209
- ## CLI Commands
210
-
211
- ```bash
212
- # Initialize configuration (interactive wizard)
213
- npx drizzle-multitenant init
214
-
215
- # Generate a new migration
216
- npx drizzle-multitenant generate --name=add-users-table
217
-
218
- # Apply migrations to all tenants (with progress bar)
219
- npx drizzle-multitenant migrate --all --concurrency=10
220
-
221
- # Interactive tenant selection
222
- npx drizzle-multitenant migrate # Shows checkbox to select tenants
158
+ </details>
223
159
 
224
- # Check migration status
225
- npx drizzle-multitenant status
160
+ <br />
226
161
 
227
- # Create a new tenant schema
228
- npx drizzle-multitenant tenant:create --id=new-tenant
162
+ ## Documentation
229
163
 
230
- # Drop a tenant schema
231
- npx drizzle-multitenant tenant:drop --id=old-tenant --force
232
-
233
- # Convert migration table format
234
- npx drizzle-multitenant convert-format --to=name --dry-run
235
-
236
- # Generate shell completions
237
- npx drizzle-multitenant completion bash >> ~/.bashrc
238
- npx drizzle-multitenant completion zsh >> ~/.zshrc
239
- ```
240
-
241
- ### Global Options
242
-
243
- ```bash
244
- --json # Output as JSON (for scripts/CI)
245
- --verbose # Show detailed output
246
- --quiet # Only show errors
247
- --no-color # Disable colored output
248
- ```
249
-
250
- ### JSON Output
251
-
252
- ```bash
253
- # Get status as JSON for scripting
254
- npx drizzle-multitenant status --json | jq '.tenants[] | select(.pending > 0)'
255
-
256
- # Parse migration results
257
- npx drizzle-multitenant migrate --all --json | jq '.summary'
258
- ```
259
-
260
- ### Status Output
261
-
262
- ```
263
- ┌──────────────────┬──────────────┬────────────┬─────────┬─────────┬──────────┐
264
- │ Tenant │ Schema │ Format │ Applied │ Pending │ Status │
265
- ├──────────────────┼──────────────┼────────────┼─────────┼─────────┼──────────┤
266
- │ abc-123 │ tenant_abc │ drizzle-kit│ 45 │ 3 │ Behind │
267
- │ def-456 │ tenant_def │ name │ 48 │ 0 │ OK │
268
- │ ghi-789 │ tenant_ghi │ (new) │ 0 │ 48 │ Behind │
269
- └──────────────────┴──────────────┴────────────┴─────────┴─────────┴──────────┘
270
- ```
271
-
272
- ## Migration Table Formats
273
-
274
- `drizzle-multitenant` supports multiple migration table formats for compatibility with existing databases:
275
-
276
- ### Supported Formats
277
-
278
- | Format | Identifier | Timestamp | Compatible With |
279
- |--------|------------|-----------|-----------------|
280
- | `name` | Filename | `applied_at` (timestamp) | drizzle-multitenant native |
281
- | `hash` | SHA-256 | `created_at` (timestamp) | Custom scripts |
282
- | `drizzle-kit` | SHA-256 | `created_at` (bigint) | drizzle-kit migrate |
283
-
284
- ### Configuration
285
-
286
- ```typescript
287
- // tenant.config.ts
288
- export default defineConfig({
289
- // ...
290
- migrations: {
291
- tenantFolder: './drizzle/tenant',
292
- tenantDiscovery: async () => getTenantIds(),
293
-
294
- /**
295
- * Table format for tracking migrations
296
- * - "auto": Auto-detect existing format (default)
297
- * - "name": Filename-based (drizzle-multitenant native)
298
- * - "hash": SHA-256 hash
299
- * - "drizzle-kit": Exact drizzle-kit format
300
- */
301
- tableFormat: 'auto',
302
-
303
- /**
304
- * Format to use when creating new tables (only for "auto" mode)
305
- * @default "name"
306
- */
307
- defaultFormat: 'name',
308
- },
309
- });
310
- ```
311
-
312
- ### Migrating from drizzle-kit
313
-
314
- If you have existing databases with migrations applied via `drizzle-kit migrate`, the CLI will automatically detect the format:
315
-
316
- ```bash
317
- # Check current format for all tenants
318
- npx drizzle-multitenant status
319
-
320
- # Apply new migrations (works with any format)
321
- npx drizzle-multitenant migrate --all
322
- ```
323
-
324
- ### Converting Between Formats
325
-
326
- Use the `convert-format` command to standardize all tenants to a single format:
327
-
328
- ```bash
329
- # Preview conversion (dry-run)
330
- npx drizzle-multitenant convert-format --to=name --dry-run
331
-
332
- # Convert all tenants to name format
333
- npx drizzle-multitenant convert-format --to=name
334
-
335
- # Convert specific tenant
336
- npx drizzle-multitenant convert-format --to=name --tenant=abc-123
337
-
338
- # Convert to drizzle-kit format (for compatibility)
339
- npx drizzle-multitenant convert-format --to=drizzle-kit
340
- ```
341
-
342
- ## Cross-Schema Queries
343
-
344
- ```typescript
345
- import { createCrossSchemaQuery } from 'drizzle-multitenant/cross-schema';
346
-
347
- const query = createCrossSchemaQuery({
348
- tenantDb: tenants.getDb('tenant-123'),
349
- sharedDb: tenants.getSharedDb(),
350
- tenantSchema: 'tenant_123',
351
- sharedSchema: 'public',
352
- });
353
-
354
- // Type-safe join between tenant and shared tables
355
- const result = await query
356
- .select({
357
- orderId: orders.id,
358
- planName: subscriptionPlans.name,
359
- })
360
- .from(orders)
361
- .leftJoin(subscriptionPlans, eq(orders.planId, subscriptionPlans.id));
362
- ```
164
+ <table>
165
+ <tr>
166
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/getting-started">Getting Started</a></td>
167
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/configuration">Configuration</a></td>
168
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/cli">CLI Commands</a></td>
169
+ </tr>
170
+ <tr>
171
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/frameworks/express">Express</a></td>
172
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/frameworks/fastify">Fastify</a></td>
173
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/frameworks/nestjs">NestJS</a></td>
174
+ </tr>
175
+ <tr>
176
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/cross-schema">Cross-Schema Queries</a></td>
177
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/guide/advanced">Advanced Features</a></td>
178
+ <td><a href="https://mateusflorez.github.io/drizzle-multitenant/api/reference">API Reference</a></td>
179
+ </tr>
180
+ </table>
363
181
 
364
- ## API Reference
365
-
366
- ### Core
367
-
368
- | Export | Description |
369
- |--------|-------------|
370
- | `defineConfig()` | Create typed configuration |
371
- | `createTenantManager()` | Create pool manager instance |
372
- | `createTenantContext()` | Create AsyncLocalStorage context |
373
-
374
- ### Manager Methods
375
-
376
- | Method | Description |
377
- |--------|-------------|
378
- | `getDb(tenantId)` | Get Drizzle instance for tenant |
379
- | `getSharedDb()` | Get Drizzle instance for shared schema |
380
- | `getSchemaName(tenantId)` | Get schema name for tenant |
381
- | `hasPool(tenantId)` | Check if pool exists |
382
- | `evictPool(tenantId)` | Force evict a pool |
383
- | `dispose()` | Cleanup all pools |
384
-
385
- ### NestJS Decorators
386
-
387
- | Decorator | Description |
388
- |-----------|-------------|
389
- | `@InjectTenantDb()` | Inject tenant database (request-scoped) |
390
- | `@InjectTenantDbFactory()` | Inject factory for singleton services |
391
- | `@InjectSharedDb()` | Inject shared database |
392
- | `@InjectTenantContext()` | Inject tenant context |
393
- | `@InjectTenantManager()` | Inject tenant manager |
394
- | `@RequiresTenant()` | Mark route as requiring tenant |
395
- | `@PublicRoute()` | Mark route as public |
396
-
397
- ### TenantDbFactory Methods
398
-
399
- | Method | Description |
400
- |--------|-------------|
401
- | `getDb(tenantId)` | Get Drizzle instance for tenant |
402
- | `getSharedDb()` | Get shared database instance |
403
- | `getSchemaName(tenantId)` | Get schema name for tenant |
404
- | `getDebugInfo(tenantId)` | Get debug info (tenantId, schema, pool stats) |
405
- | `getManager()` | Get underlying TenantManager |
182
+ <br />
406
183
 
407
184
  ## Requirements
408
185
 
@@ -410,29 +187,7 @@ const result = await query
410
187
  - PostgreSQL 12+
411
188
  - Drizzle ORM 0.29+
412
189
 
413
- ## Tech Stack
414
-
415
- | Package | Purpose |
416
- |---------|---------|
417
- | `drizzle-orm` | Type-safe ORM |
418
- | `pg` | PostgreSQL driver |
419
- | `lru-cache` | Pool management |
420
- | `commander` | CLI framework |
421
- | `chalk` | Terminal styling |
422
- | `ora` | Loading spinners |
423
- | `cli-table3` | Table formatting |
424
- | `cli-progress` | Progress bars |
425
- | `@inquirer/prompts` | Interactive prompts |
426
-
427
- ## Comparison
428
-
429
- | Feature | drizzle-multitenant | Manual Implementation |
430
- |---------|---------------------|----------------------|
431
- | Pool management | Automatic LRU | Manual |
432
- | Context propagation | AsyncLocalStorage | Pass through params |
433
- | Parallel migrations | Built-in CLI | Custom scripts |
434
- | Cross-schema queries | Type-safe builder | Raw SQL |
435
- | Framework support | Express/Fastify/NestJS/Hono | DIY |
190
+ <br />
436
191
 
437
192
  ## License
438
193