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 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 `.transaction()` throws there instead of running
564
- non-atomically. Reach for `withUserTransaction`/`withPublicTransaction`
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 round-trip) — see
581
- [Multi-statement transactions](#multi-statement-transactions-withusertransaction-withpublictransaction)
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 (`withUserTransaction`/`withPublicTransaction`)
583
+ #### Multi-statement transactions (`db.transaction()`)
585
584
 
586
- `withUserDb`/`withPublicDb` cannot provide atomicity across statements in
587
- Supabase mode there is no shared session for `.transaction()` to open, so
588
- it throws there instead of silently running non-atomically. Reach for
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
- They take a `build` callback that **builds** queries instead of executing
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 { withUserTransaction } from "cloudflare-next-intl/db";
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 withUserTransaction((db) => [
602
- db.insert(invitations).values({ email: "a@b.com" }).returning().toSQL(),
603
- db.insert(contractorAccessGrants).values({ propertyId: 1 }).toSQL(),
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 rendered and sent to Postgres as **one**
608
- `cfni_exec_batch` call: the function runs each statement in order inside a
609
- single plpgsql call, which is itself an implicit transaction, so a failure on
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
- `await`ing a query directly inside `build` (instead of calling `.toSQL()`)
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
- In connection-string mode, use `withUserDb`/`withPublicDb`'s own
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 in `withUserDb`/`withPublicDb` | Not supported | Not supported (each call is its own round-trip) |
670
- | Multi-statement transactions via `withUserTransaction`/`withPublicTransaction` | N/A — batch-only API | Supported (`cfni_exec_batch`, one round-trip) |
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
- Four query wrappers, all from `cloudflare-next-intl/db`. Choose by who is
686
- allowed to see the rows, then by whether you need more than one statement to
687
- succeed or fail together:
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-withusertransaction-withpublictransaction)
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`.
@@ -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 (the Postgres proxy
41
- * Drizzle uses in this mode cannot open one). Either way this is the wrapper
42
- * to use for anything user-owned.
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 do not rely on
46
- * multi-statement atomicity there.
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 `withUserTransaction`/`withPublicTransaction` batch. */
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[]>;
@@ -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
- // pg-proxy has no session to open a real transaction over — every
42
- // statement is its own PostgREST round-trip — so failing loudly here
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
- * Builds a Drizzle handle with no working transport, for
53
- * `withUserTransaction`/`withPublicTransaction` callbacks in Supabase mode.
54
- * Query builders' `.toSQL()` never touches the session, so this is safe to
55
- * hand out purely for building statements but `await`ing a query directly
56
- * (instead of collecting its `.toSQL()` output) throws immediately here
57
- * instead of hanging or silently running outside the batch.
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 withUserTransaction/withPublicTransaction callback runs it outside the ' +
64
- 'batch, with no atomicity, which is exactly what these wrappers exist to prevent.');
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
- return await fn(drizzle(client));
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 (the Postgres proxy
115
- * Drizzle uses in this mode cannot open one). Either way this is the wrapper
116
- * to use for anything user-owned.
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 do not rely on
120
- * multi-statement atomicity there.
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
- return fn(transaction);
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, whichever transport mode is active.
162
- *
163
- * `build` does not execute its queries — it **builds** them and returns the
164
- * array. Call `.toSQL()` on each Drizzle query instead of `await`ing it
165
- * (`await`ing throws immediately here; see {@link buildOnlyDb}). This is
166
- * only reachable in Supabase mode connection-string mode already has real
167
- * atomicity through `withUserDb`/`withPublicDb`'s own `.transaction()`, so
168
- * `withUserTransaction`/`withPublicTransaction` throw there instead of
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 db The already-resolved `db` config.
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 requireSupabaseTransactionMode(db) {
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: withUserTransaction/withPublicTransaction need `cfni_exec_batch`, which runs through ' +
190
- '`cfni_exec` — both are unavailable while `db.supabase.rawSql` is `false`. Install ' +
191
- 'cfni_exec.sql and drop `rawSql: false`, or use `db.connectionString` for a direct ' +
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
- }
@@ -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
- * - {@link withPublicTransaction} / {@link withUserTransaction} build
12
- * queries with `.toSQL()` instead of executing them; every statement then
13
- * runs atomically, whichever transport mode is active.
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. `withPublicDb`/`withUserDb`'s `.transaction()` is never
27
- * available in Supabase mode — reach for `withPublicTransaction`/
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, withPublicTransaction, withUserTransaction } from './context';
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';
@@ -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
- * - {@link withPublicTransaction} / {@link withUserTransaction} build
12
- * queries with `.toSQL()` instead of executing them; every statement then
13
- * runs atomically, whichever transport mode is active.
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. `withPublicDb`/`withUserDb`'s `.transaction()` is never
27
- * available in Supabase mode — reach for `withPublicTransaction`/
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, withPublicTransaction, withUserTransaction } from './context';
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 {@link ../context.withPublicTransaction}, a user JWT for
24
- * {@link ../context.withUserTransaction}.
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 {@link ../context.withPublicTransaction}, a user JWT for
27
- * {@link ../context.withUserTransaction}.
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 `withUserTransaction`/`withPublicTransaction`: their
866
- * `cfni_exec_batch` function ships in the same `supabase/cfni_exec.sql`
867
- * file and needs `cfni_exec` itself to run each statement, so batching
868
- * is on whenever this is (there is no separate flag for it) and throws
869
- * the same install-or-use-`connectionString` error when this is `false`.
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
- * No multi-statement transactions: unlike connection-string mode, each
920
- * statement inside a `withUserDb` callback is its own round-trip. Do not
921
- * rely on multi-statement atomicity in this mode.
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.8.15",
3
+ "version": "0.8.17",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",