cloudflare-next-intl 0.8.15 → 0.8.17
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/README.md +29 -53
- package/dist/src/db/context.d.ts +12 -49
- package/dist/src/db/context.js +81 -104
- package/dist/src/db/index.d.ts +14 -8
- package/dist/src/db/index.js +14 -8
- package/dist/src/db/transaction_batch.d.ts +2 -2
- package/dist/src/db/transaction_batch.js +2 -2
- package/dist/src/types/types.d.ts +11 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -560,9 +560,8 @@ user's Firebase ID token is used automatically.
|
|
|
560
560
|
|
|
561
561
|
- **Per-statement transactions in `withUserDb`/`withPublicDb`.** Each
|
|
562
562
|
statement in one of their callbacks is its own round-trip, so it is its own
|
|
563
|
-
implicit transaction
|
|
564
|
-
|
|
565
|
-
(below) when you need more than one statement to succeed or fail together.
|
|
563
|
+
implicit transaction. Call `.transaction(...)` on the handle (below) when
|
|
564
|
+
you need more than one statement to succeed or fail together.
|
|
566
565
|
- **Wider SQL surface.** `cfni_exec` runs statements your app generates, so any
|
|
567
566
|
role that can execute it can run arbitrary SQL *within that role's own
|
|
568
567
|
privileges* — a broader surface than PostgREST's normal verbs, though still
|
|
@@ -577,54 +576,37 @@ writable CTEs (`with x as (update ... returning ...) select ... from x`).
|
|
|
577
576
|
Every value round-trips through Postgres' own text representation, not JSON,
|
|
578
577
|
so arrays/`numeric`/timestamps/`bytea` decode the same way they do in
|
|
579
578
|
connection-string mode. `withUserDb`/`withPublicDb` do not run multiple
|
|
580
|
-
statements atomically (each call is its own PostgREST
|
|
581
|
-
[Multi-statement transactions](#multi-statement-transactions-
|
|
582
|
-
below for the API that does.
|
|
579
|
+
statements atomically on their own (each call is its own PostgREST
|
|
580
|
+
round-trip) — see [Multi-statement transactions](#multi-statement-transactions-dbtransaction)
|
|
581
|
+
below for the `.transaction()` API that does.
|
|
583
582
|
|
|
584
|
-
#### Multi-statement transactions (`
|
|
583
|
+
#### Multi-statement transactions (`db.transaction()`)
|
|
585
584
|
|
|
586
|
-
`withUserDb`/`withPublicDb`
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
`withUserTransaction`/`withPublicTransaction` (also from
|
|
590
|
-
`cloudflare-next-intl/db`) whenever a write needs more than one statement to
|
|
591
|
-
succeed or fail together.
|
|
585
|
+
Call `.transaction(...)` on the handle `withUserDb`/`withPublicDb` hand your
|
|
586
|
+
callback whenever a write needs more than one statement to succeed or fail
|
|
587
|
+
together — same method name and same signature in both transport modes.
|
|
592
588
|
|
|
593
|
-
|
|
594
|
-
them — call `.toSQL()` on each Drizzle query and return the array, rather
|
|
595
|
-
than `await`ing the query directly:
|
|
589
|
+
To achieve mode-transparency between Postgres/connection-string mode and Supabase/REST mode, the callback **builds** queries rather than executing them directly: call `.toSQL()` on each Drizzle query and return the array, rather than `await`ing the query directly.
|
|
596
590
|
|
|
597
591
|
```typescript
|
|
598
|
-
import {
|
|
592
|
+
import { withUserDb } from "cloudflare-next-intl/db";
|
|
599
593
|
import { invitations, contractorAccessGrants } from "@/shared/db/generated/schema";
|
|
600
594
|
|
|
601
|
-
const [invitationResult, grantResult] = await
|
|
602
|
-
db.
|
|
603
|
-
|
|
604
|
-
|
|
595
|
+
const [invitationResult, grantResult] = await withUserDb((db) =>
|
|
596
|
+
db.transaction((tx) => [
|
|
597
|
+
tx.insert(invitations).values({ email: "a@b.com" }).returning().toSQL(),
|
|
598
|
+
tx.insert(contractorAccessGrants).values({ propertyId: 1 }).toSQL(),
|
|
599
|
+
]),
|
|
600
|
+
);
|
|
605
601
|
```
|
|
606
602
|
|
|
607
|
-
Every query in the array is
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
any statement rolls back every statement that ran before it in the same
|
|
611
|
-
batch. `cfni_exec_batch` ships alongside `cfni_exec` in the same
|
|
612
|
-
`supabase/cfni_exec.sql` file (installed the same way — see
|
|
613
|
-
[Schema codegen](#schema-codegen-cfni-db-codegen) below) and is available
|
|
614
|
-
whenever `cfni_exec` is: there is no separate config flag, and
|
|
615
|
-
`db.supabase.rawSql: false` turns off both.
|
|
616
|
-
|
|
617
|
-
Each result is the same `{ rows, rowCount }` shape a single `cfni_exec` call
|
|
618
|
-
returns — decode rows the same way you would from `db.execute(sql\`...\`)`.
|
|
603
|
+
Every query in the array is executed sequentially in a single transaction blocks/batch:
|
|
604
|
+
- In **Supabase mode**, they are sent in one round-trip to `cfni_exec_batch` which runs them inside a single `plpgsql` block.
|
|
605
|
+
- In **connection-string mode**, they are run sequentially over the Postgres client inside a standard Drizzle transaction.
|
|
619
606
|
|
|
620
|
-
|
|
621
|
-
throws immediately, naming the mistake, rather than hanging or silently
|
|
622
|
-
running that one statement outside the batch with no atomicity.
|
|
607
|
+
Either way, a failure on any statement rolls back every statement that ran before it in the transaction.
|
|
623
608
|
|
|
624
|
-
|
|
625
|
-
`.transaction()` instead — it already provides real atomicity there, so
|
|
626
|
-
`withUserTransaction`/`withPublicTransaction` throw rather than duplicate
|
|
627
|
-
that path.
|
|
609
|
+
Each result is the `{ rows, rowCount }` shape. Because the callback only builds queries, a later statement in a `.transaction()` callback cannot read an earlier one's result — build every statement from arguments/closures you already have. `await`ing a query directly inside `.transaction()` throws immediately to prevent running queries outside the transaction boundary.
|
|
628
610
|
|
|
629
611
|
#### Supabase mode and REST translation
|
|
630
612
|
|
|
@@ -666,8 +648,8 @@ await withPublicDb((db) => db.delete(bonds).where(eq(bonds.id, 1)));
|
|
|
666
648
|
| `RETURNING` clauses | Supported | Supported |
|
|
667
649
|
| Operators: `=`, `<>`, `!=`, `>`, `>=`, `<`, `<=`, `like`, `ilike`, `is [not] null`, `[not] in`, `is [not] distinct from`, `~`, `~*`, `@>`, `<@`, `&&`, `>>`, `<<`, `&>`, `&<`, `-|-`, `@@` | Supported | Supported |
|
|
668
650
|
| Multi-table joins, CTEs, non-count aggregates, `GROUP BY`, `UNION`, `DISTINCT`, raw SQL | Not supported by REST | Supported |
|
|
669
|
-
| Multi-statement transactions
|
|
670
|
-
| Multi-statement transactions via `
|
|
651
|
+
| Multi-statement transactions without calling `.transaction()` | Not supported | Not supported (each call is its own round-trip) |
|
|
652
|
+
| Multi-statement transactions via `db.transaction()` | N/A — build-and-return API | Supported (`cfni_exec_batch`, one round-trip) |
|
|
671
653
|
|
|
672
654
|
#### Enforcing single API via ESLint (`cloudflare-next-intl/dbEslint`)
|
|
673
655
|
|
|
@@ -682,9 +664,10 @@ export default [
|
|
|
682
664
|
];
|
|
683
665
|
```
|
|
684
666
|
|
|
685
|
-
|
|
686
|
-
allowed to see the rows
|
|
687
|
-
succeed or
|
|
667
|
+
Two query wrappers, both from `cloudflare-next-intl/db`. Choose by who is
|
|
668
|
+
allowed to see the rows; call `.transaction(...)` on the handle either one
|
|
669
|
+
hands your callback when a write needs more than one statement to succeed or
|
|
670
|
+
fail together (see [Multi-statement transactions](#multi-statement-transactions-dbtransaction) above):
|
|
688
671
|
|
|
689
672
|
- `withPublicDb(fn)` — runs `fn` as the anonymous role: a pooled connection
|
|
690
673
|
with no transaction/role switch in connection-string mode, or the anon key
|
|
@@ -701,13 +684,6 @@ succeed or fail together:
|
|
|
701
684
|
the id comes from `db.getUserId()` if set, otherwise automatically from the
|
|
702
685
|
signed-in Firebase user when `firebaseAuth` is configured — you rarely need
|
|
703
686
|
to pass it explicitly.
|
|
704
|
-
- `withPublicTransaction(build)` / `withUserTransaction(build)` — same role
|
|
705
|
-
split as above, but for a write that needs more than one statement to
|
|
706
|
-
succeed or fail together. See
|
|
707
|
-
[Multi-statement transactions](#multi-statement-transactions-withusertransaction-withpublictransaction)
|
|
708
|
-
above — `build` returns built (`.toSQL()`) queries rather than executing
|
|
709
|
-
them, and these two are currently Supabase-mode only (connection-string
|
|
710
|
-
mode already has real atomicity via `withUserDb`/`withPublicDb`).
|
|
711
687
|
|
|
712
688
|
```typescript
|
|
713
689
|
// anywhere on the server
|
|
@@ -798,7 +774,7 @@ your project — `--rpc-dir` (defaulting inside `--ddl-dir`, so `rpcs` under
|
|
|
798
774
|
always holding the current version of the function, without a manual
|
|
799
775
|
copy-paste step. Since the file now ships both `cfni_exec` and
|
|
800
776
|
`cfni_exec_batch` (see
|
|
801
|
-
[Multi-statement transactions](#multi-statement-transactions-
|
|
777
|
+
[Multi-statement transactions](#multi-statement-transactions-dbtransaction)
|
|
802
778
|
above), pass `--rpc-file-name=`/`--tests-file-name=` (or
|
|
803
779
|
`CFNI_DB_RPC_FILE_NAME`/`CFNI_DB_TESTS_FILE_NAME`) if you'd rather install it
|
|
804
780
|
under a name that reflects that, e.g. `cfni_exec_and_batch.sql`.
|
package/dist/src/db/context.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
2
|
-
import type { Query } from 'drizzle-orm';
|
|
3
|
-
import type { ExecResult } from './supabase_transport';
|
|
4
2
|
/**
|
|
5
3
|
* The Drizzle handle passed to `withPublicDb`/`withUserDb` callbacks. Use it
|
|
6
4
|
* exactly like a normal Drizzle database (`db.select().from(table)`); it is
|
|
@@ -18,7 +16,9 @@ export type DrizzleDb = NodePgDatabase<Record<string, never>>;
|
|
|
18
16
|
* In connection-string mode the connection is taken from the request's
|
|
19
17
|
* shared client and released when `fn` settles, even if it throws. In
|
|
20
18
|
* Supabase mode there is no connection to release — each call is one
|
|
21
|
-
* PostgREST round-trip authenticated as the anon key.
|
|
19
|
+
* PostgREST round-trip authenticated as the anon key. Either way, call
|
|
20
|
+
* `.transaction(...)` on the handle `fn` receives for atomicity across more
|
|
21
|
+
* than one statement — see the module doc for the shape that takes in each mode.
|
|
22
22
|
*
|
|
23
23
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
24
24
|
* @returns Whatever `fn` resolves to.
|
|
@@ -37,13 +37,16 @@ export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>): Prom
|
|
|
37
37
|
* PostgREST-issued call. In Supabase mode identity instead rides on the JWT
|
|
38
38
|
* sent as `Authorization: Bearer` — PostgREST resolves the `authenticated`
|
|
39
39
|
* role and populates `request.jwt.claims` itself, and each statement is its
|
|
40
|
-
* own round-trip with no cross-statement transaction
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* own round-trip with no cross-statement transaction unless you call
|
|
41
|
+
* `.transaction(...)` on the handle (the Postgres proxy Drizzle uses in this
|
|
42
|
+
* mode cannot open a real session, so that runs as one atomic
|
|
43
|
+
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
44
|
+
* the wrapper to use for anything user-owned.
|
|
43
45
|
*
|
|
44
46
|
* @param fn Receives the Drizzle handle. In connection-string mode it is
|
|
45
|
-
* bound to a transaction; in Supabase mode it is not
|
|
46
|
-
*
|
|
47
|
+
* also bound to a transaction; in Supabase mode it is not, but its own
|
|
48
|
+
* `.transaction(...)` still provides atomicity across statements there
|
|
49
|
+
* (build-and-return shape, not a live session — see the module doc).
|
|
47
50
|
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
48
51
|
* pass `null`, in normal use — either way the id then comes from
|
|
49
52
|
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
@@ -60,45 +63,5 @@ export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>): Prom
|
|
|
60
63
|
* const mine = await withUserDb((db) => db.select().from(orders));
|
|
61
64
|
*/
|
|
62
65
|
export declare function withUserDb<T>(fn: (db: DrizzleDb) => Promise<T>, uid?: string | null): Promise<T>;
|
|
63
|
-
/** One statement's `{rows, rowCount}` result from a `
|
|
66
|
+
/** One statement's `{rows, rowCount}` result from a Supabase-mode `db.transaction()` batch. */
|
|
64
67
|
export type { ExecResult as TransactionResult } from './supabase_transport';
|
|
65
|
-
/**
|
|
66
|
-
* Runs several statements atomically as the **anonymous** role.
|
|
67
|
-
*
|
|
68
|
-
* See {@link runTransaction} for the batching mechanism. `build` must not
|
|
69
|
-
* execute its queries — return their `.toSQL()` form instead.
|
|
70
|
-
*
|
|
71
|
-
* @param build Returns the queries to run, in order.
|
|
72
|
-
* @returns One `{rows, rowCount}` result per query, in the same order.
|
|
73
|
-
* @throws If `db` is not set, if this isn't Supabase mode (connection-string
|
|
74
|
-
* mode already has real transactions via `withPublicDb`), or if any
|
|
75
|
-
* statement in the batch fails — the whole batch is then rolled back.
|
|
76
|
-
*
|
|
77
|
-
* @example
|
|
78
|
-
* const [inserted] = await withPublicTransaction((db) => [
|
|
79
|
-
* db.insert(logEntries).values({ event: 'visit' }).toSQL(),
|
|
80
|
-
* ]);
|
|
81
|
-
*/
|
|
82
|
-
export declare function withPublicTransaction(build: (db: DrizzleDb) => Promise<Query[]> | Query[]): Promise<ExecResult[]>;
|
|
83
|
-
/**
|
|
84
|
-
* Runs several statements atomically as the **signed-in user**.
|
|
85
|
-
*
|
|
86
|
-
* See {@link runTransaction} for the batching mechanism and
|
|
87
|
-
* {@link withUserDb} for how the caller's identity is resolved. In Supabase
|
|
88
|
-
* mode this is the wrapper to reach for whenever a user-owned write needs
|
|
89
|
-
* more than one statement to succeed or fail together — `withUserDb` alone
|
|
90
|
-
* cannot provide that there.
|
|
91
|
-
*
|
|
92
|
-
* @param build Returns the queries to run, in order. Do not execute them —
|
|
93
|
-
* call `.toSQL()` on each and return the array.
|
|
94
|
-
* @returns One `{rows, rowCount}` result per query, in the same order.
|
|
95
|
-
* @throws If `db` is not set, if no access token can be resolved, if this
|
|
96
|
-
* isn't Supabase mode, or if any statement in the batch fails — the whole
|
|
97
|
-
* batch is then rolled back.
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* const [invitation] = await withUserTransaction((db) => [
|
|
101
|
-
* db.insert(invitations).values({ email }).returning().toSQL(),
|
|
102
|
-
* ]);
|
|
103
|
-
*/
|
|
104
|
-
export declare function withUserTransaction(build: (db: DrizzleDb) => Promise<Query[]> | Query[]): Promise<ExecResult[]>;
|
package/dist/src/db/context.js
CHANGED
|
@@ -6,6 +6,7 @@ import resolveSupabaseEndpoint from './supabase_config';
|
|
|
6
6
|
import createSupabaseTransport from './supabase_transport';
|
|
7
7
|
import resolveAccessToken from './access_token';
|
|
8
8
|
import runTransactionBatch from './transaction_batch';
|
|
9
|
+
import inlineParams from './inline_params';
|
|
9
10
|
const DEFAULT_ROLE = 'authenticated';
|
|
10
11
|
/**
|
|
11
12
|
* Resolves the user id for `withUserDb`, trying, in order: the explicit `uid`
|
|
@@ -33,35 +34,68 @@ async function resolveUserId(uid) {
|
|
|
33
34
|
/**
|
|
34
35
|
* Builds a Drizzle handle backed by PostgREST. `bearerToken` decides the role
|
|
35
36
|
* Postgres sees: the anon key for public access, a user JWT for `withUserDb`.
|
|
37
|
+
*
|
|
38
|
+
* `.transaction()` on this handle runs atomically via `cfni_exec_batch` (see
|
|
39
|
+
* {@link runTransaction}) — pg-proxy has no session to open a real
|
|
40
|
+
* transaction over, so every statement the callback returns is queued and
|
|
41
|
+
* sent as one PostgREST round trip instead. Because of that, the callback
|
|
42
|
+
* must *build* its queries (`.toSQL()`), not `await`/execute them — a later
|
|
43
|
+
* statement cannot read an earlier one's result the way it could inside a
|
|
44
|
+
* real session, unlike connection-string mode's `.transaction()`.
|
|
36
45
|
*/
|
|
37
46
|
async function supabaseDb(supabase, bearerToken) {
|
|
38
47
|
const { drizzle } = await import('drizzle-orm/pg-proxy');
|
|
39
48
|
const db = drizzle(createSupabaseTransport(supabase, bearerToken));
|
|
40
49
|
return Object.assign(db, {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// beats silently running the callback non-atomically.
|
|
44
|
-
transaction() {
|
|
45
|
-
throw new Error('db: transactions are not available in Supabase mode. Each statement runs as its ' +
|
|
46
|
-
'own PostgREST round-trip with no shared session, so `.transaction()` cannot provide ' +
|
|
47
|
-
'atomicity. Use connection-string mode (`db.connectionString`) if you need it.');
|
|
50
|
+
transaction(build) {
|
|
51
|
+
return runTransaction(supabase, bearerToken, build);
|
|
48
52
|
},
|
|
49
53
|
});
|
|
50
54
|
}
|
|
51
55
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* Wraps a live Drizzle postgres transaction handle with a `.transaction()`
|
|
57
|
+
* override that mirrors the Supabase-mode batch API: `build` receives a
|
|
58
|
+
* build-only proxy, must return an array of `.toSQL()` objects (same shape),
|
|
59
|
+
* and this function executes each one sequentially on the real pg session,
|
|
60
|
+
* collecting `ExecResult[]`. Both modes therefore share the identical
|
|
61
|
+
* callback shape — callers never need to detect the transport themselves.
|
|
62
|
+
*/
|
|
63
|
+
async function postgresDb(drizzleHandle, rawClient) {
|
|
64
|
+
return Object.assign(drizzleHandle, {
|
|
65
|
+
async transaction(build) {
|
|
66
|
+
return runPostgresTransaction(rawClient, build);
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Postgres-mode equivalent of `runTransaction`: calls `build` with a
|
|
72
|
+
* build-only handle, then executes each returned query on the raw pg client
|
|
73
|
+
* via an inline-parameterised `query()` call and returns `ExecResult[]`.
|
|
74
|
+
*/
|
|
75
|
+
async function runPostgresTransaction(rawClient, build) {
|
|
76
|
+
const queries = await build(buildOnlyDb());
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const q of queries) {
|
|
79
|
+
const statement = inlineParams(q.sql, q.params);
|
|
80
|
+
const res = await rawClient.query(statement);
|
|
81
|
+
results.push({ rows: res.rows ?? [], rowCount: res.rowCount ?? null });
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Builds a Drizzle handle with no working transport, for Supabase-mode
|
|
87
|
+
* `db.transaction(...)` callbacks. Query builders' `.toSQL()` never touches
|
|
88
|
+
* the session, so this is safe to hand out purely for building statements —
|
|
89
|
+
* but `await`ing a query directly (instead of collecting its `.toSQL()`
|
|
90
|
+
* output) throws immediately here instead of hanging or silently running
|
|
91
|
+
* outside the batch.
|
|
58
92
|
*/
|
|
59
93
|
function buildOnlyDb() {
|
|
60
94
|
const throwIfExecuted = () => {
|
|
61
95
|
throw new Error('db: this Drizzle handle is for building statements only — call `.toSQL()` on each ' +
|
|
62
96
|
'query and return the array, do not `await`/execute it directly. Awaiting a query ' +
|
|
63
|
-
'inside a
|
|
64
|
-
'
|
|
97
|
+
'inside a Supabase-mode db.transaction() callback runs it outside the batch, with no ' +
|
|
98
|
+
'atomicity, which is exactly what `.transaction()` exists to prevent.');
|
|
65
99
|
};
|
|
66
100
|
return new Proxy({}, { get: () => throwIfExecuted });
|
|
67
101
|
}
|
|
@@ -76,7 +110,9 @@ function buildOnlyDb() {
|
|
|
76
110
|
* In connection-string mode the connection is taken from the request's
|
|
77
111
|
* shared client and released when `fn` settles, even if it throws. In
|
|
78
112
|
* Supabase mode there is no connection to release — each call is one
|
|
79
|
-
* PostgREST round-trip authenticated as the anon key.
|
|
113
|
+
* PostgREST round-trip authenticated as the anon key. Either way, call
|
|
114
|
+
* `.transaction(...)` on the handle `fn` receives for atomicity across more
|
|
115
|
+
* than one statement — see the module doc for the shape that takes in each mode.
|
|
80
116
|
*
|
|
81
117
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
82
118
|
* @returns Whatever `fn` resolves to.
|
|
@@ -96,7 +132,9 @@ export async function withPublicDb(fn) {
|
|
|
96
132
|
const client = await connectToPostgres(config, resolved.connectionString);
|
|
97
133
|
try {
|
|
98
134
|
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
99
|
-
|
|
135
|
+
const drizzleHandle = drizzle(client);
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
137
|
+
return await fn(await postgresDb(drizzleHandle, client));
|
|
100
138
|
}
|
|
101
139
|
finally {
|
|
102
140
|
disconnectPostgres(config);
|
|
@@ -111,13 +149,16 @@ export async function withPublicDb(fn) {
|
|
|
111
149
|
* PostgREST-issued call. In Supabase mode identity instead rides on the JWT
|
|
112
150
|
* sent as `Authorization: Bearer` — PostgREST resolves the `authenticated`
|
|
113
151
|
* role and populates `request.jwt.claims` itself, and each statement is its
|
|
114
|
-
* own round-trip with no cross-statement transaction
|
|
115
|
-
*
|
|
116
|
-
*
|
|
152
|
+
* own round-trip with no cross-statement transaction unless you call
|
|
153
|
+
* `.transaction(...)` on the handle (the Postgres proxy Drizzle uses in this
|
|
154
|
+
* mode cannot open a real session, so that runs as one atomic
|
|
155
|
+
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
156
|
+
* the wrapper to use for anything user-owned.
|
|
117
157
|
*
|
|
118
158
|
* @param fn Receives the Drizzle handle. In connection-string mode it is
|
|
119
|
-
* bound to a transaction; in Supabase mode it is not
|
|
120
|
-
*
|
|
159
|
+
* also bound to a transaction; in Supabase mode it is not, but its own
|
|
160
|
+
* `.transaction(...)` still provides atomicity across statements there
|
|
161
|
+
* (build-and-return shape, not a live session — see the module doc).
|
|
121
162
|
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
122
163
|
* pass `null`, in normal use — either way the id then comes from
|
|
123
164
|
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
@@ -150,7 +191,10 @@ export async function withUserDb(fn, uid) {
|
|
|
150
191
|
return await drizzle(client).transaction(async (transaction) => {
|
|
151
192
|
await transaction.execute(sql `select set_config('request.jwt.claims', ${JSON.stringify({ sub: userId })}, true)`);
|
|
152
193
|
await transaction.execute(sql `set local role ${sql.raw(role)}`);
|
|
153
|
-
|
|
194
|
+
// The transaction handle's session.client is the live pg socket — use it directly.
|
|
195
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
196
|
+
const txClient = transaction.session?.client ?? client;
|
|
197
|
+
return fn(await postgresDb(transaction, txClient));
|
|
154
198
|
});
|
|
155
199
|
}
|
|
156
200
|
finally {
|
|
@@ -158,95 +202,28 @@ export async function withUserDb(fn, uid) {
|
|
|
158
202
|
}
|
|
159
203
|
}
|
|
160
204
|
/**
|
|
161
|
-
* Runs several statements atomically
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* duplicating that path. In Supabase mode, where `.transaction()` cannot
|
|
170
|
-
* open a session, every query is inlined and sent as one `cfni_exec_batch`
|
|
171
|
-
* call: the Postgres function runs them in order inside a single plpgsql
|
|
172
|
-
* call, which is itself an implicit transaction, so a failure on any
|
|
173
|
-
* statement rolls back every statement before it.
|
|
205
|
+
* Runs several statements atomically over `cfni_exec_batch`, backing
|
|
206
|
+
* Supabase-mode `db.transaction()`. `build` does not execute its queries —
|
|
207
|
+
* it **builds** them and returns the array; call `.toSQL()` on each Drizzle
|
|
208
|
+
* query instead of `await`ing it (`await`ing throws immediately; see
|
|
209
|
+
* {@link buildOnlyDb}). Every query is inlined and sent as one round trip:
|
|
210
|
+
* the Postgres function runs them in order inside a single plpgsql call,
|
|
211
|
+
* which is itself an implicit transaction, so a failure on any statement
|
|
212
|
+
* rolls back every statement before it.
|
|
174
213
|
*
|
|
175
|
-
* @param
|
|
214
|
+
* @param supabase The `db.supabase` config block.
|
|
176
215
|
* @param bearerToken The anon key or user JWT.
|
|
177
216
|
* @param build Returns the queries to run, via `.toSQL()` — never executes them directly.
|
|
178
217
|
* @returns One result per query, in the same order as `build`'s array.
|
|
218
|
+
* @throws If `db.supabase.rawSql` is `false` — `cfni_exec_batch` needs `cfni_exec`, which is disabled too.
|
|
179
219
|
*/
|
|
180
|
-
async function
|
|
181
|
-
const resolved = await resolveDbMode(db);
|
|
182
|
-
if (resolved.mode !== 'supabase') {
|
|
183
|
-
throw new Error('db: withUserTransaction/withPublicTransaction only run their Supabase-mode batch path ' +
|
|
184
|
-
'right now. In connection-string mode, use withUserDb/withPublicDb\'s own `.transaction()` ' +
|
|
185
|
-
'— it already provides real atomicity there.');
|
|
186
|
-
}
|
|
187
|
-
const supabase = resolved.supabase;
|
|
220
|
+
async function runTransaction(supabase, bearerToken, build) {
|
|
188
221
|
if (supabase.rawSql === false) {
|
|
189
|
-
throw new Error('db:
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'Postgres connection instead.');
|
|
222
|
+
throw new Error('db: transaction() needs `cfni_exec_batch`, which runs through `cfni_exec` — both are ' +
|
|
223
|
+
'unavailable while `db.supabase.rawSql` is `false`. Install cfni_exec.sql and drop ' +
|
|
224
|
+
'`rawSql: false`, or use `db.connectionString` for a direct Postgres connection instead.');
|
|
193
225
|
}
|
|
194
|
-
return supabase;
|
|
195
|
-
}
|
|
196
|
-
async function runTransaction(supabase, bearerToken, build) {
|
|
197
226
|
const queries = await build(buildOnlyDb());
|
|
198
227
|
const batchQueries = queries.map((query) => ({ sql: query.sql, params: query.params }));
|
|
199
228
|
return runTransactionBatch(supabase, bearerToken, batchQueries);
|
|
200
229
|
}
|
|
201
|
-
/**
|
|
202
|
-
* Runs several statements atomically as the **anonymous** role.
|
|
203
|
-
*
|
|
204
|
-
* See {@link runTransaction} for the batching mechanism. `build` must not
|
|
205
|
-
* execute its queries — return their `.toSQL()` form instead.
|
|
206
|
-
*
|
|
207
|
-
* @param build Returns the queries to run, in order.
|
|
208
|
-
* @returns One `{rows, rowCount}` result per query, in the same order.
|
|
209
|
-
* @throws If `db` is not set, if this isn't Supabase mode (connection-string
|
|
210
|
-
* mode already has real transactions via `withPublicDb`), or if any
|
|
211
|
-
* statement in the batch fails — the whole batch is then rolled back.
|
|
212
|
-
*
|
|
213
|
-
* @example
|
|
214
|
-
* const [inserted] = await withPublicTransaction((db) => [
|
|
215
|
-
* db.insert(logEntries).values({ event: 'visit' }).toSQL(),
|
|
216
|
-
* ]);
|
|
217
|
-
*/
|
|
218
|
-
export async function withPublicTransaction(build) {
|
|
219
|
-
const db = config.db;
|
|
220
|
-
requireDbConfig(db);
|
|
221
|
-
const supabase = await requireSupabaseTransactionMode(db);
|
|
222
|
-
const { anonKey } = await resolveSupabaseEndpoint(supabase);
|
|
223
|
-
return runTransaction(supabase, anonKey, build);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Runs several statements atomically as the **signed-in user**.
|
|
227
|
-
*
|
|
228
|
-
* See {@link runTransaction} for the batching mechanism and
|
|
229
|
-
* {@link withUserDb} for how the caller's identity is resolved. In Supabase
|
|
230
|
-
* mode this is the wrapper to reach for whenever a user-owned write needs
|
|
231
|
-
* more than one statement to succeed or fail together — `withUserDb` alone
|
|
232
|
-
* cannot provide that there.
|
|
233
|
-
*
|
|
234
|
-
* @param build Returns the queries to run, in order. Do not execute them —
|
|
235
|
-
* call `.toSQL()` on each and return the array.
|
|
236
|
-
* @returns One `{rows, rowCount}` result per query, in the same order.
|
|
237
|
-
* @throws If `db` is not set, if no access token can be resolved, if this
|
|
238
|
-
* isn't Supabase mode, or if any statement in the batch fails — the whole
|
|
239
|
-
* batch is then rolled back.
|
|
240
|
-
*
|
|
241
|
-
* @example
|
|
242
|
-
* const [invitation] = await withUserTransaction((db) => [
|
|
243
|
-
* db.insert(invitations).values({ email }).returning().toSQL(),
|
|
244
|
-
* ]);
|
|
245
|
-
*/
|
|
246
|
-
export async function withUserTransaction(build) {
|
|
247
|
-
const db = config.db;
|
|
248
|
-
requireDbConfig(db);
|
|
249
|
-
const supabase = await requireSupabaseTransactionMode(db);
|
|
250
|
-
const token = await resolveAccessToken(config);
|
|
251
|
-
return runTransaction(supabase, token, build);
|
|
252
|
-
}
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -7,10 +7,17 @@
|
|
|
7
7
|
* - {@link withPublicDb} — anonymous role, for data any visitor may read.
|
|
8
8
|
* - {@link withUserDb} — the signed-in user, with RLS applied to their id.
|
|
9
9
|
*
|
|
10
|
-
* Need more than one statement to succeed or fail together?
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Need more than one statement to succeed or fail together? Call
|
|
11
|
+
* `db.transaction(...)` on the handle either wrapper hands your callback —
|
|
12
|
+
* same method name in both transport modes. In connection-string mode it is
|
|
13
|
+
* a real Drizzle transaction: `db.transaction(async (tx) => { await
|
|
14
|
+
* tx.insert(...); await tx.update(...); })`, and a later statement may use an
|
|
15
|
+
* earlier one's result. In Supabase mode there is no session to run that
|
|
16
|
+
* over, so the callback instead *builds* queries and returns them —
|
|
17
|
+
* `db.transaction((tx) => [tx.insert(...).values(...).toSQL(), tx.update(...).toSQL()])`
|
|
18
|
+
* — call `.toSQL()` on each instead of `await`ing it; every statement then
|
|
19
|
+
* runs atomically as one `cfni_exec_batch` call, but a later statement cannot
|
|
20
|
+
* read an earlier one's result there.
|
|
14
21
|
*
|
|
15
22
|
* Two transports reach Postgres behind that same Drizzle query API, chosen by
|
|
16
23
|
* `resolveDbMode` from which `db` config fields are set: `connectionString`
|
|
@@ -23,14 +30,13 @@
|
|
|
23
30
|
* statement is first translated into `@supabase/supabase-js` `.from()` calls;
|
|
24
31
|
* anything PostgREST cannot express falls back to `cfni_exec`, and if
|
|
25
32
|
* `db.supabase.rawSql` is `false` the call throws naming the construct that
|
|
26
|
-
* needs raw SQL
|
|
27
|
-
*
|
|
28
|
-
* `withUserTransaction` there instead.
|
|
33
|
+
* needs raw SQL — including `.transaction()`, which needs `cfni_exec_batch`
|
|
34
|
+
* from the same file.
|
|
29
35
|
*
|
|
30
36
|
* Generic Drizzle SQL helpers (`excluded`, `onConflictSet`, `ago`, …) live in
|
|
31
37
|
* the separate `cloudflare-next-intl/dbHelpers` entry point.
|
|
32
38
|
*/
|
|
33
|
-
export { withPublicDb, withUserDb
|
|
39
|
+
export { withPublicDb, withUserDb } from './context';
|
|
34
40
|
export type { DrizzleDb, TransactionResult } from './context';
|
|
35
41
|
export { default as connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
|
|
36
42
|
export type { DbRoutingConfig } from '../types/types';
|
package/dist/src/db/index.js
CHANGED
|
@@ -7,10 +7,17 @@
|
|
|
7
7
|
* - {@link withPublicDb} — anonymous role, for data any visitor may read.
|
|
8
8
|
* - {@link withUserDb} — the signed-in user, with RLS applied to their id.
|
|
9
9
|
*
|
|
10
|
-
* Need more than one statement to succeed or fail together?
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Need more than one statement to succeed or fail together? Call
|
|
11
|
+
* `db.transaction(...)` on the handle either wrapper hands your callback —
|
|
12
|
+
* same method name in both transport modes. In connection-string mode it is
|
|
13
|
+
* a real Drizzle transaction: `db.transaction(async (tx) => { await
|
|
14
|
+
* tx.insert(...); await tx.update(...); })`, and a later statement may use an
|
|
15
|
+
* earlier one's result. In Supabase mode there is no session to run that
|
|
16
|
+
* over, so the callback instead *builds* queries and returns them —
|
|
17
|
+
* `db.transaction((tx) => [tx.insert(...).values(...).toSQL(), tx.update(...).toSQL()])`
|
|
18
|
+
* — call `.toSQL()` on each instead of `await`ing it; every statement then
|
|
19
|
+
* runs atomically as one `cfni_exec_batch` call, but a later statement cannot
|
|
20
|
+
* read an earlier one's result there.
|
|
14
21
|
*
|
|
15
22
|
* Two transports reach Postgres behind that same Drizzle query API, chosen by
|
|
16
23
|
* `resolveDbMode` from which `db` config fields are set: `connectionString`
|
|
@@ -23,12 +30,11 @@
|
|
|
23
30
|
* statement is first translated into `@supabase/supabase-js` `.from()` calls;
|
|
24
31
|
* anything PostgREST cannot express falls back to `cfni_exec`, and if
|
|
25
32
|
* `db.supabase.rawSql` is `false` the call throws naming the construct that
|
|
26
|
-
* needs raw SQL
|
|
27
|
-
*
|
|
28
|
-
* `withUserTransaction` there instead.
|
|
33
|
+
* needs raw SQL — including `.transaction()`, which needs `cfni_exec_batch`
|
|
34
|
+
* from the same file.
|
|
29
35
|
*
|
|
30
36
|
* Generic Drizzle SQL helpers (`excluded`, `onConflictSet`, `ago`, …) live in
|
|
31
37
|
* the separate `cloudflare-next-intl/dbHelpers` entry point.
|
|
32
38
|
*/
|
|
33
|
-
export { withPublicDb, withUserDb
|
|
39
|
+
export { withPublicDb, withUserDb } from './context';
|
|
34
40
|
export { default as connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
|
|
@@ -20,8 +20,8 @@ export interface BatchQuery {
|
|
|
20
20
|
*
|
|
21
21
|
* @param supabase The `db.supabase` config block.
|
|
22
22
|
* @param bearerToken Token resolved as the caller's identity — the anon key
|
|
23
|
-
* for
|
|
24
|
-
*
|
|
23
|
+
* for `withPublicDb`'s handle, a user JWT for `withUserDb`'s — see
|
|
24
|
+
* `context.ts`'s `runTransaction`, which backs both handles' `.transaction()`.
|
|
25
25
|
* @param queries The statements to run, in order. An empty array is a no-op
|
|
26
26
|
* that still makes the round trip, matching `cfni_exec_batch(array[]::text[])`.
|
|
27
27
|
* @returns One `{rows, rowCount}` result per query, in the same order.
|
|
@@ -23,8 +23,8 @@ const BATCH_FUNCTION = 'cfni_exec_batch';
|
|
|
23
23
|
*
|
|
24
24
|
* @param supabase The `db.supabase` config block.
|
|
25
25
|
* @param bearerToken Token resolved as the caller's identity — the anon key
|
|
26
|
-
* for
|
|
27
|
-
*
|
|
26
|
+
* for `withPublicDb`'s handle, a user JWT for `withUserDb`'s — see
|
|
27
|
+
* `context.ts`'s `runTransaction`, which backs both handles' `.transaction()`.
|
|
28
28
|
* @param queries The statements to run, in order. An empty array is a no-op
|
|
29
29
|
* that still makes the round trip, matching `cfni_exec_batch(array[]::text[])`.
|
|
30
30
|
* @returns One `{rows, rowCount}` result per query, in the same order.
|
|
@@ -862,11 +862,11 @@ export interface SupabaseDbConfig {
|
|
|
862
862
|
* aggregates, CTEs, transactions — naming the construct that needs raw
|
|
863
863
|
* SQL. Defaults to `true`.
|
|
864
864
|
*
|
|
865
|
-
* Also gates `
|
|
866
|
-
*
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
865
|
+
* Also gates Supabase-mode `db.transaction(...)`: its `cfni_exec_batch`
|
|
866
|
+
* function ships in the same `supabase/cfni_exec.sql` file and needs
|
|
867
|
+
* `cfni_exec` itself to run each statement, so batching is on whenever
|
|
868
|
+
* this is (there is no separate flag for it) and throws the same
|
|
869
|
+
* install-or-use-`connectionString` error when this is `false`.
|
|
870
870
|
*/
|
|
871
871
|
rawSql?: boolean;
|
|
872
872
|
}
|
|
@@ -916,9 +916,12 @@ export interface DbRoutingConfig {
|
|
|
916
916
|
* live traffic. Requires the `cfni_exec` function from
|
|
917
917
|
* `supabase/cfni_exec.sql` to be installed in your database.
|
|
918
918
|
*
|
|
919
|
-
*
|
|
920
|
-
*
|
|
921
|
-
*
|
|
919
|
+
* Each statement inside a plain `withUserDb` callback is its own
|
|
920
|
+
* round-trip — no shared session. Call `.transaction(...)` on the handle
|
|
921
|
+
* for atomicity across statements instead: it batches them into one
|
|
922
|
+
* `cfni_exec_batch` call, though (unlike connection-string mode) a later
|
|
923
|
+
* statement in the callback cannot read an earlier one's result — see
|
|
924
|
+
* the `db` entry point's module doc.
|
|
922
925
|
*/
|
|
923
926
|
supabase?: SupabaseDbConfig;
|
|
924
927
|
/**
|