@zeno-lib/db 0.3.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.
@@ -0,0 +1,197 @@
1
+ import { sql } from "drizzle-orm";
2
+ import { drizzle } from "drizzle-orm/postgres-js";
3
+ import postgres from "postgres";
4
+ //#region src/rls-query-client.ts
5
+ function replayAsUserChain(tx, path) {
6
+ let receiver = tx;
7
+ let current = tx;
8
+ for (const step of path) if (step.kind === "get") {
9
+ receiver = current;
10
+ current = current[step.prop];
11
+ } else current = current.apply(receiver, step.args);
12
+ return current;
13
+ }
14
+ const PROMISE_METHODS = /* @__PURE__ */ new Set([
15
+ "then",
16
+ "catch",
17
+ "finally"
18
+ ]);
19
+ function replayPromiseMethod(prop, path, runTransaction) {
20
+ return (...promiseArgs) => {
21
+ const promise = runTransaction((tx) => replayAsUserChain(tx, path));
22
+ return promise[prop].apply(promise, promiseArgs);
23
+ };
24
+ }
25
+ function createRlsQueryClient(runTransaction, close) {
26
+ const build = (path, isRoot) => {
27
+ const target = () => void 0;
28
+ return new Proxy(target, {
29
+ apply(_target, _thisArg, args) {
30
+ if (isRoot) throw new Error("The createSupabaseDrizzle() client is queried directly (e.g. db.select().from(table)). Use db.transaction(cb) to run multiple statements in one RLS transaction.");
31
+ return build([...path, {
32
+ args,
33
+ kind: "apply"
34
+ }], false);
35
+ },
36
+ get(_target, prop) {
37
+ if (isRoot && prop === "transaction") return runTransaction;
38
+ if (isRoot && prop === "close") return close;
39
+ if (PROMISE_METHODS.has(prop)) return isRoot ? void 0 : replayPromiseMethod(prop, path, runTransaction);
40
+ if (typeof prop === "symbol") return;
41
+ return build([...path, {
42
+ kind: "get",
43
+ prop
44
+ }], false);
45
+ }
46
+ });
47
+ };
48
+ return build([], true);
49
+ }
50
+ //#endregion
51
+ //#region src/clients.ts
52
+ const ALLOWED_RLS_ROLES = /* @__PURE__ */ new Set(["anon", "authenticated"]);
53
+ const POOLER_PORT = "6543";
54
+ const LOCAL_HOSTS = /* @__PURE__ */ new Set([
55
+ "127.0.0.1",
56
+ "localhost",
57
+ "[::1]",
58
+ "0.0.0.0"
59
+ ]);
60
+ /**
61
+ * The connection string every factory uses: `connectionString`, else
62
+ * `SUPABASE_DATABASE_URL`. Throws when neither is set, and with
63
+ * `requirePooler` also when the URL is not the transaction pooler. Read per
64
+ * call, never at import, so a build without server secrets does not fail.
65
+ * Errors never echo the URL, which carries the password.
66
+ */
67
+ function resolveDatabaseUrl({ connectionString, requirePooler = false } = {}) {
68
+ const url = connectionString ?? process.env.SUPABASE_DATABASE_URL ?? "";
69
+ if (!url) throw new Error("Missing SUPABASE_DATABASE_URL environment variable");
70
+ if (!requirePooler) return url;
71
+ let parsed;
72
+ try {
73
+ parsed = new URL(url);
74
+ } catch {
75
+ throw new Error("SUPABASE_DATABASE_URL is not a valid URL");
76
+ }
77
+ if (parsed.protocol !== "postgresql:" && parsed.protocol !== "postgres:") throw new Error(`SUPABASE_DATABASE_URL must be a postgresql:// URL, got ${parsed.protocol}//`);
78
+ if (LOCAL_HOSTS.has(parsed.hostname)) throw new Error("SUPABASE_DATABASE_URL points at a local database, but the transaction pooler is required");
79
+ if (parsed.port !== POOLER_PORT) throw new Error(`SUPABASE_DATABASE_URL must use the transaction pooler (port ${POOLER_PORT}), got port ${parsed.port || "(default)"}. Session mode holds one connection per instance for its whole lifetime.`);
80
+ return url;
81
+ }
82
+ function clampClaims(token) {
83
+ const role = token.role && ALLOWED_RLS_ROLES.has(token.role) ? token.role : "anon";
84
+ return {
85
+ claims: JSON.stringify({
86
+ ...token,
87
+ role
88
+ }),
89
+ role,
90
+ sub: token.sub ?? ""
91
+ };
92
+ }
93
+ function fixedContext(role) {
94
+ return {
95
+ claims: JSON.stringify({ role }),
96
+ role,
97
+ sub: ""
98
+ };
99
+ }
100
+ const poolCache = /* @__PURE__ */ new Map();
101
+ function acquirePool(connectionString, kind) {
102
+ const key = `${kind} ${connectionString}`;
103
+ let entry = poolCache.get(key);
104
+ if (!entry) {
105
+ entry = {
106
+ client: postgres(connectionString, { prepare: false }),
107
+ ended: false,
108
+ key,
109
+ refCount: 0
110
+ };
111
+ poolCache.set(key, entry);
112
+ }
113
+ entry.refCount += 1;
114
+ return entry;
115
+ }
116
+ function makeClose(entry) {
117
+ let released = false;
118
+ return async (options) => {
119
+ if (released) return;
120
+ released = true;
121
+ entry.refCount -= 1;
122
+ if (entry.refCount <= 0 && !entry.ended) {
123
+ entry.ended = true;
124
+ if (poolCache.get(entry.key) === entry) poolCache.delete(entry.key);
125
+ await entry.client.end(options);
126
+ }
127
+ };
128
+ }
129
+ function buildDrizzle(kind, config) {
130
+ const { connectionString, requirePooler, ...drizzleConfig } = config ?? {};
131
+ const entry = acquirePool(resolveDatabaseUrl({
132
+ connectionString,
133
+ requirePooler
134
+ }), kind);
135
+ const db = drizzle({
136
+ client: entry.client,
137
+ ...drizzleConfig
138
+ });
139
+ return {
140
+ close: makeClose(entry),
141
+ db
142
+ };
143
+ }
144
+ function buildRlsClient(resolveContext, config) {
145
+ const { close, db } = buildDrizzle("rls", config);
146
+ const runTransaction = async (transaction) => {
147
+ const { claims, role, sub } = await resolveContext();
148
+ return db.transaction(async (tx) => {
149
+ await tx.execute(sql`select set_config('request.jwt.claims', ${claims}, true), set_config('request.jwt.claim.sub', ${sub}, true)`);
150
+ await tx.execute(sql`set local role ${sql.raw(role)}`);
151
+ return transaction(tx);
152
+ });
153
+ };
154
+ return createRlsQueryClient(runTransaction, close);
155
+ }
156
+ /**
157
+ * RLS-bypassing client (the docs' `admin`). Connects with
158
+ * `SUPABASE_DATABASE_URL` and queries drizzle directly. Use for webhooks,
159
+ * background jobs, and seeding, never for user-scoped reads/writes.
160
+ */
161
+ function createAdminClient(config) {
162
+ const { close, db } = buildDrizzle("admin", config);
163
+ return Object.assign(db, { close });
164
+ }
165
+ /**
166
+ * RLS client scoped to an already-verified, decoded token (e.g. from
167
+ * `supabase.auth.getClaims()`). The role is clamped to the allowlist.
168
+ */
169
+ function createSupabaseClient(accessToken, config) {
170
+ const context = clampClaims(accessToken);
171
+ return buildRlsClient(() => Promise.resolve(context), config);
172
+ }
173
+ /**
174
+ * RLS client bound to a Supabase client. Verified claims are resolved via
175
+ * `supabase.auth.getClaims()` on every query, so it always reflects the live
176
+ * session.
177
+ */
178
+ function createAuthClient(supabase, config) {
179
+ return buildRlsClient(async () => {
180
+ const { data, error } = await supabase.auth.getClaims();
181
+ if (error) throw error;
182
+ return clampClaims(data?.claims ?? {});
183
+ }, config);
184
+ }
185
+ /** RLS client that runs every query as the `anon` role. */
186
+ function createAnonClient(config) {
187
+ return buildRlsClient(() => Promise.resolve(fixedContext("anon")), config);
188
+ }
189
+ /**
190
+ * Client that runs every query as `service_role`, which bypasses RLS via
191
+ * Supabase's BYPASSRLS grant. Use for trusted server-side work.
192
+ */
193
+ function createServiceClient(config) {
194
+ return buildRlsClient(() => Promise.resolve(fixedContext("service_role")), config);
195
+ }
196
+ //#endregion
197
+ export { createSupabaseClient as a, createServiceClient as i, createAnonClient as n, resolveDatabaseUrl as o, createAuthClient as r, createAdminClient as t };
@@ -0,0 +1,20 @@
1
+ import { Config, defineConfig } from "drizzle-kit";
2
+ //#region src/config.d.ts
3
+ /**
4
+ * Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
5
+ * not already exclude. That provider covers eight names (`anon`,
6
+ * `authenticator`, `authenticated`, `service_role`, `supabase_auth_admin`,
7
+ * `supabase_storage_admin`, `dashboard_user`, `supabase_admin`); a real
8
+ * instance has these too, and drizzle-kit manages every role it finds but
9
+ * cannot see declared, which means a `DROP ROLE` for each.
10
+ *
11
+ * The alternative is every consumer keeping a generated `roles.ts` nobody
12
+ * reads, or its own copy of this list, which rots as Supabase adds roles. That
13
+ * is the argument for it living in a versioned dependency.
14
+ *
15
+ * Verified against Supabase CLI 2.84.1 / Postgres 17.
16
+ */
17
+ declare const supabaseManagedRoles: readonly ["pg_checkpoint", "pg_database_owner", "pg_execute_server_program", "pg_monitor", "pg_read_all_data", "pg_read_all_settings", "pg_read_all_stats", "pg_read_server_files", "pg_signal_backend", "pg_stat_scan_tables", "pg_write_all_data", "pg_write_server_files", "pgbouncer", "postgres", "supabase_functions_admin", "supabase_read_only_user", "supabase_realtime_admin", "supabase_replication_admin"];
18
+ declare function defineDrizzleConfig(overrides?: Partial<Config>): ReturnType<typeof defineConfig>;
19
+ //#endregion
20
+ export { defineDrizzleConfig, supabaseManagedRoles };
@@ -0,0 +1,64 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+ //#region src/config.ts
3
+ /**
4
+ * Roles a Supabase project ships that drizzle-kit's `provider: "supabase"` does
5
+ * not already exclude. That provider covers eight names (`anon`,
6
+ * `authenticator`, `authenticated`, `service_role`, `supabase_auth_admin`,
7
+ * `supabase_storage_admin`, `dashboard_user`, `supabase_admin`); a real
8
+ * instance has these too, and drizzle-kit manages every role it finds but
9
+ * cannot see declared, which means a `DROP ROLE` for each.
10
+ *
11
+ * The alternative is every consumer keeping a generated `roles.ts` nobody
12
+ * reads, or its own copy of this list, which rots as Supabase adds roles. That
13
+ * is the argument for it living in a versioned dependency.
14
+ *
15
+ * Verified against Supabase CLI 2.84.1 / Postgres 17.
16
+ */
17
+ const supabaseManagedRoles = [
18
+ "pg_checkpoint",
19
+ "pg_database_owner",
20
+ "pg_execute_server_program",
21
+ "pg_monitor",
22
+ "pg_read_all_data",
23
+ "pg_read_all_settings",
24
+ "pg_read_all_stats",
25
+ "pg_read_server_files",
26
+ "pg_signal_backend",
27
+ "pg_stat_scan_tables",
28
+ "pg_write_all_data",
29
+ "pg_write_server_files",
30
+ "pgbouncer",
31
+ "postgres",
32
+ "supabase_functions_admin",
33
+ "supabase_read_only_user",
34
+ "supabase_realtime_admin",
35
+ "supabase_replication_admin"
36
+ ];
37
+ /**
38
+ * Schemas drizzle-kit is allowed to diff. Supabase owns `auth`, `storage`,
39
+ * `realtime` and the rest, and changes them on upgrades, so migrating them is
40
+ * never right. Add your own schemas here rather than dropping the filter.
41
+ */
42
+ const DEFAULT_SCHEMA_FILTER = ["public"];
43
+ function defineDrizzleConfig(overrides = {}) {
44
+ const { entities, ...configOverrides } = overrides;
45
+ const roleOverrides = typeof entities?.roles === "object" ? entities.roles : {};
46
+ return defineConfig({
47
+ dbCredentials: { url: process.env.SUPABASE_DATABASE_URL ?? "" },
48
+ dialect: "postgresql",
49
+ entities: {
50
+ ...entities,
51
+ roles: {
52
+ ...roleOverrides,
53
+ exclude: [...roleOverrides.exclude ?? [], ...supabaseManagedRoles],
54
+ provider: "supabase"
55
+ }
56
+ },
57
+ out: "./supabase/migrations",
58
+ schema: "./src/schema.ts",
59
+ schemaFilter: DEFAULT_SCHEMA_FILTER,
60
+ ...configOverrides
61
+ });
62
+ }
63
+ //#endregion
64
+ export { defineDrizzleConfig, supabaseManagedRoles };
@@ -0,0 +1,70 @@
1
+ import { PostgresError } from "postgres";
2
+ //#region src/errors.d.ts
3
+ /**
4
+ * SQLSTATE codes worth naming. The full list is Postgres's "Error Codes"
5
+ * appendix; these are the ones an application branches on.
6
+ */
7
+ declare const SqlState: {
8
+ /** `check_violation`. */
9
+ readonly checkViolation: "23514";
10
+ /** `exclusion_violation`. */
11
+ readonly exclusionViolation: "23P01";
12
+ /** `foreign_key_violation`. */
13
+ readonly foreignKeyViolation: "23503";
14
+ /** `insufficient_privilege`: an RLS policy or a missing grant refused it. */
15
+ readonly insufficientPrivilege: "42501";
16
+ /**
17
+ * `invalid_column_reference`, which is what an `ON CONFLICT` target that no
18
+ * unique index can arbitrate raises. It is raised while the statement is
19
+ * planned, so it fires on the first insert, not on the first conflict.
20
+ */
21
+ readonly invalidColumnReference: "42P10";
22
+ /** `not_null_violation`. */
23
+ readonly notNullViolation: "23502";
24
+ /** `undefined_function`: a function missing, or called with the wrong signature. */
25
+ readonly undefinedFunction: "42883";
26
+ /** `unique_violation`. */
27
+ readonly uniqueViolation: "23505";
28
+ };
29
+ type SqlStateCode = (typeof SqlState)[keyof typeof SqlState];
30
+ /**
31
+ * The postgres.js error underneath whatever Drizzle threw, or `undefined`.
32
+ *
33
+ * Drizzle wraps **every** driver error in a `DrizzleQueryError` whose own
34
+ * `code` is `undefined`, so `error.code === "23505"` is always false:
35
+ *
36
+ * ```
37
+ * depth 0: DrizzleQueryError code=undefined "Failed query: insert into …"
38
+ * depth 1: PostgresError code=23505 "duplicate key value violates …"
39
+ * ```
40
+ *
41
+ * Matched by `name` rather than `instanceof`, so a second copy of `postgres`
42
+ * in the tree cannot make it miss.
43
+ */
44
+ declare const toPostgresError: (error: unknown) => PostgresError | undefined;
45
+ type ConstraintViolationOptions = {
46
+ /**
47
+ * The SQLSTATE to require, e.g. `SqlState.uniqueViolation`. Defaults to any
48
+ * code in class 23 (`integrity_constraint_violation`).
49
+ */
50
+ code?: string;
51
+ };
52
+ /**
53
+ * Whether `error` is an integrity-constraint violation, optionally one of the
54
+ * named constraints. Returns the `PostgresError` so a caller can read its
55
+ * `constraint_name` or `detail`, or `undefined` when it does not match.
56
+ *
57
+ * ```ts
58
+ * try {
59
+ * await db.insert(contacts).values(row)
60
+ * } catch (error) {
61
+ * if (isConstraintViolation(error, ["contacts_email_key"], { code: SqlState.uniqueViolation })) {
62
+ * return { error: "That email is already a contact" }
63
+ * }
64
+ * throw error
65
+ * }
66
+ * ```
67
+ */
68
+ declare const isConstraintViolation: (error: unknown, constraints?: Iterable<string>, { code }?: ConstraintViolationOptions) => PostgresError | undefined;
69
+ //#endregion
70
+ export { ConstraintViolationOptions, SqlState, SqlStateCode, isConstraintViolation, toPostgresError };
@@ -0,0 +1,78 @@
1
+ //#region src/errors.ts
2
+ /**
3
+ * SQLSTATE codes worth naming. The full list is Postgres's "Error Codes"
4
+ * appendix; these are the ones an application branches on.
5
+ */
6
+ const SqlState = {
7
+ /** `check_violation`. */
8
+ checkViolation: "23514",
9
+ /** `exclusion_violation`. */
10
+ exclusionViolation: "23P01",
11
+ /** `foreign_key_violation`. */
12
+ foreignKeyViolation: "23503",
13
+ /** `insufficient_privilege`: an RLS policy or a missing grant refused it. */
14
+ insufficientPrivilege: "42501",
15
+ /**
16
+ * `invalid_column_reference`, which is what an `ON CONFLICT` target that no
17
+ * unique index can arbitrate raises. It is raised while the statement is
18
+ * planned, so it fires on the first insert, not on the first conflict.
19
+ */
20
+ invalidColumnReference: "42P10",
21
+ /** `not_null_violation`. */
22
+ notNullViolation: "23502",
23
+ /** `undefined_function`: a function missing, or called with the wrong signature. */
24
+ undefinedFunction: "42883",
25
+ /** `unique_violation`. */
26
+ uniqueViolation: "23505"
27
+ };
28
+ /** SQLSTATE class 23, every `integrity_constraint_violation`. */
29
+ const INTEGRITY_CONSTRAINT_CLASS = "23";
30
+ const MAX_CAUSE_DEPTH = 5;
31
+ /**
32
+ * The postgres.js error underneath whatever Drizzle threw, or `undefined`.
33
+ *
34
+ * Drizzle wraps **every** driver error in a `DrizzleQueryError` whose own
35
+ * `code` is `undefined`, so `error.code === "23505"` is always false:
36
+ *
37
+ * ```
38
+ * depth 0: DrizzleQueryError code=undefined "Failed query: insert into …"
39
+ * depth 1: PostgresError code=23505 "duplicate key value violates …"
40
+ * ```
41
+ *
42
+ * Matched by `name` rather than `instanceof`, so a second copy of `postgres`
43
+ * in the tree cannot make it miss.
44
+ */
45
+ const toPostgresError = (error) => {
46
+ let current = error;
47
+ for (let depth = 0; current instanceof Error && depth < MAX_CAUSE_DEPTH; depth += 1) {
48
+ if (current.name === "PostgresError") return current;
49
+ current = current.cause;
50
+ }
51
+ };
52
+ /**
53
+ * Whether `error` is an integrity-constraint violation, optionally one of the
54
+ * named constraints. Returns the `PostgresError` so a caller can read its
55
+ * `constraint_name` or `detail`, or `undefined` when it does not match.
56
+ *
57
+ * ```ts
58
+ * try {
59
+ * await db.insert(contacts).values(row)
60
+ * } catch (error) {
61
+ * if (isConstraintViolation(error, ["contacts_email_key"], { code: SqlState.uniqueViolation })) {
62
+ * return { error: "That email is already a contact" }
63
+ * }
64
+ * throw error
65
+ * }
66
+ * ```
67
+ */
68
+ const isConstraintViolation = (error, constraints, { code } = {}) => {
69
+ const postgresError = toPostgresError(error);
70
+ if (!postgresError) return;
71
+ if (!(code ? postgresError.code === code : postgresError.code.startsWith(INTEGRITY_CONSTRAINT_CLASS))) return;
72
+ if (constraints === void 0) return postgresError;
73
+ const name = postgresError.constraint_name;
74
+ if (name === void 0) return;
75
+ for (const constraint of constraints) if (constraint === name) return postgresError;
76
+ };
77
+ //#endregion
78
+ export { SqlState, isConstraintViolation, toPostgresError };
@@ -0,0 +1,10 @@
1
+ //#region src/function-names.ts
2
+ const FUNCTION_POLICY_OPERATIONS = [
3
+ "select",
4
+ "insert",
5
+ "update",
6
+ "delete"
7
+ ];
8
+ const functionPolicyName = (prefix, operation, tableName) => `${prefix}_${operation}_${tableName}`;
9
+ //#endregion
10
+ export { functionPolicyName as n, FUNCTION_POLICY_OPERATIONS as t };
@@ -0,0 +1,5 @@
1
+ //#region src/function-names.d.ts
2
+ declare const FUNCTION_POLICY_OPERATIONS: readonly ["select", "insert", "update", "delete"];
3
+ type FunctionPolicyOperation = (typeof FUNCTION_POLICY_OPERATIONS)[number];
4
+ //#endregion
5
+ export { FunctionPolicyOperation as t };
@@ -0,0 +1,2 @@
1
+ import { a as createAdminClient, c as createServiceClient, i as SupabaseToken, l as createSupabaseClient, n as DrizzleClient, o as createAnonClient, r as ResolveDatabaseUrlOptions, s as createAuthClient, t as CreateClientConfig, u as resolveDatabaseUrl } from "./clients-BlUEH7QJ.mjs";
2
+ export { type CreateClientConfig, type DrizzleClient, type ResolveDatabaseUrlOptions, type SupabaseToken, createAdminClient, createAnonClient, createAuthClient, createServiceClient, createSupabaseClient, resolveDatabaseUrl };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { a as createSupabaseClient, i as createServiceClient, n as createAnonClient, o as resolveDatabaseUrl, r as createAuthClient, t as createAdminClient } from "./clients-CdPr3mCZ.mjs";
2
+ export { createAdminClient, createAnonClient, createAuthClient, createServiceClient, createSupabaseClient, resolveDatabaseUrl };
@@ -0,0 +1,87 @@
1
+ import { n as DrizzleClient, t as CreateClientConfig } from "./clients-BlUEH7QJ.mjs";
2
+ import { AnyRelations, EmptyRelations } from "drizzle-orm";
3
+ import { JwtPayload, SupabaseClient } from "@supabase/supabase-js";
4
+ //#region src/define-action.d.ts
5
+ /**
6
+ * The slice of a schema `defineAction` needs: a throwing `parse`, plus the
7
+ * Standard Schema `types` marker it reads the caller-facing input type from.
8
+ * Zod 4 schemas satisfy it as they are; so does anything else that implements
9
+ * Standard Schema and exposes `parse`.
10
+ */
11
+ interface ActionSchema<TInput, TOutput> {
12
+ readonly "~standard": {
13
+ readonly types?: {
14
+ readonly input: TInput;
15
+ readonly output: TOutput;
16
+ } | undefined;
17
+ };
18
+ parse(input: unknown): TOutput;
19
+ }
20
+ /** What a request context must carry for `defineAction` to hand it over. */
21
+ interface ActionContext<TDb> {
22
+ readonly db: TDb;
23
+ }
24
+ /** An action's body: the RLS-bound `db`, the parsed input, and the context. */
25
+ type ActionHandler<TContext extends ActionContext<unknown>, TOutput, TResult> = (db: TContext["db"], input: TOutput, context: TContext) => TResult | Promise<TResult>;
26
+ /**
27
+ * `defineAction(schema, handler)` returns the server action itself: an async
28
+ * function that parses its argument, resolves the request context, then calls
29
+ * `handler(db, input, context)`.
30
+ */
31
+ type DefineAction<TContext extends ActionContext<unknown>> = <TInput, TOutput, TResult>(schema: ActionSchema<TInput, TOutput>, handler: ActionHandler<TContext, TOutput, TResult>) => (input: TInput) => Promise<TResult>;
32
+ //#endregion
33
+ //#region src/next.d.ts
34
+ /**
35
+ * Thrown when a request carries no verified session. A named class so a caller
36
+ * with a legitimate answer for that case (a permission check, where "not signed
37
+ * in" means "no") can tell it apart from a real failure with `instanceof`.
38
+ */
39
+ declare class UnauthenticatedError extends Error {
40
+ constructor(message?: string);
41
+ }
42
+ /** The only part of a Supabase client the request context reads. */
43
+ type ClaimsSource = {
44
+ auth: Pick<SupabaseClient["auth"], "getClaims">;
45
+ };
46
+ /** A verified request: the full `getClaims()` payload and the RLS client. */
47
+ type RequestContext<TRelations extends AnyRelations = EmptyRelations> = {
48
+ readonly claims: JwtPayload;
49
+ readonly db: DrizzleClient<TRelations>;
50
+ };
51
+ type CreateRequestDbOptions<TRelations extends AnyRelations = EmptyRelations> = Omit<CreateClientConfig<TRelations>, "connectionString"> & {
52
+ /**
53
+ * Builds the request's Supabase client, e.g. `createClient` from
54
+ * `@zeno-lib/supabase/next-server` (cookie session via `next/headers`).
55
+ */
56
+ supabase: () => ClaimsSource | Promise<ClaimsSource>;
57
+ /**
58
+ * Overrides `SUPABASE_DATABASE_URL`. A function is resolved per request, not
59
+ * at import, so a build without server secrets can still import the module.
60
+ */
61
+ connectionString?: string | (() => string);
62
+ };
63
+ type RequestDb<TRelations extends AnyRelations = EmptyRelations> = {
64
+ /** Memoised per render: `{ claims, db }` for the verified caller. */
65
+ getRequestContext: () => Promise<RequestContext<TRelations>>;
66
+ /** The common case: just the RLS-bound client. */
67
+ getRequestDb: () => Promise<DrizzleClient<TRelations>>;
68
+ /** Wraps a handler into a `"use server"` export; see `DefineAction`. */
69
+ defineAction: DefineAction<RequestContext<TRelations>>;
70
+ };
71
+ /**
72
+ * Request-scoped, RLS-bound Drizzle access for Next.js. Verifies the session
73
+ * with `getClaims()` (which checks the signature) and installs the **whole**
74
+ * claims object, so custom claims reach `auth.jwt()` in policies. There is no
75
+ * fallback to `anon`: a caller without a verified `sub` gets
76
+ * `UnauthenticatedError` rather than the empty result of a policy it never
77
+ * satisfied.
78
+ *
79
+ * `getRequestContext` is wrapped in React's `cache`, so one server render shares
80
+ * one verification and one handle. A server action invoked from the browser
81
+ * runs outside a render and resolves afresh on every call.
82
+ *
83
+ * Do not `close()` the handle: pools are shared and reference-counted.
84
+ */
85
+ declare function createRequestDb<TRelations extends AnyRelations = EmptyRelations>(options: CreateRequestDbOptions<TRelations>): RequestDb<TRelations>;
86
+ //#endregion
87
+ export { type ActionContext, type ActionHandler, type ActionSchema, ClaimsSource, CreateRequestDbOptions, type DefineAction, RequestContext, RequestDb, UnauthenticatedError, createRequestDb };
package/dist/next.mjs ADDED
@@ -0,0 +1,69 @@
1
+ import { a as createSupabaseClient } from "./clients-CdPr3mCZ.mjs";
2
+ import { cache } from "react";
3
+ //#region src/define-action.ts
4
+ /**
5
+ * Binds `defineAction` to a request-context resolver. Deliberately not a
6
+ * `"use server"` module: it only returns a function, and the app file that
7
+ * exports the result is the one that carries the directive.
8
+ *
9
+ * Input is parsed before the context is resolved, so a malformed call fails
10
+ * without a session round trip, and the handler never sees an unparsed value.
11
+ */
12
+ function createDefineAction(getContext) {
13
+ return (schema, handler) => async (input) => {
14
+ const parsed = schema.parse(input);
15
+ const context = await getContext();
16
+ return await handler(context.db, parsed, context);
17
+ };
18
+ }
19
+ //#endregion
20
+ //#region src/next.ts
21
+ /**
22
+ * Thrown when a request carries no verified session. A named class so a caller
23
+ * with a legitimate answer for that case (a permission check, where "not signed
24
+ * in" means "no") can tell it apart from a real failure with `instanceof`.
25
+ */
26
+ var UnauthenticatedError = class extends Error {
27
+ constructor(message = "Unauthorized: no verified session on this request") {
28
+ super(message);
29
+ this.name = "UnauthenticatedError";
30
+ }
31
+ };
32
+ /**
33
+ * Request-scoped, RLS-bound Drizzle access for Next.js. Verifies the session
34
+ * with `getClaims()` (which checks the signature) and installs the **whole**
35
+ * claims object, so custom claims reach `auth.jwt()` in policies. There is no
36
+ * fallback to `anon`: a caller without a verified `sub` gets
37
+ * `UnauthenticatedError` rather than the empty result of a policy it never
38
+ * satisfied.
39
+ *
40
+ * `getRequestContext` is wrapped in React's `cache`, so one server render shares
41
+ * one verification and one handle. A server action invoked from the browser
42
+ * runs outside a render and resolves afresh on every call.
43
+ *
44
+ * Do not `close()` the handle: pools are shared and reference-counted.
45
+ */
46
+ function createRequestDb(options) {
47
+ const { connectionString, supabase, ...config } = options;
48
+ const getRequestContext = cache(async () => {
49
+ const { data, error } = await (await supabase()).auth.getClaims();
50
+ if (error) throw error;
51
+ if (!data?.claims.sub) throw new UnauthenticatedError();
52
+ const url = typeof connectionString === "function" ? connectionString() : connectionString;
53
+ const db = createSupabaseClient(data.claims, url === void 0 ? config : {
54
+ ...config,
55
+ connectionString: url
56
+ });
57
+ return {
58
+ claims: data.claims,
59
+ db
60
+ };
61
+ });
62
+ return {
63
+ defineAction: createDefineAction(getRequestContext),
64
+ getRequestContext,
65
+ getRequestDb: async () => (await getRequestContext()).db
66
+ };
67
+ }
68
+ //#endregion
69
+ export { UnauthenticatedError, createRequestDb };