@zeno-lib/db 0.3.2 → 0.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.
@@ -0,0 +1,58 @@
1
+ import { AnyRelations, EmptyRelations } from "drizzle-orm";
2
+ import { DrizzlePgConfig } from "drizzle-orm/pg-core";
3
+ import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
+ import { JwtPayload, SupabaseClient } from "@supabase/supabase-js";
5
+ //#region src/clients.d.ts
6
+ type CloseFn = (options?: {
7
+ timeout?: number;
8
+ }) => Promise<void>;
9
+ type CreateClientConfig<TRelations extends AnyRelations = EmptyRelations> = DrizzlePgConfig<TRelations> & {
10
+ /** Overrides `process.env.SUPABASE_DATABASE_URL`. */
11
+ connectionString?: string;
12
+ /**
13
+ * Require the Supabase transaction pooler: port 6543 on a non-local host.
14
+ * Pass `true` in production (e.g. `process.env.VERCEL_ENV === "production"`)
15
+ * to fail at client creation rather than exhaust connections under load.
16
+ * Default `false`, which checks only that a URL is present.
17
+ */
18
+ requirePooler?: boolean;
19
+ };
20
+ type ResolveDatabaseUrlOptions = Pick<CreateClientConfig, "connectionString" | "requirePooler">;
21
+ /**
22
+ * The connection string every factory uses: `connectionString`, else
23
+ * `SUPABASE_DATABASE_URL`. Throws when neither is set, and with
24
+ * `requirePooler` also when the URL is not the transaction pooler. Read per
25
+ * call, never at import, so a build without server secrets does not fail.
26
+ * Errors never echo the URL, which carries the password.
27
+ */
28
+ declare function resolveDatabaseUrl({ connectionString, requirePooler }?: ResolveDatabaseUrlOptions): string;
29
+ type DrizzleClient<TRelations extends AnyRelations = EmptyRelations> = PostgresJsDatabase<TRelations> & {
30
+ close: CloseFn;
31
+ };
32
+ type SupabaseToken = Pick<JwtPayload, "role" | "sub">;
33
+ /**
34
+ * RLS-bypassing client (the docs' `admin`). Connects with
35
+ * `SUPABASE_DATABASE_URL` and queries drizzle directly. Use for webhooks,
36
+ * background jobs, and seeding, never for user-scoped reads/writes.
37
+ */
38
+ declare function createAdminClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
39
+ /**
40
+ * RLS client scoped to an already-verified, decoded token (e.g. from
41
+ * `supabase.auth.getClaims()`). The role is clamped to the allowlist.
42
+ */
43
+ declare function createSupabaseClient<TRelations extends AnyRelations = EmptyRelations>(accessToken: SupabaseToken, config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
44
+ /**
45
+ * RLS client bound to a Supabase client. Verified claims are resolved via
46
+ * `supabase.auth.getClaims()` on every query, so it always reflects the live
47
+ * session.
48
+ */
49
+ declare function createAuthClient<TRelations extends AnyRelations = EmptyRelations>(supabase: SupabaseClient, config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
50
+ /** RLS client that runs every query as the `anon` role. */
51
+ declare function createAnonClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
52
+ /**
53
+ * Client that runs every query as `service_role`, which bypasses RLS via
54
+ * Supabase's BYPASSRLS grant. Use for trusted server-side work.
55
+ */
56
+ declare function createServiceClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
57
+ //#endregion
58
+ export { createAdminClient as a, createServiceClient as c, SupabaseToken as i, createSupabaseClient as l, DrizzleClient as n, createAnonClient as o, ResolveDatabaseUrlOptions as r, createAuthClient as s, CreateClientConfig as t, resolveDatabaseUrl as u };
@@ -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,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 };
package/dist/index.d.mts CHANGED
@@ -1,42 +1,2 @@
1
- import { AnyRelations, EmptyRelations } from "drizzle-orm";
2
- import { DrizzlePgConfig } from "drizzle-orm/pg-core";
3
- import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
4
- import { JwtPayload, SupabaseClient } from "@supabase/supabase-js";
5
- //#region src/clients.d.ts
6
- type CloseFn = (options?: {
7
- timeout?: number;
8
- }) => Promise<void>;
9
- type CreateClientConfig<TRelations extends AnyRelations = EmptyRelations> = DrizzlePgConfig<TRelations> & {
10
- /** Overrides `process.env.SUPABASE_DATABASE_URL`. */
11
- connectionString?: string;
12
- };
13
- type DrizzleClient<TRelations extends AnyRelations = EmptyRelations> = PostgresJsDatabase<TRelations> & {
14
- close: CloseFn;
15
- };
16
- type SupabaseToken = Pick<JwtPayload, "role" | "sub">;
17
- /**
18
- * RLS-bypassing client (the docs' `admin`). Connects with
19
- * `SUPABASE_DATABASE_URL` and queries drizzle directly. Use for webhooks,
20
- * background jobs, and seeding, never for user-scoped reads/writes.
21
- */
22
- declare function createAdminClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
23
- /**
24
- * RLS client scoped to an already-verified, decoded token (e.g. from
25
- * `supabase.auth.getClaims()`). The role is clamped to the allowlist.
26
- */
27
- declare function createSupabaseClient<TRelations extends AnyRelations = EmptyRelations>(accessToken: SupabaseToken, config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
28
- /**
29
- * RLS client bound to a Supabase client. Verified claims are resolved via
30
- * `supabase.auth.getClaims()` on every query, so it always reflects the live
31
- * session.
32
- */
33
- declare function createAuthClient<TRelations extends AnyRelations = EmptyRelations>(supabase: SupabaseClient, config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
34
- /** RLS client that runs every query as the `anon` role. */
35
- declare function createAnonClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
36
- /**
37
- * Client that runs every query as `service_role`, which bypasses RLS via
38
- * Supabase's BYPASSRLS grant. Use for trusted server-side work.
39
- */
40
- declare function createServiceClient<TRelations extends AnyRelations = EmptyRelations>(config?: CreateClientConfig<TRelations>): DrizzleClient<TRelations>;
41
- //#endregion
42
- export { type CreateClientConfig, type DrizzleClient, type SupabaseToken, createAdminClient, createAnonClient, createAuthClient, createServiceClient, createSupabaseClient };
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 CHANGED
@@ -1,167 +1,2 @@
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
- function clampClaims(token) {
54
- const role = token.role && ALLOWED_RLS_ROLES.has(token.role) ? token.role : "anon";
55
- return {
56
- claims: JSON.stringify({
57
- ...token,
58
- role
59
- }),
60
- role,
61
- sub: token.sub ?? ""
62
- };
63
- }
64
- function fixedContext(role) {
65
- return {
66
- claims: JSON.stringify({ role }),
67
- role,
68
- sub: ""
69
- };
70
- }
71
- const poolCache = /* @__PURE__ */ new Map();
72
- function acquirePool(connectionString, kind) {
73
- const key = `${kind} ${connectionString}`;
74
- let entry = poolCache.get(key);
75
- if (!entry) {
76
- entry = {
77
- client: postgres(connectionString, { prepare: false }),
78
- ended: false,
79
- key,
80
- refCount: 0
81
- };
82
- poolCache.set(key, entry);
83
- }
84
- entry.refCount += 1;
85
- return entry;
86
- }
87
- function makeClose(entry) {
88
- let released = false;
89
- return async (options) => {
90
- if (released) return;
91
- released = true;
92
- entry.refCount -= 1;
93
- if (entry.refCount <= 0 && !entry.ended) {
94
- entry.ended = true;
95
- if (poolCache.get(entry.key) === entry) poolCache.delete(entry.key);
96
- await entry.client.end(options);
97
- }
98
- };
99
- }
100
- function buildDrizzle(kind, config) {
101
- const { connectionString, ...drizzleConfig } = config ?? {};
102
- const url = connectionString ?? process.env.SUPABASE_DATABASE_URL ?? "";
103
- if (!url) throw new Error("Missing SUPABASE_DATABASE_URL environment variable");
104
- const entry = acquirePool(url, kind);
105
- const db = drizzle({
106
- client: entry.client,
107
- ...drizzleConfig
108
- });
109
- return {
110
- close: makeClose(entry),
111
- db
112
- };
113
- }
114
- function buildRlsClient(resolveContext, config) {
115
- const { close, db } = buildDrizzle("rls", config);
116
- const runTransaction = async (transaction) => {
117
- const { claims, role, sub } = await resolveContext();
118
- return db.transaction(async (tx) => {
119
- await tx.execute(sql`select set_config('request.jwt.claims', ${claims}, true), set_config('request.jwt.claim.sub', ${sub}, true)`);
120
- await tx.execute(sql`set local role ${sql.raw(role)}`);
121
- return transaction(tx);
122
- });
123
- };
124
- return createRlsQueryClient(runTransaction, close);
125
- }
126
- /**
127
- * RLS-bypassing client (the docs' `admin`). Connects with
128
- * `SUPABASE_DATABASE_URL` and queries drizzle directly. Use for webhooks,
129
- * background jobs, and seeding, never for user-scoped reads/writes.
130
- */
131
- function createAdminClient(config) {
132
- const { close, db } = buildDrizzle("admin", config);
133
- return Object.assign(db, { close });
134
- }
135
- /**
136
- * RLS client scoped to an already-verified, decoded token (e.g. from
137
- * `supabase.auth.getClaims()`). The role is clamped to the allowlist.
138
- */
139
- function createSupabaseClient(accessToken, config) {
140
- const context = clampClaims(accessToken);
141
- return buildRlsClient(() => Promise.resolve(context), config);
142
- }
143
- /**
144
- * RLS client bound to a Supabase client. Verified claims are resolved via
145
- * `supabase.auth.getClaims()` on every query, so it always reflects the live
146
- * session.
147
- */
148
- function createAuthClient(supabase, config) {
149
- return buildRlsClient(async () => {
150
- const { data, error } = await supabase.auth.getClaims();
151
- if (error) throw error;
152
- return clampClaims(data?.claims ?? {});
153
- }, config);
154
- }
155
- /** RLS client that runs every query as the `anon` role. */
156
- function createAnonClient(config) {
157
- return buildRlsClient(() => Promise.resolve(fixedContext("anon")), config);
158
- }
159
- /**
160
- * Client that runs every query as `service_role`, which bypasses RLS via
161
- * Supabase's BYPASSRLS grant. Use for trusted server-side work.
162
- */
163
- function createServiceClient(config) {
164
- return buildRlsClient(() => Promise.resolve(fixedContext("service_role")), config);
165
- }
166
- //#endregion
167
- export { createAdminClient, createAnonClient, createAuthClient, createServiceClient, createSupabaseClient };
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 };