drizzle-multitenant 1.0.9 → 1.1.0

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/README.md CHANGED
@@ -1,20 +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
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Documentation](https://img.shields.io/badge/docs-online-blue.svg)](https://mateusflorez.github.io/drizzle-multitenant/)
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>
12
+
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 />
8
21
 
9
22
  ## Features
10
23
 
11
- - **Schema Isolation** - PostgreSQL schema-per-tenant with LRU pool management
12
- - **Context Propagation** - AsyncLocalStorage-based tenant context
13
- - **Parallel Migrations** - Concurrent migrations with progress tracking
14
- - **Cross-Schema Queries** - Type-safe joins between tenant and shared tables
15
- - **Connection Retry** - Automatic retry with exponential backoff
16
- - **Framework Support** - Express, Fastify, NestJS middleware/plugins
17
- - **CLI Tools** - Generate, migrate, status, tenant management
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 />
18
34
 
19
35
  ## Installation
20
36
 
@@ -22,8 +38,12 @@ Multi-tenancy toolkit for Drizzle ORM with schema isolation, tenant context, and
22
38
  npm install drizzle-multitenant drizzle-orm pg
23
39
  ```
24
40
 
41
+ <br />
42
+
25
43
  ## Quick Start
26
44
 
45
+ ### 1. Define your configuration
46
+
27
47
  ```typescript
28
48
  // tenant.config.ts
29
49
  import { defineConfig } from 'drizzle-multitenant';
@@ -39,6 +59,8 @@ export default defineConfig({
39
59
  });
40
60
  ```
41
61
 
62
+ ### 2. Create the tenant manager
63
+
42
64
  ```typescript
43
65
  // app.ts
44
66
  import { createTenantManager } from 'drizzle-multitenant';
@@ -46,51 +68,118 @@ import config from './tenant.config';
46
68
 
47
69
  const tenants = createTenantManager(config);
48
70
 
49
- // Get typed DB for a tenant
50
- const db = tenants.getDb('tenant-123');
71
+ // Type-safe database for each tenant
72
+ const db = tenants.getDb('acme');
51
73
  const users = await db.select().from(schema.users);
52
-
53
- // With retry and validation
54
- const db = await tenants.getDbAsync('tenant-123');
55
74
  ```
56
75
 
57
- ## CLI
76
+ <br />
77
+
78
+ ## CLI Commands
58
79
 
59
80
  ```bash
60
- npx drizzle-multitenant init # Setup wizard
61
- npx drizzle-multitenant generate --name=users # Create migration
62
- npx drizzle-multitenant migrate --all # Apply to all tenants
63
- npx drizzle-multitenant status # Check status
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
64
86
  ```
65
87
 
88
+ <br />
89
+
66
90
  ## Framework Integrations
67
91
 
92
+ <details>
93
+ <summary><strong>Express</strong></summary>
94
+
68
95
  ```typescript
69
- // Express
70
96
  import { createExpressMiddleware } from 'drizzle-multitenant/express';
71
- app.use(createExpressMiddleware({ manager: tenants, extractTenantId: (req) => req.headers['x-tenant-id'] }));
72
97
 
73
- // Fastify
98
+ app.use(createExpressMiddleware({
99
+ manager: tenants,
100
+ extractTenantId: (req) => req.headers['x-tenant-id'] as string,
101
+ }));
102
+
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
+ });
108
+ ```
109
+
110
+ </details>
111
+
112
+ <details>
113
+ <summary><strong>Fastify</strong></summary>
114
+
115
+ ```typescript
74
116
  import { fastifyTenantPlugin } from 'drizzle-multitenant/fastify';
75
- fastify.register(fastifyTenantPlugin, { manager: tenants, extractTenantId: (req) => req.headers['x-tenant-id'] });
76
117
 
