@stratum-hq/db-adapters 1.3.0 → 1.5.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 +59 -3
- package/dist/adapters/drizzle.d.ts +3 -2
- package/dist/adapters/drizzle.d.ts.map +1 -1
- package/dist/adapters/drizzle.js +3 -2
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/adapters/sequelize.d.ts +3 -2
- package/dist/adapters/sequelize.d.ts.map +1 -1
- package/dist/adapters/sequelize.js +3 -2
- package/dist/adapters/sequelize.js.map +1 -1
- package/dist/database/manager.d.ts +1 -1
- package/dist/database/manager.js +2 -2
- package/dist/database/manager.js.map +1 -1
- package/dist/database/pool-manager.d.ts +1 -1
- package/dist/database/pool-manager.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/migration-helpers.d.ts +2 -1
- package/dist/migration-helpers.d.ts.map +1 -1
- package/dist/migration-helpers.js +8 -1
- package/dist/migration-helpers.js.map +1 -1
- package/dist/pglite/index.d.ts +60 -0
- package/dist/pglite/index.d.ts.map +1 -0
- package/dist/pglite/index.js +196 -0
- package/dist/pglite/index.js.map +1 -0
- package/dist/rls/manager.d.ts +27 -1
- package/dist/rls/manager.d.ts.map +1 -1
- package/dist/rls/manager.js +43 -6
- package/dist/rls/manager.js.map +1 -1
- package/dist/rls/policy-check.d.ts +49 -1
- package/dist/rls/policy-check.d.ts.map +1 -1
- package/dist/rls/policy-check.js +136 -11
- package/dist/rls/policy-check.js.map +1 -1
- package/dist/rls/session.d.ts +34 -2
- package/dist/rls/session.d.ts.map +1 -1
- package/dist/rls/session.js +39 -5
- package/dist/rls/session.js.map +1 -1
- package/dist/schema/manager.js +1 -1
- package/dist/schema/manager.js.map +1 -1
- package/package.json +24 -6
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
PostgreSQL adapters for [Stratum](https://github.com/stratum-hq/Stratum) that automatically scope queries to the current tenant using Row-Level Security. Supports raw `pg`, Prisma, Drizzle, and Sequelize, plus helpers for enabling RLS on your tables.
|
|
4
4
|
|
|
5
|
+
Read the documentation at [docs.stratum-hq.org/packages/db-adapters](https://docs.stratum-hq.org/packages/db-adapters/).
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -35,11 +37,13 @@ import { prismaWithTenant } from "@stratum-hq/db-adapters"; // Prisma
|
|
|
35
37
|
import { drizzleWithTenant } from "@stratum-hq/db-adapters"; // Drizzle
|
|
36
38
|
import { SequelizeAdapter, sequelizeWithTenantScope } from "@stratum-hq/db-adapters"; // Sequelize
|
|
37
39
|
|
|
38
|
-
// Prisma
|
|
40
|
+
// Prisma: all queries scoped to the current tenant
|
|
39
41
|
const tenantPrisma = prismaWithTenant(prisma, () => getTenantContext().tenant_id, pool);
|
|
40
42
|
const orders = await tenantPrisma.order.findMany();
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
The Sequelize wrapper scopes its `query()` method only. Model methods such as `Order.findAll()` do not go through it and run without a tenant.
|
|
46
|
+
|
|
43
47
|
## RLS & Migration Helpers
|
|
44
48
|
|
|
45
49
|
```typescript
|
|
@@ -62,7 +66,9 @@ try {
|
|
|
62
66
|
- Restrictive policies can only narrow access, so they may check anything.
|
|
63
67
|
- An existing `tenant_isolation` policy must be permissive, apply to all commands, and apply to `PUBLIC` (no `TO` clause), like the one `createPolicy` generates and every policy Stratum ships.
|
|
64
68
|
|
|
65
|
-
The check recognizes the form Stratum generates, with the operands in either order, with casts, ANDed with other conditions, or ORed with Stratum's `app.bypass_rls` bypass. A policy that isolates correctly but is written in another form is also refused; replace it with the generated form. This is a breaking change for callers that relied on the old skip, shipped in a minor release.
|
|
69
|
+
The check recognizes the form Stratum generates, with the operands in either order, with casts, ANDed with other conditions, or ORed with Stratum's `app.bypass_rls` bypass, directly or through `stratum_legacy_bypass()` (migration 032). A policy that isolates correctly but is written in another form is also refused; replace it with the generated form. This is a breaking change for callers that relied on the old skip, shipped in a minor release.
|
|
70
|
+
|
|
71
|
+
A permissive policy named `stratum_control_plane` is accepted only when it applies to exactly the control role of migration 032 (`stratum_control` by default; pass `{ controlRole }` to `createPolicy` for another name). A policy that checks `app.bypass_rls` directly still passes in 1.x, but `createPolicy` emits a `STRATUM_GUC_BYPASS_POLICY` process warning for it: the control role, which PostgreSQL role membership decides, replaces it. 2.0 refuses such policies.
|
|
66
72
|
|
|
67
73
|
`isRLSEnabled` reports on the table that the name resolves to through the `search_path`, not on a table with the same name in another schema.
|
|
68
74
|
|
|
@@ -103,10 +109,60 @@ When the pool count reaches `maxPools`, the manager ends the least recently used
|
|
|
103
109
|
|
|
104
110
|
Concurrent first requests for one tenant share one pool.
|
|
105
111
|
|
|
112
|
+
## PGlite
|
|
113
|
+
|
|
114
|
+
`@stratum-hq/db-adapters/pglite` runs `@stratum-hq/lib` on [PGlite](https://pglite.dev), which is PostgreSQL compiled to WebAssembly. It works in Node and in the browser, and it needs no database server. Use it for fast local tests and for demos.
|
|
115
|
+
|
|
116
|
+
Install PGlite. It is an optional peer dependency, so npm does not install it for you.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install @electric-sql/pglite
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Stratum, bootstrapRolesSql } from "@stratum-hq/lib";
|
|
124
|
+
import { withTenantContext } from "@stratum-hq/db-adapters";
|
|
125
|
+
import { createPglitePool, createRestrictedPool } from "@stratum-hq/db-adapters/pglite";
|
|
126
|
+
|
|
127
|
+
// In-memory database. Pass PGlite options, such as { dataDir: "idb://my-db" }, to keep data.
|
|
128
|
+
// The library runs on adminPool (the PGlite superuser). Row-level security applies
|
|
129
|
+
// only to a role that is not a superuser, so the application uses a restricted pool.
|
|
130
|
+
const adminPool = await createPglitePool();
|
|
131
|
+
const appPool = await createRestrictedPool(adminPool);
|
|
132
|
+
const stratum = new Stratum({ adminPool, pool: appPool, autoMigrate: true });
|
|
133
|
+
await stratum.initialize();
|
|
134
|
+
|
|
135
|
+
// Limit the application role to reading the Stratum tables, as in production,
|
|
136
|
+
// and close the legacy app.bypass_rls path (migration 032).
|
|
137
|
+
await adminPool.query(bootstrapRolesSql({ appRole: "stratum_app" }));
|
|
138
|
+
await adminPool.query("UPDATE stratum_security SET legacy_guc_bypass = false");
|
|
139
|
+
|
|
140
|
+
const acme = await stratum.createTenant({ name: "Acme", slug: "acme" });
|
|
141
|
+
await withTenantContext(appPool, acme.id, (client) => client.query("SELECT * FROM config_entries"));
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`createPglitePool(source?)` returns a `pg.Pool`-compatible object:
|
|
145
|
+
|
|
146
|
+
- `source` is a PGlite instance or PGlite options. When you pass options, the pool creates the instance and loads the `ltree` and `uuid_ossp` extensions. `pool.end()` then closes the instance.
|
|
147
|
+
- When you pass your own instance, load `ltree` and `uuid_ossp` yourself. `pool.end()` does not close your instance.
|
|
148
|
+
- `pool.pglite` is the PGlite instance.
|
|
149
|
+
|
|
150
|
+
`createRestrictedPool(pool, { role? })` creates the role `stratum_app` (or the name you give) with `NOSUPERUSER NOBYPASSRLS`. It grants the role read and write access to every table and sequence in the `public` schema. Default privileges extend the grants to tables that the superuser creates later. The returned pool runs every query as that role. `bootstrapRolesSql({ appRole })` from `@stratum-hq/lib` narrows those grants on the Stratum tables to the recommended read-only set; `Stratum.initialize()` warns about the wider grants until you run it.
|
|
151
|
+
|
|
152
|
+
The restricted role is a test and demo convenience, not a security boundary: any query can leave it with `RESET ROLE`.
|
|
153
|
+
|
|
154
|
+
Limits:
|
|
155
|
+
|
|
156
|
+
- **One connection.** A client from `connect()` holds the only connection until `release()`. Other callers wait in order. If you hold a client and call `pool.query()`, the call waits forever. `release()` rolls back a transaction the client left open and resets the session settings and the role, so the next client starts clean.
|
|
157
|
+
- **No concurrency.** Queries run one at a time. Do not use this adapter to test race conditions or lock contention.
|
|
158
|
+
- **Superuser by default.** PGlite connects as the superuser `postgres`, and a superuser bypasses row-level security. Use `createRestrictedPool` when a test must prove isolation.
|
|
159
|
+
- **Only part of `pg.Pool`.** The pool supports `query`, `connect`, `end` and `on`. Callbacks, cursors, `totalCount` and the other pool counters do not exist.
|
|
160
|
+
- PGlite `^0.4.2` is the supported version.
|
|
161
|
+
|
|
106
162
|
## Security
|
|
107
163
|
|
|
108
164
|
- All DDL validates table names against `/^[a-zA-Z_][a-zA-Z0-9_]*$/`.
|
|
109
|
-
- Tenant ID is always set via `set_config($1, true)
|
|
165
|
+
- Tenant ID is always set via `set_config($1, true)`, fully parameterized.
|
|
110
166
|
- `enableRLS()` always applies `FORCE ROW LEVEL SECURITY`, preventing bypass by table owners.
|
|
111
167
|
- Always reset the tenant context when returning connections to the pool; `createTenantPool` handles this for you.
|
|
112
168
|
|
|
@@ -14,8 +14,9 @@ export declare class DrizzleAdapter extends BaseAdapter {
|
|
|
14
14
|
* on the same connection.
|
|
15
15
|
*
|
|
16
16
|
* contextFn should return the current tenant ID (e.g. from AsyncLocalStorage).
|
|
17
|
-
* When contextFn returns an empty string
|
|
18
|
-
*
|
|
17
|
+
* When contextFn returns an empty string, `transaction()` and `execute()`
|
|
18
|
+
* throw instead of running. Use the unwrapped Drizzle instance for system or
|
|
19
|
+
* admin queries that run without a tenant.
|
|
19
20
|
*/
|
|
20
21
|
withTenant<D extends DrizzleLike>(db: D, contextFn: () => string): Pick<D, "execute" | "transaction">;
|
|
21
22
|
}
|
|
@@ -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;AAQD,qBAAa,cAAe,SAAQ,WAAW;gBACjC,IAAI,EAAE,EAAE,CAAC,IAAI;IAIzB
|
|
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;;;;;;;;;;;OAWG;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
|
@@ -20,8 +20,9 @@ class DrizzleAdapter extends base_adapter_js_1.BaseAdapter {
|
|
|
20
20
|
* on the same connection.
|
|
21
21
|
*
|
|
22
22
|
* contextFn should return the current tenant ID (e.g. from AsyncLocalStorage).
|
|
23
|
-
* When contextFn returns an empty string
|
|
24
|
-
*
|
|
23
|
+
* When contextFn returns an empty string, `transaction()` and `execute()`
|
|
24
|
+
* throw instead of running. Use the unwrapped Drizzle instance for system or
|
|
25
|
+
* admin queries that run without a tenant.
|
|
25
26
|
*/
|
|
26
27
|
withTenant(db, contextFn) {
|
|
27
28
|
const original = db;
|
|
@@ -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":";;;AA6EA,gCAOC;AApFD,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;;;;;;;;;;;OAWG;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;AA1DD,wCA0DC;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"}
|
|
@@ -15,8 +15,9 @@ export declare class SequelizeAdapter extends BaseAdapter {
|
|
|
15
15
|
* connection-pool hops.
|
|
16
16
|
*
|
|
17
17
|
* contextFn should return the current tenant ID (e.g. from AsyncLocalStorage).
|
|
18
|
-
* When contextFn returns an empty string
|
|
19
|
-
*
|
|
18
|
+
* When contextFn returns an empty string, `query()` throws instead of
|
|
19
|
+
* running the query. Use the unwrapped Sequelize instance for system or
|
|
20
|
+
* admin queries that run without a tenant.
|
|
20
21
|
*/
|
|
21
22
|
withTenantScope(sequelize: SequelizeLike, contextFn: () => string): SequelizeLike;
|
|
22
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sequelize.d.ts","sourceRoot":"","sources":["../../src/adapters/sequelize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,UAAU,aAAa;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxE,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3D,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnF;AAED,qBAAa,gBAAiB,SAAQ,WAAW;gBACnC,IAAI,EAAE,EAAE,CAAC,IAAI;IAIzB
|
|
1
|
+
{"version":3,"file":"sequelize.d.ts","sourceRoot":"","sources":["../../src/adapters/sequelize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,IAAI,CAAC;AAIpB,UAAU,aAAa;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxE,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3D,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACnF;AAED,qBAAa,gBAAiB,SAAQ,WAAW;gBACnC,IAAI,EAAE,EAAE,CAAC,IAAI;IAIzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,SAAS,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,MAAM,GAAG,aAAa;CA0BlF;AAGD,wBAAgB,eAAe,CAC7B,SAAS,EAAE,aAAa,EACxB,SAAS,EAAE,MAAM,MAAM,EACvB,IAAI,EAAE,EAAE,CAAC,IAAI,GACZ,aAAa,CAGf"}
|
|
@@ -15,8 +15,9 @@ class SequelizeAdapter extends base_adapter_js_1.BaseAdapter {
|
|
|
15
15
|
* connection-pool hops.
|
|
16
16
|
*
|
|
17
17
|
* contextFn should return the current tenant ID (e.g. from AsyncLocalStorage).
|
|
18
|
-
* When contextFn returns an empty string
|
|
19
|
-
*
|
|
18
|
+
* When contextFn returns an empty string, `query()` throws instead of
|
|
19
|
+
* running the query. Use the unwrapped Sequelize instance for system or
|
|
20
|
+
* admin queries that run without a tenant.
|
|
20
21
|
*/
|
|
21
22
|
withTenantScope(sequelize, contextFn) {
|
|
22
23
|
const original = sequelize;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sequelize.js","sourceRoot":"","sources":["../../src/adapters/sequelize.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"sequelize.js","sourceRoot":"","sources":["../../src/adapters/sequelize.ts"],"names":[],"mappings":";;;AAyDA,0CAOC;AAhED,wDAAiD;AAWjD,MAAa,gBAAiB,SAAQ,6BAAW;IAC/C,YAAY,IAAa;QACvB,KAAK,CAAC,IAAI,CAAC,CAAC;IACd,CAAC;IAED;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,SAAwB,EAAE,SAAuB;QAC/D,MAAM,QAAQ,GAAG,SAAS,CAAC;QAE3B,MAAM,YAAY,GAAG,KAAK,EAAE,GAAW,EAAE,OAAiC,EAAE,EAAE;YAC5E,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,CAAU,EAAE,EAAE;gBAC/C,MAAM,QAAQ,CAAC,KAAK,CAClB,sDAAsD,EACtD,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAC,EAA6B,CAChE,CAAC;gBACF,OAAO,QAAQ,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,OAAO,EAAE,WAAW,EAAE,CAAC,EAA6B,CAAC,CAAC;YACxF,CAAC,CAAC,CAAC;QACL,CAAC,CAAC;QAEF,OAAO;YACL,KAAK,EAAE,YAAY;YACnB,WAAW,EAAE,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC;YAChD,OAAO,EAAE,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC;SACzC,CAAC;IACJ,CAAC;CACF;AA3CD,4CA2CC;AAED,sEAAsE;AACtE,SAAgB,eAAe,CAC7B,SAAwB,EACxB,SAAuB,EACvB,IAAa;IAEb,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAC3C,OAAO,OAAO,CAAC,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -21,7 +21,7 @@ export declare function createDatabase(client: pg.PoolClient | pg.Client, tenant
|
|
|
21
21
|
* Drops the per-tenant database if it exists.
|
|
22
22
|
*
|
|
23
23
|
* IMPORTANT: DROP DATABASE cannot run inside a transaction block.
|
|
24
|
-
* Same constraint as createDatabase
|
|
24
|
+
* Same constraint as createDatabase: use a standalone client.
|
|
25
25
|
*/
|
|
26
26
|
export declare function dropDatabase(client: pg.PoolClient | pg.Client, tenantSlug: string): Promise<void>;
|
|
27
27
|
//# sourceMappingURL=manager.d.ts.map
|
package/dist/database/manager.js
CHANGED
|
@@ -38,7 +38,7 @@ async function databaseExists(client, tenantSlug) {
|
|
|
38
38
|
*/
|
|
39
39
|
async function createDatabase(client, tenantSlug, templateDb) {
|
|
40
40
|
const dbName = getDatabaseName(tenantSlug);
|
|
41
|
-
// Identifiers validated by regex
|
|
41
|
+
// Identifiers validated by regex: no user-supplied interpolation outside of validated values.
|
|
42
42
|
// pg does not support parameterized DDL identifiers, so we construct the SQL string directly.
|
|
43
43
|
let sql = `CREATE DATABASE "${dbName}"`;
|
|
44
44
|
if (templateDb) {
|
|
@@ -54,7 +54,7 @@ async function createDatabase(client, tenantSlug, templateDb) {
|
|
|
54
54
|
* Drops the per-tenant database if it exists.
|
|
55
55
|
*
|
|
56
56
|
* IMPORTANT: DROP DATABASE cannot run inside a transaction block.
|
|
57
|
-
* Same constraint as createDatabase
|
|
57
|
+
* Same constraint as createDatabase: use a standalone client.
|
|
58
58
|
*/
|
|
59
59
|
async function dropDatabase(client, tenantSlug) {
|
|
60
60
|
const dbName = getDatabaseName(tenantSlug);
|
|
@@ -1 +1 @@
|
|
|
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
|
|
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,8FAA8F;IAC9F,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,6 +1,6 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
2
|
export interface DatabasePoolManagerOptions {
|
|
3
|
-
/** Template connection config (host, port, user, password, ssl, etc.)
|
|
3
|
+
/** Template connection config (host, port, user, password, ssl, etc.). The database name is overridden per tenant. */
|
|
4
4
|
baseConnectionConfig: pg.PoolConfig;
|
|
5
5
|
/** Maximum number of tenant pools to keep open simultaneously. Default: 50. */
|
|
6
6
|
maxPools?: number;
|
|
@@ -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,
|
|
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,sHAAsH;IACtH,oBAAoB,EAAE,EAAE,CAAC,UAAU,CAAC;IACpC,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AASD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqC;IAC3D,8FAA8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,OAAO,EAAE,0BAA0B;IAM/C;;;;;;;OAOG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,EAAE,CAAC,IAAI,CAAC;IAgCtE;;;;;;;;OAQG;IACH,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI;IAqBxD;gFAC4E;IACtE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBrE,kEAAkE;IAC5D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B,uGAAuG;IACvG,OAAO,CAAC,iBAAiB;IAKzB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAWtB,qDAAqD;IACrD,QAAQ,IAAI;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE;IAW5D;;;OAGG;IACH,OAAO,CAAC,OAAO;CAgBhB"}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,11 @@ export { SchemaRawAdapter, createSchemaTenantPool, } from "./adapters/schema-raw
|
|
|
5
5
|
export type { SchemaAdapterOptions } from "./adapters/schema-raw.js";
|
|
6
6
|
export { SchemaPrismaAdapter, withSchemaTenant, } from "./adapters/schema-prisma.js";
|
|
7
7
|
export { createPolicy, dropPolicy, enableRLS, disableRLS, isRLSEnabled, } from "./rls/manager.js";
|
|
8
|
+
export type { CreatePolicyOptions } from "./rls/manager.js";
|
|
9
|
+
export { tablePolicyIssues, tablePolicyWarnings, permissivePolicyIssue, isControlPlanePolicy, DEFAULT_CONTROL_ROLE, } from "./rls/policy-check.js";
|
|
10
|
+
export type { PolicyRow } from "./rls/policy-check.js";
|
|
8
11
|
export { setTenantContext, resetTenantContext, withTenantContext, withRlsBypass, getCurrentTenantId, } from "./rls/session.js";
|
|
12
|
+
export type { TenantScope, TenantContextOptions } from "./rls/session.js";
|
|
9
13
|
export { createSchema, dropSchema, schemaExists, listTenantSchemas, replicateTableToSchema, tenantSchemaName, } from "./schema/manager.js";
|
|
10
14
|
export { setSchemaSearchPath, resetSearchPath, getCurrentSearchPath, } from "./schema/session.js";
|
|
11
15
|
export { addTenantColumn, enableRLS as enableRLSForMigration, createIsolationPolicy, migrateTable, } from "./migration-helpers.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,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;
|
|
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;AAC1B,YAAY,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAEvD,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.drizzleWithTenantScope = exports.drizzleWithTenant = exports.DrizzleAdapter = exports.sequelizeWithTenantScope = exports.SequelizeAdapter = exports.DatabasePrismaAdapter = exports.DatabaseRawAdapter = exports.DatabasePoolManager = exports.dropDatabase = exports.createDatabase = exports.databaseExists = exports.getDatabaseName = exports.migrateTable = exports.createIsolationPolicy = exports.enableRLSForMigration = exports.addTenantColumn = exports.getCurrentSearchPath = exports.resetSearchPath = exports.setSchemaSearchPath = exports.tenantSchemaName = exports.replicateTableToSchema = exports.listTenantSchemas = exports.schemaExists = exports.dropSchema = exports.createSchema = exports.getCurrentTenantId = exports.withRlsBypass = exports.withTenantContext = exports.resetTenantContext = exports.setTenantContext = exports.isRLSEnabled = exports.disableRLS = exports.enableRLS = exports.dropPolicy = exports.createPolicy = exports.withSchemaTenant = exports.SchemaPrismaAdapter = exports.createSchemaTenantPool = exports.SchemaRawAdapter = exports.prismaWithTenant = exports.PrismaAdapter = exports.createTenantPool = exports.RawAdapter = exports.BaseAdapter = void 0;
|
|
3
|
+
exports.drizzleWithTenantScope = exports.drizzleWithTenant = exports.DrizzleAdapter = exports.sequelizeWithTenantScope = exports.SequelizeAdapter = exports.DatabasePrismaAdapter = exports.DatabaseRawAdapter = exports.DatabasePoolManager = exports.dropDatabase = exports.createDatabase = exports.databaseExists = exports.getDatabaseName = exports.migrateTable = exports.createIsolationPolicy = exports.enableRLSForMigration = exports.addTenantColumn = exports.getCurrentSearchPath = exports.resetSearchPath = exports.setSchemaSearchPath = exports.tenantSchemaName = exports.replicateTableToSchema = exports.listTenantSchemas = exports.schemaExists = exports.dropSchema = exports.createSchema = exports.getCurrentTenantId = exports.withRlsBypass = exports.withTenantContext = exports.resetTenantContext = exports.setTenantContext = exports.DEFAULT_CONTROL_ROLE = exports.isControlPlanePolicy = exports.permissivePolicyIssue = exports.tablePolicyWarnings = exports.tablePolicyIssues = exports.isRLSEnabled = exports.disableRLS = exports.enableRLS = exports.dropPolicy = exports.createPolicy = exports.withSchemaTenant = exports.SchemaPrismaAdapter = exports.createSchemaTenantPool = exports.SchemaRawAdapter = exports.prismaWithTenant = exports.PrismaAdapter = exports.createTenantPool = exports.RawAdapter = exports.BaseAdapter = void 0;
|
|
4
4
|
var base_adapter_js_1 = require("./base-adapter.js");
|
|
5
5
|
Object.defineProperty(exports, "BaseAdapter", { enumerable: true, get: function () { return base_adapter_js_1.BaseAdapter; } });
|
|
6
6
|
var raw_js_1 = require("./adapters/raw.js");
|
|
@@ -21,6 +21,12 @@ Object.defineProperty(exports, "dropPolicy", { enumerable: true, get: function (
|
|
|
21
21
|
Object.defineProperty(exports, "enableRLS", { enumerable: true, get: function () { return manager_js_1.enableRLS; } });
|
|
22
22
|
Object.defineProperty(exports, "disableRLS", { enumerable: true, get: function () { return manager_js_1.disableRLS; } });
|
|
23
23
|
Object.defineProperty(exports, "isRLSEnabled", { enumerable: true, get: function () { return manager_js_1.isRLSEnabled; } });
|
|
24
|
+
var policy_check_js_1 = require("./rls/policy-check.js");
|
|
25
|
+
Object.defineProperty(exports, "tablePolicyIssues", { enumerable: true, get: function () { return policy_check_js_1.tablePolicyIssues; } });
|
|
26
|
+
Object.defineProperty(exports, "tablePolicyWarnings", { enumerable: true, get: function () { return policy_check_js_1.tablePolicyWarnings; } });
|
|
27
|
+
Object.defineProperty(exports, "permissivePolicyIssue", { enumerable: true, get: function () { return policy_check_js_1.permissivePolicyIssue; } });
|
|
28
|
+
Object.defineProperty(exports, "isControlPlanePolicy", { enumerable: true, get: function () { return policy_check_js_1.isControlPlanePolicy; } });
|
|
29
|
+
Object.defineProperty(exports, "DEFAULT_CONTROL_ROLE", { enumerable: true, get: function () { return policy_check_js_1.DEFAULT_CONTROL_ROLE; } });
|
|
24
30
|
var session_js_1 = require("./rls/session.js");
|
|
25
31
|
Object.defineProperty(exports, "setTenantContext", { enumerable: true, get: function () { return session_js_1.setTenantContext; } });
|
|
26
32
|
Object.defineProperty(exports, "resetTenantContext", { enumerable: true, get: function () { return session_js_1.resetTenantContext; } });
|
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;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;
|
|
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,yDAM+B;AAL7B,oHAAA,iBAAiB,OAAA;AACjB,sHAAA,mBAAmB,OAAA;AACnB,wHAAA,qBAAqB,OAAA;AACrB,uHAAA,oBAAoB,OAAA;AACpB,uHAAA,oBAAoB,OAAA;AAItB,+CAM0B;AALxB,8GAAA,gBAAgB,OAAA;AAChB,gHAAA,kBAAkB,OAAA;AAClB,+GAAA,iBAAiB,OAAA;AACjB,2GAAA,aAAa,OAAA;AACb,gHAAA,kBAAkB,OAAA;AAIpB,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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
+
import { type CreatePolicyOptions } from "./rls/manager.js";
|
|
2
3
|
export declare function addTenantColumn(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
3
4
|
export declare function enableRLS(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
4
|
-
export declare function createIsolationPolicy(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
5
|
+
export declare function createIsolationPolicy(client: pg.PoolClient, tableName: string, options?: CreatePolicyOptions): Promise<void>;
|
|
5
6
|
export declare function migrateTable(client: pg.PoolClient, tableName: string): Promise<void>;
|
|
6
7
|
//# sourceMappingURL=migration-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-helpers.d.ts","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"migration-helpers.d.ts","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,EAA8C,KAAK,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAUxG,wBAAsB,eAAe,CACnC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,wBAAsB,SAAS,CAC7B,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAcf;AAED,wBAAsB,YAAY,CAChC,MAAM,EAAE,EAAE,CAAC,UAAU,EACrB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAIf"}
|
|
@@ -4,6 +4,7 @@ exports.addTenantColumn = addTenantColumn;
|
|
|
4
4
|
exports.enableRLS = enableRLS;
|
|
5
5
|
exports.createIsolationPolicy = createIsolationPolicy;
|
|
6
6
|
exports.migrateTable = migrateTable;
|
|
7
|
+
const manager_js_1 = require("./rls/manager.js");
|
|
7
8
|
// Validate table name to prevent SQL injection (only allows alphanumeric + underscores)
|
|
8
9
|
function validateTableName(name) {
|
|
9
10
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
|
|
@@ -20,10 +21,16 @@ async function enableRLS(client, tableName) {
|
|
|
20
21
|
await client.query(`ALTER TABLE ${safe} ENABLE ROW LEVEL SECURITY`);
|
|
21
22
|
await client.query(`ALTER TABLE ${safe} FORCE ROW LEVEL SECURITY`);
|
|
22
23
|
}
|
|
23
|
-
async function createIsolationPolicy(client, tableName) {
|
|
24
|
+
async function createIsolationPolicy(client, tableName, options = {}) {
|
|
24
25
|
const safe = validateTableName(tableName);
|
|
26
|
+
if (options.subtreeRead) {
|
|
27
|
+
await (0, manager_js_1.assertSubtreeFunction)(client);
|
|
28
|
+
}
|
|
25
29
|
await client.query(`CREATE POLICY tenant_isolation ON ${safe}
|
|
26
30
|
USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid)`);
|
|
31
|
+
if (options.subtreeRead) {
|
|
32
|
+
await client.query(`CREATE POLICY tenant_subtree_read ON ${safe} FOR SELECT USING (${manager_js_1.SUBTREE_READ_FILTER})`);
|
|
33
|
+
}
|
|
27
34
|
}
|
|
28
35
|
async function migrateTable(client, tableName) {
|
|
29
36
|
await addTenantColumn(client, tableName);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"migration-helpers.js","sourceRoot":"","sources":["../src/migration-helpers.ts"],"names":[],"mappings":";;AAWA,0CAQC;AAED,8BAOC;AAED,sDAkBC;AAED,oCAOC;AAxDD,iDAAwG;AAExG,wFAAwF;AACxF,SAAS,iBAAiB,CAAC,IAAY;IACrC,IAAI,CAAC,0BAA0B,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CAAC,uBAAuB,IAAI,EAAE,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAEM,KAAK,UAAU,eAAe,CACnC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAChB,eAAe,IAAI,qCAAqC,CACzD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,SAAS,CAC7B,MAAqB,EACrB,SAAiB;IAEjB,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,4BAA4B,CAAC,CAAC;IACpE,MAAM,MAAM,CAAC,KAAK,CAAC,eAAe,IAAI,2BAA2B,CAAC,CAAC;AACrE,CAAC;AAEM,KAAK,UAAU,qBAAqB,CACzC,MAAqB,EACrB,SAAiB,EACjB,UAA+B,EAAE;IAEjC,MAAM,IAAI,GAAG,iBAAiB,CAAC,SAAS,CAAC,CAAC;IAC1C,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,IAAA,kCAAqB,EAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IACD,MAAM,MAAM,CAAC,KAAK,CAChB,qCAAqC,IAAI;0FAC6C,CACvF,CAAC;IACF,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,MAAM,MAAM,CAAC,KAAK,CAChB,wCAAwC,IAAI,sBAAsB,gCAAmB,GAAG,CACzF,CAAC;IACJ,CAAC;AACH,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,MAAqB,EACrB,SAAiB;IAEjB,MAAM,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACzC,MAAM,SAAS,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACnC,MAAM,qBAAqB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AACjD,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type pg from "pg";
|
|
2
|
+
import type { PGliteInterface, PGliteOptions } from "@electric-sql/pglite" with { "resolution-mode": "import" };
|
|
3
|
+
/**
|
|
4
|
+
* A `pg.Pool`-compatible object over one PGlite instance.
|
|
5
|
+
*
|
|
6
|
+
* Only these members work: `query`, `connect` (a client with `query` and
|
|
7
|
+
* `release`), `end` and `on`. Other `pg.Pool` members are undefined.
|
|
8
|
+
*/
|
|
9
|
+
export type PglitePool = pg.Pool & {
|
|
10
|
+
/** The PGlite instance that runs every query of this pool. */
|
|
11
|
+
readonly pglite: PGliteInterface;
|
|
12
|
+
};
|
|
13
|
+
export interface RestrictedPoolOptions {
|
|
14
|
+
/**
|
|
15
|
+
* The name of the role. It must match `^[a-z_][a-z0-9_]{0,62}$`.
|
|
16
|
+
* Defaults to `stratum_app`.
|
|
17
|
+
*/
|
|
18
|
+
role?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Return a `pg.Pool`-compatible pool that runs every query on one PGlite instance.
|
|
22
|
+
*
|
|
23
|
+
* The pool has one connection. A client from `connect()` holds it until
|
|
24
|
+
* `release()`, and other callers wait in order. `release()` rolls back a
|
|
25
|
+
* transaction the client left open and resets the session settings, so the
|
|
26
|
+
* next client does not inherit them. Queries run as the PGlite
|
|
27
|
+
* superuser, so row-level security does not apply; use
|
|
28
|
+
* {@link createRestrictedPool} for that.
|
|
29
|
+
*
|
|
30
|
+
* @param source - A PGlite instance, or options to create one. When the
|
|
31
|
+
* pool creates the instance, it loads the `ltree` and `uuid_ossp`
|
|
32
|
+
* extensions, and `end()` closes the instance. When you pass an instance,
|
|
33
|
+
* you load the extensions and you close it.
|
|
34
|
+
* @returns The pool, after PGlite is ready.
|
|
35
|
+
*/
|
|
36
|
+
export declare function createPglitePool(source?: PGliteInterface | PGliteOptions): Promise<PglitePool>;
|
|
37
|
+
/**
|
|
38
|
+
* Return a pool that runs every query as a role that is not a superuser, so
|
|
39
|
+
* that row-level security applies.
|
|
40
|
+
*
|
|
41
|
+
* The function creates the role when it does not exist, with NOSUPERUSER and
|
|
42
|
+
* NOBYPASSRLS. It grants the role SELECT, INSERT, UPDATE and DELETE on every
|
|
43
|
+
* table in the `public` schema, and usage of every sequence. Default
|
|
44
|
+
* privileges extend these grants to tables that the current user creates
|
|
45
|
+
* later.
|
|
46
|
+
*
|
|
47
|
+
* The returned pool shares the connection and the lock of `pool`. Each client
|
|
48
|
+
* runs `SET ROLE` when it gets the connection and `RESET ROLE` at release.
|
|
49
|
+
* Its `end()` closes nothing; end the source pool instead.
|
|
50
|
+
*
|
|
51
|
+
* The restricted role is a convenience for tests and demos, not a security
|
|
52
|
+
* boundary: any query can leave it with `RESET ROLE`, because the session
|
|
53
|
+
* belongs to the superuser.
|
|
54
|
+
*
|
|
55
|
+
* @param pool - A pool from {@link createPglitePool} that runs as the superuser.
|
|
56
|
+
* @param options - The role name.
|
|
57
|
+
* @throws Error - The role name is not a plain lowercase identifier.
|
|
58
|
+
*/
|
|
59
|
+
export declare function createRestrictedPool(pool: PglitePool, options?: RestrictedPoolOptions): Promise<PglitePool>;
|
|
60
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAW,MAAM,sBAAsB,CAAC,OAAO,iBAAiB,EAAE,QAAQ,EAAE,CAAC;AAMzH;;;;;GAKG;AACH,MAAM,MAAM,UAAU,GAAG,EAAE,CAAC,IAAI,GAAG;IACjC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;CAClC,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAuID;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,CAAC,EAAE,eAAe,GAAG,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAgBpG;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPglitePool = createPglitePool;
|
|
4
|
+
exports.createRestrictedPool = createRestrictedPool;
|
|
5
|
+
// pg returns int8 and numeric values as strings, because a JavaScript number
|
|
6
|
+
// loses precision above 2^53. PGlite returns numbers, so these parsers
|
|
7
|
+
// restore the pg behavior.
|
|
8
|
+
const PG_PARSERS = { 20: (value) => value, 1700: (value) => value };
|
|
9
|
+
const ROLE_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
10
|
+
// A restricted pool must share the lock of its source pool, because both
|
|
11
|
+
// use the same connection.
|
|
12
|
+
const locks = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* Return a lock that gives one holder at a time the connection, in request order.
|
|
15
|
+
*/
|
|
16
|
+
function createLock() {
|
|
17
|
+
let tail = Promise.resolve();
|
|
18
|
+
return () => {
|
|
19
|
+
let unlock;
|
|
20
|
+
const held = new Promise((resolve) => (unlock = resolve));
|
|
21
|
+
const acquired = tail.then(() => unlock);
|
|
22
|
+
tail = tail.then(() => held);
|
|
23
|
+
return acquired;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// pg sends a plain object as JSON text for every parameter type. PGlite
|
|
27
|
+
// serializes by the inferred parameter type and rejects an object for a text
|
|
28
|
+
// parameter, so the conversion happens here.
|
|
29
|
+
function prepareValue(value) {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return null;
|
|
32
|
+
if (value !== null &&
|
|
33
|
+
typeof value === "object" &&
|
|
34
|
+
!(value instanceof Date) &&
|
|
35
|
+
!(value instanceof Uint8Array) &&
|
|
36
|
+
!Array.isArray(value)) {
|
|
37
|
+
return JSON.stringify(value);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
function toQueryResult(result) {
|
|
42
|
+
// PGlite reports affectedRows 0 for a SELECT. pg reports the number of rows
|
|
43
|
+
// for any statement that returns rows.
|
|
44
|
+
const rowCount = result.fields.length > 0 ? result.rows.length : (result.affectedRows ?? 0);
|
|
45
|
+
return { rows: result.rows, rowCount, fields: result.fields };
|
|
46
|
+
}
|
|
47
|
+
async function run(db, textOrConfig, values) {
|
|
48
|
+
const text = typeof textOrConfig === "string" ? textOrConfig : textOrConfig.text;
|
|
49
|
+
const params = values ?? (typeof textOrConfig === "string" ? undefined : textOrConfig.values);
|
|
50
|
+
if (!params || params.length === 0) {
|
|
51
|
+
// pg uses the simple query protocol when there are no parameters, and
|
|
52
|
+
// that protocol accepts several statements, as migration files hold.
|
|
53
|
+
// PGlite exec() is the same protocol. It returns one result per statement,
|
|
54
|
+
// and pg returns the last one.
|
|
55
|
+
const results = await db.exec(text, { parsers: PG_PARSERS });
|
|
56
|
+
return toQueryResult(results[results.length - 1] ?? { rows: [], fields: [] });
|
|
57
|
+
}
|
|
58
|
+
return toQueryResult(await db.query(text, params.map(prepareValue), { parsers: PG_PARSERS }));
|
|
59
|
+
}
|
|
60
|
+
function makePool(db, acquire, ownsInstance, role) {
|
|
61
|
+
const connect = async () => {
|
|
62
|
+
const unlock = await acquire();
|
|
63
|
+
try {
|
|
64
|
+
if (role)
|
|
65
|
+
await db.exec(`SET ROLE "${role}"`);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
unlock();
|
|
69
|
+
throw err;
|
|
70
|
+
}
|
|
71
|
+
let released = false;
|
|
72
|
+
return {
|
|
73
|
+
query: (textOrConfig, values) => run(db, textOrConfig, values),
|
|
74
|
+
// pg gives each client its own session and destroys the connection when
|
|
75
|
+
// release() receives an error. This pool has one session for every
|
|
76
|
+
// client, so release cleans it instead: it rolls back a transaction the
|
|
77
|
+
// client left open, and resets the settings and the role the client
|
|
78
|
+
// set. The lock stays held until the cleanup ends, so the next client
|
|
79
|
+
// gets a clean session.
|
|
80
|
+
release: () => {
|
|
81
|
+
if (released)
|
|
82
|
+
return;
|
|
83
|
+
released = true;
|
|
84
|
+
// RESET ALL leaves the role alone, so RESET ROLE follows it.
|
|
85
|
+
db.exec("ROLLBACK; RESET ALL; RESET ROLE").then(unlock, unlock);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
const pool = {
|
|
90
|
+
pglite: db,
|
|
91
|
+
connect,
|
|
92
|
+
async query(textOrConfig, values) {
|
|
93
|
+
const client = await connect();
|
|
94
|
+
try {
|
|
95
|
+
return await client.query(textOrConfig, values);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
client.release();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
async end() {
|
|
102
|
+
if (ownsInstance && !db.closed)
|
|
103
|
+
await db.close();
|
|
104
|
+
},
|
|
105
|
+
on() {
|
|
106
|
+
return pool;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
const result = pool;
|
|
110
|
+
locks.set(result, acquire);
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
function isPglite(value) {
|
|
114
|
+
return (typeof value === "object" &&
|
|
115
|
+
value !== null &&
|
|
116
|
+
typeof value.exec === "function" &&
|
|
117
|
+
typeof value.query === "function");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Return a `pg.Pool`-compatible pool that runs every query on one PGlite instance.
|
|
121
|
+
*
|
|
122
|
+
* The pool has one connection. A client from `connect()` holds it until
|
|
123
|
+
* `release()`, and other callers wait in order. `release()` rolls back a
|
|
124
|
+
* transaction the client left open and resets the session settings, so the
|
|
125
|
+
* next client does not inherit them. Queries run as the PGlite
|
|
126
|
+
* superuser, so row-level security does not apply; use
|
|
127
|
+
* {@link createRestrictedPool} for that.
|
|
128
|
+
*
|
|
129
|
+
* @param source - A PGlite instance, or options to create one. When the
|
|
130
|
+
* pool creates the instance, it loads the `ltree` and `uuid_ossp`
|
|
131
|
+
* extensions, and `end()` closes the instance. When you pass an instance,
|
|
132
|
+
* you load the extensions and you close it.
|
|
133
|
+
* @returns The pool, after PGlite is ready.
|
|
134
|
+
*/
|
|
135
|
+
async function createPglitePool(source) {
|
|
136
|
+
if (isPglite(source)) {
|
|
137
|
+
await source.waitReady;
|
|
138
|
+
return makePool(source, createLock(), false, null);
|
|
139
|
+
}
|
|
140
|
+
const [{ PGlite }, { ltree }, { uuid_ossp }] = await Promise.all([
|
|
141
|
+
import("@electric-sql/pglite"),
|
|
142
|
+
import("@electric-sql/pglite/contrib/ltree"),
|
|
143
|
+
import("@electric-sql/pglite/contrib/uuid_ossp"),
|
|
144
|
+
]);
|
|
145
|
+
const options = source ?? {};
|
|
146
|
+
const db = await PGlite.create({
|
|
147
|
+
...options,
|
|
148
|
+
extensions: { ltree, uuid_ossp, ...options.extensions },
|
|
149
|
+
});
|
|
150
|
+
return makePool(db, createLock(), true, null);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Return a pool that runs every query as a role that is not a superuser, so
|
|
154
|
+
* that row-level security applies.
|
|
155
|
+
*
|
|
156
|
+
* The function creates the role when it does not exist, with NOSUPERUSER and
|
|
157
|
+
* NOBYPASSRLS. It grants the role SELECT, INSERT, UPDATE and DELETE on every
|
|
158
|
+
* table in the `public` schema, and usage of every sequence. Default
|
|
159
|
+
* privileges extend these grants to tables that the current user creates
|
|
160
|
+
* later.
|
|
161
|
+
*
|
|
162
|
+
* The returned pool shares the connection and the lock of `pool`. Each client
|
|
163
|
+
* runs `SET ROLE` when it gets the connection and `RESET ROLE` at release.
|
|
164
|
+
* Its `end()` closes nothing; end the source pool instead.
|
|
165
|
+
*
|
|
166
|
+
* The restricted role is a convenience for tests and demos, not a security
|
|
167
|
+
* boundary: any query can leave it with `RESET ROLE`, because the session
|
|
168
|
+
* belongs to the superuser.
|
|
169
|
+
*
|
|
170
|
+
* @param pool - A pool from {@link createPglitePool} that runs as the superuser.
|
|
171
|
+
* @param options - The role name.
|
|
172
|
+
* @throws Error - The role name is not a plain lowercase identifier.
|
|
173
|
+
*/
|
|
174
|
+
async function createRestrictedPool(pool, options = {}) {
|
|
175
|
+
const role = options.role ?? "stratum_app";
|
|
176
|
+
if (!ROLE_NAME.test(role)) {
|
|
177
|
+
throw new Error(`Invalid role name '${role}': use lowercase letters, digits and underscores`);
|
|
178
|
+
}
|
|
179
|
+
await pool.query(`
|
|
180
|
+
DO $$ BEGIN
|
|
181
|
+
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = '${role}') THEN
|
|
182
|
+
CREATE ROLE "${role}" NOLOGIN NOSUPERUSER NOBYPASSRLS;
|
|
183
|
+
END IF;
|
|
184
|
+
END $$;
|
|
185
|
+
GRANT USAGE ON SCHEMA public TO "${role}";
|
|
186
|
+
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "${role}";
|
|
187
|
+
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "${role}";
|
|
188
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "${role}";
|
|
189
|
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO "${role}";
|
|
190
|
+
`);
|
|
191
|
+
const acquire = locks.get(pool);
|
|
192
|
+
if (!acquire)
|
|
193
|
+
throw new Error("createRestrictedPool needs a pool from createPglitePool");
|
|
194
|
+
return makePool(pool.pglite, acquire, false, role);
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=index.js.map
|