@refpool/nestjs 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Atul Singh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # @refpool/nestjs
2
+
3
+ NestJS integration for [`@refpool/core`](../core): a dynamic module, an injectable
4
+ service with lifecycle hooks, multi-tenant connection middleware, and a
5
+ connection-health controller.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @refpool/nestjs @refpool/core
11
+ ```
12
+
13
+ Peer dependencies (required): `@nestjs/common`, `@nestjs/core`,
14
+ `reflect-metadata`, `rxjs`.
15
+
16
+ ## Module wiring
17
+
18
+ `RefPoolModule.forRoot` takes the same [`PoolOptions`](../core#pooloptionst) as the
19
+ core pool, plus a few module-level flags (`isGlobal`, `warmOnInit`, `middleware`).
20
+
21
+ ```ts
22
+ import { Module } from '@nestjs/common';
23
+ import { RefPoolModule } from '@refpool/nestjs';
24
+ import { Pool } from 'pg';
25
+
26
+ @Module({
27
+ imports: [
28
+ RefPoolModule.forRoot<Pool>({
29
+ isGlobal: true,
30
+ max: 20,
31
+ idleTtlMs: 60_000,
32
+ factory: async (tenantId) =>
33
+ new Pool({ connectionString: `postgres://localhost:5432/tenant_${tenantId}` }),
34
+ dispose: async (pool) => pool.end(),
35
+ middleware: { header: 'x-tenant-id', onMissing: 'error' },
36
+ }),
37
+ ],
38
+ })
39
+ export class AppModule {}
40
+ ```
41
+
42
+ Async configuration (inject config/other providers):
43
+
44
+ ```ts
45
+ RefPoolModule.forRootAsync<Pool>({
46
+ inject: [ConfigService],
47
+ useFactory: (config: ConfigService) => ({
48
+ max: config.get('REFPOOL_MAX', 20),
49
+ factory: async (tenantId) => new Pool({ connectionString: config.urlFor(tenantId) }),
50
+ dispose: async (pool) => pool.end(),
51
+ }),
52
+ });
53
+ ```
54
+
55
+ `RefPoolService` starts the pool on `OnModuleInit` (and runs `warm()` when
56
+ `prewarm` is set or `warmOnInit: true`) and drains it on `OnApplicationShutdown`.
57
+ Enable shutdown hooks in `main.ts` with `app.enableShutdownHooks()`.
58
+
59
+ ## RefPoolService
60
+
61
+ Inject it anywhere to use the pool:
62
+
63
+ ```ts
64
+ import { Injectable } from '@nestjs/common';
65
+ import { RefPoolService } from '@refpool/nestjs';
66
+ import type { Pool } from 'pg';
67
+
68
+ @Injectable()
69
+ export class ReportService {
70
+ constructor(private readonly refpool: RefPoolService<Pool>) {}
71
+
72
+ run(tenantId: string) {
73
+ // acquire → run → release (released even on throw)
74
+ return this.refpool.withResource(tenantId, (pool) => pool.query('select now()'));
75
+ }
76
+
77
+ stats() {
78
+ return this.refpool.getStats();
79
+ }
80
+ }
81
+ ```
82
+
83
+ `RefPoolService<T>` exposes `acquire(key)`, `withResource(key, fn)`, `getStats()`,
84
+ and the underlying `.pool` (for event subscription, etc.).
85
+
86
+ ## Tenant middleware
87
+
88
+ `TenantConnectionMiddleware` reads the tenant key from a request header
89
+ (default `x-tenant-id`), acquires that tenant's resource for the lifetime of the
90
+ request, and releases it when the response finishes/closes. The resource is
91
+ attached to the request (default property `tenantResource`, plus
92
+ `tenantResourceKey`).
93
+
94
+ ### Drop-in (auto-applied)
95
+
96
+ `RefPoolModule` implements `NestModule` and wires the middleware for you when you
97
+ opt in via `applyMiddleware: true` or `middleware.routes`. No `configure()` in
98
+ your `AppModule` required:
99
+
100
+ ```ts
101
+ @Module({
102
+ imports: [
103
+ RefPoolModule.forRoot<Pool>({
104
+ max: 20,
105
+ factory: async (tenantId) => new Pool({ /* ... */ }),
106
+ // Opt in to auto-wiring. Either flag works:
107
+ applyMiddleware: true, // applies to '*' (all routes)
108
+ middleware: {
109
+ header: 'x-tenant-id',
110
+ onMissing: 'error',
111
+ routes: ['tenant/*'], // or scope to specific routes
112
+ },
113
+ }),
114
+ ],
115
+ })
116
+ export class AppModule {}
117
+ ```
118
+
119
+ `applyMiddleware` defaults to `true` when `middleware.routes` is set, otherwise
120
+ `false`. When enabled without explicit `routes`, the middleware is applied to
121
+ `'*'`.
122
+
123
+ ### Manual wiring
124
+
125
+ You can still apply it yourself for full control over the route configuration:
126
+
127
+ ```ts
128
+ import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
129
+ import { TenantConnectionMiddleware } from '@refpool/nestjs';
130
+
131
+ @Module({ /* imports: [RefPoolModule.forRoot(...)] */ })
132
+ export class AppModule implements NestModule {
133
+ configure(consumer: MiddlewareConsumer) {
134
+ consumer.apply(TenantConnectionMiddleware).forRoutes('tenant/*');
135
+ }
136
+ }
137
+ ```
138
+
139
+ Middleware options (set via `forRoot({ middleware })`): `header`,
140
+ `onMissing` (`'next' | 'error'`), `requestProperty`, `routes`. Plus the
141
+ module-level `applyMiddleware` flag.
142
+
143
+ ## Health route
144
+
145
+ `RefPoolModule` automatically mounts `ConnectionHealthController`, exposing:
146
+
147
+ ```
148
+ GET /health/connections → PoolStats (live, idle, inUse, waiters, hits, …)
149
+ ```
150
+
151
+ ## Exports
152
+
153
+ `RefPoolModule`, `RefPoolService`, `TenantConnectionMiddleware`,
154
+ `ConnectionHealthController`, the DI tokens `REFPOOL_OPTIONS` /
155
+ `REFPOOL_MIDDLEWARE_OPTIONS`, and the option types.
156
+
157
+ ## License
158
+
159
+ MIT © Atul Singh
package/dist/index.cjs ADDED
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var __decorateClass = (decorators, target, key, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
+ if (decorator = decorators[i])
23
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
24
+ if (kind && result) __defProp(target, key, result);
25
+ return result;
26
+ };
27
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
28
+
29
+ // src/index.ts
30
+ var index_exports = {};
31
+ __export(index_exports, {
32
+ ConnectionHealthController: () => ConnectionHealthController,
33
+ REFPOOL_MIDDLEWARE_OPTIONS: () => REFPOOL_MIDDLEWARE_OPTIONS,
34
+ REFPOOL_OPTIONS: () => REFPOOL_OPTIONS,
35
+ RefPoolModule: () => RefPoolModule,
36
+ RefPoolService: () => RefPoolService,
37
+ TenantConnectionMiddleware: () => TenantConnectionMiddleware
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/ref-pool.module.ts
42
+ var import_common4 = require("@nestjs/common");
43
+
44
+ // src/health.controller.ts
45
+ var import_common2 = require("@nestjs/common");
46
+
47
+ // src/ref-pool.service.ts
48
+ var import_common = require("@nestjs/common");
49
+ var import_core = require("@refpool/core");
50
+
51
+ // src/tokens.ts
52
+ var REFPOOL_OPTIONS = /* @__PURE__ */ Symbol.for("refpool:options");
53
+ var REFPOOL_MIDDLEWARE_OPTIONS = /* @__PURE__ */ Symbol.for("refpool:middleware-options");
54
+
55
+ // src/ref-pool.service.ts
56
+ var RefPoolService = class {
57
+ constructor(options) {
58
+ this.options = options;
59
+ this.pool = new import_core.RefCountedLruPool(options);
60
+ }
61
+ async onModuleInit() {
62
+ this.pool.start();
63
+ const shouldWarm = this.options.warmOnInit ?? Boolean(this.options.prewarm);
64
+ if (shouldWarm) {
65
+ await this.pool.warm();
66
+ }
67
+ }
68
+ async onApplicationShutdown() {
69
+ await this.pool.stop();
70
+ }
71
+ acquire(key) {
72
+ return this.pool.acquire(key);
73
+ }
74
+ /** Acquire a resource for `key`, run `fn`, and release the handle regardless of outcome. */
75
+ async withResource(key, fn) {
76
+ const handle = await this.pool.acquire(key);
77
+ try {
78
+ return await fn(handle.resource);
79
+ } finally {
80
+ handle.release();
81
+ }
82
+ }
83
+ getStats() {
84
+ return this.pool.getStats();
85
+ }
86
+ };
87
+ RefPoolService = __decorateClass([
88
+ (0, import_common.Injectable)(),
89
+ __decorateParam(0, (0, import_common.Inject)(REFPOOL_OPTIONS))
90
+ ], RefPoolService);
91
+
92
+ // src/health.controller.ts
93
+ var ConnectionHealthController = class {
94
+ constructor(service) {
95
+ this.service = service;
96
+ }
97
+ connections() {
98
+ return this.service.getStats();
99
+ }
100
+ };
101
+ __decorateClass([
102
+ (0, import_common2.Get)("connections")
103
+ ], ConnectionHealthController.prototype, "connections", 1);
104
+ ConnectionHealthController = __decorateClass([
105
+ (0, import_common2.Controller)("health"),
106
+ __decorateParam(0, (0, import_common2.Inject)(RefPoolService))
107
+ ], ConnectionHealthController);
108
+
109
+ // src/tenant-connection.middleware.ts
110
+ var import_common3 = require("@nestjs/common");
111
+ var DEFAULT_HEADER = "x-tenant-id";
112
+ var DEFAULT_PROPERTY = "tenantResource";
113
+ var TenantConnectionMiddleware = class {
114
+ constructor(service, options) {
115
+ this.service = service;
116
+ this.options = options;
117
+ }
118
+ async use(req, res, next) {
119
+ const header = (this.options.header ?? DEFAULT_HEADER).toLowerCase();
120
+ const property = this.options.requestProperty ?? DEFAULT_PROPERTY;
121
+ const onMissing = this.options.onMissing ?? "next";
122
+ const raw = req.headers?.[header];
123
+ const tenantId = Array.isArray(raw) ? raw[0] : raw;
124
+ if (!tenantId) {
125
+ if (onMissing === "error") {
126
+ next(new Error(`Missing required tenant header "${header}"`));
127
+ return;
128
+ }
129
+ next();
130
+ return;
131
+ }
132
+ let handle;
133
+ try {
134
+ handle = await this.service.acquire(tenantId);
135
+ } catch (error) {
136
+ next(error);
137
+ return;
138
+ }
139
+ let released = false;
140
+ const release = () => {
141
+ if (released) return;
142
+ released = true;
143
+ handle.release();
144
+ };
145
+ res.on("finish", release);
146
+ res.on("close", release);
147
+ req[property] = handle.resource;
148
+ req[`${property}Key`] = tenantId;
149
+ next();
150
+ }
151
+ };
152
+ TenantConnectionMiddleware = __decorateClass([
153
+ (0, import_common3.Injectable)(),
154
+ __decorateParam(0, (0, import_common3.Inject)(RefPoolService)),
155
+ __decorateParam(1, (0, import_common3.Inject)(REFPOOL_MIDDLEWARE_OPTIONS))
156
+ ], TenantConnectionMiddleware);
157
+
158
+ // src/ref-pool.module.ts
159
+ var SHARED = [RefPoolService, TenantConnectionMiddleware];
160
+ var EXPORTS = [
161
+ RefPoolService,
162
+ TenantConnectionMiddleware,
163
+ REFPOOL_OPTIONS,
164
+ REFPOOL_MIDDLEWARE_OPTIONS
165
+ ];
166
+ var RefPoolModule = class {
167
+ constructor(options) {
168
+ this.options = options;
169
+ }
170
+ /**
171
+ * Auto-wires {@link TenantConnectionMiddleware} when the drop-in multi-tenant
172
+ * path is opted into via `applyMiddleware` or `middleware.routes`. Defaults to
173
+ * `'*'` (all routes) when enabled without explicit routes.
174
+ */
175
+ configure(consumer) {
176
+ const routes = this.options.middleware?.routes;
177
+ const shouldApply = this.options.applyMiddleware ?? routes !== void 0;
178
+ if (!shouldApply) return;
179
+ const forRoutes = routes === void 0 ? ["*"] : Array.isArray(routes) ? routes : [routes];
180
+ consumer.apply(TenantConnectionMiddleware).forRoutes(...forRoutes);
181
+ }
182
+ static forRoot(options) {
183
+ const providers = [
184
+ { provide: REFPOOL_OPTIONS, useValue: options },
185
+ { provide: REFPOOL_MIDDLEWARE_OPTIONS, useValue: options.middleware ?? {} },
186
+ ...SHARED
187
+ ];
188
+ return {
189
+ module: RefPoolModule,
190
+ global: options.isGlobal ?? false,
191
+ providers,
192
+ controllers: [ConnectionHealthController],
193
+ exports: EXPORTS
194
+ };
195
+ }
196
+ static forRootAsync(options) {
197
+ const optionsProvider = {
198
+ provide: REFPOOL_OPTIONS,
199
+ useFactory: options.useFactory,
200
+ inject: options.inject ?? []
201
+ };
202
+ const middlewareOptionsProvider = {
203
+ provide: REFPOOL_MIDDLEWARE_OPTIONS,
204
+ useFactory: (resolved) => resolved.middleware ?? {},
205
+ inject: [REFPOOL_OPTIONS]
206
+ };
207
+ return {
208
+ module: RefPoolModule,
209
+ global: options.isGlobal ?? false,
210
+ imports: options.imports ?? [],
211
+ providers: [optionsProvider, middlewareOptionsProvider, ...SHARED],
212
+ controllers: [ConnectionHealthController],
213
+ exports: EXPORTS
214
+ };
215
+ }
216
+ };
217
+ RefPoolModule = __decorateClass([
218
+ (0, import_common4.Module)({}),
219
+ __decorateParam(0, (0, import_common4.Inject)(REFPOOL_OPTIONS))
220
+ ], RefPoolModule);
221
+ // Annotate the CommonJS export names for ESM import in node:
222
+ 0 && (module.exports = {
223
+ ConnectionHealthController,
224
+ REFPOOL_MIDDLEWARE_OPTIONS,
225
+ REFPOOL_OPTIONS,
226
+ RefPoolModule,
227
+ RefPoolService,
228
+ TenantConnectionMiddleware
229
+ });
230
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/ref-pool.module.ts","../src/health.controller.ts","../src/ref-pool.service.ts","../src/tokens.ts","../src/tenant-connection.middleware.ts"],"sourcesContent":["export { RefPoolModule } from './ref-pool.module.js';\nexport { RefPoolService } from './ref-pool.service.js';\nexport { TenantConnectionMiddleware } from './tenant-connection.middleware.js';\nexport { ConnectionHealthController } from './health.controller.js';\nexport { REFPOOL_OPTIONS, REFPOOL_MIDDLEWARE_OPTIONS } from './tokens.js';\nexport type {\n RefPoolModuleOptions,\n RefPoolModuleAsyncOptions,\n TenantMiddlewareOptions,\n MissingTenantBehavior,\n} from './interfaces.js';\n","import { Inject, Module } from '@nestjs/common';\nimport type {\n DynamicModule,\n MiddlewareConsumer,\n NestModule,\n Provider,\n} from '@nestjs/common';\nimport { ConnectionHealthController } from './health.controller.js';\nimport { RefPoolService } from './ref-pool.service.js';\nimport { TenantConnectionMiddleware } from './tenant-connection.middleware.js';\nimport { REFPOOL_MIDDLEWARE_OPTIONS, REFPOOL_OPTIONS } from './tokens.js';\nimport type {\n RefPoolModuleAsyncOptions,\n RefPoolModuleOptions,\n TenantMiddlewareOptions,\n} from './interfaces.js';\n\nconst SHARED = [RefPoolService, TenantConnectionMiddleware];\nconst EXPORTS = [\n RefPoolService,\n TenantConnectionMiddleware,\n REFPOOL_OPTIONS,\n REFPOOL_MIDDLEWARE_OPTIONS,\n];\n\n@Module({})\nexport class RefPoolModule implements NestModule {\n constructor(\n @Inject(REFPOOL_OPTIONS) private readonly options: RefPoolModuleOptions,\n ) {}\n\n /**\n * Auto-wires {@link TenantConnectionMiddleware} when the drop-in multi-tenant\n * path is opted into via `applyMiddleware` or `middleware.routes`. Defaults to\n * `'*'` (all routes) when enabled without explicit routes.\n */\n configure(consumer: MiddlewareConsumer): void {\n const routes = this.options.middleware?.routes;\n const shouldApply = this.options.applyMiddleware ?? routes !== undefined;\n if (!shouldApply) return;\n const forRoutes = routes === undefined ? ['*'] : Array.isArray(routes) ? routes : [routes];\n consumer.apply(TenantConnectionMiddleware).forRoutes(...forRoutes);\n }\n\n static forRoot<T = unknown>(options: RefPoolModuleOptions<T>): DynamicModule {\n const providers: Provider[] = [\n { provide: REFPOOL_OPTIONS, useValue: options },\n { provide: REFPOOL_MIDDLEWARE_OPTIONS, useValue: options.middleware ?? {} },\n ...SHARED,\n ];\n return {\n module: RefPoolModule,\n global: options.isGlobal ?? false,\n providers,\n controllers: [ConnectionHealthController],\n exports: EXPORTS,\n };\n }\n\n static forRootAsync<T = unknown>(options: RefPoolModuleAsyncOptions<T>): DynamicModule {\n const optionsProvider: Provider = {\n provide: REFPOOL_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n };\n const middlewareOptionsProvider: Provider = {\n provide: REFPOOL_MIDDLEWARE_OPTIONS,\n useFactory: (resolved: RefPoolModuleOptions<T>): TenantMiddlewareOptions =>\n resolved.middleware ?? {},\n inject: [REFPOOL_OPTIONS],\n };\n return {\n module: RefPoolModule,\n global: options.isGlobal ?? false,\n imports: options.imports ?? [],\n providers: [optionsProvider, middlewareOptionsProvider, ...SHARED],\n controllers: [ConnectionHealthController],\n exports: EXPORTS,\n };\n }\n}\n","import { Controller, Get, Inject } from '@nestjs/common';\nimport type { PoolStats } from '@refpool/core';\nimport { RefPoolService } from './ref-pool.service.js';\n\n@Controller('health')\nexport class ConnectionHealthController {\n constructor(@Inject(RefPoolService) private readonly service: RefPoolService) {}\n\n @Get('connections')\n connections(): PoolStats {\n return this.service.getStats();\n }\n}\n","import { Inject, Injectable } from '@nestjs/common';\nimport type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';\nimport { RefCountedLruPool } from '@refpool/core';\nimport type { AcquireHandle, PoolStats } from '@refpool/core';\nimport { REFPOOL_OPTIONS } from './tokens.js';\nimport type { RefPoolModuleOptions } from './interfaces.js';\n\n@Injectable()\nexport class RefPoolService<T = unknown> implements OnModuleInit, OnApplicationShutdown {\n /** The underlying pool. Exposed for advanced use (event subscription, etc.). */\n readonly pool: RefCountedLruPool<T>;\n\n constructor(@Inject(REFPOOL_OPTIONS) private readonly options: RefPoolModuleOptions<T>) {\n this.pool = new RefCountedLruPool<T>(options);\n }\n\n async onModuleInit(): Promise<void> {\n this.pool.start();\n const shouldWarm = this.options.warmOnInit ?? Boolean(this.options.prewarm);\n if (shouldWarm) {\n await this.pool.warm();\n }\n }\n\n async onApplicationShutdown(): Promise<void> {\n await this.pool.stop();\n }\n\n acquire(key: string): Promise<AcquireHandle<T>> {\n return this.pool.acquire(key);\n }\n\n /** Acquire a resource for `key`, run `fn`, and release the handle regardless of outcome. */\n async withResource<R>(key: string, fn: (resource: T) => R | Promise<R>): Promise<R> {\n const handle = await this.pool.acquire(key);\n try {\n return await fn(handle.resource);\n } finally {\n handle.release();\n }\n }\n\n getStats(): PoolStats {\n return this.pool.getStats();\n }\n}\n","/** DI token holding the resolved {@link RefPoolModuleOptions}. */\nexport const REFPOOL_OPTIONS = Symbol.for('refpool:options');\n\n/** DI token holding the resolved {@link TenantMiddlewareOptions}. */\nexport const REFPOOL_MIDDLEWARE_OPTIONS = Symbol.for('refpool:middleware-options');\n","import { Inject, Injectable } from '@nestjs/common';\nimport type { NestMiddleware } from '@nestjs/common';\nimport type { AcquireHandle } from '@refpool/core';\nimport { RefPoolService } from './ref-pool.service.js';\nimport { REFPOOL_MIDDLEWARE_OPTIONS } from './tokens.js';\nimport type { TenantMiddlewareOptions } from './interfaces.js';\n\nconst DEFAULT_HEADER = 'x-tenant-id';\nconst DEFAULT_PROPERTY = 'tenantResource';\n\ninterface MinimalRequest {\n headers?: Record<string, string | string[] | undefined>;\n [key: string]: unknown;\n}\n\ninterface MinimalResponse {\n on(event: string, listener: () => void): unknown;\n}\n\ntype NextFn = (error?: unknown) => void;\n\n@Injectable()\nexport class TenantConnectionMiddleware implements NestMiddleware {\n constructor(\n @Inject(RefPoolService) private readonly service: RefPoolService,\n @Inject(REFPOOL_MIDDLEWARE_OPTIONS) private readonly options: TenantMiddlewareOptions,\n ) {}\n\n async use(req: MinimalRequest, res: MinimalResponse, next: NextFn): Promise<void> {\n const header = (this.options.header ?? DEFAULT_HEADER).toLowerCase();\n const property = this.options.requestProperty ?? DEFAULT_PROPERTY;\n const onMissing = this.options.onMissing ?? 'next';\n\n const raw = req.headers?.[header];\n const tenantId = Array.isArray(raw) ? raw[0] : raw;\n\n if (!tenantId) {\n if (onMissing === 'error') {\n next(new Error(`Missing required tenant header \"${header}\"`));\n return;\n }\n next();\n return;\n }\n\n let handle: AcquireHandle<unknown>;\n try {\n handle = await this.service.acquire(tenantId);\n } catch (error) {\n next(error);\n return;\n }\n\n let released = false;\n const release = (): void => {\n if (released) return;\n released = true;\n handle.release();\n };\n res.on('finish', release);\n res.on('close', release);\n\n req[property] = handle.resource;\n req[`${property}Key`] = tenantId;\n next();\n }\n}\n"],"mappings":"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","names":["import_common","import_common","import_common"]}
@@ -0,0 +1,92 @@
1
+ import { ModuleMetadata, InjectionToken, OptionalFactoryDependency, NestModule, MiddlewareConsumer, DynamicModule, OnModuleInit, OnApplicationShutdown, NestMiddleware } from '@nestjs/common';
2
+ import { PoolOptions, RefCountedLruPool, AcquireHandle, PoolStats } from '@refpool/core';
3
+
4
+ /** Behavior when an incoming request lacks the configured tenant header. */
5
+ type MissingTenantBehavior = 'next' | 'error';
6
+ interface TenantMiddlewareOptions {
7
+ /** Request header carrying the tenant key. Default `x-tenant-id`. */
8
+ header?: string;
9
+ /** What to do when the header is absent. Default `next` (skip acquisition). */
10
+ onMissing?: MissingTenantBehavior;
11
+ /** Property on the request used to attach the acquired resource. Default `tenantResource`. */
12
+ requestProperty?: string;
13
+ /**
14
+ * Routes to auto-apply {@link TenantConnectionMiddleware} to. Setting this
15
+ * opts into wiring; omit it (or pass `applyMiddleware`) to fall back to `'*'`.
16
+ */
17
+ routes?: string | string[];
18
+ }
19
+ interface RefPoolModuleOptions<T = unknown> extends PoolOptions<T> {
20
+ /** Register the module globally so its exports are available app-wide. */
21
+ isGlobal?: boolean;
22
+ /** Run `pool.warm()` during `OnModuleInit`. Defaults to `true` when `prewarm` is set. */
23
+ warmOnInit?: boolean;
24
+ /** Defaults for {@link TenantConnectionMiddleware}. */
25
+ middleware?: TenantMiddlewareOptions;
26
+ /**
27
+ * Auto-apply {@link TenantConnectionMiddleware} for the whole app. Defaults to
28
+ * `true` when `middleware.routes` is set, otherwise `false`. When enabled
29
+ * without explicit `routes`, the middleware is applied to `'*'`.
30
+ */
31
+ applyMiddleware?: boolean;
32
+ }
33
+ interface RefPoolModuleAsyncOptions<T = unknown> extends Pick<ModuleMetadata, 'imports'> {
34
+ inject?: Array<InjectionToken | OptionalFactoryDependency>;
35
+ useFactory: (...args: any[]) => RefPoolModuleOptions<T> | Promise<RefPoolModuleOptions<T>>;
36
+ /** Register the module globally so its exports are available app-wide. */
37
+ isGlobal?: boolean;
38
+ }
39
+
40
+ declare class RefPoolModule implements NestModule {
41
+ private readonly options;
42
+ constructor(options: RefPoolModuleOptions);
43
+ /**
44
+ * Auto-wires {@link TenantConnectionMiddleware} when the drop-in multi-tenant
45
+ * path is opted into via `applyMiddleware` or `middleware.routes`. Defaults to
46
+ * `'*'` (all routes) when enabled without explicit routes.
47
+ */
48
+ configure(consumer: MiddlewareConsumer): void;
49
+ static forRoot<T = unknown>(options: RefPoolModuleOptions<T>): DynamicModule;
50
+ static forRootAsync<T = unknown>(options: RefPoolModuleAsyncOptions<T>): DynamicModule;
51
+ }
52
+
53
+ declare class RefPoolService<T = unknown> implements OnModuleInit, OnApplicationShutdown {
54
+ private readonly options;
55
+ /** The underlying pool. Exposed for advanced use (event subscription, etc.). */
56
+ readonly pool: RefCountedLruPool<T>;
57
+ constructor(options: RefPoolModuleOptions<T>);
58
+ onModuleInit(): Promise<void>;
59
+ onApplicationShutdown(): Promise<void>;
60
+ acquire(key: string): Promise<AcquireHandle<T>>;
61
+ /** Acquire a resource for `key`, run `fn`, and release the handle regardless of outcome. */
62
+ withResource<R>(key: string, fn: (resource: T) => R | Promise<R>): Promise<R>;
63
+ getStats(): PoolStats;
64
+ }
65
+
66
+ interface MinimalRequest {
67
+ headers?: Record<string, string | string[] | undefined>;
68
+ [key: string]: unknown;
69
+ }
70
+ interface MinimalResponse {
71
+ on(event: string, listener: () => void): unknown;
72
+ }
73
+ type NextFn = (error?: unknown) => void;
74
+ declare class TenantConnectionMiddleware implements NestMiddleware {
75
+ private readonly service;
76
+ private readonly options;
77
+ constructor(service: RefPoolService, options: TenantMiddlewareOptions);
78
+ use(req: MinimalRequest, res: MinimalResponse, next: NextFn): Promise<void>;
79
+ }
80
+
81
+ declare class ConnectionHealthController {
82
+ private readonly service;
83
+ constructor(service: RefPoolService);
84
+ connections(): PoolStats;
85
+ }
86
+
87
+ /** DI token holding the resolved {@link RefPoolModuleOptions}. */
88
+ declare const REFPOOL_OPTIONS: unique symbol;
89
+ /** DI token holding the resolved {@link TenantMiddlewareOptions}. */
90
+ declare const REFPOOL_MIDDLEWARE_OPTIONS: unique symbol;
91
+
92
+ export { ConnectionHealthController, type MissingTenantBehavior, REFPOOL_MIDDLEWARE_OPTIONS, REFPOOL_OPTIONS, RefPoolModule, type RefPoolModuleAsyncOptions, type RefPoolModuleOptions, RefPoolService, TenantConnectionMiddleware, type TenantMiddlewareOptions };
@@ -0,0 +1,92 @@
1
+ import { ModuleMetadata, InjectionToken, OptionalFactoryDependency, NestModule, MiddlewareConsumer, DynamicModule, OnModuleInit, OnApplicationShutdown, NestMiddleware } from '@nestjs/common';
2
+ import { PoolOptions, RefCountedLruPool, AcquireHandle, PoolStats } from '@refpool/core';
3
+
4
+ /** Behavior when an incoming request lacks the configured tenant header. */
5
+ type MissingTenantBehavior = 'next' | 'error';
6
+ interface TenantMiddlewareOptions {
7
+ /** Request header carrying the tenant key. Default `x-tenant-id`. */
8
+ header?: string;
9
+ /** What to do when the header is absent. Default `next` (skip acquisition). */
10
+ onMissing?: MissingTenantBehavior;
11
+ /** Property on the request used to attach the acquired resource. Default `tenantResource`. */
12
+ requestProperty?: string;
13
+ /**
14
+ * Routes to auto-apply {@link TenantConnectionMiddleware} to. Setting this
15
+ * opts into wiring; omit it (or pass `applyMiddleware`) to fall back to `'*'`.
16
+ */
17
+ routes?: string | string[];
18
+ }
19
+ interface RefPoolModuleOptions<T = unknown> extends PoolOptions<T> {
20
+ /** Register the module globally so its exports are available app-wide. */
21
+ isGlobal?: boolean;
22
+ /** Run `pool.warm()` during `OnModuleInit`. Defaults to `true` when `prewarm` is set. */
23
+ warmOnInit?: boolean;
24
+ /** Defaults for {@link TenantConnectionMiddleware}. */
25
+ middleware?: TenantMiddlewareOptions;
26
+ /**
27
+ * Auto-apply {@link TenantConnectionMiddleware} for the whole app. Defaults to
28
+ * `true` when `middleware.routes` is set, otherwise `false`. When enabled
29
+ * without explicit `routes`, the middleware is applied to `'*'`.
30
+ */
31
+ applyMiddleware?: boolean;
32
+ }
33
+ interface RefPoolModuleAsyncOptions<T = unknown> extends Pick<ModuleMetadata, 'imports'> {
34
+ inject?: Array<InjectionToken | OptionalFactoryDependency>;
35
+ useFactory: (...args: any[]) => RefPoolModuleOptions<T> | Promise<RefPoolModuleOptions<T>>;
36
+ /** Register the module globally so its exports are available app-wide. */
37
+ isGlobal?: boolean;
38
+ }
39
+
40
+ declare class RefPoolModule implements NestModule {
41
+ private readonly options;
42
+ constructor(options: RefPoolModuleOptions);
43
+ /**
44
+ * Auto-wires {@link TenantConnectionMiddleware} when the drop-in multi-tenant
45
+ * path is opted into via `applyMiddleware` or `middleware.routes`. Defaults to
46
+ * `'*'` (all routes) when enabled without explicit routes.
47
+ */
48
+ configure(consumer: MiddlewareConsumer): void;
49
+ static forRoot<T = unknown>(options: RefPoolModuleOptions<T>): DynamicModule;
50
+ static forRootAsync<T = unknown>(options: RefPoolModuleAsyncOptions<T>): DynamicModule;
51
+ }
52
+
53
+ declare class RefPoolService<T = unknown> implements OnModuleInit, OnApplicationShutdown {
54
+ private readonly options;
55
+ /** The underlying pool. Exposed for advanced use (event subscription, etc.). */
56
+ readonly pool: RefCountedLruPool<T>;
57
+ constructor(options: RefPoolModuleOptions<T>);
58
+ onModuleInit(): Promise<void>;
59
+ onApplicationShutdown(): Promise<void>;
60
+ acquire(key: string): Promise<AcquireHandle<T>>;
61
+ /** Acquire a resource for `key`, run `fn`, and release the handle regardless of outcome. */
62
+ withResource<R>(key: string, fn: (resource: T) => R | Promise<R>): Promise<R>;
63
+ getStats(): PoolStats;
64
+ }
65
+
66
+ interface MinimalRequest {
67
+ headers?: Record<string, string | string[] | undefined>;
68
+ [key: string]: unknown;
69
+ }
70
+ interface MinimalResponse {
71
+ on(event: string, listener: () => void): unknown;
72
+ }
73
+ type NextFn = (error?: unknown) => void;
74
+ declare class TenantConnectionMiddleware implements NestMiddleware {
75
+ private readonly service;
76
+ private readonly options;
77
+ constructor(service: RefPoolService, options: TenantMiddlewareOptions);
78
+ use(req: MinimalRequest, res: MinimalResponse, next: NextFn): Promise<void>;
79
+ }
80
+
81
+ declare class ConnectionHealthController {
82
+ private readonly service;
83
+ constructor(service: RefPoolService);
84
+ connections(): PoolStats;
85
+ }
86
+
87
+ /** DI token holding the resolved {@link RefPoolModuleOptions}. */
88
+ declare const REFPOOL_OPTIONS: unique symbol;
89
+ /** DI token holding the resolved {@link TenantMiddlewareOptions}. */
90
+ declare const REFPOOL_MIDDLEWARE_OPTIONS: unique symbol;
91
+
92
+ export { ConnectionHealthController, type MissingTenantBehavior, REFPOOL_MIDDLEWARE_OPTIONS, REFPOOL_OPTIONS, RefPoolModule, type RefPoolModuleAsyncOptions, type RefPoolModuleOptions, RefPoolService, TenantConnectionMiddleware, type TenantMiddlewareOptions };
package/dist/index.js ADDED
@@ -0,0 +1,201 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __decorateClass = (decorators, target, key, kind) => {
4
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
+ if (decorator = decorators[i])
7
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
+ if (kind && result) __defProp(target, key, result);
9
+ return result;
10
+ };
11
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
12
+
13
+ // src/ref-pool.module.ts
14
+ import { Inject as Inject4, Module } from "@nestjs/common";
15
+
16
+ // src/health.controller.ts
17
+ import { Controller, Get, Inject as Inject2 } from "@nestjs/common";
18
+
19
+ // src/ref-pool.service.ts
20
+ import { Inject, Injectable } from "@nestjs/common";
21
+ import { RefCountedLruPool } from "@refpool/core";
22
+
23
+ // src/tokens.ts
24
+ var REFPOOL_OPTIONS = /* @__PURE__ */ Symbol.for("refpool:options");
25
+ var REFPOOL_MIDDLEWARE_OPTIONS = /* @__PURE__ */ Symbol.for("refpool:middleware-options");
26
+
27
+ // src/ref-pool.service.ts
28
+ var RefPoolService = class {
29
+ constructor(options) {
30
+ this.options = options;
31
+ this.pool = new RefCountedLruPool(options);
32
+ }
33
+ async onModuleInit() {
34
+ this.pool.start();
35
+ const shouldWarm = this.options.warmOnInit ?? Boolean(this.options.prewarm);
36
+ if (shouldWarm) {
37
+ await this.pool.warm();
38
+ }
39
+ }
40
+ async onApplicationShutdown() {
41
+ await this.pool.stop();
42
+ }
43
+ acquire(key) {
44
+ return this.pool.acquire(key);
45
+ }
46
+ /** Acquire a resource for `key`, run `fn`, and release the handle regardless of outcome. */
47
+ async withResource(key, fn) {
48
+ const handle = await this.pool.acquire(key);
49
+ try {
50
+ return await fn(handle.resource);
51
+ } finally {
52
+ handle.release();
53
+ }
54
+ }
55
+ getStats() {
56
+ return this.pool.getStats();
57
+ }
58
+ };
59
+ RefPoolService = __decorateClass([
60
+ Injectable(),
61
+ __decorateParam(0, Inject(REFPOOL_OPTIONS))
62
+ ], RefPoolService);
63
+
64
+ // src/health.controller.ts
65
+ var ConnectionHealthController = class {
66
+ constructor(service) {
67
+ this.service = service;
68
+ }
69
+ connections() {
70
+ return this.service.getStats();
71
+ }
72
+ };
73
+ __decorateClass([
74
+ Get("connections")
75
+ ], ConnectionHealthController.prototype, "connections", 1);
76
+ ConnectionHealthController = __decorateClass([
77
+ Controller("health"),
78
+ __decorateParam(0, Inject2(RefPoolService))
79
+ ], ConnectionHealthController);
80
+
81
+ // src/tenant-connection.middleware.ts
82
+ import { Inject as Inject3, Injectable as Injectable2 } from "@nestjs/common";
83
+ var DEFAULT_HEADER = "x-tenant-id";
84
+ var DEFAULT_PROPERTY = "tenantResource";
85
+ var TenantConnectionMiddleware = class {
86
+ constructor(service, options) {
87
+ this.service = service;
88
+ this.options = options;
89
+ }
90
+ async use(req, res, next) {
91
+ const header = (this.options.header ?? DEFAULT_HEADER).toLowerCase();
92
+ const property = this.options.requestProperty ?? DEFAULT_PROPERTY;
93
+ const onMissing = this.options.onMissing ?? "next";
94
+ const raw = req.headers?.[header];
95
+ const tenantId = Array.isArray(raw) ? raw[0] : raw;
96
+ if (!tenantId) {
97
+ if (onMissing === "error") {
98
+ next(new Error(`Missing required tenant header "${header}"`));
99
+ return;
100
+ }
101
+ next();
102
+ return;
103
+ }
104
+ let handle;
105
+ try {
106
+ handle = await this.service.acquire(tenantId);
107
+ } catch (error) {
108
+ next(error);
109
+ return;
110
+ }
111
+ let released = false;
112
+ const release = () => {
113
+ if (released) return;
114
+ released = true;
115
+ handle.release();
116
+ };
117
+ res.on("finish", release);
118
+ res.on("close", release);
119
+ req[property] = handle.resource;
120
+ req[`${property}Key`] = tenantId;
121
+ next();
122
+ }
123
+ };
124
+ TenantConnectionMiddleware = __decorateClass([
125
+ Injectable2(),
126
+ __decorateParam(0, Inject3(RefPoolService)),
127
+ __decorateParam(1, Inject3(REFPOOL_MIDDLEWARE_OPTIONS))
128
+ ], TenantConnectionMiddleware);
129
+
130
+ // src/ref-pool.module.ts
131
+ var SHARED = [RefPoolService, TenantConnectionMiddleware];
132
+ var EXPORTS = [
133
+ RefPoolService,
134
+ TenantConnectionMiddleware,
135
+ REFPOOL_OPTIONS,
136
+ REFPOOL_MIDDLEWARE_OPTIONS
137
+ ];
138
+ var RefPoolModule = class {
139
+ constructor(options) {
140
+ this.options = options;
141
+ }
142
+ /**
143
+ * Auto-wires {@link TenantConnectionMiddleware} when the drop-in multi-tenant
144
+ * path is opted into via `applyMiddleware` or `middleware.routes`. Defaults to
145
+ * `'*'` (all routes) when enabled without explicit routes.
146
+ */
147
+ configure(consumer) {
148
+ const routes = this.options.middleware?.routes;
149
+ const shouldApply = this.options.applyMiddleware ?? routes !== void 0;
150
+ if (!shouldApply) return;
151
+ const forRoutes = routes === void 0 ? ["*"] : Array.isArray(routes) ? routes : [routes];
152
+ consumer.apply(TenantConnectionMiddleware).forRoutes(...forRoutes);
153
+ }
154
+ static forRoot(options) {
155
+ const providers = [
156
+ { provide: REFPOOL_OPTIONS, useValue: options },
157
+ { provide: REFPOOL_MIDDLEWARE_OPTIONS, useValue: options.middleware ?? {} },
158
+ ...SHARED
159
+ ];
160
+ return {
161
+ module: RefPoolModule,
162
+ global: options.isGlobal ?? false,
163
+ providers,
164
+ controllers: [ConnectionHealthController],
165
+ exports: EXPORTS
166
+ };
167
+ }
168
+ static forRootAsync(options) {
169
+ const optionsProvider = {
170
+ provide: REFPOOL_OPTIONS,
171
+ useFactory: options.useFactory,
172
+ inject: options.inject ?? []
173
+ };
174
+ const middlewareOptionsProvider = {
175
+ provide: REFPOOL_MIDDLEWARE_OPTIONS,
176
+ useFactory: (resolved) => resolved.middleware ?? {},
177
+ inject: [REFPOOL_OPTIONS]
178
+ };
179
+ return {
180
+ module: RefPoolModule,
181
+ global: options.isGlobal ?? false,
182
+ imports: options.imports ?? [],
183
+ providers: [optionsProvider, middlewareOptionsProvider, ...SHARED],
184
+ controllers: [ConnectionHealthController],
185
+ exports: EXPORTS
186
+ };
187
+ }
188
+ };
189
+ RefPoolModule = __decorateClass([
190
+ Module({}),
191
+ __decorateParam(0, Inject4(REFPOOL_OPTIONS))
192
+ ], RefPoolModule);
193
+ export {
194
+ ConnectionHealthController,
195
+ REFPOOL_MIDDLEWARE_OPTIONS,
196
+ REFPOOL_OPTIONS,
197
+ RefPoolModule,
198
+ RefPoolService,
199
+ TenantConnectionMiddleware
200
+ };
201
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/ref-pool.module.ts","../src/health.controller.ts","../src/ref-pool.service.ts","../src/tokens.ts","../src/tenant-connection.middleware.ts"],"sourcesContent":["import { Inject, Module } from '@nestjs/common';\nimport type {\n DynamicModule,\n MiddlewareConsumer,\n NestModule,\n Provider,\n} from '@nestjs/common';\nimport { ConnectionHealthController } from './health.controller.js';\nimport { RefPoolService } from './ref-pool.service.js';\nimport { TenantConnectionMiddleware } from './tenant-connection.middleware.js';\nimport { REFPOOL_MIDDLEWARE_OPTIONS, REFPOOL_OPTIONS } from './tokens.js';\nimport type {\n RefPoolModuleAsyncOptions,\n RefPoolModuleOptions,\n TenantMiddlewareOptions,\n} from './interfaces.js';\n\nconst SHARED = [RefPoolService, TenantConnectionMiddleware];\nconst EXPORTS = [\n RefPoolService,\n TenantConnectionMiddleware,\n REFPOOL_OPTIONS,\n REFPOOL_MIDDLEWARE_OPTIONS,\n];\n\n@Module({})\nexport class RefPoolModule implements NestModule {\n constructor(\n @Inject(REFPOOL_OPTIONS) private readonly options: RefPoolModuleOptions,\n ) {}\n\n /**\n * Auto-wires {@link TenantConnectionMiddleware} when the drop-in multi-tenant\n * path is opted into via `applyMiddleware` or `middleware.routes`. Defaults to\n * `'*'` (all routes) when enabled without explicit routes.\n */\n configure(consumer: MiddlewareConsumer): void {\n const routes = this.options.middleware?.routes;\n const shouldApply = this.options.applyMiddleware ?? routes !== undefined;\n if (!shouldApply) return;\n const forRoutes = routes === undefined ? ['*'] : Array.isArray(routes) ? routes : [routes];\n consumer.apply(TenantConnectionMiddleware).forRoutes(...forRoutes);\n }\n\n static forRoot<T = unknown>(options: RefPoolModuleOptions<T>): DynamicModule {\n const providers: Provider[] = [\n { provide: REFPOOL_OPTIONS, useValue: options },\n { provide: REFPOOL_MIDDLEWARE_OPTIONS, useValue: options.middleware ?? {} },\n ...SHARED,\n ];\n return {\n module: RefPoolModule,\n global: options.isGlobal ?? false,\n providers,\n controllers: [ConnectionHealthController],\n exports: EXPORTS,\n };\n }\n\n static forRootAsync<T = unknown>(options: RefPoolModuleAsyncOptions<T>): DynamicModule {\n const optionsProvider: Provider = {\n provide: REFPOOL_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n };\n const middlewareOptionsProvider: Provider = {\n provide: REFPOOL_MIDDLEWARE_OPTIONS,\n useFactory: (resolved: RefPoolModuleOptions<T>): TenantMiddlewareOptions =>\n resolved.middleware ?? {},\n inject: [REFPOOL_OPTIONS],\n };\n return {\n module: RefPoolModule,\n global: options.isGlobal ?? false,\n imports: options.imports ?? [],\n providers: [optionsProvider, middlewareOptionsProvider, ...SHARED],\n controllers: [ConnectionHealthController],\n exports: EXPORTS,\n };\n }\n}\n","import { Controller, Get, Inject } from '@nestjs/common';\nimport type { PoolStats } from '@refpool/core';\nimport { RefPoolService } from './ref-pool.service.js';\n\n@Controller('health')\nexport class ConnectionHealthController {\n constructor(@Inject(RefPoolService) private readonly service: RefPoolService) {}\n\n @Get('connections')\n connections(): PoolStats {\n return this.service.getStats();\n }\n}\n","import { Inject, Injectable } from '@nestjs/common';\nimport type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';\nimport { RefCountedLruPool } from '@refpool/core';\nimport type { AcquireHandle, PoolStats } from '@refpool/core';\nimport { REFPOOL_OPTIONS } from './tokens.js';\nimport type { RefPoolModuleOptions } from './interfaces.js';\n\n@Injectable()\nexport class RefPoolService<T = unknown> implements OnModuleInit, OnApplicationShutdown {\n /** The underlying pool. Exposed for advanced use (event subscription, etc.). */\n readonly pool: RefCountedLruPool<T>;\n\n constructor(@Inject(REFPOOL_OPTIONS) private readonly options: RefPoolModuleOptions<T>) {\n this.pool = new RefCountedLruPool<T>(options);\n }\n\n async onModuleInit(): Promise<void> {\n this.pool.start();\n const shouldWarm = this.options.warmOnInit ?? Boolean(this.options.prewarm);\n if (shouldWarm) {\n await this.pool.warm();\n }\n }\n\n async onApplicationShutdown(): Promise<void> {\n await this.pool.stop();\n }\n\n acquire(key: string): Promise<AcquireHandle<T>> {\n return this.pool.acquire(key);\n }\n\n /** Acquire a resource for `key`, run `fn`, and release the handle regardless of outcome. */\n async withResource<R>(key: string, fn: (resource: T) => R | Promise<R>): Promise<R> {\n const handle = await this.pool.acquire(key);\n try {\n return await fn(handle.resource);\n } finally {\n handle.release();\n }\n }\n\n getStats(): PoolStats {\n return this.pool.getStats();\n }\n}\n","/** DI token holding the resolved {@link RefPoolModuleOptions}. */\nexport const REFPOOL_OPTIONS = Symbol.for('refpool:options');\n\n/** DI token holding the resolved {@link TenantMiddlewareOptions}. */\nexport const REFPOOL_MIDDLEWARE_OPTIONS = Symbol.for('refpool:middleware-options');\n","import { Inject, Injectable } from '@nestjs/common';\nimport type { NestMiddleware } from '@nestjs/common';\nimport type { AcquireHandle } from '@refpool/core';\nimport { RefPoolService } from './ref-pool.service.js';\nimport { REFPOOL_MIDDLEWARE_OPTIONS } from './tokens.js';\nimport type { TenantMiddlewareOptions } from './interfaces.js';\n\nconst DEFAULT_HEADER = 'x-tenant-id';\nconst DEFAULT_PROPERTY = 'tenantResource';\n\ninterface MinimalRequest {\n headers?: Record<string, string | string[] | undefined>;\n [key: string]: unknown;\n}\n\ninterface MinimalResponse {\n on(event: string, listener: () => void): unknown;\n}\n\ntype NextFn = (error?: unknown) => void;\n\n@Injectable()\nexport class TenantConnectionMiddleware implements NestMiddleware {\n constructor(\n @Inject(RefPoolService) private readonly service: RefPoolService,\n @Inject(REFPOOL_MIDDLEWARE_OPTIONS) private readonly options: TenantMiddlewareOptions,\n ) {}\n\n async use(req: MinimalRequest, res: MinimalResponse, next: NextFn): Promise<void> {\n const header = (this.options.header ?? DEFAULT_HEADER).toLowerCase();\n const property = this.options.requestProperty ?? DEFAULT_PROPERTY;\n const onMissing = this.options.onMissing ?? 'next';\n\n const raw = req.headers?.[header];\n const tenantId = Array.isArray(raw) ? raw[0] : raw;\n\n if (!tenantId) {\n if (onMissing === 'error') {\n next(new Error(`Missing required tenant header \"${header}\"`));\n return;\n }\n next();\n return;\n }\n\n let handle: AcquireHandle<unknown>;\n try {\n handle = await this.service.acquire(tenantId);\n } catch (error) {\n next(error);\n return;\n }\n\n let released = false;\n const release = (): void => {\n if (released) return;\n released = true;\n handle.release();\n };\n res.on('finish', release);\n res.on('close', release);\n\n req[property] = handle.resource;\n req[`${property}Key`] = tenantId;\n next();\n }\n}\n"],"mappings":"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","names":["Inject","Inject","Inject","Inject","Injectable","Injectable","Inject","Inject"]}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@refpool/nestjs",
3
+ "version": "0.1.0",
4
+ "description": "NestJS integration for refpool: dynamic module, injectable service, multi-tenant connection middleware, and a connection health controller.",
5
+ "license": "MIT",
6
+ "author": "Atul Singh <atulsingh.harsh@gmail.com> (https://atulsingh.io)",
7
+ "type": "module",
8
+ "sideEffects": false,
9
+ "keywords": [
10
+ "nestjs",
11
+ "pool",
12
+ "resource-pool",
13
+ "reference-counting",
14
+ "multi-tenant",
15
+ "connection-pool",
16
+ "middleware"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/expedite-atul/refpool.git",
21
+ "directory": "packages/nestjs"
22
+ },
23
+ "homepage": "https://github.com/expedite-atul/refpool/tree/main/packages/nestjs#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/expedite-atul/refpool/issues"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public"
29
+ },
30
+ "main": "./dist/index.cjs",
31
+ "module": "./dist/index.js",
32
+ "types": "./dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "types": "./dist/index.d.ts",
36
+ "import": "./dist/index.js",
37
+ "require": "./dist/index.cjs"
38
+ }
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "README.md",
43
+ "LICENSE"
44
+ ],
45
+ "dependencies": {
46
+ "@refpool/core": "0.1.0"
47
+ },
48
+ "peerDependencies": {
49
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
50
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
51
+ "reflect-metadata": "^0.1.13 || ^0.2.0",
52
+ "rxjs": "^7.8.0"
53
+ },
54
+ "devDependencies": {
55
+ "@nestjs/common": "^11.0.0",
56
+ "@nestjs/core": "^11.0.0",
57
+ "@nestjs/testing": "^11.0.0",
58
+ "@types/node": "^22.10.2",
59
+ "reflect-metadata": "^0.2.2",
60
+ "rxjs": "^7.8.1",
61
+ "tsup": "^8.3.5",
62
+ "typescript": "^5.6.3",
63
+ "vitest": "^2.1.8"
64
+ },
65
+ "engines": {
66
+ "node": ">=18"
67
+ },
68
+ "scripts": {
69
+ "build": "tsup",
70
+ "typecheck": "tsc --noEmit",
71
+ "test": "vitest run"
72
+ }
73
+ }