@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.
- package/dist/clients-BlUEH7QJ.d.mts +58 -0
- package/dist/clients-CdPr3mCZ.mjs +197 -0
- package/dist/errors.d.mts +70 -0
- package/dist/errors.mjs +78 -0
- package/dist/function-names-BDc-AuFm.mjs +10 -0
- package/dist/function-names-hv2mj_x1.d.mts +5 -0
- package/dist/index.d.mts +2 -42
- package/dist/index.mjs +2 -167
- package/dist/next.d.mts +182 -0
- package/dist/next.mjs +167 -0
- package/dist/query.d.mts +103 -0
- package/dist/query.mjs +115 -0
- package/dist/schema.d.mts +20 -11
- package/dist/schema.mjs +11 -12
- package/package.json +31 -6
- package/src/action-result.test.ts +88 -0
- package/src/action-result.ts +109 -0
- package/src/casing.test.ts +43 -20
- package/src/clients.ts +60 -5
- package/src/database-url.test.ts +88 -0
- package/src/define-action.ts +137 -0
- package/src/errors.test.ts +95 -0
- package/src/errors.ts +133 -0
- package/src/function-names.ts +21 -0
- package/src/index.ts +2 -0
- package/src/next.test-d.ts +60 -0
- package/src/next.test.ts +296 -0
- package/src/next.ts +134 -0
- package/src/query.test-d.ts +20 -0
- package/src/query.test.ts +261 -0
- package/src/query.ts +252 -0
- package/src/schema-zod.test.ts +37 -0
- package/src/schema.test-d.ts +24 -0
- package/src/schema.ts +51 -21
|
@@ -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 };
|
package/dist/errors.mjs
ADDED
|
@@ -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 };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,42 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
2
|
-
|
|
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 };
|