@stratum-hq/db-adapters 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -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/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 +9 -5
- 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.map +1 -1
- package/dist/database/pool-manager.js +2 -3
- 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/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 +3 -2
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,24 @@ 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
|
+
|
|
61
79
|
## Security
|
|
62
80
|
|
|
63
81
|
- 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"}
|
|
@@ -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;AAED,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;CAqBd;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,EAAE,CAAC,IAAI,EACb,SAAS,EAAE,MAAM,MAAM,EACvB,OAAO,GAAE,oBAAyB,GACjC,gBAAgB,CAWlB"}
|
|
@@ -7,8 +7,11 @@ const manager_js_1 = require("../schema/manager.js");
|
|
|
7
7
|
const session_js_1 = require("../schema/session.js");
|
|
8
8
|
class SchemaRawAdapter {
|
|
9
9
|
pool;
|
|
10
|
-
|
|
10
|
+
extraSearchPath;
|
|
11
|
+
constructor(pool, options = {}) {
|
|
11
12
|
this.pool = pool;
|
|
13
|
+
// Validate up front so a bad entry fails at construction, not per query.
|
|
14
|
+
this.extraSearchPath = (options.extraSearchPath ?? []).map(manager_js_1.validateSchemaName);
|
|
12
15
|
}
|
|
13
16
|
async query(tenantSlug, text, values) {
|
|
14
17
|
return this.executeWithTenantContext(tenantSlug, async (client) => {
|
|
@@ -17,7 +20,8 @@ class SchemaRawAdapter {
|
|
|
17
20
|
}
|
|
18
21
|
/**
|
|
19
22
|
* Executes a callback within a transaction scoped to the tenant's schema.
|
|
20
|
-
* Sets `search_path` to `tenant_{slug}
|
|
23
|
+
* Sets `search_path` to `tenant_{slug}`, followed only by any `extraSearchPath`
|
|
24
|
+
* schemas, for the duration of the transaction.
|
|
21
25
|
*/
|
|
22
26
|
async executeWithTenantContext(tenantSlug, fn) {
|
|
23
27
|
const safe = (0, core_1.validateSlug)(tenantSlug);
|
|
@@ -26,7 +30,7 @@ class SchemaRawAdapter {
|
|
|
26
30
|
try {
|
|
27
31
|
await client.query("BEGIN");
|
|
28
32
|
// SET LOCAL is transaction-scoped; schemaName is derived from a validated slug.
|
|
29
|
-
await client.query(`SET LOCAL search_path TO ${schemaName
|
|
33
|
+
await client.query(`SET LOCAL search_path TO ${(0, session_js_1.tenantSearchPath)(schemaName, this.extraSearchPath)}`);
|
|
30
34
|
const result = await fn(client);
|
|
31
35
|
await client.query("COMMIT");
|
|
32
36
|
return result;
|
|
@@ -42,8 +46,8 @@ class SchemaRawAdapter {
|
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
exports.SchemaRawAdapter = SchemaRawAdapter;
|
|
45
|
-
function createSchemaTenantPool(pool, contextFn) {
|
|
46
|
-
const adapter = new SchemaRawAdapter(pool);
|
|
49
|
+
function createSchemaTenantPool(pool, contextFn, options = {}) {
|
|
50
|
+
const adapter = new SchemaRawAdapter(pool, options);
|
|
47
51
|
return new Proxy(adapter, {
|
|
48
52
|
get(target, prop) {
|
|
49
53
|
if (prop === "query") {
|
|
@@ -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":";;;AAmEA,wDAeC;AAjFD,2CAAgD;AAChD,qDAA4E;AAC5E,qDAAyE;AAYzE,MAAa,gBAAgB;IAIjB;IAHO,eAAe,CAAW;IAE3C,YACU,IAAa,EACrB,UAAgC,EAAE;QAD1B,SAAI,GAAJ,IAAI,CAAS;QAGrB,yEAAyE;QACzE,IAAI,CAAC,eAAe,GAAG,CAAC,OAAO,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,+BAAkB,CAAC,CAAC;IACjF,CAAC;IAED,KAAK,CAAC,KAAK,CACT,UAAkB,EAClB,IAAY,EACZ,MAAkB;QAElB,OAAO,IAAI,CAAC,wBAAwB,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YAChE,OAAO,MAAM,CAAC,KAAK,CAAI,IAAI,EAAE,MAAM,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,wBAAwB,CAC5B,UAAkB,EAClB,EAAyC;QAEzC,MAAM,IAAI,GAAG,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;QACtC,MAAM,UAAU,GAAG,IAAA,6BAAgB,EAAC,IAAI,CAAC,CAAC;QAC1C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QACzC,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC5B,gFAAgF;YAChF,MAAM,MAAM,CAAC,KAAK,CAChB,4BAA4B,IAAA,6BAAgB,EAAC,UAAU,EAAE,IAAI,CAAC,eAAe,CAAC,EAAE,CACjF,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;YAChC,MAAM,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YAC7B,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;YAC/B,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,MAAM,IAAA,4BAAe,EAAC,MAAM,CAAC,CAAC;YAC9B,MAAM,CAAC,OAAO,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;CACF;AAlDD,4CAkDC;AAED,SAAgB,sBAAsB,CACpC,IAAa,EACb,SAAuB,EACvB,UAAgC,EAAE;IAElC,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACpD,OAAO,IAAI,KAAK,CAAC,OAAO,EAAE;QACxB,GAAG,CAAC,MAAM,EAAE,IAAI;YACd,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;gBACrB,OAAO,CAAC,IAAY,EAAE,MAAkB,EAAE,EAAE,CAC1C,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;YAC5C,CAAC;YACD,OAAQ,MAAsD,CAAC,IAAI,CAAC,CAAC;QACvE,CAAC;KACF,CAAqB,CAAC;AACzB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAOpB;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAS1D;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,MAAM,EACjC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,MAAM,EACjC,UAAU,EAAE,MAAM,EAClB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC,CAaf;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,EAAE,CAAC,MAAM,EACjC,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
package/dist/database/manager.js
CHANGED
|
@@ -6,13 +6,19 @@ exports.createDatabase = createDatabase;
|
|
|
6
6
|
exports.dropDatabase = dropDatabase;
|
|
7
7
|
const core_1 = require("@stratum-hq/core");
|
|
8
8
|
const DB_PREFIX = "stratum_tenant_";
|
|
9
|
+
// PostgreSQL silently truncates database names longer than this many bytes.
|
|
10
|
+
const MAX_IDENTIFIER_BYTES = 63;
|
|
9
11
|
/**
|
|
10
12
|
* Returns the database name for a given tenant slug.
|
|
11
13
|
* Format: stratum_tenant_{slug}
|
|
12
14
|
*/
|
|
13
15
|
function getDatabaseName(tenantSlug) {
|
|
14
16
|
(0, core_1.validateSlug)(tenantSlug);
|
|
15
|
-
|
|
17
|
+
const dbName = `${DB_PREFIX}${tenantSlug}`;
|
|
18
|
+
if (Buffer.byteLength(dbName) > MAX_IDENTIFIER_BYTES) {
|
|
19
|
+
throw new Error(`Database name for tenant slug "${tenantSlug}" exceeds ${MAX_IDENTIFIER_BYTES} bytes`);
|
|
20
|
+
}
|
|
21
|
+
return dbName;
|
|
16
22
|
}
|
|
17
23
|
/**
|
|
18
24
|
* Checks whether the per-tenant database exists.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/database/manager.ts"],"names":[],"mappings":";;AAWA,0CASC;AAMD,wCAUC;AASD,wCAiBC;AAQD,oCAMC;AA3ED,2CAAgD;AAEhD,MAAM,SAAS,GAAG,iBAAiB,CAAC;AACpC,4EAA4E;AAC5E,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC;;;GAGG;AACH,SAAgB,eAAe,CAAC,UAAkB;IAChD,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC;IACzB,MAAM,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,EAAE,CAAC;IAC3C,IAAI,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,kCAAkC,UAAU,aAAa,oBAAoB,QAAQ,CACtF,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,uEAAuE,EACvE,CAAC,MAAM,CAAC,CACT,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC5B,CAAC;AAED;;;;;;GAMG;AACI,KAAK,UAAU,cAAc,CAClC,MAAiC,EACjC,UAAkB,EAClB,UAAmB;IAEnB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,+FAA+F;IAC/F,8FAA8F;IAC9F,IAAI,GAAG,GAAG,oBAAoB,MAAM,GAAG,CAAC;IACxC,IAAI,UAAU,EAAE,CAAC;QACf,iEAAiE;QACjE,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,oCAAoC,UAAU,GAAG,CAAC,CAAC;QACrE,CAAC;QACD,GAAG,IAAI,cAAc,UAAU,GAAG,CAAC;IACrC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;GAKG;AACI,KAAK,UAAU,YAAY,CAChC,MAAiC,EACjC,UAAkB;IAElB,MAAM,MAAM,GAAG,eAAe,CAAC,UAAU,CAAC,CAAC;IAC3C,MAAM,MAAM,CAAC,KAAK,CAAC,4BAA4B,MAAM,GAAG,CAAC,CAAC;AAC5D,CAAC"}
|
|
@@ -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,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAOD;;;;;GAKG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,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;;;;;;OAMG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;
|
|
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,6FAA6F;IAC7F,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAOD;;;;;GAKG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,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;;;;;;OAMG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;IAwBtE;gFAC4E;IACtE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBrE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAW5D,sDAAsD;YACxC,QAAQ;CAevB"}
|
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.DatabasePoolManager = void 0;
|
|
7
7
|
const pg_1 = __importDefault(require("pg"));
|
|
8
|
-
const
|
|
8
|
+
const manager_js_1 = require("./manager.js");
|
|
9
9
|
/**
|
|
10
10
|
* Manages a collection of per-tenant pg.Pool instances.
|
|
11
11
|
*
|
|
@@ -30,7 +30,7 @@ class DatabasePoolManager {
|
|
|
30
30
|
* multi-region deployments where the same slug may exist in different regions.
|
|
31
31
|
*/
|
|
32
32
|
async getPool(tenantSlug, regionId) {
|
|
33
|
-
(0,
|
|
33
|
+
const dbName = (0, manager_js_1.getDatabaseName)(tenantSlug);
|
|
34
34
|
const poolKey = regionId ? `${regionId}:${tenantSlug}` : tenantSlug;
|
|
35
35
|
const existing = this.pools.get(poolKey);
|
|
36
36
|
if (existing) {
|
|
@@ -41,7 +41,6 @@ class DatabasePoolManager {
|
|
|
41
41
|
if (this.pools.size >= this.maxPools) {
|
|
42
42
|
await this.evictLRU();
|
|
43
43
|
}
|
|
44
|
-
const dbName = `stratum_tenant_${tenantSlug}`;
|
|
45
44
|
const pool = new pg_1.default.Pool({
|
|
46
45
|
...this.baseConfig,
|
|
47
46
|
database: dbName,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pool-manager.js","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":";;;;;;AAAA,4CAAoB;AACpB,
|
|
1
|
+
{"version":3,"file":"pool-manager.js","sourceRoot":"","sources":["../../src/database/pool-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { BaseAdapter } from "./base-adapter.js";
|
|
|
2
2
|
export { RawAdapter, createTenantPool } from "./adapters/raw.js";
|
|
3
3
|
export { PrismaAdapter, withTenant as prismaWithTenant } from "./adapters/prisma.js";
|
|
4
4
|
export { SchemaRawAdapter, createSchemaTenantPool, } from "./adapters/schema-raw.js";
|
|
5
|
+
export type { SchemaAdapterOptions } from "./adapters/schema-raw.js";
|
|
5
6
|
export { SchemaPrismaAdapter, withSchemaTenant, } from "./adapters/schema-prisma.js";
|
|
6
7
|
export { createPolicy, dropPolicy, enableRLS, disableRLS, isRLSEnabled, } from "./rls/manager.js";
|
|
7
8
|
export { setTenantContext, resetTenantContext, withTenantContext, withRlsBypass, getCurrentTenantId, } from "./rls/session.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EACL,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,eAAe,EACf,SAAS,IAAI,qBAAqB,EAClC,qBAAqB,EACrB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,GACb,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxG,OAAO,EACL,cAAc,EACd,UAAU,IAAI,iBAAiB,EAC/B,eAAe,IAAI,sBAAsB,GAC1C,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACrF,OAAO,EACL,gBAAgB,EAChB,sBAAsB,GACvB,MAAM,0BAA0B,CAAC;AAClC,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EACL,mBAAmB,EACnB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,YAAY,EACZ,UAAU,EACV,SAAS,EACT,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,oBAAoB,GACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,eAAe,EACf,SAAS,IAAI,qBAAqB,EAClC,qBAAqB,EACrB,YAAY,GACb,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,GACb,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAE7E,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,eAAe,IAAI,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACxG,OAAO,EACL,cAAc,EACd,UAAU,IAAI,iBAAiB,EAC/B,eAAe,IAAI,sBAAsB,GAC1C,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAEpB,4CAAiE;AAAxD,oGAAA,UAAU,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AACrC,kDAAqF;AAA5E,0GAAA,aAAa,OAAA;AAAE,6GAAA,UAAU,OAAoB;AACtD,0DAGkC;AAFhC,iHAAA,gBAAgB,OAAA;AAChB,uHAAA,sBAAsB,OAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA;AAEpB,4CAAiE;AAAxD,oGAAA,UAAU,OAAA;AAAE,0GAAA,gBAAgB,OAAA;AACrC,kDAAqF;AAA5E,0GAAA,aAAa,OAAA;AAAE,6GAAA,UAAU,OAAoB;AACtD,0DAGkC;AAFhC,iHAAA,gBAAgB,OAAA;AAChB,uHAAA,sBAAsB,OAAA;AAGxB,gEAGqC;AAFnC,uHAAA,mBAAmB,OAAA;AACnB,oHAAA,gBAAgB,OAAA;AAGlB,+CAM0B;AALxB,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,uGAAA,SAAS,OAAA;AACT,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AAGd,+CAM0B;AALxB,8GAAA,gBAAgB,OAAA;AAChB,gHAAA,kBAAkB,OAAA;AAClB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,gHAAA,kBAAkB,OAAA;AAGpB,kDAO6B;AAN3B,0GAAA,YAAY,OAAA;AACZ,wGAAA,UAAU,OAAA;AACV,0GAAA,YAAY,OAAA;AACZ,+GAAA,iBAAiB,OAAA;AACjB,oHAAA,sBAAsB,OAAA;AACtB,8GAAA,gBAAgB,OAAA;AAGlB,kDAI6B;AAH3B,iHAAA,mBAAmB,OAAA;AACnB,6GAAA,eAAe,OAAA;AACf,kHAAA,oBAAoB,OAAA;AAGtB,+DAKgC;AAJ9B,uHAAA,eAAe,OAAA;AACf,6HAAA,SAAS,OAAyB;AAClC,6HAAA,qBAAqB,OAAA;AACrB,oHAAA,YAAY,OAAA;AAGd,0BAA0B;AAC1B,oDAK+B;AAJ7B,6GAAA,eAAe,OAAA;AACf,4GAAA,cAAc,OAAA;AACd,4GAAA,cAAc,OAAA;AACd,0GAAA,YAAY,OAAA;AAGd,8DAAiE;AAAxD,sHAAA,mBAAmB,OAAA;AAG5B,8DAAgE;AAAvD,qHAAA,kBAAkB,OAAA;AAC3B,oEAAsE;AAA7D,2HAAA,qBAAqB,OAAA;AAC9B,wDAAwG;AAA/F,gHAAA,gBAAgB,OAAA;AAAE,wHAAA,eAAe,OAA4B;AACtE,oDAI+B;AAH7B,4GAAA,cAAc,OAAA;AACd,+GAAA,UAAU,OAAqB;AAC/B,oHAAA,eAAe,OAA0B"}
|
package/dist/schema/manager.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
+
export declare function validateSchemaName(schemaName: string): string;
|
|
2
3
|
export declare function tenantSchemaName(tenantSlug: string): string;
|
|
3
4
|
export declare function createSchema(client: pg.PoolClient, tenantSlug: string): Promise<void>;
|
|
4
5
|
export declare function dropSchema(client: pg.PoolClient, tenantSlug: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/schema/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"manager.d.ts","sourceRoot":"","sources":["../../src/schema/manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAK7D;AAaD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,UAAU,CAC9B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAGf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAOlB;AAED,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,EAAE,CAAC,UAAU,GACpB,OAAO,CAAC,MAAM,EAAE,CAAC,CAKnB;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAMf"}
|
package/dist/schema/manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateSchemaName = validateSchemaName;
|
|
3
4
|
exports.tenantSchemaName = tenantSchemaName;
|
|
4
5
|
exports.createSchema = createSchema;
|
|
5
6
|
exports.dropSchema = dropSchema;
|
|
@@ -20,12 +21,20 @@ function validateTableName(tableName) {
|
|
|
20
21
|
}
|
|
21
22
|
return tableName;
|
|
22
23
|
}
|
|
24
|
+
// PostgreSQL silently truncates identifiers longer than this many bytes.
|
|
25
|
+
const MAX_IDENTIFIER_BYTES = 63;
|
|
23
26
|
function tenantSchemaName(tenantSlug) {
|
|
24
|
-
|
|
27
|
+
const schemaName = `tenant_${tenantSlug}`;
|
|
28
|
+
if (Buffer.byteLength(schemaName) > MAX_IDENTIFIER_BYTES) {
|
|
29
|
+
throw new Error(`Schema name for tenant slug "${tenantSlug}" exceeds ${MAX_IDENTIFIER_BYTES} bytes`);
|
|
30
|
+
}
|
|
31
|
+
return schemaName;
|
|
25
32
|
}
|
|
26
33
|
async function createSchema(client, tenantSlug) {
|
|
27
34
|
const schemaName = validateSchemaName(tenantSchemaName(tenantSlug));
|
|
28
|
-
|
|
35
|
+
// No IF NOT EXISTS: an existing schema may hold another tenant's data, so
|
|
36
|
+
// provisioning must fail rather than adopt it.
|
|
37
|
+
await client.query(`CREATE SCHEMA ${schemaName}`);
|
|
29
38
|
}
|
|
30
39
|
async function dropSchema(client, tenantSlug) {
|
|
31
40
|
const schemaName = validateSchemaName(tenantSchemaName(tenantSlug));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/schema/manager.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/schema/manager.ts"],"names":[],"mappings":";;AAGA,gDAKC;AAaD,4CAQC;AAED,oCAQC;AAED,gCAMC;AAED,oCAUC;AAED,8CAOC;AAED,wDAUC;AA9ED,yFAAyF;AACzF,SAAgB,kBAAkB,CAAC,UAAkB;IACnD,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,wBAAwB,UAAU,EAAE,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,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;AAED,yEAAyE;AACzE,MAAM,oBAAoB,GAAG,EAAE,CAAC;AAEhC,SAAgB,gBAAgB,CAAC,UAAkB;IACjD,MAAM,UAAU,GAAG,UAAU,UAAU,EAAE,CAAC;IAC1C,IAAI,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,oBAAoB,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CACb,gCAAgC,UAAU,aAAa,oBAAoB,QAAQ,CACpF,CAAC;IACJ,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,UAAkB;IAElB,MAAM,UAAU,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC;IACpE,0EAA0E;IAC1E,+CAA+C;IAC/C,MAAM,MAAM,CAAC,KAAK,CAAC,iBAAiB,UAAU,EAAE,CAAC,CAAC;AACpD,CAAC;AAEM,KAAK,UAAU,UAAU,CAC9B,MAAqB,EACrB,UAAkB;IAElB,MAAM,UAAU,GAAG,kBAAkB,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,yBAAyB,UAAU,UAAU,CAAC,CAAC;AACpE,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,UAAkB;IAElB,MAAM,UAAU,GAAG,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,sDAAsD,EACtD,CAAC,UAAU,CAAC,CACb,CAAC;IACF,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;AAC7C,CAAC;AAEM,KAAK,UAAU,iBAAiB,CACrC,MAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,iFAAiF,CAClF,CAAC;IACF,OAAO,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;AACxC,CAAC;AAEM,KAAK,UAAU,sBAAsB,CAC1C,MAAqB,EACrB,SAAiB,EACjB,UAAkB;IAElB,MAAM,SAAS,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAC;IAClD,MAAM,MAAM,CAAC,KAAK,CAChB,8BAA8B,UAAU,IAAI,SAAS,iBAAiB,SAAS,iBAAiB,CACjG,CAAC;AACJ,CAAC"}
|
package/dist/schema/session.d.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Builds the search_path list for a tenant: the tenant schema first, then any
|
|
4
|
+
* explicitly opted-in extra schemas (for example one holding extension
|
|
5
|
+
* functions). Every extra entry is validated as a plain identifier, the same
|
|
6
|
+
* way tenant schema names are. Note that an unqualified table missing from the
|
|
7
|
+
* tenant schema resolves in the extra schemas, so list only schemas that hold
|
|
8
|
+
* no tenant data.
|
|
9
|
+
*/
|
|
10
|
+
export declare function tenantSearchPath(schemaName: string, extraSearchPath?: string[]): string;
|
|
11
|
+
export declare function setSchemaSearchPath(client: pg.PoolClient, tenantSlug: string, extraSearchPath?: string[]): Promise<void>;
|
|
3
12
|
export declare function resetSearchPath(client: pg.PoolClient): Promise<void>;
|
|
4
13
|
export declare function getCurrentSearchPath(client: pg.PoolClient): Promise<string>;
|
|
5
14
|
//# sourceMappingURL=session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/schema/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/schema/session.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,eAAe,GAAE,MAAM,EAAO,GAAG,MAAM,CAE3F;AAED,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,UAAU,EAAE,MAAM,EAClB,eAAe,GAAE,MAAM,EAAO,GAC7B,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAsB,eAAe,CAAC,MAAM,EAAE,EAAE,CAAC,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1E;AAED,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,EAAE,CAAC,UAAU,GACpB,OAAO,CAAC,MAAM,CAAC,CAGjB"}
|
package/dist/schema/session.js
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.tenantSearchPath = tenantSearchPath;
|
|
3
4
|
exports.setSchemaSearchPath = setSchemaSearchPath;
|
|
4
5
|
exports.resetSearchPath = resetSearchPath;
|
|
5
6
|
exports.getCurrentSearchPath = getCurrentSearchPath;
|
|
6
7
|
const core_1 = require("@stratum-hq/core");
|
|
7
8
|
const manager_js_1 = require("./manager.js");
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Builds the search_path list for a tenant: the tenant schema first, then any
|
|
11
|
+
* explicitly opted-in extra schemas (for example one holding extension
|
|
12
|
+
* functions). Every extra entry is validated as a plain identifier, the same
|
|
13
|
+
* way tenant schema names are. Note that an unqualified table missing from the
|
|
14
|
+
* tenant schema resolves in the extra schemas, so list only schemas that hold
|
|
15
|
+
* no tenant data.
|
|
16
|
+
*/
|
|
17
|
+
function tenantSearchPath(schemaName, extraSearchPath = []) {
|
|
18
|
+
return [schemaName, ...extraSearchPath.map(manager_js_1.validateSchemaName)].join(", ");
|
|
19
|
+
}
|
|
20
|
+
async function setSchemaSearchPath(client, tenantSlug, extraSearchPath = []) {
|
|
9
21
|
// The schema name is interpolated (identifiers cannot be bound), so the slug
|
|
10
22
|
// must be validated against the canonical slug charset first; validateSlug
|
|
11
23
|
// throws on anything else. SET LOCAL only takes effect for the current
|
|
12
|
-
// transaction.
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
// transaction. The path is the tenant schema alone (pg_catalog is always
|
|
25
|
+
// searched), so a table missing from it errors instead of resolving to a
|
|
26
|
+
// shared table in public. Extra schemas are appended only when opted in.
|
|
27
|
+
const searchPath = tenantSearchPath((0, manager_js_1.tenantSchemaName)((0, core_1.validateSlug)(tenantSlug)), extraSearchPath);
|
|
28
|
+
await client.query(`SET LOCAL search_path TO ${searchPath}`);
|
|
29
|
+
// Outside a transaction SET LOCAL is a no-op with only a warning. SHOW
|
|
30
|
+
// reports unquoted identifiers folded to lower case.
|
|
31
|
+
const res = await client.query("SHOW search_path");
|
|
32
|
+
if (res.rows[0].search_path !== searchPath.toLowerCase()) {
|
|
33
|
+
throw new Error("setSchemaSearchPath must be called inside a transaction");
|
|
34
|
+
}
|
|
15
35
|
}
|
|
16
36
|
async function resetSearchPath(client) {
|
|
17
37
|
await client.query("RESET search_path");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/schema/session.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"session.js","sourceRoot":"","sources":["../../src/schema/session.ts"],"names":[],"mappings":";;AAYA,4CAEC;AAED,kDAmBC;AAED,0CAEC;AAED,oDAKC;AA7CD,2CAAgD;AAChD,6CAAoE;AAEpE;;;;;;;GAOG;AACH,SAAgB,gBAAgB,CAAC,UAAkB,EAAE,kBAA4B,EAAE;IACjF,OAAO,CAAC,UAAU,EAAE,GAAG,eAAe,CAAC,GAAG,CAAC,+BAAkB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC7E,CAAC;AAEM,KAAK,UAAU,mBAAmB,CACvC,MAAqB,EACrB,UAAkB,EAClB,kBAA4B,EAAE;IAE9B,6EAA6E;IAC7E,2EAA2E;IAC3E,uEAAuE;IACvE,yEAAyE;IACzE,yEAAyE;IACzE,yEAAyE;IACzE,MAAM,UAAU,GAAG,gBAAgB,CAAC,IAAA,6BAAgB,EAAC,IAAA,mBAAY,EAAC,UAAU,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC;IACjG,MAAM,MAAM,CAAC,KAAK,CAAC,4BAA4B,UAAU,EAAE,CAAC,CAAC;IAC7D,uEAAuE;IACvE,qDAAqD;IACrD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAA0B,kBAAkB,CAAC,CAAC;IAC5E,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,KAAK,UAAU,CAAC,WAAW,EAAE,EAAE,CAAC;QACzD,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;IAC7E,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,MAAqB;IACzD,MAAM,MAAM,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC;AAC1C,CAAC;AAEM,KAAK,UAAU,oBAAoB,CACxC,MAAqB;IAErB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAA0B,kBAAkB,CAAC,CAAC;IAC5E,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;AACjC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratum-hq/db-adapters",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Stratum PostgreSQL adapters for tenant-scoped query isolation",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multi-tenancy",
|
|
@@ -55,8 +55,9 @@
|
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@prisma/client": "^5.10.0",
|
|
57
57
|
"@types/pg": "^8.11.0",
|
|
58
|
+
"drizzle-orm": "^0.45.3",
|
|
58
59
|
"typescript": "^5.4.0",
|
|
59
|
-
"vitest": "^1.
|
|
60
|
+
"vitest": "^4.1.11"
|
|
60
61
|
},
|
|
61
62
|
"license": "MIT",
|
|
62
63
|
"author": "Christian Crank",
|