@stratum-hq/db-adapters 1.1.0 → 1.2.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 +17 -0
- package/dist/adapters/database-raw.d.ts +2 -0
- package/dist/adapters/database-raw.d.ts.map +1 -1
- package/dist/adapters/database-raw.js +36 -13
- package/dist/adapters/database-raw.js.map +1 -1
- package/dist/adapters/schema-raw.d.ts.map +1 -1
- package/dist/adapters/schema-raw.js +21 -3
- package/dist/adapters/schema-raw.js.map +1 -1
- package/dist/database/pool-manager.d.ts +38 -7
- package/dist/database/pool-manager.d.ts.map +1 -1
- package/dist/database/pool-manager.js +101 -24
- package/dist/database/pool-manager.js.map +1 -1
- package/dist/migration-helpers.js +1 -1
- package/dist/migration-helpers.js.map +1 -1
- package/dist/rls/manager.js +1 -1
- package/dist/rls/manager.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -76,6 +76,23 @@ Extra schemas come **after** the tenant schema, and each entry must be a plain i
|
|
|
76
76
|
|
|
77
77
|
Prisma qualifies every table with its datasource schema, so `search_path` does not route it. For Prisma, use `new SchemaPrismaAdapter(PrismaClient, datasourceUrl).getClient(tenantSlug)`, which gives each tenant a client bound to its own schema.
|
|
78
78
|
|
|
79
|
+
## Database-per-tenant pools
|
|
80
|
+
|
|
81
|
+
`DatabasePoolManager` keeps one `pg.Pool` for each tenant database. Each `getPool(slug)` call holds the pool until you call `releasePool(slug)` with the same arguments. The manager never ends a held pool. Thus a request that is using a pool cannot lose it to eviction.
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const pool = await poolManager.getPool("acme");
|
|
85
|
+
try {
|
|
86
|
+
await pool.query("SELECT 1");
|
|
87
|
+
} finally {
|
|
88
|
+
poolManager.releasePool("acme");
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
When the pool count reaches `maxPools`, the manager ends the least recently used pool that no caller holds. If every pool is held, the count goes above `maxPools` until a caller releases one. A pool that you never release stays open until `closePool` or `closeAll`. `DatabaseRawAdapter` releases its pool after each call.
|
|
93
|
+
|
|
94
|
+
Concurrent first requests for one tenant share one pool.
|
|
95
|
+
|
|
79
96
|
## Security
|
|
80
97
|
|
|
81
98
|
- All DDL validates table names against `/^[a-zA-Z_][a-zA-Z0-9_]*$/`.
|
|
@@ -5,6 +5,8 @@ import { DatabasePoolManager } from "../database/pool-manager.js";
|
|
|
5
5
|
*
|
|
6
6
|
* Each query is routed to the dedicated database for the given tenant slug
|
|
7
7
|
* by obtaining a connection from the tenant's pool via DatabasePoolManager.
|
|
8
|
+
* Each method releases its hold on the pool when it finishes, so the manager
|
|
9
|
+
* can evict the pool while no query uses it.
|
|
8
10
|
*/
|
|
9
11
|
export declare class DatabaseRawAdapter {
|
|
10
12
|
private readonly poolManager;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-raw.d.ts","sourceRoot":"","sources":["../../src/adapters/database-raw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE
|
|
1
|
+
{"version":3,"file":"database-raw.d.ts","sourceRoot":"","sources":["../../src/adapters/database-raw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAElE;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IACjB,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAAX,WAAW,EAAE,mBAAmB;IAE7D;;;OAGG;IACG,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,cAAc,EACzD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAc7B;;;OAGG;IACG,wBAAwB,CAAC,CAAC,EAC9B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC;CA6Bd"}
|
|
@@ -6,6 +6,8 @@ exports.DatabaseRawAdapter = void 0;
|
|
|
6
6
|
*
|
|
7
7
|
* Each query is routed to the dedicated database for the given tenant slug
|
|
8
8
|
* by obtaining a connection from the tenant's pool via DatabasePoolManager.
|
|
9
|
+
* Each method releases its hold on the pool when it finishes, so the manager
|
|
10
|
+
* can evict the pool while no query uses it.
|
|
9
11
|
*/
|
|
10
12
|
class DatabaseRawAdapter {
|
|
11
13
|
poolManager;
|
|
@@ -18,12 +20,17 @@ class DatabaseRawAdapter {
|
|
|
18
20
|
*/
|
|
19
21
|
async query(tenantSlug, text, values) {
|
|
20
22
|
const pool = await this.poolManager.getPool(tenantSlug);
|
|
21
|
-
const client = await pool.connect();
|
|
22
23
|
try {
|
|
23
|
-
|
|
24
|
+
const client = await pool.connect();
|
|
25
|
+
try {
|
|
26
|
+
return await client.query(text, values);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
client.release();
|
|
30
|
+
}
|
|
24
31
|
}
|
|
25
32
|
finally {
|
|
26
|
-
|
|
33
|
+
this.poolManager.releasePool(tenantSlug);
|
|
27
34
|
}
|
|
28
35
|
}
|
|
29
36
|
/**
|
|
@@ -32,19 +39,35 @@ class DatabaseRawAdapter {
|
|
|
32
39
|
*/
|
|
33
40
|
async executeWithTenantContext(tenantSlug, queryFn) {
|
|
34
41
|
const pool = await this.poolManager.getPool(tenantSlug);
|
|
35
|
-
const client = await pool.connect();
|
|
36
42
|
try {
|
|
37
|
-
await
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const client = await pool.connect();
|
|
44
|
+
// A failed ROLLBACK must not replace the caller's error. That error goes
|
|
45
|
+
// to client.release instead: a truthy argument makes pg-pool destroy the
|
|
46
|
+
// connection, so no later caller gets a connection in an unknown
|
|
47
|
+
// transaction state.
|
|
48
|
+
let releaseErr;
|
|
49
|
+
try {
|
|
50
|
+
await client.query("BEGIN");
|
|
51
|
+
const result = await queryFn(client);
|
|
52
|
+
await client.query("COMMIT");
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
try {
|
|
57
|
+
await client.query("ROLLBACK");
|
|
58
|
+
}
|
|
59
|
+
catch (rollbackErr) {
|
|
60
|
+
releaseErr =
|
|
61
|
+
rollbackErr instanceof Error ? rollbackErr : new Error(String(rollbackErr));
|
|
62
|
+
}
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
client.release(releaseErr);
|
|
67
|
+
}
|
|
45
68
|
}
|
|
46
69
|
finally {
|
|
47
|
-
|
|
70
|
+
this.poolManager.releasePool(tenantSlug);
|
|
48
71
|
}
|
|
49
72
|
}
|
|
50
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-raw.js","sourceRoot":"","sources":["../../src/adapters/database-raw.ts"],"names":[],"mappings":";;;AAGA
|
|
1
|
+
{"version":3,"file":"database-raw.js","sourceRoot":"","sources":["../../src/adapters/database-raw.ts"],"names":[],"mappings":";;;AAGA;;;;;;;GAOG;AACH,MAAa,kBAAkB;IACA;IAA7B,YAA6B,WAAgC;QAAhC,gBAAW,GAAX,WAAW,CAAqB;IAAG,CAAC;IAEjE;;;OAGG;IACH,KAAK,CAAC,KAAK,CACT,UAAkB,EAClB,IAAY,EACZ,MAAkB;QAElB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,OAAO,MAAM,MAAM,CAAC,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;YAC7C,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,KAAK,CAAC,wBAAwB,CAC5B,UAAkB,EAClB,OAA8C;QAE9C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;YACpC,yEAAyE;YACzE,yEAAyE;YACzE,iEAAiE;YACjE,qBAAqB;YACrB,IAAI,UAA6B,CAAC;YAClC,IAAI,CAAC;gBACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;gBAC5B,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;gBACrC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC7B,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC;oBACH,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;gBACjC,CAAC;gBAAC,OAAO,WAAW,EAAE,CAAC;oBACrB,UAAU;wBACR,WAAW,YAAY,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC;gBAChF,CAAC;gBACD,MAAM,GAAG,CAAC;YACZ,CAAC;oBAAS,CAAC;gBACT,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,IAAI,CAAC,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;CACF;AA7DD,gDA6DC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-raw.d.ts","sourceRoot":"","sources":["../../src/adapters/schema-raw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAKpB,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;
|
|
1
|
+
{"version":3,"file":"schema-raw.d.ts","sourceRoot":"","sources":["../../src/adapters/schema-raw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAKpB,MAAM,WAAW,oBAAoB;IACnC;;;;;OAKG;IACH,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAMD,qBAAa,gBAAgB;IAIzB,OAAO,CAAC,IAAI;IAHd,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAW;gBAGjC,IAAI,EAAE,EAAE,CAAC,IAAI,EACrB,OAAO,GAAE,oBAAyB;IAM9B,KAAK,CAAC,CAAC,SAAS,EAAE,CAAC,cAAc,GAAG,EAAE,CAAC,cAAc,EACzD,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,GACjB,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC;IAM7B;;;;OAIG;IACG,wBAAwB,CAAC,CAAC,EAC9B,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,GACxC,OAAO,CAAC,CAAC,CAAC;CAkCd;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,SAAS,EAAE,MAAM,MAAM,EACvB,OAAO,GAAE,oBAAyB,GACjC,gBAAgB,CAWlB"}
|
|
@@ -5,6 +5,9 @@ exports.createSchemaTenantPool = createSchemaTenantPool;
|
|
|
5
5
|
const core_1 = require("@stratum-hq/core");
|
|
6
6
|
const manager_js_1 = require("../schema/manager.js");
|
|
7
7
|
const session_js_1 = require("../schema/session.js");
|
|
8
|
+
function toError(err) {
|
|
9
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
10
|
+
}
|
|
8
11
|
class SchemaRawAdapter {
|
|
9
12
|
pool;
|
|
10
13
|
extraSearchPath;
|
|
@@ -27,6 +30,11 @@ class SchemaRawAdapter {
|
|
|
27
30
|
const safe = (0, core_1.validateSlug)(tenantSlug);
|
|
28
31
|
const schemaName = (0, manager_js_1.tenantSchemaName)(safe);
|
|
29
32
|
const client = await this.pool.connect();
|
|
33
|
+
// A failed ROLLBACK or RESET must not replace the result or the caller's
|
|
34
|
+
// error. That error goes to client.release instead: a truthy argument makes
|
|
35
|
+
// pg-pool destroy the connection, so no later caller gets a connection in
|
|
36
|
+
// an unknown transaction state or with an unknown search_path.
|
|
37
|
+
let releaseErr;
|
|
30
38
|
try {
|
|
31
39
|
await client.query("BEGIN");
|
|
32
40
|
// SET LOCAL is transaction-scoped; schemaName is derived from a validated slug.
|
|
@@ -36,12 +44,22 @@ class SchemaRawAdapter {
|
|
|
36
44
|
return result;
|
|
37
45
|
}
|
|
38
46
|
catch (err) {
|
|
39
|
-
|
|
47
|
+
try {
|
|
48
|
+
await client.query("ROLLBACK");
|
|
49
|
+
}
|
|
50
|
+
catch (rollbackErr) {
|
|
51
|
+
releaseErr = toError(rollbackErr);
|
|
52
|
+
}
|
|
40
53
|
throw err;
|
|
41
54
|
}
|
|
42
55
|
finally {
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
try {
|
|
57
|
+
await (0, session_js_1.resetSearchPath)(client);
|
|
58
|
+
}
|
|
59
|
+
catch (resetErr) {
|
|
60
|
+
releaseErr ??= toError(resetErr);
|
|
61
|
+
}
|
|
62
|
+
client.release(releaseErr);
|
|
45
63
|
}
|
|
46
64
|
}
|
|
47
65
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-raw.js","sourceRoot":"","sources":["../../src/adapters/schema-raw.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"schema-raw.js","sourceRoot":"","sources":["../../src/adapters/schema-raw.ts"],"names":[],"mappings":"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"}
|
|
@@ -4,40 +4,71 @@ export interface DatabasePoolManagerOptions {
|
|
|
4
4
|
baseConnectionConfig: pg.PoolConfig;
|
|
5
5
|
/** Maximum number of tenant pools to keep open simultaneously. Default: 50. */
|
|
6
6
|
maxPools?: number;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Milliseconds an idle connection stays open inside a tenant pool before pg
|
|
9
|
+
* closes it. The manager passes it to pg.Pool as idleTimeoutMillis. It does
|
|
10
|
+
* not control LRU eviction. Default: 30000.
|
|
11
|
+
*/
|
|
8
12
|
idleTimeoutMs?: number;
|
|
9
13
|
}
|
|
10
14
|
/**
|
|
11
15
|
* Manages a collection of per-tenant pg.Pool instances.
|
|
12
16
|
*
|
|
13
|
-
* Pools are keyed by tenant slug and created on first access.
|
|
14
|
-
*
|
|
17
|
+
* Pools are keyed by tenant slug and created on first access. Each getPool call
|
|
18
|
+
* holds the pool until the caller calls releasePool. When the pool count reaches
|
|
19
|
+
* maxPools, the least-recently-used pool that no caller holds is evicted. A pool
|
|
20
|
+
* that a caller holds is never evicted, so the count can exceed maxPools while
|
|
21
|
+
* every pool is in use.
|
|
15
22
|
*/
|
|
16
23
|
export declare class DatabasePoolManager {
|
|
17
24
|
private readonly pools;
|
|
25
|
+
/** Holds that callers still have on pools that closePool or closeAll removed, by pool key. */
|
|
26
|
+
private readonly closedHolds;
|
|
18
27
|
private readonly baseConfig;
|
|
19
28
|
private readonly maxPools;
|
|
20
29
|
private readonly idleTimeoutMs;
|
|
21
30
|
constructor(options: DatabasePoolManagerOptions);
|
|
22
31
|
/**
|
|
23
|
-
* Returns
|
|
24
|
-
*
|
|
32
|
+
* Returns the pool for the tenant slug, and creates it on first access.
|
|
33
|
+
* Each call holds the pool until the caller calls releasePool with the same
|
|
34
|
+
* arguments. A held pool is never evicted.
|
|
25
35
|
*
|
|
26
36
|
* When a regionId is provided, the pool is keyed as `regionId:slug` to support
|
|
27
37
|
* multi-region deployments where the same slug may exist in different regions.
|
|
28
38
|
*/
|
|
29
39
|
getPool(tenantSlug: string, regionId?: string): Promise<pg.Pool>;
|
|
40
|
+
/**
|
|
41
|
+
* Ends one hold on the pool that getPool returned for the same arguments.
|
|
42
|
+
* Call it once for each getPool call. No-op if the pool is not tracked.
|
|
43
|
+
*
|
|
44
|
+
* Without a regionId, a bare slug also matches one region-prefixed pool
|
|
45
|
+
* (`regionId:slug`), as closePool does. If the slug matches pools in more
|
|
46
|
+
* than one region, the call releases nothing, because the manager cannot
|
|
47
|
+
* tell which pool the caller holds. Pass the regionId in that case.
|
|
48
|
+
*/
|
|
49
|
+
releasePool(tenantSlug: string, regionId?: string): void;
|
|
30
50
|
/** Closes and removes the pool for the given tenant slug. No-op if not found.
|
|
31
51
|
* Accepts either a bare slug or a region-prefixed key (`regionId:slug`). */
|
|
32
52
|
closePool(tenantSlug: string, regionId?: string): Promise<void>;
|
|
33
53
|
/** Closes all managed pools. Call during application shutdown. */
|
|
34
54
|
closeAll(): Promise<void>;
|
|
55
|
+
/** Keeps the holds of a removed pool, so that their releases do not reach a later pool for the key. */
|
|
56
|
+
private recordClosedHolds;
|
|
57
|
+
/**
|
|
58
|
+
* Returns the key that releasePool acts on: the exact key when the manager
|
|
59
|
+
* tracks it, else the one region-prefixed key for a bare slug. Returns
|
|
60
|
+
* undefined when no key or more than one key matches.
|
|
61
|
+
*/
|
|
62
|
+
private resolveHeldKey;
|
|
35
63
|
/** Returns a snapshot of current pool statistics. */
|
|
36
64
|
getStats(): {
|
|
37
65
|
poolCount: number;
|
|
38
66
|
activeConnections: number;
|
|
39
67
|
};
|
|
40
|
-
/**
|
|
41
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Removes the least-recently-used pool that no caller holds from the map and
|
|
70
|
+
* returns it. The caller ends it. Returns undefined when every pool is held.
|
|
71
|
+
*/
|
|
72
|
+
private takeLRU;
|
|
42
73
|
}
|
|
43
74
|
//# sourceMappingURL=pool-manager.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,0BAA0B;IACzC,mHAAmH;IACnH,oBAAoB,EAAE,EAAE,CAAC,UAAU,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB
|
|
1
|
+
{"version":3,"file":"pool-manager.d.ts","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,0BAA0B;IACzC,mHAAmH;IACnH,oBAAoB,EAAE,EAAE,CAAC,UAAU,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,0BAA0B;IAM/C;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;IAgCtE;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAqBxD;gFAC4E;IACtE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBrE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,uGAAuG;IACvG,OAAO,CAAC,iBAAiB;IAKzB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAWtB,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAW5D;;;OAGG;IACH,OAAO,CAAC,OAAO;CAgBhB"}
|
|
@@ -9,11 +9,16 @@ const manager_js_1 = require("./manager.js");
|
|
|
9
9
|
/**
|
|
10
10
|
* Manages a collection of per-tenant pg.Pool instances.
|
|
11
11
|
*
|
|
12
|
-
* Pools are keyed by tenant slug and created on first access.
|
|
13
|
-
*
|
|
12
|
+
* Pools are keyed by tenant slug and created on first access. Each getPool call
|
|
13
|
+
* holds the pool until the caller calls releasePool. When the pool count reaches
|
|
14
|
+
* maxPools, the least-recently-used pool that no caller holds is evicted. A pool
|
|
15
|
+
* that a caller holds is never evicted, so the count can exceed maxPools while
|
|
16
|
+
* every pool is in use.
|
|
14
17
|
*/
|
|
15
18
|
class DatabasePoolManager {
|
|
16
19
|
pools = new Map();
|
|
20
|
+
/** Holds that callers still have on pools that closePool or closeAll removed, by pool key. */
|
|
21
|
+
closedHolds = new Map();
|
|
17
22
|
baseConfig;
|
|
18
23
|
maxPools;
|
|
19
24
|
idleTimeoutMs;
|
|
@@ -23,8 +28,9 @@ class DatabasePoolManager {
|
|
|
23
28
|
this.idleTimeoutMs = options.idleTimeoutMs ?? 30_000;
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
|
-
* Returns
|
|
27
|
-
*
|
|
31
|
+
* Returns the pool for the tenant slug, and creates it on first access.
|
|
32
|
+
* Each call holds the pool until the caller calls releasePool with the same
|
|
33
|
+
* arguments. A held pool is never evicted.
|
|
28
34
|
*
|
|
29
35
|
* When a regionId is provided, the pool is keyed as `regionId:slug` to support
|
|
30
36
|
* multi-region deployments where the same slug may exist in different regions.
|
|
@@ -32,22 +38,63 @@ class DatabasePoolManager {
|
|
|
32
38
|
async getPool(tenantSlug, regionId) {
|
|
33
39
|
const dbName = (0, manager_js_1.getDatabaseName)(tenantSlug);
|
|
34
40
|
const poolKey = regionId ? `${regionId}:${tenantSlug}` : tenantSlug;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
41
|
+
// The lookup, the eviction choice and the insert run with no await between
|
|
42
|
+
// them. Thus a concurrent call for the same key always finds this entry.
|
|
43
|
+
let entry = this.pools.get(poolKey);
|
|
44
|
+
let victim;
|
|
45
|
+
if (!entry) {
|
|
46
|
+
if (this.pools.size >= this.maxPools)
|
|
47
|
+
victim = this.takeLRU();
|
|
48
|
+
entry = {
|
|
49
|
+
pool: new pg_1.default.Pool({
|
|
50
|
+
...this.baseConfig,
|
|
51
|
+
database: dbName,
|
|
52
|
+
idleTimeoutMillis: this.idleTimeoutMs,
|
|
53
|
+
}),
|
|
54
|
+
lastUsed: Date.now(),
|
|
55
|
+
refCount: 0,
|
|
56
|
+
};
|
|
57
|
+
this.pools.set(poolKey, entry);
|
|
39
58
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
entry.lastUsed = Date.now();
|
|
60
|
+
entry.refCount++;
|
|
61
|
+
if (victim) {
|
|
62
|
+
// The evicted pool belongs to a different tenant. Its end error must not
|
|
63
|
+
// fail this request, which already has a usable pool.
|
|
64
|
+
await victim.end().catch(() => undefined);
|
|
43
65
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
66
|
+
return entry.pool;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Ends one hold on the pool that getPool returned for the same arguments.
|
|
70
|
+
* Call it once for each getPool call. No-op if the pool is not tracked.
|
|
71
|
+
*
|
|
72
|
+
* Without a regionId, a bare slug also matches one region-prefixed pool
|
|
73
|
+
* (`regionId:slug`), as closePool does. If the slug matches pools in more
|
|
74
|
+
* than one region, the call releases nothing, because the manager cannot
|
|
75
|
+
* tell which pool the caller holds. Pass the regionId in that case.
|
|
76
|
+
*/
|
|
77
|
+
releasePool(tenantSlug, regionId) {
|
|
78
|
+
const poolKey = this.resolveHeldKey(tenantSlug, regionId);
|
|
79
|
+
if (poolKey === undefined)
|
|
80
|
+
return;
|
|
81
|
+
// The caller passes a key, not the pool, so the manager cannot tell a
|
|
82
|
+
// hold on a closed pool from a hold on its replacement. It ends holds on
|
|
83
|
+
// closed pools first. The replacement then counts as held for longer than
|
|
84
|
+
// it is, which is safe: eviction never ends a pool that is in use.
|
|
85
|
+
const closed = this.closedHolds.get(poolKey);
|
|
86
|
+
if (closed !== undefined) {
|
|
87
|
+
if (closed === 1)
|
|
88
|
+
this.closedHolds.delete(poolKey);
|
|
89
|
+
else
|
|
90
|
+
this.closedHolds.set(poolKey, closed - 1);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const entry = this.pools.get(poolKey);
|
|
94
|
+
if (!entry || entry.refCount === 0)
|
|
95
|
+
return;
|
|
96
|
+
entry.refCount--;
|
|
97
|
+
entry.lastUsed = Date.now();
|
|
51
98
|
}
|
|
52
99
|
/** Closes and removes the pool for the given tenant slug. No-op if not found.
|
|
53
100
|
* Accepts either a bare slug or a region-prefixed key (`regionId:slug`). */
|
|
@@ -68,14 +115,39 @@ class DatabasePoolManager {
|
|
|
68
115
|
if (!entry)
|
|
69
116
|
return;
|
|
70
117
|
this.pools.delete(resolvedKey);
|
|
118
|
+
this.recordClosedHolds(resolvedKey, entry);
|
|
71
119
|
await entry.pool.end();
|
|
72
120
|
}
|
|
73
121
|
/** Closes all managed pools. Call during application shutdown. */
|
|
74
122
|
async closeAll() {
|
|
75
123
|
const entries = Array.from(this.pools.entries());
|
|
76
124
|
this.pools.clear();
|
|
125
|
+
for (const [key, entry] of entries)
|
|
126
|
+
this.recordClosedHolds(key, entry);
|
|
77
127
|
await Promise.all(entries.map(([, entry]) => entry.pool.end()));
|
|
78
128
|
}
|
|
129
|
+
/** Keeps the holds of a removed pool, so that their releases do not reach a later pool for the key. */
|
|
130
|
+
recordClosedHolds(poolKey, entry) {
|
|
131
|
+
if (entry.refCount === 0)
|
|
132
|
+
return;
|
|
133
|
+
this.closedHolds.set(poolKey, (this.closedHolds.get(poolKey) ?? 0) + entry.refCount);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Returns the key that releasePool acts on: the exact key when the manager
|
|
137
|
+
* tracks it, else the one region-prefixed key for a bare slug. Returns
|
|
138
|
+
* undefined when no key or more than one key matches.
|
|
139
|
+
*/
|
|
140
|
+
resolveHeldKey(tenantSlug, regionId) {
|
|
141
|
+
const poolKey = regionId ? `${regionId}:${tenantSlug}` : tenantSlug;
|
|
142
|
+
if (this.pools.has(poolKey) || this.closedHolds.has(poolKey) || regionId)
|
|
143
|
+
return poolKey;
|
|
144
|
+
const matches = new Set();
|
|
145
|
+
for (const key of [...this.pools.keys(), ...this.closedHolds.keys()]) {
|
|
146
|
+
if (key.endsWith(`:${tenantSlug}`))
|
|
147
|
+
matches.add(key);
|
|
148
|
+
}
|
|
149
|
+
return matches.size === 1 ? [...matches][0] : undefined;
|
|
150
|
+
}
|
|
79
151
|
/** Returns a snapshot of current pool statistics. */
|
|
80
152
|
getStats() {
|
|
81
153
|
let activeConnections = 0;
|
|
@@ -87,19 +159,24 @@ class DatabasePoolManager {
|
|
|
87
159
|
}
|
|
88
160
|
return { poolCount: this.pools.size, activeConnections };
|
|
89
161
|
}
|
|
90
|
-
/**
|
|
91
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Removes the least-recently-used pool that no caller holds from the map and
|
|
164
|
+
* returns it. The caller ends it. Returns undefined when every pool is held.
|
|
165
|
+
*/
|
|
166
|
+
takeLRU() {
|
|
92
167
|
let oldestKey;
|
|
93
168
|
let oldestTime = Infinity;
|
|
94
169
|
for (const [key, entry] of this.pools.entries()) {
|
|
95
|
-
if (entry.lastUsed < oldestTime) {
|
|
170
|
+
if (entry.refCount === 0 && entry.lastUsed < oldestTime) {
|
|
96
171
|
oldestTime = entry.lastUsed;
|
|
97
172
|
oldestKey = key;
|
|
98
173
|
}
|
|
99
174
|
}
|
|
100
|
-
if (oldestKey
|
|
101
|
-
|
|
102
|
-
|
|
175
|
+
if (oldestKey === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
const entry = this.pools.get(oldestKey);
|
|
178
|
+
this.pools.delete(oldestKey);
|
|
179
|
+
return entry.pool;
|
|
103
180
|
}
|
|
104
181
|
}
|
|
105
182
|
exports.DatabasePoolManager = DatabasePoolManager;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-manager.js","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,6CAA+C;
|
|
1
|
+
{"version":3,"file":"pool-manager.js","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -23,7 +23,7 @@ async function enableRLS(client, tableName) {
|
|
|
23
23
|
async function createIsolationPolicy(client, tableName) {
|
|
24
24
|
const safe = validateTableName(tableName);
|
|
25
25
|
await client.query(`CREATE POLICY tenant_isolation ON ${safe}
|
|
26
|
-
USING (tenant_id = current_setting('app.current_tenant_id')::uuid)`);
|
|
26
|
+
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`);
|
|
27
27
|
}
|
|
28
28
|
async function migrateTable(client, tableName) {
|
|
29
29
|
await addTenantColumn(client, tableName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;AAUA,0CAQC;AAED,8BAOC;AAED,sDASC;AAED,oCAOC;AA7CD,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,eAAe,IAAI,qCAAqC,CACzD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI;
|
|
1
|
+
{"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;AAUA,0CAQC;AAED,8BAOC;AAED,sDASC;AAED,oCAOC;AA7CD,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,eAAe,IAAI,qCAAqC,CACzD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI;0FAC6C,CACvF,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
|
package/dist/rls/manager.js
CHANGED
|
@@ -18,7 +18,7 @@ async function createPolicy(client, tableName) {
|
|
|
18
18
|
// Table name is validated via allowlist regex above.
|
|
19
19
|
const exists = await client.query(`SELECT count(*) FROM pg_policies WHERE tablename = $1 AND policyname = 'tenant_isolation'`, [safe]);
|
|
20
20
|
if (parseInt(exists.rows[0].count, 10) === 0) {
|
|
21
|
-
await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (tenant_id = current_setting('app.current_tenant_id')::uuid)`);
|
|
21
|
+
await client.query(`CREATE POLICY tenant_isolation ON ${safe} USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`);
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
async function dropPolicy(client, tableName) {
|
package/dist/rls/manager.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":";;AAUA,oCAgBC;AAED,gCAMC;AAED,8BAOC;AAED,gCAOC;AAED,oCAYC;AAhED,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,4EAA4E;IAC5E,qDAAqD;IACrD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B,2FAA2F,EAC3F,CAAC,IAAI,CAAC,CACP,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI,
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/rls/manager.ts"],"names":[],"mappings":";;AAUA,oCAgBC;AAED,gCAMC;AAED,8BAOC;AAED,gCAOC;AAED,oCAYC;AAhED,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,SAAiB;IAC1C,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,KAAK,CAAC,uBAAuB,SAAS,EAAE,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,4EAA4E;IAC5E,qDAAqD;IACrD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAC/B,2FAA2F,EAC3F,CAAC,IAAI,CAAC,CACP,CAAC;IACF,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI,uFAAuF,CACjI,CAAC;IACJ,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,6CAA6C,IAAI,EAAE,CAAC,CAAC;AAC1E,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,8BAA8B,CAAC,CAAC;IACtE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,6BAA6B,CAAC,CAAC;AACvE,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,wDAAwD,EACxD,CAAC,SAAS,CAAC,CACZ,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC;AACpC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratum-hq/db-adapters",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Stratum PostgreSQL adapters for tenant-scoped query isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multi-tenancy",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"registry": "https://registry.npmjs.org/"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@stratum-hq/core": "
|
|
44
|
+
"@stratum-hq/core": "^1.4.0",
|
|
45
45
|
"pg": "^8.11.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|