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 +129 -38
- package/dist/{context-DoHx79MS.d.ts → context-Vki959ri.d.ts} +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +227 -0
- package/dist/index.js.map +1 -1
- package/dist/integrations/express.d.ts +3 -3
- package/dist/integrations/fastify.d.ts +3 -3
- package/dist/integrations/nestjs/index.d.ts +1 -1
- package/dist/integrations/nestjs/index.js +227 -0
- package/dist/integrations/nestjs/index.js.map +1 -1
- package/dist/migrator/index.d.ts +1 -1
- package/dist/{types-B5eSRLFW.d.ts → types-BhK96FPC.d.ts} +115 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/banner.svg" alt="drizzle-multitenant" width="500" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>Multi-tenancy toolkit for Drizzle ORM</strong>
|
|
7
|
+
</p>
|
|
6
8
|
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
//
|
|
50
|
-
const db = tenants.getDb('
|
|
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
|
-
|
|
76
|
+
<br />
|
|
77
|
+
|
|
78
|
+
## CLI Commands
|
|
58
79
|
|
|
59
80
|
```bash
|
|
60
|
-
npx drizzle-multitenant init
|
|
61
|
-
npx drizzle-multitenant generate --name=users
|
|
62
|
-
npx drizzle-multitenant migrate --all
|
|
63
|
-
npx drizzle-multitenant 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
</details>
|
|
83
159
|
|
|
84
|
-
|
|
160
|
+
<br />
|
|
85
161
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { C as Config, T as TenantManager, R as RetryConfig } from './types-
|
|
2
|
-
export { b as ConnectionConfig,
|
|
3
|
-
export { B as BaseTenantContext, a as TenantContext, T as TenantContextData, c as createTenantContext } from './context-
|
|
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
|
}
|