@ingram-tech/nk-db 0.2.1 → 0.4.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 +50 -1
- package/dist/drizzle.d.ts +18 -0
- package/dist/drizzle.d.ts.map +1 -1
- package/dist/drizzle.js +20 -0
- package/dist/drizzle.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/pglite/index.d.ts +8 -0
- package/dist/pglite/index.d.ts.map +1 -1
- package/dist/pglite/index.js +10 -7
- package/dist/pglite/index.js.map +1 -1
- package/dist/pglite/reset.d.ts +17 -0
- package/dist/pglite/reset.d.ts.map +1 -0
- package/dist/pglite/reset.js +23 -0
- package/dist/pglite/reset.js.map +1 -0
- package/dist/queries.d.ts +10 -0
- package/dist/queries.d.ts.map +1 -1
- package/dist/queries.js +12 -2
- package/dist/queries.js.map +1 -1
- package/dist/rls.d.ts +82 -0
- package/dist/rls.d.ts.map +1 -0
- package/dist/rls.js +49 -0
- package/dist/rls.js.map +1 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ import * as schema from "./schema";
|
|
|
39
39
|
|
|
40
40
|
export const pool = createPool(); // TLS-aware; local socket → max:1
|
|
41
41
|
export const db = createDb(pool, schema); // Drizzle — the default query path
|
|
42
|
-
export const { query, one, maybeOne, execute, withTx } = createQueries(pool);
|
|
42
|
+
export const { query, one, maybeOne, execute, withTx, withRls } = createQueries(pool);
|
|
43
43
|
export { schema };
|
|
44
44
|
```
|
|
45
45
|
|
|
@@ -55,6 +55,55 @@ same pool: `betterAuth({ database: pool, … })`.
|
|
|
55
55
|
`timestamptz` as ISO strings (on the golden path, prefer Drizzle's
|
|
56
56
|
`timestamp(..., { mode: "string" })` per column).
|
|
57
57
|
|
|
58
|
+
## Keeping RLS on a direct connection (`withRls` / `withRlsTransaction`)
|
|
59
|
+
|
|
60
|
+
A direct `pg`/Drizzle connection has no PostgREST, so the `SET ROLE authenticated`
|
|
61
|
+
+ `request.jwt.claims` setup that made `auth.uid()` policies fire is gone — a
|
|
62
|
+
plain query runs as the connection's role with no claims. These helpers reproduce
|
|
63
|
+
that setup **per transaction**, so your **existing RLS policies keep working
|
|
64
|
+
unchanged**, whether you're still on Supabase Postgres or already on DO. It's
|
|
65
|
+
pure Postgres and behaves identically on both.
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { withRlsTransaction } from "@ingram-tech/nk-db";
|
|
69
|
+
import { auth } from "@/lib/auth"; // your Better Auth instance
|
|
70
|
+
import { db } from "@/lib/db";
|
|
71
|
+
|
|
72
|
+
const session = await auth.api.getSession({ headers });
|
|
73
|
+
// scoped: sets request.jwt.claims + SET LOCAL ROLE authenticated, then runs fn
|
|
74
|
+
const notes = await withRlsTransaction(db, { sub: session.user.id }, (tx) =>
|
|
75
|
+
tx.select().from(schema.notes), // returns only this user's rows
|
|
76
|
+
);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The claims come **straight from the Better Auth session** (`sub` = `user.id`) — no
|
|
80
|
+
JWT minting, no JWKS issuer, no `supabase.auth`. The raw helpers expose the same
|
|
81
|
+
thing as `withRls` (sibling of `withTx`):
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
const { withRls } = createQueries(pool);
|
|
85
|
+
const rows = await withRls({ sub: userId }, (tx) =>
|
|
86
|
+
tx.query<Note>("select * from notes"),
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Two requirements **you** own (they can't be enforced from the library):
|
|
91
|
+
|
|
92
|
+
- **Connect as a role that doesn't bypass RLS** for user-facing rows — not the
|
|
93
|
+
table owner, not a `BYPASSRLS` superuser. After `SET ROLE authenticated`, RLS
|
|
94
|
+
applies even if the underlying connection is `postgres` (exactly what PostgREST
|
|
95
|
+
relied on). Service-role/admin paths keep using plain `db` / `query` and bypass
|
|
96
|
+
RLS as before.
|
|
97
|
+
- **The connecting role must be allowed to `SET ROLE`** to the target
|
|
98
|
+
(Supabase's `authenticator`/`postgres` already can; on DO, `GRANT app_user TO …`).
|
|
99
|
+
|
|
100
|
+
Override the role / claims GUC when your DB role name differs from the JWT claim:
|
|
101
|
+
`withRlsTransaction(db, { sub }, fn, { role: "app_user" })`. Both helpers set the
|
|
102
|
+
GUCs **transaction-locally** (`is_local = true`), so they reset at
|
|
103
|
+
commit/rollback and never leak across pooled connections. See
|
|
104
|
+
[`docs/db-package.md` §RLS](https://github.com/ingram-technologies/nextkit/blob/main/docs/db-package.md)
|
|
105
|
+
and [`docs/better-auth-migration.md`](https://github.com/ingram-technologies/nextkit/blob/main/docs/better-auth-migration.md).
|
|
106
|
+
|
|
58
107
|
## PGlite dev & test (`@ingram-tech/nk-db/pglite`)
|
|
59
108
|
|
|
60
109
|
`nk dev` runs the `nk-pglite-dev` bin automatically when this package is
|
package/dist/drizzle.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
2
|
import type { Pool } from "pg";
|
|
3
|
+
import { type RlsClaims, type RlsOptions } from "./rls.js";
|
|
3
4
|
/**
|
|
4
5
|
* Build the Drizzle instance on the shared pool. This is the default query path
|
|
5
6
|
* for app tables — schema-first, with `drizzle-kit` generating the migrations
|
|
@@ -11,4 +12,21 @@ import type { Pool } from "pg";
|
|
|
11
12
|
* export const db = createDb(pool, schema);
|
|
12
13
|
*/
|
|
13
14
|
export declare const createDb: <TSchema extends Record<string, unknown>>(pool: Pool, schema: TSchema) => NodePgDatabase<TSchema>;
|
|
15
|
+
/** The transaction handle Drizzle hands its `db.transaction(fn)` callback. */
|
|
16
|
+
type DrizzleTx<TSchema extends Record<string, unknown>> = Parameters<Parameters<NodePgDatabase<TSchema>["transaction"]>[0]>[0];
|
|
17
|
+
/**
|
|
18
|
+
* Run `fn` inside a Drizzle transaction **scoped to a user**, so RLS policies
|
|
19
|
+
* apply on a direct connection (no PostgREST). It sets `request.jwt.claims` +
|
|
20
|
+
* `SET LOCAL ROLE` (default `authenticated`) as the first statement, so existing
|
|
21
|
+
* `auth.uid()` policies fire unchanged; the `tx` passed to `fn` is a normal
|
|
22
|
+
* Drizzle transaction (full query builder + `tx.execute`). Use for user-facing
|
|
23
|
+
* reads/writes; use plain `db` for service-role paths. The connection role must
|
|
24
|
+
* not bypass RLS for the rows touched — see `rls.ts`.
|
|
25
|
+
*
|
|
26
|
+
* const notes = await withRlsTransaction(db, { sub: user.id }, (tx) =>
|
|
27
|
+
* tx.select().from(schema.notes),
|
|
28
|
+
* );
|
|
29
|
+
*/
|
|
30
|
+
export declare const withRlsTransaction: <TSchema extends Record<string, unknown>, T>(db: NodePgDatabase<TSchema>, claims: RlsClaims, fn: (tx: DrizzleTx<TSchema>) => Promise<T>, options?: RlsOptions) => Promise<T>;
|
|
31
|
+
export {};
|
|
14
32
|
//# sourceMappingURL=drizzle.d.ts.map
|
package/dist/drizzle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"drizzle.d.ts","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,cAAc,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,UAAU,EAAoB,MAAM,UAAU,CAAC;AAE7E;;;;;;;;;GASG;AACH,eAAO,MAAM,QAAQ,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,IAAI,EACV,QAAQ,OAAO,KACb,cAAc,CAAC,OAAO,CAA8B,CAAC;AAExD,8EAA8E;AAC9E,KAAK,SAAS,CAAC,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,UAAU,CACnE,UAAU,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CACrD,CAAC,CAAC,CAAC,CAAC;AAEL;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,EAC5E,IAAI,cAAc,CAAC,OAAO,CAAC,EAC3B,QAAQ,SAAS,EACjB,IAAI,CAAC,EAAE,EAAE,SAAS,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,UAAS,UAAe,KACtB,OAAO,CAAC,CAAC,CAOT,CAAC"}
|
package/dist/drizzle.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
1
2
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
3
|
+
import { resolveRlsConfig } from "./rls.js";
|
|
2
4
|
/**
|
|
3
5
|
* Build the Drizzle instance on the shared pool. This is the default query path
|
|
4
6
|
* for app tables — schema-first, with `drizzle-kit` generating the migrations
|
|
@@ -10,4 +12,22 @@ import { drizzle } from "drizzle-orm/node-postgres";
|
|
|
10
12
|
* export const db = createDb(pool, schema);
|
|
11
13
|
*/
|
|
12
14
|
export const createDb = (pool, schema) => drizzle(pool, { schema });
|
|
15
|
+
/**
|
|
16
|
+
* Run `fn` inside a Drizzle transaction **scoped to a user**, so RLS policies
|
|
17
|
+
* apply on a direct connection (no PostgREST). It sets `request.jwt.claims` +
|
|
18
|
+
* `SET LOCAL ROLE` (default `authenticated`) as the first statement, so existing
|
|
19
|
+
* `auth.uid()` policies fire unchanged; the `tx` passed to `fn` is a normal
|
|
20
|
+
* Drizzle transaction (full query builder + `tx.execute`). Use for user-facing
|
|
21
|
+
* reads/writes; use plain `db` for service-role paths. The connection role must
|
|
22
|
+
* not bypass RLS for the rows touched — see `rls.ts`.
|
|
23
|
+
*
|
|
24
|
+
* const notes = await withRlsTransaction(db, { sub: user.id }, (tx) =>
|
|
25
|
+
* tx.select().from(schema.notes),
|
|
26
|
+
* );
|
|
27
|
+
*/
|
|
28
|
+
export const withRlsTransaction = (db, claims, fn, options = {}) => db.transaction(async (tx) => {
|
|
29
|
+
const { role, claimsSetting, claimsJson } = resolveRlsConfig(claims, options);
|
|
30
|
+
await tx.execute(sql `select set_config(${claimsSetting}, ${claimsJson}, true), set_config('role', ${role}, true)`);
|
|
31
|
+
return fn(tx);
|
|
32
|
+
});
|
|
13
33
|
//# sourceMappingURL=drizzle.js.map
|
package/dist/drizzle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"drizzle.js","sourceRoot":"","sources":["../src/drizzle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AAEzE,OAAO,EAAmC,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAE7E;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,CACvB,IAAU,EACV,MAAe,EACW,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAOxD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CACjC,EAA2B,EAC3B,MAAiB,EACjB,EAA0C,EAC1C,UAAsB,EAAE,EACX,EAAE,CACf,EAAE,CAAC,WAAW,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;IAC3B,MAAM,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,GAAG,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9E,MAAM,EAAE,CAAC,OAAO,CACf,GAAG,CAAA,qBAAqB,aAAa,KAAK,UAAU,+BAA+B,IAAI,SAAS,CAChG,CAAC;IACF,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;AACf,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { createDb } from "./drizzle.js";
|
|
1
|
+
export { createDb, withRlsTransaction } from "./drizzle.js";
|
|
2
2
|
export { type DbEnv, dbEnv, getDatabaseUrl, isConfigured } from "./keys.js";
|
|
3
3
|
export { type CreatePoolConfig, createPool } from "./pool.js";
|
|
4
4
|
export { createQueries, type PoolQueries, type Queries } from "./queries.js";
|
|
5
|
+
export { RLS_CLAIMS_SETTING, RLS_DEFAULT_ROLE, type ResolvedRlsConfig, type RlsClaims, type RlsOptions, resolveRlsConfig, rlsPreamble, } from "./rls.js";
|
|
5
6
|
export { configureTimestampsAsStrings } from "./types.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,KAAK,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAE,KAAK,gBAAgB,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAC7E,OAAO,EACN,kBAAkB,EAClB,gBAAgB,EAChB,KAAK,iBAAiB,EACtB,KAAK,SAAS,EACd,KAAK,UAAU,EACf,gBAAgB,EAChB,WAAW,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// The Ingram Postgres data layer. The PGlite dev/test harness lives behind the
|
|
2
2
|
// "@ingram-tech/nk-db/pglite" subpath so its WASM/dev-only deps never reach a
|
|
3
3
|
// production bundle.
|
|
4
|
-
export { createDb } from "./drizzle.js";
|
|
4
|
+
export { createDb, withRlsTransaction } from "./drizzle.js";
|
|
5
5
|
export { dbEnv, getDatabaseUrl, isConfigured } from "./keys.js";
|
|
6
6
|
export { createPool } from "./pool.js";
|
|
7
7
|
export { createQueries } from "./queries.js";
|
|
8
|
+
export { RLS_CLAIMS_SETTING, RLS_DEFAULT_ROLE, resolveRlsConfig, rlsPreamble, } from "./rls.js";
|
|
8
9
|
export { configureTimestampsAsStrings } from "./types.js";
|
|
9
10
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,qBAAqB;AAErB,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,8EAA8E;AAC9E,qBAAqB;AAErB,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAc,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC5E,OAAO,EAAyB,UAAU,EAAE,MAAM,WAAW,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAkC,MAAM,cAAc,CAAC;AAC7E,OAAO,EACN,kBAAkB,EAClB,gBAAgB,EAIhB,gBAAgB,EAChB,WAAW,GACX,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/pglite/index.d.ts
CHANGED
|
@@ -57,6 +57,14 @@ export interface TestDb {
|
|
|
57
57
|
* An in-memory PGlite for integration tests. Real Postgres semantics, instant,
|
|
58
58
|
* no cleanup. Run with Vitest `fileParallelism: false` (the socket is
|
|
59
59
|
* single-connection) and close in `afterAll` (modules are isolated per file).
|
|
60
|
+
*
|
|
61
|
+
* Transport note: this harness is socket + `pg.Pool`, which is what a Next site
|
|
62
|
+
* dialing `DATABASE_URL` wants — but that single-connection socket is also why it
|
|
63
|
+
* needs serial files. A pure in-process Drizzle/PGlite harness (no socket) runs
|
|
64
|
+
* files in parallel and is faster, which is what an actual service reached for.
|
|
65
|
+
* Both share `resetPublicTables`. If a second in-process consumer appears, the
|
|
66
|
+
* move is to promote an in-process harness to first-class here, not to bolt one
|
|
67
|
+
* beside this socket default.
|
|
60
68
|
*/
|
|
61
69
|
export declare const createTestDb: (options?: PgliteServerOptions) => Promise<TestDb>;
|
|
62
70
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAG/B,MAAM,WAAW,mBAAmB;IACnC,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,kBAAkB,CAAC;IAC3B,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAUD;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,GAC9B,UAAS,mBAAwB,KAC/B,OAAO,CAAC,YAAY,CAyBtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc,GAC1B,UAAS,mBAAmB,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,KACzD,OAAO,CAAC,IAAI,CA0Bd,CAAC;AAEF,MAAM,WAAW,MAAM;IACtB,2EAA2E;IAC3E,IAAI,EAAE,IAAI,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,4DAA4D;IAC5D,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,YAAY,GACxB,UAAS,mBAAwB,KAC/B,OAAO,CAAC,MAAM,CAWhB,CAAC"}
|
package/dist/pglite/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { existsSync } from "node:fs";
|
|
|
11
11
|
import { rm } from "node:fs/promises";
|
|
12
12
|
import { PGlite } from "@electric-sql/pglite";
|
|
13
13
|
import { PGLiteSocketServer } from "@electric-sql/pglite-socket";
|
|
14
|
+
import { resetPublicTables } from "./reset.js";
|
|
14
15
|
const defaultMigrate = (migrationsFolder) => async (db) => {
|
|
15
16
|
const { drizzle } = await import("drizzle-orm/pglite");
|
|
16
17
|
const { migrate } = await import("drizzle-orm/pglite/migrator");
|
|
@@ -75,18 +76,20 @@ export const startPgliteDev = async (options = {}) => {
|
|
|
75
76
|
* An in-memory PGlite for integration tests. Real Postgres semantics, instant,
|
|
76
77
|
* no cleanup. Run with Vitest `fileParallelism: false` (the socket is
|
|
77
78
|
* single-connection) and close in `afterAll` (modules are isolated per file).
|
|
79
|
+
*
|
|
80
|
+
* Transport note: this harness is socket + `pg.Pool`, which is what a Next site
|
|
81
|
+
* dialing `DATABASE_URL` wants — but that single-connection socket is also why it
|
|
82
|
+
* needs serial files. A pure in-process Drizzle/PGlite harness (no socket) runs
|
|
83
|
+
* files in parallel and is faster, which is what an actual service reached for.
|
|
84
|
+
* Both share `resetPublicTables`. If a second in-process consumer appears, the
|
|
85
|
+
* move is to promote an in-process harness to first-class here, not to bolt one
|
|
86
|
+
* beside this socket default.
|
|
78
87
|
*/
|
|
79
88
|
export const createTestDb = async (options = {}) => {
|
|
80
89
|
const { Pool } = await import("pg");
|
|
81
90
|
const { databaseUrl, stop } = await createPgliteServer(options);
|
|
82
91
|
const pool = new Pool({ connectionString: databaseUrl, max: 1 });
|
|
83
|
-
const reset =
|
|
84
|
-
const result = await pool.query("select tablename from pg_tables where schemaname = 'public'");
|
|
85
|
-
if (result.rows.length === 0)
|
|
86
|
-
return;
|
|
87
|
-
const list = result.rows.map((row) => `"${row.tablename}"`).join(", ");
|
|
88
|
-
await pool.query(`truncate table ${list} restart identity cascade`);
|
|
89
|
-
};
|
|
92
|
+
const reset = () => resetPublicTables((sql) => pool.query(sql));
|
|
90
93
|
const close = async () => {
|
|
91
94
|
await pool.end();
|
|
92
95
|
await stop();
|
package/dist/pglite/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,iFAAiF;AACjF,uEAAuE;AACvE,iFAAiF;AACjF,sBAAsB;AACtB,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,2CAA2C;AAE3C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAE,MAAM,EAAmB,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/pglite/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A row-returning query — `pg.Pool.query`, raw PGlite `.query`, etc. The
|
|
3
|
+
* `pg_tables` introspection reads rows back, so this can't be a fire-and-forget
|
|
4
|
+
* `exec(sql)`: the contract has to surface `{ rows }`.
|
|
5
|
+
*/
|
|
6
|
+
export type Queryable = (sql: string) => Promise<{
|
|
7
|
+
rows: {
|
|
8
|
+
tablename: string;
|
|
9
|
+
}[];
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* TRUNCATE every table in the `public` schema, resetting identity sequences.
|
|
13
|
+
* Call in a Vitest `beforeEach`. No-op when the schema is empty (e.g. before
|
|
14
|
+
* migrations have run), so it's safe to call unconditionally.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resetPublicTables(query: Queryable): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=reset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset.d.ts","sourceRoot":"","sources":["../../src/pglite/reset.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAC,CAAC;AAEpF;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAOvE"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Reset a PGlite/Postgres test database to empty between tests — the canonical
|
|
2
|
+
// "introspect public tables, then TRUNCATE … RESTART IDENTITY CASCADE" unit,
|
|
3
|
+
// kept transport-agnostic so the socket-backed `createTestDb` and any in-process
|
|
4
|
+
// Drizzle/PGlite harness share one implementation instead of each re-deriving the
|
|
5
|
+
// SQL (and the easy-to-forget empty-list guard) and drifting apart.
|
|
6
|
+
//
|
|
7
|
+
// Deliberately zero imports: this loads via the dedicated
|
|
8
|
+
// "@ingram-tech/nk-db/pglite/reset" subpath, so an in-process consumer gets the
|
|
9
|
+
// reset logic WITHOUT dragging in PGlite or the PGLiteSocketServer that
|
|
10
|
+
// `./index.ts` imports at the top level.
|
|
11
|
+
/**
|
|
12
|
+
* TRUNCATE every table in the `public` schema, resetting identity sequences.
|
|
13
|
+
* Call in a Vitest `beforeEach`. No-op when the schema is empty (e.g. before
|
|
14
|
+
* migrations have run), so it's safe to call unconditionally.
|
|
15
|
+
*/
|
|
16
|
+
export async function resetPublicTables(query) {
|
|
17
|
+
const { rows } = await query("select tablename from pg_tables where schemaname = 'public'");
|
|
18
|
+
if (rows.length === 0)
|
|
19
|
+
return;
|
|
20
|
+
const list = rows.map((row) => `"${row.tablename}"`).join(", ");
|
|
21
|
+
await query(`truncate table ${list} restart identity cascade`);
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=reset.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reset.js","sourceRoot":"","sources":["../../src/pglite/reset.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,6EAA6E;AAC7E,iFAAiF;AACjF,kFAAkF;AAClF,oEAAoE;AACpE,EAAE;AACF,0DAA0D;AAC1D,gFAAgF;AAChF,wEAAwE;AACxE,yCAAyC;AASzC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAgB;IACvD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,KAAK,CAC3B,6DAA6D,CAC7D,CAAC;IACF,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC9B,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,MAAM,KAAK,CAAC,kBAAkB,IAAI,2BAA2B,CAAC,CAAC;AAChE,CAAC"}
|
package/dist/queries.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Pool, QueryResultRow } from "pg";
|
|
2
|
+
import { type RlsClaims, type RlsOptions } from "./rls.js";
|
|
2
3
|
export interface Queries {
|
|
3
4
|
/** Run a query and return all rows. */
|
|
4
5
|
query: <T extends QueryResultRow = QueryResultRow>(text: string, params?: unknown[]) => Promise<T[]>;
|
|
@@ -17,6 +18,15 @@ export interface PoolQueries extends Queries {
|
|
|
17
18
|
* statements that must be atomic.
|
|
18
19
|
*/
|
|
19
20
|
withTx: <T>(fn: (tx: Queries) => Promise<T>) => Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Like `withTx`, but first **scopes the transaction to a user** so RLS
|
|
23
|
+
* policies apply: it sets `request.jwt.claims` + `SET LOCAL ROLE` (default
|
|
24
|
+
* `authenticated`) before `fn`, reproducing what PostgREST did. Use this for
|
|
25
|
+
* user-facing reads/writes on a direct connection; keep `withTx` / `query`
|
|
26
|
+
* for service-role paths that should bypass RLS. The connection role must not
|
|
27
|
+
* bypass RLS for the rows touched — see `rls.ts`.
|
|
28
|
+
*/
|
|
29
|
+
withRls: <T>(claims: RlsClaims, fn: (tx: Queries) => Promise<T>, options?: RlsOptions) => Promise<T>;
|
|
20
30
|
}
|
|
21
31
|
/**
|
|
22
32
|
* Bind the raw-SQL helpers (`query` / `one` / `maybeOne` / `execute` / `withTx`)
|
package/dist/queries.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,cAAc,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"queries.d.ts","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAc,cAAc,EAAE,MAAM,IAAI,CAAC;AAC3D,OAAO,EACN,KAAK,SAAS,EACd,KAAK,UAAU,EAGf,MAAM,UAAU,CAAC;AAKlB,MAAM,WAAW,OAAO;IACvB,uCAAuC;IACvC,KAAK,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAChD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC;IAClB,wEAAwE;IACxE,QAAQ,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EACnD,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACvB,sEAAsE;IACtE,GAAG,EAAE,CAAC,CAAC,SAAS,cAAc,GAAG,cAAc,EAC9C,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,OAAO,EAAE,KACd,OAAO,CAAC,CAAC,CAAC,CAAC;IAChB,qDAAqD;IACrD,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/D;AAoCD,MAAM,WAAW,WAAY,SAAQ,OAAO;IAC3C;;;;;OAKG;IACH,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3D;;;;;;;OAOG;IACH,OAAO,EAAE,CAAC,CAAC,EACV,MAAM,EAAE,SAAS,EACjB,EAAE,EAAE,CAAC,EAAE,EAAE,OAAO,KAAK,OAAO,CAAC,CAAC,CAAC,EAC/B,OAAO,CAAC,EAAE,UAAU,KAChB,OAAO,CAAC,CAAC,CAAC,CAAC;CAChB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,KAAG,WAkC1C,CAAC"}
|
package/dist/queries.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveRlsConfig, rlsPreamble, } from "./rls.js";
|
|
1
2
|
const bind = (executor) => {
|
|
2
3
|
const query = async (text, params = []) => {
|
|
3
4
|
const result = await executor.query(text, params);
|
|
@@ -32,10 +33,14 @@ const bind = (executor) => {
|
|
|
32
33
|
*/
|
|
33
34
|
export const createQueries = (pool) => {
|
|
34
35
|
const base = bind(pool);
|
|
35
|
-
|
|
36
|
+
// One transaction runner; `setup` (if any) runs after `begin`, before `fn` —
|
|
37
|
+
// that's where withRls injects the claims/role GUCs.
|
|
38
|
+
const runTx = async (setup, fn) => {
|
|
36
39
|
const client = await pool.connect();
|
|
37
40
|
try {
|
|
38
41
|
await client.query("begin");
|
|
42
|
+
if (setup)
|
|
43
|
+
await setup(client);
|
|
39
44
|
const result = await fn(bind(client));
|
|
40
45
|
await client.query("commit");
|
|
41
46
|
return result;
|
|
@@ -48,6 +53,11 @@ export const createQueries = (pool) => {
|
|
|
48
53
|
client.release();
|
|
49
54
|
}
|
|
50
55
|
};
|
|
51
|
-
|
|
56
|
+
const withTx = (fn) => runTx(undefined, fn);
|
|
57
|
+
const withRls = (claims, fn, options) => runTx(async (client) => {
|
|
58
|
+
const { text, values } = rlsPreamble(resolveRlsConfig(claims, options));
|
|
59
|
+
await client.query(text, values);
|
|
60
|
+
}, fn);
|
|
61
|
+
return { ...base, withTx, withRls };
|
|
52
62
|
};
|
|
53
63
|
//# sourceMappingURL=queries.js.map
|
package/dist/queries.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"queries.js","sourceRoot":"","sources":["../src/queries.ts"],"names":[],"mappings":"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"}
|
package/dist/rls.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row-Level Security for a **direct Postgres connection** (no PostgREST).
|
|
3
|
+
*
|
|
4
|
+
* On Supabase, PostgREST is what made RLS work: per request it did
|
|
5
|
+
* `SET ROLE authenticated` and set `request.jwt.claims` from the user's JWT, so
|
|
6
|
+
* policies written against `auth.uid()` fired. The moment a site queries Postgres
|
|
7
|
+
* directly (`pg` / Drizzle) — whether it's still on Supabase Postgres or has
|
|
8
|
+
* moved to our DigitalOcean cluster — that setup is gone, and a plain connection
|
|
9
|
+
* runs as the connection's role with **no claims**, so RLS is either bypassed
|
|
10
|
+
* (privileged role) or denies everything.
|
|
11
|
+
*
|
|
12
|
+
* These helpers reproduce exactly what PostgREST did, per transaction: set the
|
|
13
|
+
* claims GUC and `SET LOCAL ROLE`, so **existing `auth.uid()` policies keep
|
|
14
|
+
* working unchanged**. It is pure Postgres, so it behaves identically on Supabase
|
|
15
|
+
* and on DO. See `docs/db-package.md` (§RLS) and `docs/better-auth-migration.md`.
|
|
16
|
+
*
|
|
17
|
+
* Two requirements the caller owns (documented, not enforceable here):
|
|
18
|
+
* 1. The pool must connect as a role that **does not bypass RLS** for the rows
|
|
19
|
+
* it touches — i.e. not the table owner and not a `BYPASSRLS` superuser for
|
|
20
|
+
* user-facing reads. (After `SET ROLE authenticated`, RLS applies even if the
|
|
21
|
+
* underlying connection is `postgres`, exactly as PostgREST relied on.)
|
|
22
|
+
* 2. The connecting role must be allowed to `SET ROLE` to the target role
|
|
23
|
+
* (Supabase's `authenticator`/`postgres` can; on DO, `GRANT app_user TO …`).
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* The JWT-style claims to scope a transaction by. `sub` becomes `auth.uid()`;
|
|
27
|
+
* `role` (default `"authenticated"`) is the Postgres role assumed. Any further
|
|
28
|
+
* keys are written into the claims GUC for policies that read them
|
|
29
|
+
* (`request.jwt.claims ->> 'org_id'`, etc.).
|
|
30
|
+
*/
|
|
31
|
+
export interface RlsClaims {
|
|
32
|
+
/**
|
|
33
|
+
* The user id. Becomes the `sub` claim → `auth.uid()`. For Supabase-style
|
|
34
|
+
* policies that cast `sub` to `uuid`, this MUST be a valid UUID.
|
|
35
|
+
*/
|
|
36
|
+
sub: string;
|
|
37
|
+
/** The Postgres role to assume (the JWT `role` claim). Defaults to `"authenticated"`. */
|
|
38
|
+
role?: string;
|
|
39
|
+
/** Any further claims your policies read from `request.jwt.claims`. */
|
|
40
|
+
[claim: string]: unknown;
|
|
41
|
+
}
|
|
42
|
+
/** Overrides for {@link resolveRlsConfig} (and the helpers that build on it). */
|
|
43
|
+
export interface RlsOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Force the Postgres role to `SET LOCAL`, ignoring `claims.role`. Use when the
|
|
46
|
+
* DB role name differs from the JWT `role` claim (e.g. a dedicated `app_user`
|
|
47
|
+
* on DO while the claim stays `"authenticated"`).
|
|
48
|
+
*/
|
|
49
|
+
role?: string;
|
|
50
|
+
/** The GUC the claims JSON is written to. Defaults to `"request.jwt.claims"`. */
|
|
51
|
+
claimsSetting?: string;
|
|
52
|
+
}
|
|
53
|
+
/** The role assumed when neither `options.role` nor `claims.role` is set. */
|
|
54
|
+
export declare const RLS_DEFAULT_ROLE = "authenticated";
|
|
55
|
+
/** The GUC `auth.uid()` / `auth.role()` read; what PostgREST populated. */
|
|
56
|
+
export declare const RLS_CLAIMS_SETTING = "request.jwt.claims";
|
|
57
|
+
/** The concrete values a transaction is scoped with. */
|
|
58
|
+
export interface ResolvedRlsConfig {
|
|
59
|
+
/** Postgres role to assume for the transaction. */
|
|
60
|
+
role: string;
|
|
61
|
+
/** GUC the claims JSON is written to. */
|
|
62
|
+
claimsSetting: string;
|
|
63
|
+
/** The claims serialized for `set_config` (a JSON string). */
|
|
64
|
+
claimsJson: string;
|
|
65
|
+
}
|
|
66
|
+
/** Resolve claims + options into the role, GUC name, and serialized claims. */
|
|
67
|
+
export declare const resolveRlsConfig: (claims: RlsClaims, options?: RlsOptions) => ResolvedRlsConfig;
|
|
68
|
+
/**
|
|
69
|
+
* The single parameterized statement that scopes a transaction: writes the
|
|
70
|
+
* claims GUC and the `role` GUC, **both transaction-local** (`is_local = true`,
|
|
71
|
+
* so they reset at `commit`/`rollback` and never leak across pooled
|
|
72
|
+
* connections). Everything — including the GUC name and role — is bound, never
|
|
73
|
+
* interpolated, so it's injection-safe.
|
|
74
|
+
*
|
|
75
|
+
* Exposed for callers that manage their own connection/transaction (e.g. a
|
|
76
|
+
* framework middleware): run it as the first statement after `begin`.
|
|
77
|
+
*/
|
|
78
|
+
export declare const rlsPreamble: (config: ResolvedRlsConfig) => {
|
|
79
|
+
text: string;
|
|
80
|
+
values: string[];
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=rls.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls.d.ts","sourceRoot":"","sources":["../src/rls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IACZ,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED,iFAAiF;AACjF,MAAM,WAAW,UAAU;IAC1B;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,kBAAkB,CAAC;AAChD,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAEvD,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IACjC,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,yCAAyC;IACzC,aAAa,EAAE,MAAM,CAAC;IACtB,8DAA8D;IAC9D,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAC5B,QAAQ,SAAS,EACjB,UAAS,UAAe,KACtB,iBAID,CAAC;AAEH;;;;;;;;;GASG;AACH,eAAO,MAAM,WAAW,GACvB,QAAQ,iBAAiB,KACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAGjC,CAAC"}
|
package/dist/rls.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Row-Level Security for a **direct Postgres connection** (no PostgREST).
|
|
3
|
+
*
|
|
4
|
+
* On Supabase, PostgREST is what made RLS work: per request it did
|
|
5
|
+
* `SET ROLE authenticated` and set `request.jwt.claims` from the user's JWT, so
|
|
6
|
+
* policies written against `auth.uid()` fired. The moment a site queries Postgres
|
|
7
|
+
* directly (`pg` / Drizzle) — whether it's still on Supabase Postgres or has
|
|
8
|
+
* moved to our DigitalOcean cluster — that setup is gone, and a plain connection
|
|
9
|
+
* runs as the connection's role with **no claims**, so RLS is either bypassed
|
|
10
|
+
* (privileged role) or denies everything.
|
|
11
|
+
*
|
|
12
|
+
* These helpers reproduce exactly what PostgREST did, per transaction: set the
|
|
13
|
+
* claims GUC and `SET LOCAL ROLE`, so **existing `auth.uid()` policies keep
|
|
14
|
+
* working unchanged**. It is pure Postgres, so it behaves identically on Supabase
|
|
15
|
+
* and on DO. See `docs/db-package.md` (§RLS) and `docs/better-auth-migration.md`.
|
|
16
|
+
*
|
|
17
|
+
* Two requirements the caller owns (documented, not enforceable here):
|
|
18
|
+
* 1. The pool must connect as a role that **does not bypass RLS** for the rows
|
|
19
|
+
* it touches — i.e. not the table owner and not a `BYPASSRLS` superuser for
|
|
20
|
+
* user-facing reads. (After `SET ROLE authenticated`, RLS applies even if the
|
|
21
|
+
* underlying connection is `postgres`, exactly as PostgREST relied on.)
|
|
22
|
+
* 2. The connecting role must be allowed to `SET ROLE` to the target role
|
|
23
|
+
* (Supabase's `authenticator`/`postgres` can; on DO, `GRANT app_user TO …`).
|
|
24
|
+
*/
|
|
25
|
+
/** The role assumed when neither `options.role` nor `claims.role` is set. */
|
|
26
|
+
export const RLS_DEFAULT_ROLE = "authenticated";
|
|
27
|
+
/** The GUC `auth.uid()` / `auth.role()` read; what PostgREST populated. */
|
|
28
|
+
export const RLS_CLAIMS_SETTING = "request.jwt.claims";
|
|
29
|
+
/** Resolve claims + options into the role, GUC name, and serialized claims. */
|
|
30
|
+
export const resolveRlsConfig = (claims, options = {}) => ({
|
|
31
|
+
role: options.role ?? claims.role ?? RLS_DEFAULT_ROLE,
|
|
32
|
+
claimsSetting: options.claimsSetting ?? RLS_CLAIMS_SETTING,
|
|
33
|
+
claimsJson: JSON.stringify(claims),
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* The single parameterized statement that scopes a transaction: writes the
|
|
37
|
+
* claims GUC and the `role` GUC, **both transaction-local** (`is_local = true`,
|
|
38
|
+
* so they reset at `commit`/`rollback` and never leak across pooled
|
|
39
|
+
* connections). Everything — including the GUC name and role — is bound, never
|
|
40
|
+
* interpolated, so it's injection-safe.
|
|
41
|
+
*
|
|
42
|
+
* Exposed for callers that manage their own connection/transaction (e.g. a
|
|
43
|
+
* framework middleware): run it as the first statement after `begin`.
|
|
44
|
+
*/
|
|
45
|
+
export const rlsPreamble = (config) => ({
|
|
46
|
+
text: "select set_config($1, $2, true), set_config('role', $3, true)",
|
|
47
|
+
values: [config.claimsSetting, config.claimsJson, config.role],
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=rls.js.map
|
package/dist/rls.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rls.js","sourceRoot":"","sources":["../src/rls.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAgCH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,gBAAgB,GAAG,eAAe,CAAC;AAChD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG,oBAAoB,CAAC;AAYvD,+EAA+E;AAC/E,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC/B,MAAiB,EACjB,UAAsB,EAAE,EACJ,EAAE,CAAC,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,gBAAgB;IACrD,aAAa,EAAE,OAAO,CAAC,aAAa,IAAI,kBAAkB;IAC1D,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;CAClC,CAAC,CAAC;AAEH;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAC1B,MAAyB,EACY,EAAE,CAAC,CAAC;IACzC,IAAI,EAAE,+DAA+D;IACrE,MAAM,EAAE,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,IAAI,CAAC;CAC9D,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ingram-tech/nk-db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The Ingram Postgres data layer: one TLS-aware pg pool, raw-SQL helpers, Drizzle wiring, and a PGlite (no-Docker) dev/test harness for Next.js sites.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
"./pglite": {
|
|
27
27
|
"types": "./dist/pglite/index.d.ts",
|
|
28
28
|
"import": "./dist/pglite/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./pglite/reset": {
|
|
31
|
+
"types": "./dist/pglite/reset.d.ts",
|
|
32
|
+
"import": "./dist/pglite/reset.js"
|
|
29
33
|
}
|
|
30
34
|
},
|
|
31
35
|
"scripts": {
|
|
@@ -38,7 +42,7 @@
|
|
|
38
42
|
},
|
|
39
43
|
"peerDependencies": {
|
|
40
44
|
"@electric-sql/pglite": ">=0.5.0",
|
|
41
|
-
"@electric-sql/pglite-socket": ">=0.
|
|
45
|
+
"@electric-sql/pglite-socket": ">=0.2.4",
|
|
42
46
|
"drizzle-orm": ">=0.44.0",
|
|
43
47
|
"pg": "^8.13.0"
|
|
44
48
|
},
|
|
@@ -52,9 +56,9 @@
|
|
|
52
56
|
},
|
|
53
57
|
"devDependencies": {
|
|
54
58
|
"@electric-sql/pglite": "^0.5.0",
|
|
55
|
-
"@electric-sql/pglite-socket": "^0.
|
|
56
|
-
"@ingram-tech/typescript-config": "
|
|
57
|
-
"@types/node": "^
|
|
59
|
+
"@electric-sql/pglite-socket": "^0.2.4",
|
|
60
|
+
"@ingram-tech/typescript-config": "0.1.0",
|
|
61
|
+
"@types/node": "^25.0.0",
|
|
58
62
|
"@types/pg": "^8.11.0",
|
|
59
63
|
"drizzle-orm": "^0.45.0",
|
|
60
64
|
"pg": "^8.13.0",
|