cloudflare-next-intl 0.8.18 → 0.8.19
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 +6 -4
- package/dist/src/db/connection.d.ts +11 -6
- package/dist/src/db/connection.js +11 -6
- package/dist/src/db/context.d.ts +19 -13
- package/dist/src/db/context.js +68 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -467,8 +467,10 @@ export default setIntlConfig({
|
|
|
467
467
|
Hyperdrive immediately). Set `false` to keep the connection open for the
|
|
468
468
|
lifetime of the isolate — faster for a long-lived server, but it holds a
|
|
469
469
|
Hyperdrive connection slot between requests.
|
|
470
|
-
- `authenticatedRole` — Postgres role
|
|
471
|
-
|
|
470
|
+
- `authenticatedRole` — Postgres role `withUserDb` switches the shared
|
|
471
|
+
session to for the duration of your callback (`set role`, reset once it
|
|
472
|
+
settles — no transaction involved). Defaults to `'authenticated'` (the
|
|
473
|
+
Supabase RLS convention).
|
|
472
474
|
- `getUserId` — resolves the user id injected as
|
|
473
475
|
`request.jwt.claims->>'sub'` inside `withUserDb`. Omit when
|
|
474
476
|
`firebaseAuth` is configured — the uid then comes from this package's own
|
|
@@ -600,9 +602,9 @@ const [invitationResult, grantResult] = await withUserDb((db) =>
|
|
|
600
602
|
);
|
|
601
603
|
```
|
|
602
604
|
|
|
603
|
-
Every query in the array is executed sequentially in a single transaction
|
|
605
|
+
Every query in the array is executed sequentially in a single transaction block/batch:
|
|
604
606
|
- 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
|
|
607
|
+
- In **connection-string mode**, they are run sequentially over the Postgres client inside a real `BEGIN`/`COMMIT` transaction. Note this is separate from — and does not run inside — the `SET`/role switch `withUserDb` itself applies to the shared session; see [Choosing a transport](#choosing-a-transport) above.
|
|
606
608
|
|
|
607
609
|
Either way, a failure on any statement rolls back every statement that ran before it in the transaction.
|
|
608
610
|
|
|
@@ -6,12 +6,17 @@ export type DbConfig = RoutingConfig<Locales, LocalePrefixMode>;
|
|
|
6
6
|
* `withSessionLock` caller's queries can interleave with `fn`'s until it
|
|
7
7
|
* settles. `serializeQueries` alone only orders individual `.query()` calls —
|
|
8
8
|
* it does nothing to stop a *different* concurrent request's queries from
|
|
9
|
-
* landing between, say,
|
|
10
|
-
*
|
|
11
|
-
* gap let one request's role/RLS identity leak into another's queries
|
|
12
|
-
* whenever two requests overlapped in the same Worker isolate
|
|
13
|
-
*
|
|
14
|
-
*
|
|
9
|
+
* landing between, say, `set role`/`set_config(...)` and the query that
|
|
10
|
+
* depends on it, on the one `pg.Client` every request in the isolate shares.
|
|
11
|
+
* That gap let one request's role/RLS identity leak into another's queries
|
|
12
|
+
* whenever two requests overlapped in the same Worker isolate — and, when
|
|
13
|
+
* `withUserDb` used to wrap its call in a real `BEGIN`/`COMMIT` transaction,
|
|
14
|
+
* interleaved transaction boundaries from two overlapping callers made
|
|
15
|
+
* Postgres itself reject statements ("already a transaction in progress").
|
|
16
|
+
* `withUserDb` no longer opens a transaction on this shared client for
|
|
17
|
+
* exactly that reason (session-scoped `set role`/`set_config(..., false)`
|
|
18
|
+
* need no transaction to apply) — but every caller that still depends on
|
|
19
|
+
* session-scoped state on the shared client MUST run inside this lock.
|
|
15
20
|
*/
|
|
16
21
|
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
17
22
|
/**
|
|
@@ -13,12 +13,17 @@ let sessionLock = Promise.resolve();
|
|
|
13
13
|
* `withSessionLock` caller's queries can interleave with `fn`'s until it
|
|
14
14
|
* settles. `serializeQueries` alone only orders individual `.query()` calls —
|
|
15
15
|
* it does nothing to stop a *different* concurrent request's queries from
|
|
16
|
-
* landing between, say,
|
|
17
|
-
*
|
|
18
|
-
* gap let one request's role/RLS identity leak into another's queries
|
|
19
|
-
* whenever two requests overlapped in the same Worker isolate
|
|
20
|
-
*
|
|
21
|
-
*
|
|
16
|
+
* landing between, say, `set role`/`set_config(...)` and the query that
|
|
17
|
+
* depends on it, on the one `pg.Client` every request in the isolate shares.
|
|
18
|
+
* That gap let one request's role/RLS identity leak into another's queries
|
|
19
|
+
* whenever two requests overlapped in the same Worker isolate — and, when
|
|
20
|
+
* `withUserDb` used to wrap its call in a real `BEGIN`/`COMMIT` transaction,
|
|
21
|
+
* interleaved transaction boundaries from two overlapping callers made
|
|
22
|
+
* Postgres itself reject statements ("already a transaction in progress").
|
|
23
|
+
* `withUserDb` no longer opens a transaction on this shared client for
|
|
24
|
+
* exactly that reason (session-scoped `set role`/`set_config(..., false)`
|
|
25
|
+
* need no transaction to apply) — but every caller that still depends on
|
|
26
|
+
* session-scoped state on the shared client MUST run inside this lock.
|
|
22
27
|
*/
|
|
23
28
|
export async function withSessionLock(fn) {
|
|
24
29
|
const previous = sessionLock;
|
package/dist/src/db/context.d.ts
CHANGED
|
@@ -31,22 +31,28 @@ export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>): Prom
|
|
|
31
31
|
/**
|
|
32
32
|
* Runs a query as the **signed-in user**.
|
|
33
33
|
*
|
|
34
|
-
* In connection-string mode this
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
34
|
+
* In connection-string mode this sets the resolved user id as
|
|
35
|
+
* `auth.jwt()->>'sub'` and switches to `db.authenticatedRole` on the
|
|
36
|
+
* request's shared session (via `set_config(..., false)`/`set role`, reset
|
|
37
|
+
* once `fn` settles), so RLS policies behave exactly as they do for a
|
|
38
|
+
* PostgREST-issued call — but it does NOT open a `BEGIN`/`COMMIT`
|
|
39
|
+
* transaction: that client is shared across every concurrent caller in the
|
|
40
|
+
* isolate (see `connection.ts`), and a live transaction is itself
|
|
41
|
+
* session-scoped state that a second overlapping caller's transaction would
|
|
42
|
+
* collide with. Call `fn`'s own `.transaction(...)` (below) for atomicity
|
|
43
|
+
* across statements instead. In Supabase mode identity instead rides on the
|
|
44
|
+
* JWT sent as `Authorization: Bearer` — PostgREST resolves the
|
|
45
|
+
* `authenticated` role and populates `request.jwt.claims` itself, and each
|
|
46
|
+
* statement is its own round-trip with no cross-statement transaction unless
|
|
47
|
+
* you call `.transaction(...)` on the handle (the Postgres proxy Drizzle
|
|
48
|
+
* uses in this mode cannot open a real session, so that runs as one atomic
|
|
43
49
|
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
44
50
|
* the wrapper to use for anything user-owned.
|
|
45
51
|
*
|
|
46
|
-
* @param fn Receives the Drizzle handle
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
52
|
+
* @param fn Receives the Drizzle handle, scoped to the caller's identity/role
|
|
53
|
+
* but not wrapped in a transaction — call its own `.transaction(...)` for
|
|
54
|
+
* atomicity across statements (build-and-return shape in both modes, not a
|
|
55
|
+
* live session — see the module doc).
|
|
50
56
|
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
51
57
|
* pass `null`, in normal use — either way the id then comes from
|
|
52
58
|
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
package/dist/src/db/context.js
CHANGED
|
@@ -70,17 +70,32 @@ async function postgresDb(drizzleHandle, rawClient) {
|
|
|
70
70
|
/**
|
|
71
71
|
* Postgres-mode equivalent of `runTransaction`: calls `build` with a
|
|
72
72
|
* build-only handle, then executes each returned query on the raw pg client
|
|
73
|
-
* via an inline-parameterised `query()` call
|
|
73
|
+
* via an inline-parameterised `query()` call, wrapped in a real
|
|
74
|
+
* `BEGIN`/`COMMIT` for atomicity, and returns `ExecResult[]`.
|
|
75
|
+
*
|
|
76
|
+
* Safe to open a transaction here — unlike `withUserDb`'s own body, this
|
|
77
|
+
* only ever runs from inside `fn`, which `withUserDb`/`withPublicDb` already
|
|
78
|
+
* call from within `withSessionLock` (`connection.ts`). That lock is what
|
|
79
|
+
* keeps this `BEGIN`...`COMMIT` from overlapping with another caller's on the
|
|
80
|
+
* same shared client, so nothing here needs to reason about interleaving.
|
|
74
81
|
*/
|
|
75
82
|
async function runPostgresTransaction(rawClient, build) {
|
|
76
83
|
const queries = await build(buildOnlyDb());
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
84
|
+
await rawClient.query('begin');
|
|
85
|
+
try {
|
|
86
|
+
const results = [];
|
|
87
|
+
for (const q of queries) {
|
|
88
|
+
const statement = inlineParams(q.sql, q.params);
|
|
89
|
+
const res = await rawClient.query(statement);
|
|
90
|
+
results.push({ rows: res.rows ?? [], rowCount: res.rowCount ?? null });
|
|
91
|
+
}
|
|
92
|
+
await rawClient.query('commit');
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
await rawClient.query('rollback');
|
|
97
|
+
throw error;
|
|
82
98
|
}
|
|
83
|
-
return results;
|
|
84
99
|
}
|
|
85
100
|
/**
|
|
86
101
|
* Builds a Drizzle handle with no working transport, for Supabase-mode
|
|
@@ -145,22 +160,28 @@ export async function withPublicDb(fn) {
|
|
|
145
160
|
/**
|
|
146
161
|
* Runs a query as the **signed-in user**.
|
|
147
162
|
*
|
|
148
|
-
* In connection-string mode this
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
163
|
+
* In connection-string mode this sets the resolved user id as
|
|
164
|
+
* `auth.jwt()->>'sub'` and switches to `db.authenticatedRole` on the
|
|
165
|
+
* request's shared session (via `set_config(..., false)`/`set role`, reset
|
|
166
|
+
* once `fn` settles), so RLS policies behave exactly as they do for a
|
|
167
|
+
* PostgREST-issued call — but it does NOT open a `BEGIN`/`COMMIT`
|
|
168
|
+
* transaction: that client is shared across every concurrent caller in the
|
|
169
|
+
* isolate (see `connection.ts`), and a live transaction is itself
|
|
170
|
+
* session-scoped state that a second overlapping caller's transaction would
|
|
171
|
+
* collide with. Call `fn`'s own `.transaction(...)` (below) for atomicity
|
|
172
|
+
* across statements instead. In Supabase mode identity instead rides on the
|
|
173
|
+
* JWT sent as `Authorization: Bearer` — PostgREST resolves the
|
|
174
|
+
* `authenticated` role and populates `request.jwt.claims` itself, and each
|
|
175
|
+
* statement is its own round-trip with no cross-statement transaction unless
|
|
176
|
+
* you call `.transaction(...)` on the handle (the Postgres proxy Drizzle
|
|
177
|
+
* uses in this mode cannot open a real session, so that runs as one atomic
|
|
157
178
|
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
158
179
|
* the wrapper to use for anything user-owned.
|
|
159
180
|
*
|
|
160
|
-
* @param fn Receives the Drizzle handle
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
181
|
+
* @param fn Receives the Drizzle handle, scoped to the caller's identity/role
|
|
182
|
+
* but not wrapped in a transaction — call its own `.transaction(...)` for
|
|
183
|
+
* atomicity across statements (build-and-return shape in both modes, not a
|
|
184
|
+
* live session — see the module doc).
|
|
164
185
|
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
165
186
|
* pass `null`, in normal use — either way the id then comes from
|
|
166
187
|
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
@@ -189,16 +210,34 @@ export async function withUserDb(fn, uid) {
|
|
|
189
210
|
const role = db.authenticatedRole ?? DEFAULT_ROLE;
|
|
190
211
|
try {
|
|
191
212
|
return await withSessionLock(async () => {
|
|
213
|
+
// No `db.transaction()`/BEGIN here on purpose: this client is shared
|
|
214
|
+
// across every concurrent caller in the isolate (see connection.ts),
|
|
215
|
+
// and a live Postgres transaction is session state — `BEGIN`,
|
|
216
|
+
// `SET LOCAL ROLE`, and `COMMIT`/`ROLLBACK` all apply to whichever
|
|
217
|
+
// statement runs next on the socket, not to `fn`'s call site. Two
|
|
218
|
+
// overlapping `withUserDb` calls each opening their own transaction
|
|
219
|
+
// interleaved their BEGIN/SET LOCAL/COMMIT on one socket — Postgres
|
|
220
|
+
// then rejected the interleaved statements ("already a transaction
|
|
221
|
+
// in progress", "no transaction in progress"), which is exactly the
|
|
222
|
+
// bug `withSessionLock` alone could not fully close. Session-scoped
|
|
223
|
+
// `set_config(..., true)` (`true` = for the rest of the session, not
|
|
224
|
+
// just a transaction) and a plain `set role` need no transaction to
|
|
225
|
+
// apply, and `withSessionLock` already keeps this whole block
|
|
226
|
+
// exclusive against every other caller, so atomicity is unaffected.
|
|
227
|
+
const rawClient = client;
|
|
228
|
+
await rawClient.query(`select set_config('request.jwt.claims', $1, false)`, [JSON.stringify({ sub: userId })]);
|
|
229
|
+
await rawClient.query(`set role "${role}"`);
|
|
192
230
|
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
await
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
231
|
+
const drizzleHandle = drizzle(client);
|
|
232
|
+
try {
|
|
233
|
+
return await fn(await postgresDb(drizzleHandle, rawClient));
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
// Always hand the shared socket back to the default role —
|
|
237
|
+
// otherwise the next caller (public or a different user) could
|
|
238
|
+
// inherit this request's elevated/authenticated role.
|
|
239
|
+
await rawClient.query('reset role');
|
|
240
|
+
}
|
|
202
241
|
});
|
|
203
242
|
}
|
|
204
243
|
finally {
|