cloudflare-next-intl 0.8.16 → 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
@@ -584,17 +584,9 @@ below for the `.transaction()` API that does.
584
584
 
585
585
  Call `.transaction(...)` on the handle `withUserDb`/`withPublicDb` hand your
586
586
  callback whenever a write needs more than one statement to succeed or fail
587
- together — same method name in both transport modes.
587
+ together — same method name and same signature in both transport modes.
588
588
 
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:
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.
598
590
 
599
591
  ```typescript
600
592
  import { withUserDb } from "cloudflare-next-intl/db";
@@ -608,26 +600,13 @@ const [invitationResult, grantResult] = await withUserDb((db) =>
608
600
  );
609
601
  ```
610
602
 
611
- Every query in the array is rendered and sent to Postgres as **one**
612
- `cfni_exec_batch` call: the function runs each statement in order inside a
613
- single plpgsql call, which is itself an implicit transaction, so a failure on
614
- any statement rolls back every statement that ran before it in the same
615
- batch. `cfni_exec_batch` ships alongside `cfni_exec` in the same
616
- `supabase/cfni_exec.sql` file (installed the same way — see
617
- [Schema codegen](#schema-codegen-cfni-db-codegen) below) and is available
618
- whenever `cfni_exec` is: there is no separate config flag, and
619
- `db.supabase.rawSql: false` turns off both.
620
-
621
- Each result is the same `{ rows, rowCount }` shape a single `cfni_exec` call
622
- returns — decode rows the same way you would from `db.execute(sql\`...\`)`.
623
-
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.
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.
606
+
607
+ Either way, a failure on any statement rolls back every statement that ran before it in the transaction.
608
+
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.
631
610
 
632
611
  #### Supabase mode and REST translation
633
612
 
@@ -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`
@@ -51,6 +52,36 @@ async function supabaseDb(supabase, bearerToken) {
51
52
  },
52
53
  });
53
54
  }
55
+ /**
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
+ }
54
85
  /**
55
86
  * Builds a Drizzle handle with no working transport, for Supabase-mode
56
87
  * `db.transaction(...)` callbacks. Query builders' `.toSQL()` never touches
@@ -101,7 +132,9 @@ export async function withPublicDb(fn) {
101
132
  const client = await connectToPostgres(config, resolved.connectionString);
102
133
  try {
103
134
  const { drizzle } = await import('drizzle-orm/node-postgres');
104
- 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));
105
138
  }
106
139
  finally {
107
140
  disconnectPostgres(config);
@@ -158,7 +191,10 @@ export async function withUserDb(fn, uid) {
158
191
  return await drizzle(client).transaction(async (transaction) => {
159
192
  await transaction.execute(sql `select set_config('request.jwt.claims', ${JSON.stringify({ sub: userId })}, true)`);
160
193
  await transaction.execute(sql `set local role ${sql.raw(role)}`);
161
- 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));
162
198
  });
163
199
  }
164
200
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.8.16",
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",