77
- // NestJS
118
+ fastify.register(fastifyTenantPlugin, {
119
+ manager: tenants,
120
+ extractTenantId: (req) => req.headers['x-tenant-id'] as string,
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
+ ```
129
+
130
+ </details>
131
+
132
+ <details>
133
+ <summary><strong>NestJS</strong></summary>
134
+
135
+ ```typescript
78
136
  import { TenantModule, InjectTenantDb } from 'drizzle-multitenant/nestjs';
79
- @Module({ imports: [TenantModule.forRoot({ config, extractTenantId: (req) => req.headers['x-tenant-id'] })] })
137
+
138
+ @Module({
139
+ imports: [
140
+ TenantModule.forRoot({
141
+ config,
142
+ extractTenantId: (req) => req.headers['x-tenant-id'],
143
+ }),
144
+ ],
145
+ })
146
+ export class AppModule {}
147
+
148
+ @Injectable({ scope: Scope.REQUEST })
149
+ export class UserService {
150
+ constructor(@InjectTenantDb() private db: TenantDb) {}
151
+
152
+ findAll() {
153
+ return this.db.select().from(users);
154
+ }
155
+ }
80
156
  ```
81
157
 
82
- ## Documentation
158
+ </details>
83
159
 
84
- **[Read the full documentation →](https://mateusflorez.github.io/drizzle-multitenant/)**
160
+ <br />
85
161
 
86
- - [Getting Started](https://mateusflorez.github.io/drizzle-multitenant/guide/getting-started)
87
- - [Configuration](https://mateusflorez.github.io/drizzle-multitenant/guide/configuration)
88
- - [Framework Integrations](https://mateusflorez.github.io/drizzle-multitenant/guide/frameworks/express)
89
- - [CLI Commands](https://mateusflorez.github.io/drizzle-multitenant/guide/cli)
90
- - [Cross-Schema Queries](https://mateusflorez.github.io/drizzle-multitenant/guide/cross-schema)
91
- - [Advanced Features](https://mateusflorez.github.io/drizzle-multitenant/guide/advanced)
92
- - [API Reference](https://mateusflorez.github.io/drizzle-multitenant/api/reference)
93
- - [Examples](https://mateusflorez.github.io/drizzle-multitenant/examples/)
162
+ ## Documentation
163
+
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>
181
+
182
+ <br />
94
183
 
95
184
  ## Requirements
96
185
 
@@ -98,6 +187,8 @@ import { TenantModule, InjectTenantDb } from 'drizzle-multitenant/nestjs';
98
187
  - PostgreSQL 12+
99
188
  - Drizzle ORM 0.29+
100
189
 
190
+ <br />
191
+
101
192
  ## License
102
193
 
103
194
  MIT
@@ -1,4 +1,4 @@
1
- import { T as TenantManager, a as TenantDb, S as SharedDb } from './types-B5eSRLFW.js';
1
+ import { T as TenantManager, a as TenantDb, S as SharedDb } from './types-BhK96FPC.js';
2
2
 
3
3
  /**
4
4
  * Base tenant context data
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { C as Config, T as TenantManager, R as RetryConfig } from './types-B5eSRLFW.js';
2
- export { b as ConnectionConfig, h as DEFAULT_CONFIG, D as DebugConfig, e as DebugContext, H as Hooks, I as IsolationConfig, c as IsolationStrategy, M as MetricsConfig, P as PoolEntry, d as SchemasConfig, S as SharedDb, a as TenantDb, g as TenantWarmupResult, W as WarmupOptions, f as WarmupResult } from './types-B5eSRLFW.js';
3
- export { B as BaseTenantContext, a as TenantContext, T as TenantContextData, c as createTenantContext } from './context-DoHx79MS.js';
1
+ import { C as Config, T as TenantManager, R as RetryConfig } from './types-BhK96FPC.js';
2
+ export { b as ConnectionConfig, n as ConnectionMetrics, o as DEFAULT_CONFIG, D as DebugConfig, e as DebugContext, h as HealthCheckOptions, i as HealthCheckResult, H as Hooks, I as IsolationConfig, c as IsolationStrategy, M as MetricsConfig, l as MetricsResult, P as PoolEntry, j as PoolHealth, k as PoolHealthStatus, d as SchemasConfig, S as SharedDb, a as TenantDb, m as TenantPoolMetrics, g as TenantWarmupResult, W as WarmupOptions, f as WarmupResult } from './types-BhK96FPC.js';
3
+ export { B as BaseTenantContext, a as TenantContext, T as TenantContextData, c as createTenantContext } from './context-Vki959ri.js';
4
4
  export { AppliedMigration, CreateTenantOptions, DropTenantOptions, MigrateOptions, MigrationErrorHandler, MigrationFile, MigrationHooks, MigrationProgressCallback, MigrationResults, Migrator, MigratorConfig, TenantMigrationResult, TenantMigrationStatus, createMigrator } from './migrator/index.js';
5
5
  export { ColumnSelection, CrossSchemaContext, CrossSchemaQueryBuilder, CrossSchemaRawOptions, InferSelectResult, InferSelectedColumns, JoinCondition, JoinDefinition, JoinType, LookupResult, SchemaSource, SharedLookupConfig, TableReference, WithSharedConfig, WithSharedOptions, WithSharedQueryBuilder, buildCrossSchemaSelect, createCrossSchemaQuery, crossSchemaRaw, withShared, withSharedLookup } from './cross-schema/index.js';
6
6
  import 'pg';
package/dist/index.js CHANGED
@@ -624,6 +624,227 @@ var PoolManager = class {
624
624
  details: results
625
625
  };
626
626
  }
627
+ /**
628
+ * Get current metrics for all pools
629
+ *
630
+ * Collects metrics on demand with zero overhead when not called.
631
+ * Returns raw data that can be formatted for any monitoring system.
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * const metrics = manager.getMetrics();
636
+ * console.log(metrics.pools.total); // 15
637
+ *
638
+ * // Format for Prometheus
639
+ * for (const pool of metrics.pools.tenants) {
640
+ * gauge.labels(pool.tenantId).set(pool.connections.idle);
641
+ * }
642
+ * ```
643
+ */
644
+ getMetrics() {
645
+ this.ensureNotDisposed();
646
+ const maxPools = this.config.isolation.maxPools ?? DEFAULT_CONFIG.maxPools;
647
+ const tenantMetrics = [];
648
+ for (const [schemaName, entry] of this.pools.entries()) {
649
+ const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
650
+ const pool = entry.pool;
651
+ tenantMetrics.push({
652
+ tenantId,
653
+ schemaName,
654
+ connections: {
655
+ total: pool.totalCount,
656
+ idle: pool.idleCount,
657
+ waiting: pool.waitingCount
658
+ },
659
+ lastAccessedAt: new Date(entry.lastAccess).toISOString()
660
+ });
661
+ }
662
+ return {
663
+ pools: {
664
+ total: tenantMetrics.length,
665
+ maxPools,
666
+ tenants: tenantMetrics
667
+ },
668
+ shared: {
669
+ initialized: this.sharedPool !== null,
670
+ connections: this.sharedPool ? {
671
+ total: this.sharedPool.totalCount,
672
+ idle: this.sharedPool.idleCount,
673
+ waiting: this.sharedPool.waitingCount
674
+ } : null
675
+ },
676
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
677
+ };
678
+ }
679
+ /**
680
+ * Check health of all pools and connections
681
+ *
682
+ * Verifies the health of tenant pools and optionally the shared database.
683
+ * Returns detailed status information for monitoring and load balancer integration.
684
+ *
685
+ * @example
686
+ * ```typescript
687
+ * // Basic health check
688
+ * const health = await manager.healthCheck();
689
+ * console.log(health.healthy); // true/false
690
+ *
691
+ * // Use with Express endpoint
692
+ * app.get('/health', async (req, res) => {
693
+ * const health = await manager.healthCheck();
694
+ * res.status(health.healthy ? 200 : 503).json(health);
695
+ * });
696
+ *
697
+ * // Check specific tenants only
698
+ * const health = await manager.healthCheck({
699
+ * tenantIds: ['tenant-1', 'tenant-2'],
700
+ * ping: true,
701
+ * pingTimeoutMs: 3000,
702
+ * });
703
+ * ```
704
+ */
705
+ async healthCheck(options = {}) {
706
+ this.ensureNotDisposed();
707
+ const startTime = Date.now();
708
+ const {
709
+ ping = true,
710
+ pingTimeoutMs = 5e3,
711
+ includeShared = true,
712
+ tenantIds
713
+ } = options;
714
+ const poolHealthResults = [];
715
+ let sharedDbStatus = "ok";
716
+ let sharedDbResponseTimeMs;
717
+ let sharedDbError;
718
+ const poolsToCheck = [];
719
+ if (tenantIds && tenantIds.length > 0) {
720
+ for (const tenantId of tenantIds) {
721
+ const schemaName = this.config.isolation.schemaNameTemplate(tenantId);
722
+ const entry = this.pools.get(schemaName);
723
+ if (entry) {
724
+ poolsToCheck.push({ schemaName, tenantId, entry });
725
+ }
726
+ }
727
+ } else {
728
+ for (const [schemaName, entry] of this.pools.entries()) {
729
+ const tenantId = this.tenantIdBySchema.get(schemaName) ?? schemaName;
730
+ poolsToCheck.push({ schemaName, tenantId, entry });
731
+ }
732
+ }
733
+ const poolChecks = poolsToCheck.map(async ({ schemaName, tenantId, entry }) => {
734
+ const poolHealth = await this.checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs);
735
+ return poolHealth;
736
+ });
737
+ poolHealthResults.push(...await Promise.all(poolChecks));
738
+ if (includeShared && this.sharedPool) {
739
+ const sharedResult = await this.checkSharedDbHealth(ping, pingTimeoutMs);
740
+ sharedDbStatus = sharedResult.status;
741
+ sharedDbResponseTimeMs = sharedResult.responseTimeMs;
742
+ sharedDbError = sharedResult.error;
743
+ }
744
+ const degradedPools = poolHealthResults.filter((p) => p.status === "degraded").length;
745
+ const unhealthyPools = poolHealthResults.filter((p) => p.status === "unhealthy").length;
746
+ const healthy = unhealthyPools === 0 && sharedDbStatus !== "unhealthy";
747
+ return {
748
+ healthy,
749
+ pools: poolHealthResults,
750
+ sharedDb: sharedDbStatus,
751
+ sharedDbResponseTimeMs,
752
+ sharedDbError,
753
+ totalPools: poolHealthResults.length,
754
+ degradedPools,
755
+ unhealthyPools,
756
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
757
+ durationMs: Date.now() - startTime
758
+ };
759
+ }
760
+ /**
761
+ * Check health of a single tenant pool
762
+ */
763
+ async checkPoolHealth(tenantId, schemaName, entry, ping, pingTimeoutMs) {
764
+ const pool = entry.pool;
765
+ const totalConnections = pool.totalCount;
766
+ const idleConnections = pool.idleCount;
767
+ const waitingRequests = pool.waitingCount;
768
+ let status = "ok";
769
+ let responseTimeMs;
770
+ let error;
771
+ if (waitingRequests > 0) {
772
+ status = "degraded";
773
+ }
774
+ if (ping) {
775
+ const pingResult = await this.executePingQuery(pool, pingTimeoutMs);
776
+ responseTimeMs = pingResult.responseTimeMs;
777
+ if (!pingResult.success) {
778
+ status = "unhealthy";
779
+ error = pingResult.error;
780
+ } else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
781
+ if (status === "ok") {
782
+ status = "degraded";
783
+ }
784
+ }
785
+ }
786
+ return {
787
+ tenantId,
788
+ schemaName,
789
+ status,
790
+ totalConnections,
791
+ idleConnections,
792
+ waitingRequests,
793
+ responseTimeMs,
794
+ error
795
+ };
796
+ }
797
+ /**
798
+ * Check health of shared database
799
+ */
800
+ async checkSharedDbHealth(ping, pingTimeoutMs) {
801
+ if (!this.sharedPool) {
802
+ return { status: "ok" };
803
+ }
804
+ let status = "ok";
805
+ let responseTimeMs;
806
+ let error;
807
+ const waitingRequests = this.sharedPool.waitingCount;
808
+ if (waitingRequests > 0) {
809
+ status = "degraded";
810
+ }
811
+ if (ping) {
812
+ const pingResult = await this.executePingQuery(this.sharedPool, pingTimeoutMs);
813
+ responseTimeMs = pingResult.responseTimeMs;
814
+ if (!pingResult.success) {
815
+ status = "unhealthy";
816
+ error = pingResult.error;
817
+ } else if (pingResult.responseTimeMs && pingResult.responseTimeMs > pingTimeoutMs / 2) {
818
+ if (status === "ok") {
819
+ status = "degraded";
820
+ }
821
+ }
822
+ }
823
+ return { status, responseTimeMs, error };
824
+ }
825
+ /**
826
+ * Execute a ping query with timeout
827
+ */
828
+ async executePingQuery(pool, timeoutMs) {
829
+ const startTime = Date.now();
830
+ try {
831
+ const timeoutPromise = new Promise((_, reject) => {
832
+ setTimeout(() => reject(new Error("Health check ping timeout")), timeoutMs);
833
+ });
834
+ const queryPromise = pool.query("SELECT 1");
835
+ await Promise.race([queryPromise, timeoutPromise]);
836
+ return {
837
+ success: true,
838
+ responseTimeMs: Date.now() - startTime
839
+ };
840
+ } catch (err) {
841
+ return {
842
+ success: false,
843
+ responseTimeMs: Date.now() - startTime,
844
+ error: err.message
845
+ };
846
+ }
847
+ }
627
848
  /**
628
849
  * Manually evict a tenant pool
629
850
  */
@@ -795,6 +1016,12 @@ function createTenantManager(config) {
795
1016
  async warmup(tenantIds, options) {
796
1017
  return poolManager.warmup(tenantIds, options);
797
1018
  },
1019
+ async healthCheck(options) {
1020
+ return poolManager.healthCheck(options);
1021
+ },
1022
+ getMetrics() {
1023
+ return poolManager.getMetrics();
1024
+ },
798
1025
  async dispose() {
799
1026
  await poolManager.dispose();
800
1027
  }