cloudflare-next-intl 0.8.16 → 0.8.18
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 +9 -30
- package/dist/src/db/connection.d.ts +13 -0
- package/dist/src/db/connection.js +52 -25
- package/dist/src/db/context.js +49 -9
- package/package.json +1 -1
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
|
-
|
|
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
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { Client } from 'pg';
|
|
2
2
|
import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
|
|
3
3
|
export type DbConfig = RoutingConfig<Locales, LocalePrefixMode>;
|
|
4
|
+
/**
|
|
5
|
+
* Runs `fn` exclusively against the shared client: no other
|
|
6
|
+
* `withSessionLock` caller's queries can interleave with `fn`'s until it
|
|
7
|
+
* settles. `serializeQueries` alone only orders individual `.query()` calls —
|
|
8
|
+
* it does nothing to stop a *different* concurrent request's queries from
|
|
9
|
+
* landing between, say, a transaction's `BEGIN`/`SET LOCAL ROLE` and its
|
|
10
|
+
* `COMMIT` on the one `pg.Client` every request in the isolate shares. That
|
|
11
|
+
* gap let one request's role/RLS identity leak into another's queries
|
|
12
|
+
* whenever two requests overlapped in the same Worker isolate. Every caller
|
|
13
|
+
* that opens a transaction, or otherwise depends on session-scoped state
|
|
14
|
+
* (`SET LOCAL`, `set_config(..., true)`), MUST run inside this lock.
|
|
15
|
+
*/
|
|
16
|
+
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
4
17
|
/**
|
|
5
18
|
* Forgets the cached client and connection string so the next
|
|
6
19
|
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
@@ -5,9 +5,33 @@ const DEFAULT_DISCONNECT_TIMEOUT_MS = 2000;
|
|
|
5
5
|
let connectionString = null;
|
|
6
6
|
let client = null;
|
|
7
7
|
let connectionPromise = null;
|
|
8
|
-
let connectingPromise = null;
|
|
9
8
|
let disconnectionPromise = null;
|
|
10
9
|
let activeUsers = 0;
|
|
10
|
+
let sessionLock = Promise.resolve();
|
|
11
|
+
/**
|
|
12
|
+
* Runs `fn` exclusively against the shared client: no other
|
|
13
|
+
* `withSessionLock` caller's queries can interleave with `fn`'s until it
|
|
14
|
+
* settles. `serializeQueries` alone only orders individual `.query()` calls —
|
|
15
|
+
* it does nothing to stop a *different* concurrent request's queries from
|
|
16
|
+
* landing between, say, a transaction's `BEGIN`/`SET LOCAL ROLE` and its
|
|
17
|
+
* `COMMIT` on the one `pg.Client` every request in the isolate shares. That
|
|
18
|
+
* gap let one request's role/RLS identity leak into another's queries
|
|
19
|
+
* whenever two requests overlapped in the same Worker isolate. Every caller
|
|
20
|
+
* that opens a transaction, or otherwise depends on session-scoped state
|
|
21
|
+
* (`SET LOCAL`, `set_config(..., true)`), MUST run inside this lock.
|
|
22
|
+
*/
|
|
23
|
+
export async function withSessionLock(fn) {
|
|
24
|
+
const previous = sessionLock;
|
|
25
|
+
let release;
|
|
26
|
+
sessionLock = new Promise((resolve) => { release = resolve; });
|
|
27
|
+
await previous;
|
|
28
|
+
try {
|
|
29
|
+
return await fn();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
release();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
11
35
|
/**
|
|
12
36
|
* Forgets the cached client and connection string so the next
|
|
13
37
|
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
@@ -21,9 +45,9 @@ export function resetConnectionState() {
|
|
|
21
45
|
connectionString = null;
|
|
22
46
|
client = null;
|
|
23
47
|
connectionPromise = null;
|
|
24
|
-
connectingPromise = null;
|
|
25
48
|
disconnectionPromise = null;
|
|
26
49
|
activeUsers = 0;
|
|
50
|
+
sessionLock = Promise.resolve();
|
|
27
51
|
}
|
|
28
52
|
/**
|
|
29
53
|
* Resolves the connection string from `db.connectionString`, awaiting it when
|
|
@@ -76,31 +100,35 @@ export default async function connectToPostgres(config, resolved) {
|
|
|
76
100
|
const db = config.db;
|
|
77
101
|
requireDbConfig(db);
|
|
78
102
|
await disconnectionPromise;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// next call retries instead of replaying the same rejection for the
|
|
93
|
-
// life of the Worker isolate.
|
|
94
|
-
connectingPromise = null;
|
|
95
|
-
connectionString = null;
|
|
96
|
-
client = null;
|
|
97
|
-
connectionPromise = null;
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
103
|
+
// Guards the race between concurrent callers that both see `client ===
|
|
104
|
+
// null` before either has awaited anything: `connectionPromise` is set
|
|
105
|
+
// synchronously (before the `await`s below) so every caller in the same
|
|
106
|
+
// microtask tick shares the one client being created instead of each
|
|
107
|
+
// starting its own.
|
|
108
|
+
if (connectionPromise === null) {
|
|
109
|
+
connectionPromise = (async () => {
|
|
110
|
+
connectionString = resolved ?? await resolveConnectionString(db);
|
|
111
|
+
const { Client } = await import('pg');
|
|
112
|
+
const created = serializeQueries(new Client({ connectionString }));
|
|
113
|
+
client = created;
|
|
114
|
+
await created.connect();
|
|
115
|
+
return created;
|
|
100
116
|
})();
|
|
101
117
|
}
|
|
102
118
|
activeUsers++;
|
|
103
|
-
|
|
119
|
+
try {
|
|
120
|
+
return await connectionPromise;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
// A failed connect must not be cached forever — clear state so the
|
|
124
|
+
// next call retries instead of replaying the same rejection for the
|
|
125
|
+
// life of the Worker isolate.
|
|
126
|
+
activeUsers = Math.max(0, activeUsers - 1);
|
|
127
|
+
connectionString = null;
|
|
128
|
+
client = null;
|
|
129
|
+
connectionPromise = null;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
104
132
|
}
|
|
105
133
|
/**
|
|
106
134
|
* Releases one caller's hold on the shared client, closing it once the last
|
|
@@ -127,7 +155,6 @@ export function disconnectPostgres(config) {
|
|
|
127
155
|
// instead of reusing one that is about to close.
|
|
128
156
|
client = null;
|
|
129
157
|
connectionPromise = null;
|
|
130
|
-
connectingPromise = null;
|
|
131
158
|
const endPromise = closing.end();
|
|
132
159
|
disconnectionPromise = endPromise;
|
|
133
160
|
const timeoutMs = db.disconnectTimeoutMs ?? DEFAULT_DISCONNECT_TIMEOUT_MS;
|
package/dist/src/db/context.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import config from '../config/intl_config';
|
|
2
2
|
import requireDbConfig from './require_config';
|
|
3
|
-
import connectToPostgres, { disconnectPostgres } from './connection';
|
|
3
|
+
import connectToPostgres, { disconnectPostgres, withSessionLock } from './connection';
|
|
4
4
|
import resolveDbMode from './resolve_mode';
|
|
5
5
|
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
|
|
@@ -100,8 +131,12 @@ export async function withPublicDb(fn) {
|
|
|
100
131
|
}
|
|
101
132
|
const client = await connectToPostgres(config, resolved.connectionString);
|
|
102
133
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
134
|
+
return await withSessionLock(async () => {
|
|
135
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
136
|
+
const drizzleHandle = drizzle(client);
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
138
|
+
return await fn(await postgresDb(drizzleHandle, client));
|
|
139
|
+
});
|
|
105
140
|
}
|
|
106
141
|
finally {
|
|
107
142
|
disconnectPostgres(config);
|
|
@@ -153,12 +188,17 @@ export async function withUserDb(fn, uid) {
|
|
|
153
188
|
const client = await connectToPostgres(config, resolved.connectionString);
|
|
154
189
|
const role = db.authenticatedRole ?? DEFAULT_ROLE;
|
|
155
190
|
try {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
await transaction
|
|
160
|
-
|
|
161
|
-
|
|
191
|
+
return await withSessionLock(async () => {
|
|
192
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
193
|
+
const { sql } = await import('drizzle-orm');
|
|
194
|
+
return await drizzle(client).transaction(async (transaction) => {
|
|
195
|
+
await transaction.execute(sql `select set_config('request.jwt.claims', ${JSON.stringify({ sub: userId })}, true)`);
|
|
196
|
+
await transaction.execute(sql `set local role ${sql.raw(role)}`);
|
|
197
|
+
// The transaction handle's session.client is the live pg socket — use it directly.
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
199
|
+
const txClient = transaction.session?.client ?? client;
|
|
200
|
+
return fn(await postgresDb(transaction, txClient));
|
|
201
|
+
});
|
|
162
202
|
});
|
|
163
203
|
}
|
|
164
204
|
finally {
|