cloudflare-next-intl 0.8.14 → 0.8.16
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 +39 -42
- package/dist/src/db/context.d.ts +12 -49
- package/dist/src/db/context.js +43 -102
- package/dist/src/db/helpers.d.ts +4 -0
- package/dist/src/db/helpers.js +8 -0
- 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,31 +576,36 @@ 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 in both transport modes.
|
|
592
588
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
589
|
+
In connection-string mode this is a real Drizzle transaction: `await
|
|
590
|
+
db.transaction(async (tx) => { await tx.insert(...); await tx.update(...); })`,
|
|
591
|
+
and a later statement may use an earlier one's result, exactly like plain
|
|
592
|
+
Drizzle usage.
|
|
593
|
+
|
|
594
|
+
In Supabase mode there is no shared session for `.transaction()` to open, so
|
|
595
|
+
its callback instead **builds** queries rather than executing them — call
|
|
596
|
+
`.toSQL()` on each Drizzle query and return the array, rather than `await`ing
|
|
597
|
+
the query directly:
|
|
596
598
|
|
|
597
599
|
```typescript
|
|
598
|
-
import {
|
|
600
|
+
import { withUserDb } from "cloudflare-next-intl/db";
|
|
599
601
|
import { invitations, contractorAccessGrants } from "@/shared/db/generated/schema";
|
|
600
602
|
|
|
601
|
-
const [invitationResult, grantResult] = await
|
|
602
|
-
db.
|
|
603
|
-
|
|
604
|
-
|
|
603
|
+
const [invitationResult, grantResult] = await withUserDb((db) =>
|
|
604
|
+
db.transaction((tx) => [
|
|
605
|
+
tx.insert(invitations).values({ email: "a@b.com" }).returning().toSQL(),
|
|
606
|
+
tx.insert(contractorAccessGrants).values({ propertyId: 1 }).toSQL(),
|
|
607
|
+
]),
|
|
608
|
+
);
|
|
605
609
|
```
|
|
606
610
|
|
|
607
611
|
Every query in the array is rendered and sent to Postgres as **one**
|
|
@@ -617,14 +621,13 @@ whenever `cfni_exec` is: there is no separate config flag, and
|
|
|
617
621
|
Each result is the same `{ rows, rowCount }` shape a single `cfni_exec` call
|
|
618
622
|
returns — decode rows the same way you would from `db.execute(sql\`...\`)`.
|
|
619
623
|
|
|
620
|
-
`await`ing a query directly inside
|
|
621
|
-
throws immediately, naming the
|
|
622
|
-
running that one statement outside
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
that path.
|
|
624
|
+
`await`ing a query directly inside the Supabase-mode `.transaction()`
|
|
625
|
+
callback (instead of calling `.toSQL()`) throws immediately, naming the
|
|
626
|
+
mistake, rather than hanging or silently running that one statement outside
|
|
627
|
+
the batch with no atomicity. This also means, unlike connection-string mode,
|
|
628
|
+
a later statement in a Supabase-mode `.transaction()` callback cannot read an
|
|
629
|
+
earlier one's result — build every statement from arguments/closures you
|
|
630
|
+
already have.
|
|
628
631
|
|
|
629
632
|
#### Supabase mode and REST translation
|
|
630
633
|
|
|
@@ -666,8 +669,8 @@ await withPublicDb((db) => db.delete(bonds).where(eq(bonds.id, 1)));
|
|
|
666
669
|
| `RETURNING` clauses | Supported | Supported |
|
|
667
670
|
| Operators: `=`, `<>`, `!=`, `>`, `>=`, `<`, `<=`, `like`, `ilike`, `is [not] null`, `[not] in`, `is [not] distinct from`, `~`, `~*`, `@>`, `<@`, `&&`, `>>`, `<<`, `&>`, `&<`, `-|-`, `@@` | Supported | Supported |
|
|
668
671
|
| 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 `
|
|
672
|
+
| Multi-statement transactions without calling `.transaction()` | Not supported | Not supported (each call is its own round-trip) |
|
|
673
|
+
| Multi-statement transactions via `db.transaction()` | N/A — build-and-return API | Supported (`cfni_exec_batch`, one round-trip) |
|
|
671
674
|
|
|
672
675
|
#### Enforcing single API via ESLint (`cloudflare-next-intl/dbEslint`)
|
|
673
676
|
|
|
@@ -682,9 +685,10 @@ export default [
|
|
|
682
685
|
];
|
|
683
686
|
```
|
|
684
687
|
|
|
685
|
-
|
|
686
|
-
allowed to see the rows
|
|
687
|
-
succeed or
|
|
688
|
+
Two query wrappers, both from `cloudflare-next-intl/db`. Choose by who is
|
|
689
|
+
allowed to see the rows; call `.transaction(...)` on the handle either one
|
|
690
|
+
hands your callback when a write needs more than one statement to succeed or
|
|
691
|
+
fail together (see [Multi-statement transactions](#multi-statement-transactions-dbtransaction) above):
|
|
688
692
|
|
|
689
693
|
- `withPublicDb(fn)` — runs `fn` as the anonymous role: a pooled connection
|
|
690
694
|
with no transaction/role switch in connection-string mode, or the anon key
|
|
@@ -701,13 +705,6 @@ succeed or fail together:
|
|
|
701
705
|
the id comes from `db.getUserId()` if set, otherwise automatically from the
|
|
702
706
|
signed-in Firebase user when `firebaseAuth` is configured — you rarely need
|
|
703
707
|
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
708
|
|
|
712
709
|
```typescript
|
|
713
710
|
// anywhere on the server
|
|
@@ -798,7 +795,7 @@ your project — `--rpc-dir` (defaulting inside `--ddl-dir`, so `rpcs` under
|
|
|
798
795
|
always holding the current version of the function, without a manual
|
|
799
796
|
copy-paste step. Since the file now ships both `cfni_exec` and
|
|
800
797
|
`cfni_exec_batch` (see
|
|
801
|
-
[Multi-statement transactions](#multi-statement-transactions-
|
|
798
|
+
[Multi-statement transactions](#multi-statement-transactions-dbtransaction)
|
|
802
799
|
above), pass `--rpc-file-name=`/`--tests-file-name=` (or
|
|
803
800
|
`CFNI_DB_RPC_FILE_NAME`/`CFNI_DB_TESTS_FILE_NAME`) if you'd rather install it
|
|
804
801
|
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
|
@@ -33,35 +33,38 @@ async function resolveUserId(uid) {
|
|
|
33
33
|
/**
|
|
34
34
|
* Builds a Drizzle handle backed by PostgREST. `bearerToken` decides the role
|
|
35
35
|
* Postgres sees: the anon key for public access, a user JWT for `withUserDb`.
|
|
36
|
+
*
|
|
37
|
+
* `.transaction()` on this handle runs atomically via `cfni_exec_batch` (see
|
|
38
|
+
* {@link runTransaction}) — pg-proxy has no session to open a real
|
|
39
|
+
* transaction over, so every statement the callback returns is queued and
|
|
40
|
+
* sent as one PostgREST round trip instead. Because of that, the callback
|
|
41
|
+
* must *build* its queries (`.toSQL()`), not `await`/execute them — a later
|
|
42
|
+
* statement cannot read an earlier one's result the way it could inside a
|
|
43
|
+
* real session, unlike connection-string mode's `.transaction()`.
|
|
36
44
|
*/
|
|
37
45
|
async function supabaseDb(supabase, bearerToken) {
|
|
38
46
|
const { drizzle } = await import('drizzle-orm/pg-proxy');
|
|
39
47
|
const db = drizzle(createSupabaseTransport(supabase, bearerToken));
|
|
40
48
|
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.');
|
|
49
|
+
transaction(build) {
|
|
50
|
+
return runTransaction(supabase, bearerToken, build);
|
|
48
51
|
},
|
|
49
52
|
});
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
|
-
* Builds a Drizzle handle with no working transport, for
|
|
53
|
-
* `
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
55
|
+
* Builds a Drizzle handle with no working transport, for Supabase-mode
|
|
56
|
+
* `db.transaction(...)` callbacks. Query builders' `.toSQL()` never touches
|
|
57
|
+
* the session, so this is safe to hand out purely for building statements —
|
|
58
|
+
* but `await`ing a query directly (instead of collecting its `.toSQL()`
|
|
59
|
+
* output) throws immediately here instead of hanging or silently running
|
|
60
|
+
* outside the batch.
|
|
58
61
|
*/
|
|
59
62
|
function buildOnlyDb() {
|
|
60
63
|
const throwIfExecuted = () => {
|
|
61
64
|
throw new Error('db: this Drizzle handle is for building statements only — call `.toSQL()` on each ' +
|
|
62
65
|
'query and return the array, do not `await`/execute it directly. Awaiting a query ' +
|
|
63
|
-
'inside a
|
|
64
|
-
'
|
|
66
|
+
'inside a Supabase-mode db.transaction() callback runs it outside the batch, with no ' +
|
|
67
|
+
'atomicity, which is exactly what `.transaction()` exists to prevent.');
|
|
65
68
|
};
|
|
66
69
|
return new Proxy({}, { get: () => throwIfExecuted });
|
|
67
70
|
}
|
|
@@ -76,7 +79,9 @@ function buildOnlyDb() {
|
|
|
76
79
|
* In connection-string mode the connection is taken from the request's
|
|
77
80
|
* shared client and released when `fn` settles, even if it throws. In
|
|
78
81
|
* Supabase mode there is no connection to release — each call is one
|
|
79
|
-
* PostgREST round-trip authenticated as the anon key.
|
|
82
|
+
* PostgREST round-trip authenticated as the anon key. Either way, call
|
|
83
|
+
* `.transaction(...)` on the handle `fn` receives for atomicity across more
|
|
84
|
+
* than one statement — see the module doc for the shape that takes in each mode.
|
|
80
85
|
*
|
|
81
86
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
82
87
|
* @returns Whatever `fn` resolves to.
|
|
@@ -111,13 +116,16 @@ export async function withPublicDb(fn) {
|
|
|
111
116
|
* PostgREST-issued call. In Supabase mode identity instead rides on the JWT
|
|
112
117
|
* sent as `Authorization: Bearer` — PostgREST resolves the `authenticated`
|
|
113
118
|
* role and populates `request.jwt.claims` itself, and each statement is its
|
|
114
|
-
* own round-trip with no cross-statement transaction
|
|
115
|
-
*
|
|
116
|
-
*
|
|
119
|
+
* own round-trip with no cross-statement transaction unless you call
|
|
120
|
+
* `.transaction(...)` on the handle (the Postgres proxy Drizzle uses in this
|
|
121
|
+
* mode cannot open a real session, so that runs as one atomic
|
|
122
|
+
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
123
|
+
* the wrapper to use for anything user-owned.
|
|
117
124
|
*
|
|
118
125
|
* @param fn Receives the Drizzle handle. In connection-string mode it is
|
|
119
|
-
* bound to a transaction; in Supabase mode it is not
|
|
120
|
-
*
|
|
126
|
+
* also bound to a transaction; in Supabase mode it is not, but its own
|
|
127
|
+
* `.transaction(...)` still provides atomicity across statements there
|
|
128
|
+
* (build-and-return shape, not a live session — see the module doc).
|
|
121
129
|
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
122
130
|
* pass `null`, in normal use — either way the id then comes from
|
|
123
131
|
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
@@ -158,95 +166,28 @@ export async function withUserDb(fn, uid) {
|
|
|
158
166
|
}
|
|
159
167
|
}
|
|
160
168
|
/**
|
|
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.
|
|
169
|
+
* Runs several statements atomically over `cfni_exec_batch`, backing
|
|
170
|
+
* Supabase-mode `db.transaction()`. `build` does not execute its queries —
|
|
171
|
+
* it **builds** them and returns the array; call `.toSQL()` on each Drizzle
|
|
172
|
+
* query instead of `await`ing it (`await`ing throws immediately; see
|
|
173
|
+
* {@link buildOnlyDb}). Every query is inlined and sent as one round trip:
|
|
174
|
+
* the Postgres function runs them in order inside a single plpgsql call,
|
|
175
|
+
* which is itself an implicit transaction, so a failure on any statement
|
|
176
|
+
* rolls back every statement before it.
|
|
174
177
|
*
|
|
175
|
-
* @param
|
|
178
|
+
* @param supabase The `db.supabase` config block.
|
|
176
179
|
* @param bearerToken The anon key or user JWT.
|
|
177
180
|
* @param build Returns the queries to run, via `.toSQL()` — never executes them directly.
|
|
178
181
|
* @returns One result per query, in the same order as `build`'s array.
|
|
182
|
+
* @throws If `db.supabase.rawSql` is `false` — `cfni_exec_batch` needs `cfni_exec`, which is disabled too.
|
|
179
183
|
*/
|
|
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;
|
|
184
|
+
async function runTransaction(supabase, bearerToken, build) {
|
|
188
185
|
if (supabase.rawSql === false) {
|
|
189
|
-
throw new Error('db:
|
|
190
|
-
'
|
|
191
|
-
'
|
|
192
|
-
'Postgres connection instead.');
|
|
186
|
+
throw new Error('db: transaction() needs `cfni_exec_batch`, which runs through `cfni_exec` — both are ' +
|
|
187
|
+
'unavailable while `db.supabase.rawSql` is `false`. Install cfni_exec.sql and drop ' +
|
|
188
|
+
'`rawSql: false`, or use `db.connectionString` for a direct Postgres connection instead.');
|
|
193
189
|
}
|
|
194
|
-
return supabase;
|
|
195
|
-
}
|
|
196
|
-
async function runTransaction(supabase, bearerToken, build) {
|
|
197
190
|
const queries = await build(buildOnlyDb());
|
|
198
191
|
const batchQueries = queries.map((query) => ({ sql: query.sql, params: query.params }));
|
|
199
192
|
return runTransactionBatch(supabase, bearerToken, batchQueries);
|
|
200
193
|
}
|
|
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/helpers.d.ts
CHANGED
|
@@ -37,8 +37,12 @@ export declare function excluded<T extends Table>(table: T): {
|
|
|
37
37
|
*/
|
|
38
38
|
export declare function onConflictSet<T extends Table, K extends keyof T["_"]["columns"]>(table: T, fields: K[]): Record<string, SQL>;
|
|
39
39
|
export type TimeUnit = "days" | "hours" | "minutes" | "months" | "years" | "weeks";
|
|
40
|
+
/** General SQL helper returning `now()`. */
|
|
41
|
+
export declare function now(): SQL;
|
|
40
42
|
/** General SQL helper generating a timestamp expression relative to now (`now() - (N unit)::interval`). */
|
|
41
43
|
export declare function ago(amount: number, unit: TimeUnit): SQL;
|
|
44
|
+
/** General SQL helper generating a timestamp expression ahead of now (`now() + (N unit)::interval`). */
|
|
45
|
+
export declare function fromNow(amount: number, unit: TimeUnit): SQL;
|
|
42
46
|
/** General SQL helper returning `current_date`. */
|
|
43
47
|
export declare function currentDate(): SQL;
|
|
44
48
|
/** General SQL helper for window function `count(*) over ()`. */
|
package/dist/src/db/helpers.js
CHANGED
|
@@ -66,10 +66,18 @@ export function onConflictSet(table, fields) {
|
|
|
66
66
|
}
|
|
67
67
|
return setObj;
|
|
68
68
|
}
|
|
69
|
+
/** General SQL helper returning `now()`. */
|
|
70
|
+
export function now() {
|
|
71
|
+
return sql `now()`;
|
|
72
|
+
}
|
|
69
73
|
/** General SQL helper generating a timestamp expression relative to now (`now() - (N unit)::interval`). */
|
|
70
74
|
export function ago(amount, unit) {
|
|
71
75
|
return sql `now() - (${amount} || ' ' || ${unit})::interval`;
|
|
72
76
|
}
|
|
77
|
+
/** General SQL helper generating a timestamp expression ahead of now (`now() + (N unit)::interval`). */
|
|
78
|
+
export function fromNow(amount, unit) {
|
|
79
|
+
return sql `now() + (${amount} || ' ' || ${unit})::interval`;
|
|
80
|
+
}
|
|
73
81
|
/** General SQL helper returning `current_date`. */
|
|
74
82
|
export function currentDate() {
|
|
75
83
|
return sql `current_date`;
|
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
|
/**
|