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 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,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 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 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
+ 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 { withUserTransaction } from "cloudflare-next-intl/db";
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 withUserTransaction((db) => [
602
- db.insert(invitations).values({ email: "a@b.com" }).returning().toSQL(),
603
- db.insert(contractorAccessGrants).values({ propertyId: 1 }).toSQL(),
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 `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.
623
-
624
- In connection-string mode, use `withUserDb`/`withPublicDb`'s own
625
- `.transaction()` insteadit already provides real atomicity there, so
626
- `withUserTransaction`/`withPublicTransaction` throw rather than duplicate
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 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) |
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
- 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:
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-withusertransaction-withpublictransaction)
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`.
@@ -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[]>;
@@ -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
- // 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.');
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
- * `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.
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 withUserTransaction/withPublicTransaction callback runs it outside the ' +
64
- 'batch, with no atomicity, which is exactly what these wrappers exist to prevent.');
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 (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.
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 do not rely on
120
- * multi-statement atomicity there.
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, 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.
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 db The already-resolved `db` config.
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 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;
184
+ async function runTransaction(supabase, bearerToken, build) {
188
185
  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.');
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
- }
@@ -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 ()`. */
@@ -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`;
@@ -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.14",
3
+ "version": "0.8.16",
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",