@stratum-hq/db-adapters 1.0.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 +39 -4
- package/dist/adapters/database-prisma.d.ts +4 -2
- package/dist/adapters/database-prisma.d.ts.map +1 -1
- package/dist/adapters/database-prisma.js +19 -6
- package/dist/adapters/database-prisma.js.map +1 -1
- 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/drizzle.d.ts +2 -2
- package/dist/adapters/drizzle.d.ts.map +1 -1
- package/dist/adapters/drizzle.js +9 -8
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/prisma.d.ts +1 -1
- package/dist/adapters/prisma.d.ts.map +1 -1
- package/dist/adapters/prisma.js +12 -5
- package/dist/adapters/prisma.js.map +1 -1
- package/dist/adapters/schema-prisma.d.ts +36 -4
- package/dist/adapters/schema-prisma.d.ts.map +1 -1
- package/dist/adapters/schema-prisma.js +55 -17
- package/dist/adapters/schema-prisma.js.map +1 -1
- package/dist/adapters/schema-raw.d.ts +14 -3
- package/dist/adapters/schema-raw.d.ts.map +1 -1
- package/dist/adapters/schema-raw.js +30 -8
- package/dist/adapters/schema-raw.js.map +1 -1
- package/dist/database/manager.d.ts.map +1 -1
- package/dist/database/manager.js +7 -1
- package/dist/database/manager.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 +103 -27
- package/dist/database/pool-manager.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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/dist/schema/manager.d.ts +1 -0
- package/dist/schema/manager.d.ts.map +1 -1
- package/dist/schema/manager.js +11 -2
- package/dist/schema/manager.js.map +1 -1
- package/dist/schema/session.d.ts +10 -1
- package/dist/schema/session.d.ts.map +1 -1
- package/dist/schema/session.js +24 -4
- package/dist/schema/session.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -31,12 +31,12 @@ Every query is wrapped in a transaction that sets `app.current_tenant_id` via a
|
|
|
31
31
|
## ORM Adapters
|
|
32
32
|
|
|
33
33
|
```typescript
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import { SequelizeAdapter,
|
|
34
|
+
import { prismaWithTenant } from "@stratum-hq/db-adapters"; // Prisma
|
|
35
|
+
import { drizzleWithTenant } from "@stratum-hq/db-adapters"; // Drizzle
|
|
36
|
+
import { SequelizeAdapter, sequelizeWithTenantScope } from "@stratum-hq/db-adapters"; // Sequelize
|
|
37
37
|
|
|
38
38
|
// Prisma — all queries scoped to the current tenant
|
|
39
|
-
const tenantPrisma =
|
|
39
|
+
const tenantPrisma = prismaWithTenant(prisma, () => getTenantContext().tenant_id, pool);
|
|
40
40
|
const orders = await tenantPrisma.order.findMany();
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -58,6 +58,41 @@ try {
|
|
|
58
58
|
|
|
59
59
|
Also available: `disableRLS`, `dropPolicy`, `isRLSEnabled`, `addTenantColumn`, `createIsolationPolicy`, and low-level session helpers `setTenantContext` / `resetTenantContext` / `getCurrentTenantId`. Schema-per-tenant and database-per-tenant variants (`SchemaRawAdapter`, `DatabasePoolManager`, …) are exported too.
|
|
60
60
|
|
|
61
|
+
## Schema-per-tenant search_path
|
|
62
|
+
|
|
63
|
+
`SchemaRawAdapter`, `createSchemaTenantPool` and `setSchemaSearchPath` set `search_path` to the tenant schema (`tenant_<slug>`) **alone**, inside a transaction. `public` is not on the path, so an unqualified table missing from the tenant schema is an error instead of silently reading or writing a table every tenant shares. (`setSchemaSearchPath` throws if it is called outside a transaction.)
|
|
64
|
+
|
|
65
|
+
If your queries call extension functions or types that live in another schema (for example `uuid_generate_v4()`, `ltree` operators, `citext` or `pgcrypto` installed in `public`), either schema-qualify them or opt that schema in explicitly:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { SchemaRawAdapter, createSchemaTenantPool, setSchemaSearchPath } from "@stratum-hq/db-adapters";
|
|
69
|
+
|
|
70
|
+
const adapter = new SchemaRawAdapter(pool, { extraSearchPath: ["extensions"] });
|
|
71
|
+
const tenantPool = createSchemaTenantPool(pool, getTenantSlug, { extraSearchPath: ["extensions"] });
|
|
72
|
+
await setSchemaSearchPath(client, "acme", ["extensions"]); // inside BEGIN ... COMMIT
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Extra schemas come **after** the tenant schema, and each entry must be a plain identifier (`/^[a-zA-Z_][a-zA-Z0-9_]*$/`, validated like tenant schema names). The trade-off: an unqualified table name missing from the tenant schema resolves in the extra schemas too, so list only schemas that hold no tenant data. Prefer a dedicated extensions schema over `public`. Column defaults such as `DEFAULT uuid_generate_v4()` are bound when the table is created and work without any extra schema.
|
|
76
|
+
|
|
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
|
+
|
|
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
|
+
|
|
61
96
|
## Security
|
|
62
97
|
|
|
63
98
|
- All DDL validates table names against `/^[a-zA-Z_][a-zA-Z0-9_]*$/`.
|
|
@@ -28,11 +28,13 @@ export declare class DatabasePrismaAdapter {
|
|
|
28
28
|
private readonly poolManager;
|
|
29
29
|
private readonly PrismaClient;
|
|
30
30
|
private readonly baseDatasourceUrl;
|
|
31
|
+
private readonly maxClients;
|
|
31
32
|
private readonly clients;
|
|
32
|
-
constructor(poolManager: DatabasePoolManager, PrismaClient: PrismaConstructor, baseDatasourceUrl: string);
|
|
33
|
+
constructor(poolManager: DatabasePoolManager, PrismaClient: PrismaConstructor, baseDatasourceUrl: string, maxClients?: number);
|
|
33
34
|
/**
|
|
34
35
|
* Returns a Prisma client connected to the tenant's dedicated database.
|
|
35
|
-
* Clients are cached per tenant slug
|
|
36
|
+
* Clients are cached per tenant slug, up to maxClients; the least recently
|
|
37
|
+
* used client is disconnected when the limit is reached.
|
|
36
38
|
*/
|
|
37
39
|
getClient(tenantSlug: string): PrismaClientLike;
|
|
38
40
|
/** Disconnects and removes the cached Prisma client for the given tenant. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/database-prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"database-prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/database-prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,6BAA6B,CAAC;AAIlE,UAAU,gBAAgB;IACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,gBAAgB,CAAC;IACnD,WAAW,EAAE,CACX,KAAK,EAAE,oBAAoB,EAC3B,GAAG,MAAM,EAAE,OAAO,EAAE,KACjB,OAAO,CAAC,MAAM,CAAC,CAAC;IACrB,YAAY,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,gBAAgB,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1E,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,KAAK,iBAAiB,GAAG,KAAK,OAAO,EAAE;IAAE,WAAW,EAAE;QAAE,EAAE,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,KAAK,gBAAgB,CAAC;AAErG;;;;;;;;;;GAUG;AACH,qBAAa,qBAAqB;IAI9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4C;gBAGjD,WAAW,EAAE,mBAAmB,EAChC,YAAY,EAAE,iBAAiB,EAC/B,iBAAiB,EAAE,MAAM,EACzB,UAAU,GAAE,MAAW;IAG1C;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,gBAAgB;IAyB/C,6EAA6E;IACvE,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOzD,+EAA+E;IACzE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAMpC;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;CAK3B"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DatabasePrismaAdapter = void 0;
|
|
4
|
+
const manager_js_1 = require("../database/manager.js");
|
|
4
5
|
/**
|
|
5
6
|
* Prisma adapter for DB_PER_TENANT isolation.
|
|
6
7
|
*
|
|
@@ -16,21 +17,32 @@ class DatabasePrismaAdapter {
|
|
|
16
17
|
poolManager;
|
|
17
18
|
PrismaClient;
|
|
18
19
|
baseDatasourceUrl;
|
|
20
|
+
maxClients;
|
|
19
21
|
clients = new Map();
|
|
20
|
-
constructor(poolManager, PrismaClient, baseDatasourceUrl) {
|
|
22
|
+
constructor(poolManager, PrismaClient, baseDatasourceUrl, maxClients = 50) {
|
|
21
23
|
this.poolManager = poolManager;
|
|
22
24
|
this.PrismaClient = PrismaClient;
|
|
23
25
|
this.baseDatasourceUrl = baseDatasourceUrl;
|
|
26
|
+
this.maxClients = maxClients;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* Returns a Prisma client connected to the tenant's dedicated database.
|
|
27
|
-
* Clients are cached per tenant slug
|
|
30
|
+
* Clients are cached per tenant slug, up to maxClients; the least recently
|
|
31
|
+
* used client is disconnected when the limit is reached.
|
|
28
32
|
*/
|
|
29
33
|
getClient(tenantSlug) {
|
|
34
|
+
const dbName = (0, manager_js_1.getDatabaseName)(tenantSlug);
|
|
30
35
|
const existing = this.clients.get(tenantSlug);
|
|
31
|
-
if (existing)
|
|
36
|
+
if (existing) {
|
|
37
|
+
this.clients.delete(tenantSlug);
|
|
38
|
+
this.clients.set(tenantSlug, existing);
|
|
32
39
|
return existing;
|
|
33
|
-
|
|
40
|
+
}
|
|
41
|
+
if (this.clients.size >= this.maxClients) {
|
|
42
|
+
const [oldestKey, oldest] = this.clients.entries().next().value;
|
|
43
|
+
this.clients.delete(oldestKey);
|
|
44
|
+
void oldest.$disconnect().catch(() => { });
|
|
45
|
+
}
|
|
34
46
|
const tenantUrl = this.buildDatasourceUrl(this.baseDatasourceUrl, dbName);
|
|
35
47
|
const client = new this.PrismaClient({
|
|
36
48
|
datasources: { db: { url: tenantUrl } },
|
|
@@ -58,8 +70,9 @@ class DatabasePrismaAdapter {
|
|
|
58
70
|
* postgres://user:pass@host:port/dbname[?params]
|
|
59
71
|
*/
|
|
60
72
|
buildDatasourceUrl(baseUrl, dbName) {
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const url = new URL(baseUrl);
|
|
74
|
+
url.pathname = `/${dbName}`;
|
|
75
|
+
return url.toString();
|
|
63
76
|
}
|
|
64
77
|
}
|
|
65
78
|
exports.DatabasePrismaAdapter = DatabasePrismaAdapter;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-prisma.js","sourceRoot":"","sources":["../../src/adapters/database-prisma.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"database-prisma.js","sourceRoot":"","sources":["../../src/adapters/database-prisma.ts"],"names":[],"mappings":";;;AACA,uDAAyD;AAgBzD;;;;;;;;;;GAUG;AACH,MAAa,qBAAqB;IAIb;IACA;IACA;IACA;IANF,OAAO,GAAkC,IAAI,GAAG,EAAE,CAAC;IAEpE,YACmB,WAAgC,EAChC,YAA+B,EAC/B,iBAAyB,EACzB,aAAqB,EAAE;QAHvB,gBAAW,GAAX,WAAW,CAAqB;QAChC,iBAAY,GAAZ,YAAY,CAAmB;QAC/B,sBAAiB,GAAjB,iBAAiB,CAAQ;QACzB,eAAU,GAAV,UAAU,CAAa;IACvC,CAAC;IAEJ;;;;OAIG;IACH,SAAS,CAAC,UAAkB;QAC1B,MAAM,MAAM,GAAG,IAAA,4BAAe,EAAC,UAAU,CAAC,CAAC;QAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC9C,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAChC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACvC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAmC,CAAC;YAC9F,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/B,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;QAE1E,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC;YACnC,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE;SACxC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACrC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,6EAA6E;IAC7E,KAAK,CAAC,gBAAgB,CAAC,UAAkB;QACvC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC5C,IAAI,CAAC,MAAM;YAAE,OAAO;QACpB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAChC,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,aAAa;QACjB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IAED;;;;OAIG;IACK,kBAAkB,CAAC,OAAe,EAAE,MAAc;QACxD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,GAAG,CAAC,QAAQ,GAAG,IAAI,MAAM,EAAE,CAAC;QAC5B,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;CACF;AAjED,sDAiEC"}
|
|
@@ -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"}
|
|
@@ -17,9 +17,9 @@ export declare class DrizzleAdapter extends BaseAdapter {
|
|
|
17
17
|
* When contextFn returns an empty string the original methods are forwarded
|
|
18
18
|
* without wrapping.
|
|
19
19
|
*/
|
|
20
|
-
withTenant(db:
|
|
20
|
+
withTenant<D extends DrizzleLike>(db: D, contextFn: () => string): Pick<D, "execute" | "transaction">;
|
|
21
21
|
}
|
|
22
|
-
export declare function withTenant(db:
|
|
22
|
+
export declare function withTenant<D extends DrizzleLike>(db: D, contextFn: () => string, pool: pg.Pool): Pick<D, "execute" | "transaction">;
|
|
23
23
|
/** @deprecated Use `withTenant` instead. */
|
|
24
24
|
export declare const withTenantScope: typeof withTenant;
|
|
25
25
|
//# sourceMappingURL=drizzle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../../src/adapters/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjE;
|
|
1
|
+
{"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../../src/adapters/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,MAAM,WAAW,WAAW;IAC1B,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACjE;AAQD,qBAAa,cAAe,SAAQ,WAAW;gBACjC,IAAI,EAAE,EAAE,CAAC,IAAI;IAIzB;;;;;;;;;;OAUG;IACH,UAAU,CAAC,CAAC,SAAS,WAAW,EAC9B,EAAE,EAAE,CAAC,EACL,SAAS,EAAE,MAAM,MAAM,GACtB,IAAI,CAAC,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC;CAsCtC;AAGD,wBAAgB,UAAU,CAAC,CAAC,SAAS,WAAW,EAC9C,EAAE,EAAE,CAAC,EACL,SAAS,EAAE,MAAM,MAAM,EACvB,IAAI,EAAE,EAAE,CAAC,IAAI,GACZ,IAAI,CAAC,CAAC,EAAE,SAAS,GAAG,aAAa,CAAC,CAGpC;AAED,4CAA4C;AAC5C,eAAO,MAAM,eAAe,mBAAa,CAAC"}
|
package/dist/adapters/drizzle.js
CHANGED
|
@@ -3,6 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.withTenantScope = exports.DrizzleAdapter = void 0;
|
|
4
4
|
exports.withTenant = withTenant;
|
|
5
5
|
const base_adapter_js_1 = require("../base-adapter.js");
|
|
6
|
+
// drizzle-orm is an optional peer, so it is loaded only when this adapter runs.
|
|
7
|
+
async function setTenantConfig(tx, tenantId) {
|
|
8
|
+
const { sql } = await import("drizzle-orm");
|
|
9
|
+
await tx.execute(sql `SELECT set_config('app.current_tenant_id', ${tenantId}, true)`);
|
|
10
|
+
}
|
|
6
11
|
class DrizzleAdapter extends base_adapter_js_1.BaseAdapter {
|
|
7
12
|
constructor(pool) {
|
|
8
13
|
super(pool);
|
|
@@ -27,10 +32,7 @@ class DrizzleAdapter extends base_adapter_js_1.BaseAdapter {
|
|
|
27
32
|
"Use the unwrapped instance for system/admin operations.");
|
|
28
33
|
}
|
|
29
34
|
return original.transaction(async (tx) => {
|
|
30
|
-
await tx
|
|
31
|
-
sql: `SELECT set_config('app.current_tenant_id', $1, true)`,
|
|
32
|
-
params: [tenantId],
|
|
33
|
-
});
|
|
35
|
+
await setTenantConfig(tx, tenantId);
|
|
34
36
|
return fn(tx);
|
|
35
37
|
});
|
|
36
38
|
};
|
|
@@ -41,13 +43,12 @@ class DrizzleAdapter extends base_adapter_js_1.BaseAdapter {
|
|
|
41
43
|
"Use the unwrapped instance for system/admin operations.");
|
|
42
44
|
}
|
|
43
45
|
return original.transaction(async (tx) => {
|
|
44
|
-
await tx
|
|
45
|
-
sql: `SELECT set_config('app.current_tenant_id', $1, true)`,
|
|
46
|
-
params: [tenantId],
|
|
47
|
-
});
|
|
46
|
+
await setTenantConfig(tx, tenantId);
|
|
48
47
|
return tx.execute(query);
|
|
49
48
|
});
|
|
50
49
|
};
|
|
50
|
+
// Typed as the caller's own Drizzle instance so `tx` keeps its query
|
|
51
|
+
// builder (`tx.select()...`) inside `transaction`.
|
|
51
52
|
return {
|
|
52
53
|
execute: wrappedExecute,
|
|
53
54
|
transaction: wrappedTransaction,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../../src/adapters/drizzle.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../../src/adapters/drizzle.ts"],"names":[],"mappings":";;;AA4EA,gCAOC;AAnFD,wDAAiD;AAUjD,gFAAgF;AAChF,KAAK,UAAU,eAAe,CAAC,EAAe,EAAE,QAAgB;IAC9D,MAAM,EAAE,GAAG,EAAE,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAA,8CAA8C,QAAQ,SAAS,CAAC,CAAC;AACvF,CAAC;AAED,MAAa,cAAe,SAAQ,6BAAW;IAC7C,YAAY,IAAa;QACvB,KAAK,CAAC,IAAI,CAAC,CAAC;IACd,CAAC;IAED;;;;;;;;;;OAUG;IACH,UAAU,CACR,EAAK,EACL,SAAuB;QAEvB,MAAM,QAAQ,GAAG,EAAE,CAAC;QAEpB,MAAM,kBAAkB,GAAG,KAAK,EAAK,EAAmC,EAAc,EAAE;YACtF,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CACb,sDAAsD;oBACtD,yDAAyD,CAC1D,CAAC;YACJ,CAAC;YACD,OAAO,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,EAAe,EAAE,EAAE;gBACpD,MAAM,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;gBACpC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;YAChB,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,MAAM,cAAc,GAAG,KAAK,EAAE,KAAc,EAAoB,EAAE;YAChE,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,MAAM,IAAI,KAAK,CACb,sDAAsD;oBACtD,yDAAyD,CAC1D,CAAC;YACJ,CAAC;YACD,OAAO,QAAQ,CAAC,WAAW,CAAC,KAAK,EAAE,EAAe,EAAE,EAAE;gBACpD,MAAM,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;gBACpC,OAAO,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,qEAAqE;QACrE,mDAAmD;QACnD,OAAO;YACL,OAAO,EAAE,cAAc;YACvB,WAAW,EAAE,kBAAkB;SACiB,CAAC;IACrD,CAAC;CACF;AAzDD,wCAyDC;AAED,+DAA+D;AAC/D,SAAgB,UAAU,CACxB,EAAK,EACL,SAAuB,EACvB,IAAa;IAEb,MAAM,OAAO,GAAG,IAAI,cAAc,CAAC,IAAI,CAAC,CAAC;IACzC,OAAO,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;AAC3C,CAAC;AAED,4CAA4C;AAC/B,QAAA,eAAe,GAAG,UAAU,CAAC"}
|
|
@@ -3,7 +3,7 @@ import pg from "pg";
|
|
|
3
3
|
interface PrismaClientLike {
|
|
4
4
|
$extends: (extension: unknown) => PrismaClientLike;
|
|
5
5
|
$executeRaw: (query: TemplateStringsArray, ...values: unknown[]) => Promise<number>;
|
|
6
|
-
$transaction:
|
|
6
|
+
$transaction: (queries: unknown[]) => Promise<unknown[]>;
|
|
7
7
|
}
|
|
8
8
|
export declare class PrismaAdapter extends BaseAdapter {
|
|
9
9
|
constructor(pool: pg.Pool);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,UAAU,gBAAgB;IACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,gBAAgB,CAAC;IACnD,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpF,YAAY,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/prisma.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,UAAU,gBAAgB;IACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,gBAAgB,CAAC;IACnD,WAAW,EAAE,CAAC,KAAK,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpF,YAAY,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CAC1D;AAED,qBAAa,aAAc,SAAQ,WAAW;gBAChC,IAAI,EAAE,EAAE,CAAC,IAAI;IAIzB,UAAU,CAAC,MAAM,EAAE,gBAAgB,EAAE,SAAS,EAAE,MAAM,MAAM,GAAG,gBAAgB;CAoBhF;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,MAAM,EACvB,IAAI,EAAE,EAAE,CAAC,IAAI,GACZ,gBAAgB,CAGlB"}
|
package/dist/adapters/prisma.js
CHANGED
|
@@ -10,12 +10,19 @@ class PrismaAdapter extends base_adapter_js_1.BaseAdapter {
|
|
|
10
10
|
withTenant(prisma, contextFn) {
|
|
11
11
|
return prisma.$extends({
|
|
12
12
|
query: {
|
|
13
|
-
$allOperations({ args, query }) {
|
|
13
|
+
async $allOperations({ args, query }) {
|
|
14
14
|
const tenantId = contextFn();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
if (!tenantId) {
|
|
16
|
+
throw new Error("Tenant context is required for database operations.");
|
|
17
|
+
}
|
|
18
|
+
// Batch form: Prisma runs both statements in order on one connection
|
|
19
|
+
// inside one transaction. `query(args)` does not inherit an
|
|
20
|
+
// interactive `tx`, so the callback form would run it elsewhere.
|
|
21
|
+
const [, result] = await prisma.$transaction([
|
|
22
|
+
prisma.$executeRaw `SELECT set_config('app.current_tenant_id', ${tenantId}, true)`,
|
|
23
|
+
query(args),
|
|
24
|
+
]);
|
|
25
|
+
return result;
|
|
19
26
|
},
|
|
20
27
|
},
|
|
21
28
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["../../src/adapters/prisma.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"prisma.js","sourceRoot":"","sources":["../../src/adapters/prisma.ts"],"names":[],"mappings":";;;AAsCA,gCAOC;AA7CD,wDAAiD;AAWjD,MAAa,aAAc,SAAQ,6BAAW;IAC5C,YAAY,IAAa;QACvB,KAAK,CAAC,IAAI,CAAC,CAAC;IACd,CAAC;IAED,UAAU,CAAC,MAAwB,EAAE,SAAuB;QAC1D,OAAO,MAAM,CAAC,QAAQ,CAAC;YACrB,KAAK,EAAE;gBACL,KAAK,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,KAAK,EAAiE;oBACjG,MAAM,QAAQ,GAAG,SAAS,EAAE,CAAC;oBAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACd,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;oBACzE,CAAC;oBACD,qEAAqE;oBACrE,4DAA4D;oBAC5D,iEAAiE;oBACjE,MAAM,CAAC,EAAE,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC;wBAC3C,MAAM,CAAC,WAAW,CAAA,8CAA8C,QAAQ,SAAS;wBACjF,KAAK,CAAC,IAAI,CAAC;qBACZ,CAAC,CAAC;oBACH,OAAO,MAAM,CAAC;gBAChB,CAAC;aACF;SACF,CAAC,CAAC;IACL,CAAC;CACF;AAzBD,sCAyBC;AAED,SAAgB,UAAU,CACxB,MAAwB,EACxB,SAAuB,EACvB,IAAa;IAEb,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO,OAAO,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
interface PrismaClientLike {
|
|
2
2
|
$extends: (extension: unknown) => PrismaClientLike;
|
|
3
|
-
$
|
|
4
|
-
$transaction: <T>(fn: (tx: PrismaClientLike) => Promise<T>) => Promise<T>;
|
|
3
|
+
$disconnect: () => Promise<void>;
|
|
5
4
|
}
|
|
5
|
+
type PrismaConstructor = new (options: {
|
|
6
|
+
datasources: {
|
|
7
|
+
db: {
|
|
8
|
+
url: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}) => PrismaClientLike;
|
|
12
|
+
/**
|
|
13
|
+
* Prisma adapter for SCHEMA_PER_TENANT isolation.
|
|
14
|
+
*
|
|
15
|
+
* Prisma schema-qualifies every table with the datasource URL's `schema`
|
|
16
|
+
* parameter, so `search_path` cannot route its queries. Each tenant therefore
|
|
17
|
+
* gets its own Prisma client whose datasource URL names the tenant's schema.
|
|
18
|
+
* Clients are cached per tenant slug, least recently used first out.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* const adapter = new SchemaPrismaAdapter(PrismaClient, baseUrl);
|
|
22
|
+
* const prisma = adapter.getClient('acme_corp');
|
|
23
|
+
* const rows = await prisma.someModel.findMany();
|
|
24
|
+
*/
|
|
6
25
|
export declare class SchemaPrismaAdapter {
|
|
7
|
-
|
|
26
|
+
private readonly PrismaClient;
|
|
27
|
+
private readonly baseDatasourceUrl;
|
|
28
|
+
private readonly maxClients;
|
|
29
|
+
private readonly clients;
|
|
30
|
+
constructor(PrismaClient: PrismaConstructor, baseDatasourceUrl: string, maxClients?: number);
|
|
31
|
+
/** Returns a Prisma client bound to the tenant's schema. */
|
|
32
|
+
getClient(tenantSlug: string): PrismaClientLike;
|
|
33
|
+
/** Disconnects all cached Prisma clients. Call during application shutdown. */
|
|
34
|
+
disconnectAll(): Promise<void>;
|
|
8
35
|
}
|
|
9
|
-
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Cannot isolate tenants: Prisma ignores `search_path` and the
|
|
38
|
+
* wrapped query does not run on the transaction that would set it. Always
|
|
39
|
+
* throws. Use `new SchemaPrismaAdapter(PrismaClient, url).getClient(slug)`.
|
|
40
|
+
*/
|
|
41
|
+
export declare function withSchemaTenant(_prisma: unknown, _contextFn: () => string): never;
|
|
10
42
|
export {};
|
|
11
43
|
//# sourceMappingURL=schema-prisma.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/schema-prisma.ts"],"names":[],"mappings":"AAKA,UAAU,gBAAgB;IACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,gBAAgB,CAAC;IACnD,
|
|
1
|
+
{"version":3,"file":"schema-prisma.d.ts","sourceRoot":"","sources":["../../src/adapters/schema-prisma.ts"],"names":[],"mappings":"AAKA,UAAU,gBAAgB;IACxB,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,gBAAgB,CAAC;IACnD,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAClC;AAED,KAAK,iBAAiB,GAAG,KAAK,OAAO,EAAE;IAAE,WAAW,EAAE;QAAE,EAAE,EAAE;YAAE,GAAG,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAA;CAAE,KAAK,gBAAgB,CAAC;AAErG;;;;;;;;;;;;GAYG;AACH,qBAAa,mBAAmB;IAI5B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,iBAAiB;IAClC,OAAO,CAAC,QAAQ,CAAC,UAAU;IAL7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4C;gBAGjD,YAAY,EAAE,iBAAiB,EAC/B,iBAAiB,EAAE,MAAM,EACzB,UAAU,GAAE,MAAW;IAG1C,4DAA4D;IAC5D,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,gBAAgB;IAsB/C,+EAA+E;IACzE,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,OAAO,EAChB,UAAU,EAAE,MAAM,MAAM,GACvB,KAAK,CAKP"}
|
|
@@ -4,26 +4,64 @@ exports.SchemaPrismaAdapter = void 0;
|
|
|
4
4
|
exports.withSchemaTenant = withSchemaTenant;
|
|
5
5
|
const core_1 = require("@stratum-hq/core");
|
|
6
6
|
const manager_js_1 = require("../schema/manager.js");
|
|
7
|
+
/**
|
|
8
|
+
* Prisma adapter for SCHEMA_PER_TENANT isolation.
|
|
9
|
+
*
|
|
10
|
+
* Prisma schema-qualifies every table with the datasource URL's `schema`
|
|
11
|
+
* parameter, so `search_path` cannot route its queries. Each tenant therefore
|
|
12
|
+
* gets its own Prisma client whose datasource URL names the tenant's schema.
|
|
13
|
+
* Clients are cached per tenant slug, least recently used first out.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* const adapter = new SchemaPrismaAdapter(PrismaClient, baseUrl);
|
|
17
|
+
* const prisma = adapter.getClient('acme_corp');
|
|
18
|
+
* const rows = await prisma.someModel.findMany();
|
|
19
|
+
*/
|
|
7
20
|
class SchemaPrismaAdapter {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
PrismaClient;
|
|
22
|
+
baseDatasourceUrl;
|
|
23
|
+
maxClients;
|
|
24
|
+
clients = new Map();
|
|
25
|
+
constructor(PrismaClient, baseDatasourceUrl, maxClients = 50) {
|
|
26
|
+
this.PrismaClient = PrismaClient;
|
|
27
|
+
this.baseDatasourceUrl = baseDatasourceUrl;
|
|
28
|
+
this.maxClients = maxClients;
|
|
29
|
+
}
|
|
30
|
+
/** Returns a Prisma client bound to the tenant's schema. */
|
|
31
|
+
getClient(tenantSlug) {
|
|
32
|
+
const schemaName = (0, manager_js_1.tenantSchemaName)((0, core_1.validateSlug)(tenantSlug));
|
|
33
|
+
const existing = this.clients.get(schemaName);
|
|
34
|
+
if (existing) {
|
|
35
|
+
this.clients.delete(schemaName);
|
|
36
|
+
this.clients.set(schemaName, existing);
|
|
37
|
+
return existing;
|
|
38
|
+
}
|
|
39
|
+
if (this.clients.size >= this.maxClients) {
|
|
40
|
+
const [oldestKey, oldest] = this.clients.entries().next().value;
|
|
41
|
+
this.clients.delete(oldestKey);
|
|
42
|
+
void oldest.$disconnect().catch(() => { });
|
|
43
|
+
}
|
|
44
|
+
const url = new URL(this.baseDatasourceUrl);
|
|
45
|
+
url.searchParams.set("schema", schemaName);
|
|
46
|
+
const client = new this.PrismaClient({ datasources: { db: { url: url.toString() } } });
|
|
47
|
+
this.clients.set(schemaName, client);
|
|
48
|
+
return client;
|
|
49
|
+
}
|
|
50
|
+
/** Disconnects all cached Prisma clients. Call during application shutdown. */
|
|
51
|
+
async disconnectAll() {
|
|
52
|
+
const clients = Array.from(this.clients.values());
|
|
53
|
+
this.clients.clear();
|
|
54
|
+
await Promise.all(clients.map((client) => client.$disconnect()));
|
|
22
55
|
}
|
|
23
56
|
}
|
|
24
57
|
exports.SchemaPrismaAdapter = SchemaPrismaAdapter;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
58
|
+
/**
|
|
59
|
+
* @deprecated Cannot isolate tenants: Prisma ignores `search_path` and the
|
|
60
|
+
* wrapped query does not run on the transaction that would set it. Always
|
|
61
|
+
* throws. Use `new SchemaPrismaAdapter(PrismaClient, url).getClient(slug)`.
|
|
62
|
+
*/
|
|
63
|
+
function withSchemaTenant(_prisma, _contextFn) {
|
|
64
|
+
throw new Error("withSchemaTenant cannot isolate Prisma queries by schema. " +
|
|
65
|
+
"Use new SchemaPrismaAdapter(PrismaClient, datasourceUrl).getClient(tenantSlug) instead.");
|
|
28
66
|
}
|
|
29
67
|
//# sourceMappingURL=schema-prisma.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema-prisma.js","sourceRoot":"","sources":["../../src/adapters/schema-prisma.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"schema-prisma.js","sourceRoot":"","sources":["../../src/adapters/schema-prisma.ts"],"names":[],"mappings":";;;AAsEA,4CAQC;AA9ED,2CAAgD;AAChD,qDAAwD;AAWxD;;;;;;;;;;;;GAYG;AACH,MAAa,mBAAmB;IAIX;IACA;IACA;IALF,OAAO,GAAkC,IAAI,GAAG,EAAE,CAAC;IAEpE,YACmB,YAA+B,EAC/B,iBAAyB,EACzB,aAAqB,EAAE;QAFvB,iBAAY,GAAZ,YAAY,CAAmB;QAC/B,sBAAiB,GAAjB,iBAAiB,CAAQ;QACzB,eAAU,GAAV,UAAU,CAAa;IACvC,CAAC;IAEJ,4DAA4D;IAC5D,SAAS,CAAC,UAAkB;QAC1B,MAAM,UAAU,GAAG,IAAA,6BAAgB,EAAC,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC,CAAC;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QAC9C,IAAI,QAAQ,EAAE,CAAC;YACb,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAChC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;YACvC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACzC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,CAAC,KAAmC,CAAC;YAC9F,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC/B,KAAK,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC5C,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC5C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,YAAY,CAAC,EAAE,WAAW,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;QACvF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;QACrC,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,+EAA+E;IAC/E,KAAK,CAAC,aAAa;QACjB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAClD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC;CACF;AAtCD,kDAsCC;AAED;;;;GAIG;AACH,SAAgB,gBAAgB,CAC9B,OAAgB,EAChB,UAAwB;IAExB,MAAM,IAAI,KAAK,CACb,4DAA4D;QAC1D,yFAAyF,CAC5F,CAAC;AACJ,CAAC"}
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
+
export interface SchemaAdapterOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Schemas searched after the tenant schema, for example one holding
|
|
5
|
+
* extension functions or types. Default: none. An unqualified table missing
|
|
6
|
+
* from the tenant schema resolves in these schemas, so list only schemas
|
|
7
|
+
* that hold no tenant data.
|
|
8
|
+
*/
|
|
9
|
+
extraSearchPath?: string[];
|
|
10
|
+
}
|
|
2
11
|
export declare class SchemaRawAdapter {
|
|
3
12
|
private pool;
|
|
4
|
-
|
|
13
|
+
private readonly extraSearchPath;
|
|
14
|
+
constructor(pool: pg.Pool, options?: SchemaAdapterOptions);
|
|
5
15
|
query<T extends pg.QueryResultRow = pg.QueryResultRow>(tenantSlug: string, text: string, values?: unknown[]): Promise<pg.QueryResult<T>>;
|
|
6
16
|
/**
|
|
7
17
|
* Executes a callback within a transaction scoped to the tenant's schema.
|
|
8
|
-
* Sets `search_path` to `tenant_{slug}
|
|
18
|
+
* Sets `search_path` to `tenant_{slug}`, followed only by any `extraSearchPath`
|
|
19
|
+
* schemas, for the duration of the transaction.
|
|
9
20
|
*/
|
|
10
21
|
executeWithTenantContext<T>(tenantSlug: string, fn: (client: pg.PoolClient) => Promise<T>): Promise<T>;
|
|
11
22
|
}
|
|
12
|
-
export declare function createSchemaTenantPool(pool: pg.Pool, contextFn: () => string): SchemaRawAdapter;
|
|
23
|
+
export declare function createSchemaTenantPool(pool: pg.Pool, contextFn: () => string, options?: SchemaAdapterOptions): SchemaRawAdapter;
|
|
13
24
|
//# sourceMappingURL=schema-raw.d.ts.map
|
|
@@ -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,qBAAa,gBAAgB;
|
|
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"}
|