@prisma-next/extension-supabase 0.13.0-dev.2 → 0.13.0-dev.21

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,323 @@
1
+ import postgresAdapter from '@prisma-next/adapter-postgres/runtime';
2
+ import type { Contract } from '@prisma-next/contract/types';
3
+ import postgresDriver from '@prisma-next/driver-postgres/runtime';
4
+ import { instantiateExecutionStack } from '@prisma-next/framework-components/execution';
5
+ import type {
6
+ AsyncIterableResult,
7
+ RuntimeExecuteOptions,
8
+ } from '@prisma-next/framework-components/runtime';
9
+ import { sql } from '@prisma-next/sql-builder/runtime';
10
+ import type { Db } from '@prisma-next/sql-builder/types';
11
+ import type { SqlStorage } from '@prisma-next/sql-contract/types';
12
+ import { orm } from '@prisma-next/sql-orm-client';
13
+ import type { RawSqlTag } from '@prisma-next/sql-relational-core/expression';
14
+ import { createRawSql } from '@prisma-next/sql-relational-core/expression';
15
+ import type { SqlExecutionPlan, SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
16
+ import type {
17
+ ExecutionContext,
18
+ SqlExecutionStackWithDriver,
19
+ SqlMiddleware,
20
+ SqlRuntimeExtensionDescriptor,
21
+ TransactionContext,
22
+ VerifyMarkerOption,
23
+ } from '@prisma-next/sql-runtime';
24
+ import {
25
+ createExecutionContext,
26
+ createSqlExecutionStack,
27
+ withTransaction,
28
+ } from '@prisma-next/sql-runtime';
29
+ import postgresTarget, { PostgresContractSerializer } from '@prisma-next/target-postgres/runtime';
30
+ import { blindCast } from '@prisma-next/utils/casts';
31
+ import { ifDefined } from '@prisma-next/utils/defined';
32
+ import { createRemoteJWKSet, type JWTVerifyResult, jwtVerify } from 'jose';
33
+ import type { Client } from 'pg';
34
+ import { Pool } from 'pg';
35
+ import { supabaseRuntimeDescriptor } from './descriptor';
36
+ import type { SupabaseRoleBinding, SupabaseRuntime } from './supabase-runtime';
37
+ import { SupabaseRuntimeImpl } from './supabase-runtime';
38
+
39
+ export type SupabaseTargetId = 'postgres';
40
+
41
+ type OrmClient<TContract extends Contract<SqlStorage>> = ReturnType<typeof orm<TContract>>;
42
+
43
+ export class SupabaseConfigError extends Error {
44
+ override readonly name = 'SupabaseConfigError';
45
+ constructor(message: string) {
46
+ super(message);
47
+ }
48
+ }
49
+
50
+ export class InvalidJwtError extends Error {
51
+ override readonly name = 'InvalidJwtError';
52
+ readonly reason: string;
53
+ constructor(reason: string) {
54
+ super(`Invalid JWT: ${reason}`);
55
+ this.reason = reason;
56
+ }
57
+ }
58
+
59
+ type KeyMaterial =
60
+ | { readonly kind: 'secret'; readonly key: Uint8Array }
61
+ | { readonly kind: 'jwks'; readonly keyset: ReturnType<typeof createRemoteJWKSet> };
62
+
63
+ export interface RoleBoundDb<TContract extends Contract<SqlStorage>> {
64
+ readonly sql: Db<TContract>;
65
+ readonly orm: OrmClient<TContract>;
66
+ readonly raw: RawSqlTag;
67
+ execute<Row>(
68
+ plan: (SqlExecutionPlan<Row> | SqlQueryPlan<Row>) & { readonly _row?: Row },
69
+ options?: RuntimeExecuteOptions,
70
+ ): AsyncIterableResult<Row>;
71
+ transaction<R>(fn: (tx: TransactionContext) => PromiseLike<R>): Promise<R>;
72
+ }
73
+
74
+ export interface SupabaseDb<TContract extends Contract<SqlStorage>> {
75
+ readonly context: ExecutionContext<TContract>;
76
+ readonly stack: SqlExecutionStackWithDriver<SupabaseTargetId>;
77
+ asUser(jwt: string): Promise<RoleBoundDb<TContract>>;
78
+ asAnon(): RoleBoundDb<TContract>;
79
+ asServiceRole(): RoleBoundDb<TContract>;
80
+ close(): Promise<void>;
81
+ [Symbol.asyncDispose](): Promise<void>;
82
+ }
83
+
84
+ export interface SupabaseOptionsBase {
85
+ readonly extensions?: readonly SqlRuntimeExtensionDescriptor<SupabaseTargetId>[];
86
+ readonly middleware?: readonly SqlMiddleware[];
87
+ readonly verifyMarker?: VerifyMarkerOption;
88
+ readonly poolOptions?: {
89
+ readonly connectionTimeoutMillis?: number;
90
+ readonly idleTimeoutMillis?: number;
91
+ };
92
+ }
93
+
94
+ export interface SupabaseBindingOptions {
95
+ readonly url?: string;
96
+ readonly pg?: Pool | Client;
97
+ }
98
+
99
+ type JwtSecretOption = {
100
+ readonly jwtSecret: string;
101
+ readonly jwksUrl?: never;
102
+ };
103
+
104
+ type JwksUrlOption = {
105
+ readonly jwksUrl: string;
106
+ readonly jwtSecret?: never;
107
+ };
108
+
109
+ export type SupabaseOptionsWithContract<TContract extends Contract<SqlStorage>> =
110
+ SupabaseBindingOptions &
111
+ SupabaseOptionsBase &
112
+ (JwtSecretOption | JwksUrlOption) & {
113
+ readonly contract: TContract;
114
+ readonly contractJson?: never;
115
+ };
116
+
117
+ export type SupabaseOptionsWithContractJson<TContract extends Contract<SqlStorage>> =
118
+ SupabaseBindingOptions &
119
+ SupabaseOptionsBase &
120
+ (JwtSecretOption | JwksUrlOption) & {
121
+ readonly contractJson: unknown;
122
+ readonly contract?: never;
123
+ readonly _contract?: TContract;
124
+ };
125
+
126
+ export type SupabaseOptions<TContract extends Contract<SqlStorage>> =
127
+ | SupabaseOptionsWithContract<TContract>
128
+ | SupabaseOptionsWithContractJson<TContract>;
129
+
130
+ function hasContractJson<TContract extends Contract<SqlStorage>>(
131
+ options: SupabaseOptions<TContract>,
132
+ ): options is SupabaseOptionsWithContractJson<TContract> {
133
+ return 'contractJson' in options;
134
+ }
135
+
136
+ const contractSerializer = new PostgresContractSerializer();
137
+
138
+ function resolveContract<TContract extends Contract<SqlStorage>>(
139
+ options: SupabaseOptions<TContract>,
140
+ ): TContract {
141
+ const contractInput = hasContractJson(options) ? options.contractJson : options.contract;
142
+ return blindCast<
143
+ TContract,
144
+ 'contractSerializer.deserializeContract returns a validated TContract'
145
+ >(contractSerializer.deserializeContract(contractInput));
146
+ }
147
+
148
+ function resolveKeyMaterial<TContract extends Contract<SqlStorage>>(
149
+ options: SupabaseOptions<TContract>,
150
+ ): KeyMaterial {
151
+ const jwtSecret = 'jwtSecret' in options ? options.jwtSecret : undefined;
152
+ const jwksUrl = 'jwksUrl' in options ? options.jwksUrl : undefined;
153
+
154
+ if (jwtSecret !== undefined && jwksUrl !== undefined) {
155
+ throw new SupabaseConfigError('Provide either jwtSecret or jwksUrl, not both');
156
+ }
157
+ if (jwtSecret === undefined && jwksUrl === undefined) {
158
+ throw new SupabaseConfigError('Either jwtSecret or jwksUrl is required');
159
+ }
160
+
161
+ if (jwtSecret !== undefined) {
162
+ return { kind: 'secret', key: new TextEncoder().encode(jwtSecret) };
163
+ }
164
+
165
+ if (jwksUrl !== undefined) {
166
+ return { kind: 'jwks', keyset: createRemoteJWKSet(new URL(jwksUrl)) };
167
+ }
168
+
169
+ throw new SupabaseConfigError('Either jwtSecret or jwksUrl is required');
170
+ }
171
+
172
+ function toPool<TContract extends Contract<SqlStorage>>(
173
+ options: SupabaseOptions<TContract>,
174
+ ): { pool: Pool; owned: boolean } | undefined {
175
+ if (options.pg instanceof Pool) {
176
+ return { pool: options.pg, owned: false };
177
+ }
178
+ if (typeof options.url === 'string') {
179
+ return {
180
+ pool: new Pool({
181
+ connectionString: options.url,
182
+ connectionTimeoutMillis: options.poolOptions?.connectionTimeoutMillis ?? 20_000,
183
+ idleTimeoutMillis: options.poolOptions?.idleTimeoutMillis ?? 30_000,
184
+ }),
185
+ owned: true,
186
+ };
187
+ }
188
+ return undefined;
189
+ }
190
+
191
+ function withSupabaseDescriptor(
192
+ extensions: readonly SqlRuntimeExtensionDescriptor<SupabaseTargetId>[] | undefined,
193
+ ): readonly SqlRuntimeExtensionDescriptor<SupabaseTargetId>[] {
194
+ const packs = extensions ?? [];
195
+ return packs.some((pack) => pack.id === supabaseRuntimeDescriptor.id)
196
+ ? packs
197
+ : [...packs, supabaseRuntimeDescriptor];
198
+ }
199
+
200
+ export default async function supabase<TContract extends Contract<SqlStorage>>(
201
+ options: SupabaseOptionsWithContract<TContract>,
202
+ ): Promise<SupabaseDb<TContract>>;
203
+ export default async function supabase<TContract extends Contract<SqlStorage>>(
204
+ options: SupabaseOptionsWithContractJson<TContract>,
205
+ ): Promise<SupabaseDb<TContract>>;
206
+ export default async function supabase<TContract extends Contract<SqlStorage>>(
207
+ options: SupabaseOptions<TContract>,
208
+ ): Promise<SupabaseDb<TContract>> {
209
+ const keyMaterial = resolveKeyMaterial(options);
210
+ const contract = resolveContract(options);
211
+
212
+ const stack = createSqlExecutionStack({
213
+ target: postgresTarget,
214
+ adapter: postgresAdapter,
215
+ driver: postgresDriver,
216
+ extensionPacks: withSupabaseDescriptor(options.extensions),
217
+ });
218
+
219
+ const context = createExecutionContext({ contract, stack });
220
+ const rawCodecInferer = stack.adapter.rawCodecInferer;
221
+ const rawSqlTag: RawSqlTag = createRawSql(rawCodecInferer);
222
+
223
+ const poolEntry = toPool(options);
224
+ let closed = false;
225
+
226
+ const stackInstance = instantiateExecutionStack(stack);
227
+ const driverDescriptor = stack.driver;
228
+ if (!driverDescriptor) {
229
+ throw new Error('Driver descriptor missing from execution stack');
230
+ }
231
+ const driver = driverDescriptor.create({ cursor: { disabled: true } });
232
+
233
+ if (poolEntry) {
234
+ await driver.connect({ kind: 'pgPool', pool: poolEntry.pool });
235
+ }
236
+
237
+ const runtime: SupabaseRuntime & SupabaseRuntimeImpl<TContract> = new SupabaseRuntimeImpl({
238
+ context,
239
+ adapter: stackInstance.adapter,
240
+ driver,
241
+ ...ifDefined('verifyMarker', options.verifyMarker),
242
+ ...ifDefined('middleware', options.middleware),
243
+ });
244
+
245
+ async function verifyJwt(jwt: string): Promise<JWTVerifyResult> {
246
+ try {
247
+ if (keyMaterial.kind === 'secret') {
248
+ return await jwtVerify(jwt, keyMaterial.key);
249
+ }
250
+ return await jwtVerify(jwt, keyMaterial.keyset);
251
+ } catch (err) {
252
+ const reason = err instanceof Error ? err.message : String(err);
253
+ throw new InvalidJwtError(reason);
254
+ }
255
+ }
256
+
257
+ function buildRoleBoundDb(binding: SupabaseRoleBinding): RoleBoundDb<TContract> {
258
+ const roleSql: Db<TContract> = sql<TContract>({ context, rawCodecInferer });
259
+ const roleOrm: OrmClient<TContract> = orm({
260
+ runtime: {
261
+ execute(plan) {
262
+ return runtime.executeWithRole(plan, binding);
263
+ },
264
+ // connection() returns a role session; this is the enforcement path for ORM scope
265
+ // operations (mutations, includes) — every statement runs role-bound.
266
+ connection: () => runtime.openRoleSession(binding),
267
+ },
268
+ context,
269
+ });
270
+
271
+ return {
272
+ sql: roleSql,
273
+ orm: roleOrm,
274
+ raw: rawSqlTag,
275
+ execute<Row>(
276
+ plan: (SqlExecutionPlan<Row> | SqlQueryPlan<Row>) & { readonly _row?: Row },
277
+ execOptions?: RuntimeExecuteOptions,
278
+ ): AsyncIterableResult<Row> {
279
+ return runtime.executeWithRole<Row>(plan, binding, execOptions);
280
+ },
281
+ transaction<R>(fn: (tx: TransactionContext) => PromiseLike<R>): Promise<R> {
282
+ return withTransaction({ connection: () => runtime.openRoleSession(binding) }, fn);
283
+ },
284
+ };
285
+ }
286
+
287
+ async function closeDb(): Promise<void> {
288
+ if (closed) return;
289
+ closed = true;
290
+ await runtime.close();
291
+ if (poolEntry?.owned) {
292
+ await poolEntry.pool.end().catch(() => undefined);
293
+ }
294
+ }
295
+
296
+ return {
297
+ context,
298
+ stack,
299
+
300
+ async asUser(jwt: string): Promise<RoleBoundDb<TContract>> {
301
+ const { payload } = await verifyJwt(jwt);
302
+ const rawRole = payload['role'];
303
+ const roleStr = typeof rawRole === 'string' ? rawRole : 'authenticated';
304
+ const role: SupabaseRoleBinding['role'] =
305
+ roleStr === 'anon' || roleStr === 'authenticated' || roleStr === 'service_role'
306
+ ? roleStr
307
+ : 'authenticated';
308
+ const binding: SupabaseRoleBinding = { role, claims: payload };
309
+ return buildRoleBoundDb(binding);
310
+ },
311
+
312
+ asAnon(): RoleBoundDb<TContract> {
313
+ return buildRoleBoundDb({ role: 'anon', claims: {} });
314
+ },
315
+
316
+ asServiceRole(): RoleBoundDb<TContract> {
317
+ return buildRoleBoundDb({ role: 'service_role', claims: {} });
318
+ },
319
+
320
+ close: closeDb,
321
+ [Symbol.asyncDispose]: closeDb,
322
+ };
323
+ }