cloudflare-next-intl 0.8.28 → 0.8.30
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.
|
@@ -127,8 +127,11 @@ export function createSendSignInLinkAction(locale, actionCodeSettings) {
|
|
|
127
127
|
const { auth } = await getFirebaseAuthClient();
|
|
128
128
|
const { sendSignInLinkToEmail } = await getFirebaseAuthModule();
|
|
129
129
|
const email = (formData.get('email')?.toString() ?? '').trim();
|
|
130
|
+
const url = new URL(actionCodeSettings.url);
|
|
131
|
+
url.searchParams.set('email', email);
|
|
132
|
+
const settingsWithEmail = { ...actionCodeSettings, url: url.toString() };
|
|
130
133
|
try {
|
|
131
|
-
await sendSignInLinkToEmail(auth, email,
|
|
134
|
+
await sendSignInLinkToEmail(auth, email, settingsWithEmail);
|
|
132
135
|
return { success: true, email };
|
|
133
136
|
}
|
|
134
137
|
catch (e) {
|
|
@@ -369,33 +369,36 @@ export default async function updateSession(request, baseResponse, locale, rebui
|
|
|
369
369
|
}
|
|
370
370
|
let unverifiedEmail = false;
|
|
371
371
|
if (fa.verifyEmailPath && !isVerifyEmailPage && hasSession && decodeJwtPayload(token)?.email_verified === false) {
|
|
372
|
+
// Sending a user to `verifyEmailPath` is only safe when this claim
|
|
373
|
+
// reflects the LIVE account state, because that page resolves the
|
|
374
|
+
// same user through `getAuthUser()`/`initializeServerApp` — which
|
|
375
|
+
// reads the Auth service directly, not this frozen claim — and
|
|
376
|
+
// redirects home the moment it sees a verified user. Any disagreement
|
|
377
|
+
// between the two is therefore not a stale-data annoyance but an
|
|
378
|
+
// infinite redirect loop.
|
|
379
|
+
//
|
|
380
|
+
// The hint cookie cannot settle that: it is a client-written mirror,
|
|
381
|
+
// and after the user verifies in another tab (or the client simply
|
|
382
|
+
// never re-runs) it keeps asserting a `false` that is now wrong,
|
|
383
|
+
// while the Auth service already reports verified. Trusting a
|
|
384
|
+
// `'false'` hint as confirmation is exactly what pinned verified
|
|
385
|
+
// users in the loop. So the hint is only ever allowed to SKIP work
|
|
386
|
+
// when it agrees the user is verified — never to prove they aren't.
|
|
372
387
|
const hint = request.cookies.get(emailVerifiedHintCookieName)?.value;
|
|
373
|
-
|
|
374
|
-
if (!hintConfirms && !refreshedToken) {
|
|
388
|
+
if (!refreshedToken) {
|
|
375
389
|
const refreshToken = request.cookies.get(refreshTokenCookieName)?.value;
|
|
376
390
|
if (refreshToken) {
|
|
377
391
|
// `skipCache`: the cached entry is what produced the very
|
|
378
|
-
// token whose claim is in question, so a
|
|
379
|
-
// hand back that same token and
|
|
380
|
-
// genuinely newly-minted token carries information here.
|
|
381
|
-
const previousToken = token;
|
|
392
|
+
// token whose claim is in question, so a cached refresh can
|
|
393
|
+
// hand back that same token and confirm nothing.
|
|
382
394
|
const result = await refreshIdToken(fa.apiKey, refreshToken, { skipCache: true });
|
|
383
395
|
if (result.status === 'refreshed') {
|
|
384
396
|
refreshedToken = { idToken: result.idToken, refreshToken: result.refreshToken };
|
|
385
397
|
token = refreshedToken.idToken;
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
// reads live Auth-service state, not this frozen claim)
|
|
391
|
-
// and redirects straight back home. So only trust the
|
|
392
|
-
// claim when this mint genuinely re-stated it — the token
|
|
393
|
-
// changed — and never against a hint that already
|
|
394
|
-
// observed verification live.
|
|
395
|
-
const reconfirmed = result.idToken !== previousToken;
|
|
396
|
-
unverifiedEmail = hint !== 'true'
|
|
397
|
-
&& reconfirmed
|
|
398
|
-
&& decodeJwtPayload(token)?.email_verified === false;
|
|
398
|
+
// A `true` hint means the client already observed
|
|
399
|
+
// verification live; never redirect against it, even if
|
|
400
|
+
// this mint's claim hasn't caught up.
|
|
401
|
+
unverifiedEmail = hint !== 'true' && decodeJwtPayload(token)?.email_verified === false;
|
|
399
402
|
}
|
|
400
403
|
else if (result.status === 'invalid') {
|
|
401
404
|
clearInvalidSession = true;
|
|
@@ -413,8 +416,7 @@ export default async function updateSession(request, baseResponse, locale, rebui
|
|
|
413
416
|
}
|
|
414
417
|
}
|
|
415
418
|
else {
|
|
416
|
-
|
|
417
|
-
unverifiedEmail = true;
|
|
419
|
+
unverifiedEmail = hint !== 'true' && decodeJwtPayload(token)?.email_verified === false;
|
|
418
420
|
}
|
|
419
421
|
}
|
|
420
422
|
if (refreshWasTransientFailure) {
|
package/llms.txt
CHANGED
|
@@ -34,7 +34,7 @@ other subpath can be used.
|
|
|
34
34
|
- `./firebaseAuthServerProvider` — server-side equivalent provider (not used by the default auto-wiring path — see its doc comment).
|
|
35
35
|
- `./useFirebaseAuthUser` — `useAuthUser()`; resolves to RSC or client implementation via the `react-server` condition. Client variant throws `"useAuthUser must be used within an AuthUserProvider"` if called outside one.
|
|
36
36
|
- `./getFirebaseAuthUser` — `getAuthUser()`; unconditional server-only export of the same RSC implementation `useFirebaseAuthUser` resolves to via `react-server`. Use this when you want `await` to be visible from the type itself — TypeScript doesn't evaluate the `react-server` condition, so `useFirebaseAuthUser` always types as its client (sync) signature in editors regardless of call site.
|
|
37
|
-
- `./firebaseAuthActions` — `createLoginAction`/`createSignUpAction`/`createForgotPasswordAction`: factories returning React `useActionState`-shaped form actions. `createSendSignInLinkAction`: same factory shape, for passwordless email-link sign-in — returns `{ success: true, email }` so the caller can persist the trimmed email (e.g. to `localStorage`) for the completion step. `completeSignInWithLink(locale, url, email)`: plain async function (not `useActionState`-shaped) that completes a passwordless sign-in from the emailed link's landing page — call from an effect on mount, not a form submit.
|
|
37
|
+
- `./firebaseAuthActions` — `createLoginAction`/`createSignUpAction`/`createForgotPasswordAction`: factories returning React `useActionState`-shaped form actions. `createSendSignInLinkAction`: same factory shape, for passwordless email-link sign-in — appends the user's email as an `email` query parameter to `actionCodeSettings.url` and returns `{ success: true, email }` so the caller can persist the trimmed email (e.g. to `localStorage`) for the completion step. `completeSignInWithLink(locale, url, email)`: plain async function (not `useActionState`-shaped) that completes a passwordless sign-in from the emailed link's landing page — call from an effect on mount, not a form submit.
|
|
38
38
|
- `./firebaseAuthMiddleware` — `updateSession`: session-cookie refresh, called automatically by `./middleware`'s default handler.
|
|
39
39
|
|
|
40
40
|
## `cookieConsent*` subpaths (require `cookieConsent` set on your `RoutingConfig`)
|
package/package.json
CHANGED
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One filter's operator and value, e.g. `{ age: ['gt', 18] }`. The bare-value
|
|
3
|
-
* form `{ id: 5 }` (see {@link SupabaseWhere}) is shorthand for `['eq', 5]`.
|
|
4
|
-
* `['not', operator, value]` negates any operator (`['not', 'eq', 5]` is
|
|
5
|
-
* `column <> 5` via PostgREST's `not.eq.5`).
|
|
6
|
-
*
|
|
7
|
-
* This is the complete operator set `@supabase/supabase-js`'s query builder
|
|
8
|
-
* exposes for `.from(table)` — there's no separate "Supabase client" query
|
|
9
|
-
* layer on top of `postgrest-js`; `supabase.from()` returns a `postgrest-js`
|
|
10
|
-
* builder directly, so this list is also the ceiling for what `supabase-js`
|
|
11
|
-
* itself can express against a single table. What's still out of reach:
|
|
12
|
-
* joins/embedded resources (pass a nested `columns` string instead — that's
|
|
13
|
-
* plain PostgREST syntax, not a filter), and anything needing more than one
|
|
14
|
-
* table in a single round-trip. Those need `cfni_exec`/`withPublicDb`/
|
|
15
|
-
* `withUserDb`, or a Postgres function called via {@link supabaseRpc}.
|
|
16
|
-
*/
|
|
17
|
-
export type SupabaseFilterOperator = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'likeAllOf' | 'likeAnyOf' | 'ilike' | 'ilikeAllOf' | 'ilikeAnyOf' | 'regexMatch' | 'regexIMatch' | 'is' | 'isDistinct' | 'in' | 'contains' | 'containedBy' | 'overlaps' | 'rangeGt' | 'rangeGte' | 'rangeLt' | 'rangeLte' | 'rangeAdjacent';
|
|
18
|
-
export type SupabaseFilterValue = unknown | [SupabaseFilterOperator, unknown] | ['not', SupabaseFilterOperator, unknown];
|
|
19
|
-
/** Equality (or `[operator, value]`) filters, ANDed together column by column. */
|
|
20
|
-
export type SupabaseWhere = Record<string, SupabaseFilterValue>;
|
|
21
|
-
export interface SupabaseTextSearch {
|
|
22
|
-
column: string;
|
|
23
|
-
query: string;
|
|
24
|
-
type?: 'plain' | 'phrase' | 'websearch';
|
|
25
|
-
config?: string;
|
|
26
|
-
}
|
|
27
|
-
export interface SupabaseOrderBy {
|
|
28
|
-
column: string;
|
|
29
|
-
ascending?: boolean;
|
|
30
|
-
nullsFirst?: boolean;
|
|
31
|
-
}
|
|
32
|
-
export interface SupabaseSelectOptions {
|
|
33
|
-
/** Column list, PostgREST syntax (e.g. `'id,name'`, or `'*, related(*)'` for an embedded resource). Defaults to `'*'`. */
|
|
34
|
-
columns?: string;
|
|
35
|
-
/** Filters, ANDed together — see {@link SupabaseWhere}. */
|
|
36
|
-
where?: SupabaseWhere;
|
|
37
|
-
/** Shorthand for several `eq` filters at once — PostgREST's `.match()`. ANDed with `where`. */
|
|
38
|
-
match?: Record<string, unknown>;
|
|
39
|
-
/** A raw PostgREST `or()` filter string (e.g. `'age.gt.18,status.eq.active'`), for filters spanning multiple columns. ANDed with `where`/`match`. */
|
|
40
|
-
or?: string | {
|
|
41
|
-
filters: string;
|
|
42
|
-
referencedTable?: string;
|
|
43
|
-
};
|
|
44
|
-
/** Full-text search via PostgREST's `@@` operators. */
|
|
45
|
-
textSearch?: SupabaseTextSearch;
|
|
46
|
-
/** One or more `order by` clauses, applied in array order. */
|
|
47
|
-
orderBy?: SupabaseOrderBy | SupabaseOrderBy[];
|
|
48
|
-
/** Maximum rows to return. */
|
|
49
|
-
limit?: number;
|
|
50
|
-
/** Zero-indexed, inclusive `[from, to]` row range — PostgREST pagination. */
|
|
51
|
-
range?: [number, number];
|
|
52
|
-
/** Resolve to the single matching row instead of an array; errors if there isn't exactly one. */
|
|
53
|
-
single?: boolean;
|
|
54
|
-
/** Resolve to the single matching row, or `null` if there are none; errors if there's more than one. */
|
|
55
|
-
maybeSingle?: boolean;
|
|
56
|
-
/** Also return the total matching row count (`'exact'`, `'planned'`, or `'estimated'`). */
|
|
57
|
-
count?: 'exact' | 'planned' | 'estimated';
|
|
58
|
-
}
|
|
59
|
-
export interface SupabaseMutationOptions {
|
|
60
|
-
/** Filters, ANDed together — see {@link SupabaseWhere}. At least one of `where`/`match`/`or` is required for update/delete. */
|
|
61
|
-
where?: SupabaseWhere;
|
|
62
|
-
/** Shorthand for several `eq` filters at once. ANDed with `where`. */
|
|
63
|
-
match?: Record<string, unknown>;
|
|
64
|
-
/** A raw PostgREST `or()` filter string, for filters spanning multiple columns. ANDed with `where`/`match`. */
|
|
65
|
-
or?: string | {
|
|
66
|
-
filters: string;
|
|
67
|
-
referencedTable?: string;
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
export interface SupabaseUpsertOptions {
|
|
71
|
-
/** Column(s) forming the conflict target, comma-separated. Defaults to the table's primary key. */
|
|
72
|
-
onConflict?: string;
|
|
73
|
-
/** Skip (rather than update) rows that already exist. Defaults to `false`. */
|
|
74
|
-
ignoreDuplicates?: boolean;
|
|
75
|
-
}
|
|
76
|
-
export interface SupabaseResult<T> {
|
|
77
|
-
rows: T[];
|
|
78
|
-
/** Present only when `count` was requested on a select. */
|
|
79
|
-
count: number | null;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Selects rows from `table` through the Supabase REST API
|
|
83
|
-
* (`@supabase/supabase-js`'s `.from(table).select()`), as the **anonymous**
|
|
84
|
-
* role — no `cfni_exec`, no raw SQL, only what PostgREST's REST API itself
|
|
85
|
-
* supports: a column list, filters (equality or a named operator), one or
|
|
86
|
-
* more `order by` clauses, `limit`/`range` pagination, `single`/`maybeSingle`,
|
|
87
|
-
* and an optional row count.
|
|
88
|
-
*
|
|
89
|
-
* Reach for this — or {@link supabaseSelectAsUser} — instead of
|
|
90
|
-
* `withPublicDb`/`withUserDb` when `db.supabase.rawSql` is `false`, or
|
|
91
|
-
* whenever a query is simple enough that avoiding `cfni_exec` entirely is
|
|
92
|
-
* preferable. Cross-table joins/aggregates spanning more than an embedded
|
|
93
|
-
* resource (`columns: '*, related(*)'`) still need `withPublicDb`/
|
|
94
|
-
* `withUserDb`, or a Postgres function called via {@link supabaseRpc}.
|
|
95
|
-
*
|
|
96
|
-
* @param table Table (or view) name.
|
|
97
|
-
* @param options Column list, filters, ordering, pagination, count.
|
|
98
|
-
* @returns The matching rows, and the total count when `options.count` is set.
|
|
99
|
-
* @throws If `db`/`db.supabase` is not set, or PostgREST rejects the request.
|
|
100
|
-
*/
|
|
101
|
-
export declare function supabaseSelect<T = Record<string, unknown>>(table: string, options?: SupabaseSelectOptions): Promise<SupabaseResult<T>>;
|
|
102
|
-
/**
|
|
103
|
-
* Same as {@link supabaseSelect}, but authenticated as the **signed-in
|
|
104
|
-
* user**: the bearer token comes from `db.getAccessToken`, or the signed-in
|
|
105
|
-
* Firebase user's ID token when `firebaseAuth` is configured — see
|
|
106
|
-
* {@link resolveAccessToken}. PostgREST resolves the caller as
|
|
107
|
-
* `authenticated` and RLS applies exactly as it would for `withUserDb`.
|
|
108
|
-
*/
|
|
109
|
-
export declare function supabaseSelectAsUser<T = Record<string, unknown>>(table: string, options?: SupabaseSelectOptions): Promise<SupabaseResult<T>>;
|
|
110
|
-
/**
|
|
111
|
-
* Inserts one or more rows into `table` through the Supabase REST API, as
|
|
112
|
-
* the **anonymous** role. See {@link supabaseSelect} for when to use this
|
|
113
|
-
* over `withPublicDb`.
|
|
114
|
-
*
|
|
115
|
-
* @param table Table name.
|
|
116
|
-
* @param values A single row, or an array of rows, to insert.
|
|
117
|
-
* @returns The inserted row(s) as returned by PostgREST.
|
|
118
|
-
*/
|
|
119
|
-
export declare function supabaseInsert<T = Record<string, unknown>>(table: string, values: Record<string, unknown> | Record<string, unknown>[]): Promise<T[]>;
|
|
120
|
-
/** Same as {@link supabaseInsert}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
121
|
-
export declare function supabaseInsertAsUser<T = Record<string, unknown>>(table: string, values: Record<string, unknown> | Record<string, unknown>[]): Promise<T[]>;
|
|
122
|
-
/**
|
|
123
|
-
* Inserts one or more rows into `table`, or updates them on a conflict —
|
|
124
|
-
* PostgREST's `upsert`, i.e. `insert ... on conflict (...) do update`. As
|
|
125
|
-
* the **anonymous** role.
|
|
126
|
-
*
|
|
127
|
-
* @param table Table name.
|
|
128
|
-
* @param values A single row, or an array of rows, to upsert.
|
|
129
|
-
* @param options `onConflict` (defaults to the table's primary key) and
|
|
130
|
-
* `ignoreDuplicates` (skip instead of update on conflict).
|
|
131
|
-
* @returns The upserted row(s) as returned by PostgREST.
|
|
132
|
-
*/
|
|
133
|
-
export declare function supabaseUpsert<T = Record<string, unknown>>(table: string, values: Record<string, unknown> | Record<string, unknown>[], options?: SupabaseUpsertOptions): Promise<T[]>;
|
|
134
|
-
/** Same as {@link supabaseUpsert}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
135
|
-
export declare function supabaseUpsertAsUser<T = Record<string, unknown>>(table: string, values: Record<string, unknown> | Record<string, unknown>[], options?: SupabaseUpsertOptions): Promise<T[]>;
|
|
136
|
-
/**
|
|
137
|
-
* Updates rows in `table` matching `options.where` through the Supabase
|
|
138
|
-
* REST API, as the **anonymous** role. See {@link supabaseSelect} for when
|
|
139
|
-
* to use this over `withPublicDb`.
|
|
140
|
-
*
|
|
141
|
-
* @param table Table name.
|
|
142
|
-
* @param values Columns to set.
|
|
143
|
-
* @param options `where` — filters selecting which rows to update; required,
|
|
144
|
-
* since an unfiltered update would touch every row.
|
|
145
|
-
* @returns The updated rows as returned by PostgREST.
|
|
146
|
-
*/
|
|
147
|
-
export declare function supabaseUpdate<T = Record<string, unknown>>(table: string, values: Record<string, unknown>, options: SupabaseMutationOptions): Promise<T[]>;
|
|
148
|
-
/** Same as {@link supabaseUpdate}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
149
|
-
export declare function supabaseUpdateAsUser<T = Record<string, unknown>>(table: string, values: Record<string, unknown>, options: SupabaseMutationOptions): Promise<T[]>;
|
|
150
|
-
/**
|
|
151
|
-
* Deletes rows in `table` matching `options.where` through the Supabase REST
|
|
152
|
-
* API, as the **anonymous** role. See {@link supabaseSelect} for when to
|
|
153
|
-
* use this over `withPublicDb`.
|
|
154
|
-
*
|
|
155
|
-
* @param table Table name.
|
|
156
|
-
* @param options `where` — filters selecting which rows to delete; required,
|
|
157
|
-
* since an unfiltered delete would remove every row.
|
|
158
|
-
* @returns The deleted rows as returned by PostgREST.
|
|
159
|
-
*/
|
|
160
|
-
export declare function supabaseDelete<T = Record<string, unknown>>(table: string, options: SupabaseMutationOptions): Promise<T[]>;
|
|
161
|
-
/** Same as {@link supabaseDelete}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
162
|
-
export declare function supabaseDeleteAsUser<T = Record<string, unknown>>(table: string, options: SupabaseMutationOptions): Promise<T[]>;
|
|
163
|
-
/**
|
|
164
|
-
* Calls a Postgres function through PostgREST's `.rpc()` — the same
|
|
165
|
-
* mechanism `cfni_exec` itself uses, but for a function you define yourself.
|
|
166
|
-
* This is how to run anything the `select`/`insert`/`upsert`/`update`/
|
|
167
|
-
* `delete` helpers above can't express (joins, aggregates, custom logic)
|
|
168
|
-
* without needing `cfni_exec`/raw SQL: write a regular Postgres function,
|
|
169
|
-
* `grant execute` to `anon`/`authenticated`, and call it by name.
|
|
170
|
-
*
|
|
171
|
-
* As the **anonymous** role.
|
|
172
|
-
*
|
|
173
|
-
* @param fn Function name.
|
|
174
|
-
* @param args Named arguments, matching the function's parameter names.
|
|
175
|
-
*/
|
|
176
|
-
export declare function supabaseRpc<T = unknown>(fn: string, args?: Record<string, unknown>): Promise<T>;
|
|
177
|
-
/** Same as {@link supabaseRpc}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
178
|
-
export declare function supabaseRpcAsUser<T = unknown>(fn: string, args?: Record<string, unknown>): Promise<T>;
|
|
@@ -1,262 +0,0 @@
|
|
|
1
|
-
import config from '../config/intl_config';
|
|
2
|
-
import requireDbConfig from './require_config';
|
|
3
|
-
import resolveSupabaseEndpoint from './supabase_config';
|
|
4
|
-
import resolveAccessToken from './access_token';
|
|
5
|
-
function describeQueryFailure(error) {
|
|
6
|
-
return `db: Supabase rejected the request — ${error.message}.`;
|
|
7
|
-
}
|
|
8
|
-
async function buildClient(supabase, bearerToken) {
|
|
9
|
-
const { url, anonKey } = await resolveSupabaseEndpoint(supabase);
|
|
10
|
-
const { createClient } = await import('@supabase/supabase-js');
|
|
11
|
-
return createClient(url, anonKey, { accessToken: async () => bearerToken });
|
|
12
|
-
}
|
|
13
|
-
function applyFilters(query, where) {
|
|
14
|
-
let result = query;
|
|
15
|
-
if (!where)
|
|
16
|
-
return result;
|
|
17
|
-
for (const [column, filter] of Object.entries(where)) {
|
|
18
|
-
if (Array.isArray(filter) && filter[0] === 'not') {
|
|
19
|
-
const [, operator, value] = filter;
|
|
20
|
-
result = result.not(column, operator, value);
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const [operator, value] = Array.isArray(filter)
|
|
24
|
-
? [filter[0], filter[1]]
|
|
25
|
-
: ['eq', filter];
|
|
26
|
-
result = result[operator](column, value);
|
|
27
|
-
}
|
|
28
|
-
return result;
|
|
29
|
-
}
|
|
30
|
-
function applyExtras(query, extras) {
|
|
31
|
-
let result = query;
|
|
32
|
-
if (extras.match)
|
|
33
|
-
result = result.match(extras.match);
|
|
34
|
-
if (extras.or) {
|
|
35
|
-
const or = typeof extras.or === 'string' ? { filters: extras.or, referencedTable: undefined } : extras.or;
|
|
36
|
-
result = result.or(or.filters, { referencedTable: or.referencedTable });
|
|
37
|
-
}
|
|
38
|
-
if (extras.textSearch) {
|
|
39
|
-
result = result.textSearch(extras.textSearch.column, extras.textSearch.query, {
|
|
40
|
-
type: extras.textSearch.type,
|
|
41
|
-
config: extras.textSearch.config,
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
return result;
|
|
45
|
-
}
|
|
46
|
-
function applyOrder(query, orderBy) {
|
|
47
|
-
if (!orderBy)
|
|
48
|
-
return query;
|
|
49
|
-
let result = query;
|
|
50
|
-
for (const clause of Array.isArray(orderBy) ? orderBy : [orderBy]) {
|
|
51
|
-
result = result.order(clause.column, { ascending: clause.ascending, nullsFirst: clause.nullsFirst });
|
|
52
|
-
}
|
|
53
|
-
return result;
|
|
54
|
-
}
|
|
55
|
-
async function requireSupabaseConfig() {
|
|
56
|
-
const db = config.db;
|
|
57
|
-
requireDbConfig(db);
|
|
58
|
-
if (!db.supabase) {
|
|
59
|
-
throw new Error('db: supabaseSelect/supabaseInsert/supabaseUpsert/supabaseUpdate/supabaseDelete/' +
|
|
60
|
-
'supabaseRpc need `db.supabase` (a project URL and anon key) on your RoutingConfig, ' +
|
|
61
|
-
'even in connection-string mode.');
|
|
62
|
-
}
|
|
63
|
-
return db.supabase;
|
|
64
|
-
}
|
|
65
|
-
async function anonClient() {
|
|
66
|
-
const supabase = await requireSupabaseConfig();
|
|
67
|
-
const { anonKey } = await resolveSupabaseEndpoint(supabase);
|
|
68
|
-
return buildClient(supabase, anonKey);
|
|
69
|
-
}
|
|
70
|
-
async function userClient() {
|
|
71
|
-
const supabase = await requireSupabaseConfig();
|
|
72
|
-
const token = await resolveAccessToken(config);
|
|
73
|
-
return buildClient(supabase, token);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Selects rows from `table` through the Supabase REST API
|
|
77
|
-
* (`@supabase/supabase-js`'s `.from(table).select()`), as the **anonymous**
|
|
78
|
-
* role — no `cfni_exec`, no raw SQL, only what PostgREST's REST API itself
|
|
79
|
-
* supports: a column list, filters (equality or a named operator), one or
|
|
80
|
-
* more `order by` clauses, `limit`/`range` pagination, `single`/`maybeSingle`,
|
|
81
|
-
* and an optional row count.
|
|
82
|
-
*
|
|
83
|
-
* Reach for this — or {@link supabaseSelectAsUser} — instead of
|
|
84
|
-
* `withPublicDb`/`withUserDb` when `db.supabase.rawSql` is `false`, or
|
|
85
|
-
* whenever a query is simple enough that avoiding `cfni_exec` entirely is
|
|
86
|
-
* preferable. Cross-table joins/aggregates spanning more than an embedded
|
|
87
|
-
* resource (`columns: '*, related(*)'`) still need `withPublicDb`/
|
|
88
|
-
* `withUserDb`, or a Postgres function called via {@link supabaseRpc}.
|
|
89
|
-
*
|
|
90
|
-
* @param table Table (or view) name.
|
|
91
|
-
* @param options Column list, filters, ordering, pagination, count.
|
|
92
|
-
* @returns The matching rows, and the total count when `options.count` is set.
|
|
93
|
-
* @throws If `db`/`db.supabase` is not set, or PostgREST rejects the request.
|
|
94
|
-
*/
|
|
95
|
-
export async function supabaseSelect(table, options = {}) {
|
|
96
|
-
return runSelect(await anonClient(), table, options);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Same as {@link supabaseSelect}, but authenticated as the **signed-in
|
|
100
|
-
* user**: the bearer token comes from `db.getAccessToken`, or the signed-in
|
|
101
|
-
* Firebase user's ID token when `firebaseAuth` is configured — see
|
|
102
|
-
* {@link resolveAccessToken}. PostgREST resolves the caller as
|
|
103
|
-
* `authenticated` and RLS applies exactly as it would for `withUserDb`.
|
|
104
|
-
*/
|
|
105
|
-
export async function supabaseSelectAsUser(table, options = {}) {
|
|
106
|
-
return runSelect(await userClient(), table, options);
|
|
107
|
-
}
|
|
108
|
-
async function runSelect(client, table, options) {
|
|
109
|
-
let query = client.from(table).select(options.columns ?? '*', options.count ? { count: options.count } : undefined);
|
|
110
|
-
query = applyFilters(query, options.where);
|
|
111
|
-
query = applyExtras(query, options);
|
|
112
|
-
query = applyOrder(query, options.orderBy);
|
|
113
|
-
if (options.limit !== undefined)
|
|
114
|
-
query = query.limit(options.limit);
|
|
115
|
-
if (options.range)
|
|
116
|
-
query = query.range(options.range[0], options.range[1]);
|
|
117
|
-
if (options.single)
|
|
118
|
-
query = query.single();
|
|
119
|
-
else if (options.maybeSingle)
|
|
120
|
-
query = query.maybeSingle();
|
|
121
|
-
const { data, error, count } = (await query);
|
|
122
|
-
if (error)
|
|
123
|
-
throw new Error(describeQueryFailure(error));
|
|
124
|
-
const rows = options.single || options.maybeSingle ? (data === null ? [] : [data]) : (data ?? []);
|
|
125
|
-
return { rows, count };
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Inserts one or more rows into `table` through the Supabase REST API, as
|
|
129
|
-
* the **anonymous** role. See {@link supabaseSelect} for when to use this
|
|
130
|
-
* over `withPublicDb`.
|
|
131
|
-
*
|
|
132
|
-
* @param table Table name.
|
|
133
|
-
* @param values A single row, or an array of rows, to insert.
|
|
134
|
-
* @returns The inserted row(s) as returned by PostgREST.
|
|
135
|
-
*/
|
|
136
|
-
export async function supabaseInsert(table, values) {
|
|
137
|
-
return runInsert(await anonClient(), table, values);
|
|
138
|
-
}
|
|
139
|
-
/** Same as {@link supabaseInsert}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
140
|
-
export async function supabaseInsertAsUser(table, values) {
|
|
141
|
-
return runInsert(await userClient(), table, values);
|
|
142
|
-
}
|
|
143
|
-
async function runInsert(client, table, values) {
|
|
144
|
-
const { data, error } = (await client.from(table).insert(values).select());
|
|
145
|
-
if (error)
|
|
146
|
-
throw new Error(describeQueryFailure(error));
|
|
147
|
-
return data ?? [];
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Inserts one or more rows into `table`, or updates them on a conflict —
|
|
151
|
-
* PostgREST's `upsert`, i.e. `insert ... on conflict (...) do update`. As
|
|
152
|
-
* the **anonymous** role.
|
|
153
|
-
*
|
|
154
|
-
* @param table Table name.
|
|
155
|
-
* @param values A single row, or an array of rows, to upsert.
|
|
156
|
-
* @param options `onConflict` (defaults to the table's primary key) and
|
|
157
|
-
* `ignoreDuplicates` (skip instead of update on conflict).
|
|
158
|
-
* @returns The upserted row(s) as returned by PostgREST.
|
|
159
|
-
*/
|
|
160
|
-
export async function supabaseUpsert(table, values, options = {}) {
|
|
161
|
-
return runUpsert(await anonClient(), table, values, options);
|
|
162
|
-
}
|
|
163
|
-
/** Same as {@link supabaseUpsert}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
164
|
-
export async function supabaseUpsertAsUser(table, values, options = {}) {
|
|
165
|
-
return runUpsert(await userClient(), table, values, options);
|
|
166
|
-
}
|
|
167
|
-
async function runUpsert(client, table, values, options) {
|
|
168
|
-
const { data, error } = (await client
|
|
169
|
-
.from(table)
|
|
170
|
-
.upsert(values, { onConflict: options.onConflict, ignoreDuplicates: options.ignoreDuplicates })
|
|
171
|
-
.select());
|
|
172
|
-
if (error)
|
|
173
|
-
throw new Error(describeQueryFailure(error));
|
|
174
|
-
return data ?? [];
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Updates rows in `table` matching `options.where` through the Supabase
|
|
178
|
-
* REST API, as the **anonymous** role. See {@link supabaseSelect} for when
|
|
179
|
-
* to use this over `withPublicDb`.
|
|
180
|
-
*
|
|
181
|
-
* @param table Table name.
|
|
182
|
-
* @param values Columns to set.
|
|
183
|
-
* @param options `where` — filters selecting which rows to update; required,
|
|
184
|
-
* since an unfiltered update would touch every row.
|
|
185
|
-
* @returns The updated rows as returned by PostgREST.
|
|
186
|
-
*/
|
|
187
|
-
export async function supabaseUpdate(table, values, options) {
|
|
188
|
-
return runUpdate(await anonClient(), table, values, options);
|
|
189
|
-
}
|
|
190
|
-
/** Same as {@link supabaseUpdate}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
191
|
-
export async function supabaseUpdateAsUser(table, values, options) {
|
|
192
|
-
return runUpdate(await userClient(), table, values, options);
|
|
193
|
-
}
|
|
194
|
-
async function runUpdate(client, table, values, options) {
|
|
195
|
-
requireMutationFilter(options);
|
|
196
|
-
let query = client.from(table).update(values);
|
|
197
|
-
query = applyFilters(query, options.where);
|
|
198
|
-
query = applyExtras(query, options);
|
|
199
|
-
const { data, error } = (await query.select());
|
|
200
|
-
if (error)
|
|
201
|
-
throw new Error(describeQueryFailure(error));
|
|
202
|
-
return data ?? [];
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Deletes rows in `table` matching `options.where` through the Supabase REST
|
|
206
|
-
* API, as the **anonymous** role. See {@link supabaseSelect} for when to
|
|
207
|
-
* use this over `withPublicDb`.
|
|
208
|
-
*
|
|
209
|
-
* @param table Table name.
|
|
210
|
-
* @param options `where` — filters selecting which rows to delete; required,
|
|
211
|
-
* since an unfiltered delete would remove every row.
|
|
212
|
-
* @returns The deleted rows as returned by PostgREST.
|
|
213
|
-
*/
|
|
214
|
-
export async function supabaseDelete(table, options) {
|
|
215
|
-
return runDelete(await anonClient(), table, options);
|
|
216
|
-
}
|
|
217
|
-
/** Same as {@link supabaseDelete}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
218
|
-
export async function supabaseDeleteAsUser(table, options) {
|
|
219
|
-
return runDelete(await userClient(), table, options);
|
|
220
|
-
}
|
|
221
|
-
async function runDelete(client, table, options) {
|
|
222
|
-
requireMutationFilter(options);
|
|
223
|
-
let query = client.from(table).delete();
|
|
224
|
-
query = applyFilters(query, options.where);
|
|
225
|
-
query = applyExtras(query, options);
|
|
226
|
-
const { data, error } = (await query.select());
|
|
227
|
-
if (error)
|
|
228
|
-
throw new Error(describeQueryFailure(error));
|
|
229
|
-
return data ?? [];
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Calls a Postgres function through PostgREST's `.rpc()` — the same
|
|
233
|
-
* mechanism `cfni_exec` itself uses, but for a function you define yourself.
|
|
234
|
-
* This is how to run anything the `select`/`insert`/`upsert`/`update`/
|
|
235
|
-
* `delete` helpers above can't express (joins, aggregates, custom logic)
|
|
236
|
-
* without needing `cfni_exec`/raw SQL: write a regular Postgres function,
|
|
237
|
-
* `grant execute` to `anon`/`authenticated`, and call it by name.
|
|
238
|
-
*
|
|
239
|
-
* As the **anonymous** role.
|
|
240
|
-
*
|
|
241
|
-
* @param fn Function name.
|
|
242
|
-
* @param args Named arguments, matching the function's parameter names.
|
|
243
|
-
*/
|
|
244
|
-
export async function supabaseRpc(fn, args) {
|
|
245
|
-
return runRpc(await anonClient(), fn, args);
|
|
246
|
-
}
|
|
247
|
-
/** Same as {@link supabaseRpc}, authenticated as the signed-in user — see {@link supabaseSelectAsUser}. */
|
|
248
|
-
export async function supabaseRpcAsUser(fn, args) {
|
|
249
|
-
return runRpc(await userClient(), fn, args);
|
|
250
|
-
}
|
|
251
|
-
async function runRpc(client, fn, args) {
|
|
252
|
-
const { data, error } = (await client.rpc(fn, args));
|
|
253
|
-
if (error)
|
|
254
|
-
throw new Error(describeQueryFailure(error));
|
|
255
|
-
return data;
|
|
256
|
-
}
|
|
257
|
-
function requireMutationFilter(options) {
|
|
258
|
-
const hasWhere = options.where && Object.keys(options.where).length > 0;
|
|
259
|
-
if (!hasWhere && !options.match && !options.or) {
|
|
260
|
-
throw new Error('db: one of `where`/`match`/`or` is required — an unfiltered update/delete would affect every row.');
|
|
261
|
-
}
|
|
262
|
-
}
|