cloudflare-next-intl 0.8.22 → 0.8.24
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 +10 -12
- package/dist/src/db/connection.d.ts +23 -48
- package/dist/src/db/connection.js +90 -202
- package/dist/src/db/context.d.ts +6 -50
- package/dist/src/db/context.js +25 -118
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -1
- package/dist/src/types/types.d.ts +12 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -461,23 +461,21 @@ export default setIntlConfig({
|
|
|
461
461
|
returning one (resolved on each connect). The function form is how you reach
|
|
462
462
|
a value that isn't available at module scope — a Cloudflare Hyperdrive
|
|
463
463
|
binding, or a secret store — as in the example above.
|
|
464
|
-
- `disconnectAfterRequest` —
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
settles — no transaction involved). Defaults to `'authenticated'` (the
|
|
473
|
-
Supabase RLS convention).
|
|
464
|
+
- `disconnectAfterRequest` — **deprecated, ignored since 0.8.23.** Every
|
|
465
|
+
`withPublicDb`/`withUserDb` call now opens and closes its own client, so
|
|
466
|
+
there is no surviving connection to keep open. Hyperdrive pools the
|
|
467
|
+
server-side connection.
|
|
468
|
+
- `authenticatedRole` — Postgres role `withUserDb` switches its
|
|
469
|
+
call-scoped session to for the duration of your callback (`set role`, no
|
|
470
|
+
transaction involved; the session is closed when the call ends). Defaults
|
|
471
|
+
to `'authenticated'` (the Supabase RLS convention).
|
|
474
472
|
- `getUserId` — resolves the user id injected as
|
|
475
473
|
`request.jwt.claims->>'sub'` inside `withUserDb`. Omit when
|
|
476
474
|
`firebaseAuth` is configured — the uid then comes from this package's own
|
|
477
475
|
`getAuthUser()` automatically. Provide it to use a different auth source
|
|
478
476
|
(or when `firebaseAuth` is absent).
|
|
479
|
-
- `disconnectTimeoutMs` —
|
|
480
|
-
|
|
477
|
+
- `disconnectTimeoutMs` — **deprecated, ignored since 0.8.23.** Client
|
|
478
|
+
teardown is awaited or deferred to `ctx.waitUntil` without a timeout.
|
|
481
479
|
|
|
482
480
|
#### Choosing a transport
|
|
483
481
|
|
|
@@ -2,63 +2,38 @@ import type { Client } from 'pg';
|
|
|
2
2
|
import type { LocalePrefixMode, Locales, RoutingConfig } from '../types/types';
|
|
3
3
|
export type DbConfig = RoutingConfig<Locales, LocalePrefixMode>;
|
|
4
4
|
/**
|
|
5
|
-
* Runs `
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
5
|
+
* Runs `queryFn` on a Postgres client scoped to this single call: one
|
|
6
|
+
* `connect()`, your callback, then a guaranteed `end()`. Each call gets its own
|
|
7
|
+
* client, so concurrent renders in the same isolate can never share session
|
|
8
|
+
* state (role, `request.jwt.claims`, an open transaction) with each other.
|
|
9
|
+
* Hyperdrive pools the server-side connection behind this.
|
|
20
10
|
*/
|
|
21
|
-
export declare function
|
|
11
|
+
export declare function withDbClient<T>(config: DbConfig, queryFn: (client: Client) => Promise<T>): Promise<T>;
|
|
22
12
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* for after a `db` config change.
|
|
13
|
+
* No-op kept for backward compatibility. There is no cached connection state
|
|
14
|
+
* to reset now that every {@link withDbClient} call owns its client.
|
|
26
15
|
*
|
|
27
|
-
*
|
|
28
|
-
* call it when no query is in flight — otherwise use `disconnectPostgres`,
|
|
29
|
-
* which closes the client properly.
|
|
16
|
+
* @deprecated Connection state is per-call; this does nothing.
|
|
30
17
|
*/
|
|
31
18
|
export declare function resetConnectionState(): void;
|
|
32
19
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Prefer `withPublicDb`/`withUserDb`, which call this for you and always
|
|
37
|
-
* release the connection. Reach for this directly only when you need the raw
|
|
38
|
-
* `pg` client — and then every call **must** be paired with a
|
|
39
|
-
* `disconnectPostgres` call, or the connection is never released.
|
|
20
|
+
* Runs `fn` directly. Kept for backward compatibility: session state can no
|
|
21
|
+
* longer leak between callers, so there is nothing left to serialize.
|
|
40
22
|
*
|
|
41
|
-
* @
|
|
42
|
-
* @param resolved A connection string already resolved by the caller (e.g.
|
|
43
|
-
* `resolveDbMode`, which has to call `db.connectionString` itself to decide
|
|
44
|
-
* the transport) — pass it to skip resolving `db.connectionString` a second
|
|
45
|
-
* time. Omit it to have this function resolve it itself, as before.
|
|
46
|
-
* @returns The connected, shared client.
|
|
47
|
-
* @throws If `db` is not set, or no connection string can be resolved from
|
|
48
|
-
* `db.connectionString`.
|
|
23
|
+
* @deprecated Clients are per-call now; no lock is needed.
|
|
49
24
|
*/
|
|
50
|
-
export
|
|
25
|
+
export declare function withSessionLock<T>(fn: () => Promise<T>): Promise<T>;
|
|
51
26
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
27
|
+
* Opens a Postgres client the caller owns and must close with
|
|
28
|
+
* {@link disconnectPostgres}. Prefer {@link withDbClient}, which closes the
|
|
29
|
+
* client for you even when the callback throws.
|
|
55
30
|
*
|
|
56
|
-
*
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
*
|
|
31
|
+
* @deprecated Use {@link withDbClient} instead.
|
|
32
|
+
*/
|
|
33
|
+
export declare function connectToPostgres(config: DbConfig): Promise<Client>;
|
|
34
|
+
/**
|
|
35
|
+
* Closes a client from {@link connectToPostgres}.
|
|
61
36
|
*
|
|
62
|
-
* @
|
|
37
|
+
* @deprecated Use {@link withDbClient} instead.
|
|
63
38
|
*/
|
|
64
|
-
export declare function disconnectPostgres(
|
|
39
|
+
export declare function disconnectPostgres(client?: Client): Promise<void>;
|
|
@@ -1,63 +1,16 @@
|
|
|
1
1
|
import reportError from '../error_handling/report_error';
|
|
2
2
|
import requireDbConfig from './require_config';
|
|
3
3
|
import resolveConfigValue from './resolve_config_value';
|
|
4
|
-
|
|
5
|
-
let connectionString = null;
|
|
6
|
-
let client = null;
|
|
7
|
-
let connectionPromise = null;
|
|
8
|
-
let disconnectionPromise = null;
|
|
9
|
-
let activeUsers = 0;
|
|
10
|
-
let sessionLock = Promise.resolve();
|
|
4
|
+
let pgModule;
|
|
11
5
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* it does nothing to stop a *different* concurrent request's queries from
|
|
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.
|
|
6
|
+
* Loads `pg` lazily, so an app that never touches the Postgres transport never
|
|
7
|
+
* bundles it, and caches the module promise so concurrent callers share one
|
|
8
|
+
* resolution instead of racing separate `import()` calls.
|
|
27
9
|
*/
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sessionLock = new Promise((resolve) => { release = resolve; });
|
|
32
|
-
await previous;
|
|
33
|
-
try {
|
|
34
|
-
return await fn();
|
|
35
|
-
}
|
|
36
|
-
finally {
|
|
37
|
-
release();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* Forgets the cached client and connection string so the next
|
|
42
|
-
* `connectToPostgres` call builds both from scratch. Intended for tests and
|
|
43
|
-
* for after a `db` config change.
|
|
44
|
-
*
|
|
45
|
-
* This drops the reference **without closing** an open connection, so only
|
|
46
|
-
* call it when no query is in flight — otherwise use `disconnectPostgres`,
|
|
47
|
-
* which closes the client properly.
|
|
48
|
-
*/
|
|
49
|
-
export function resetConnectionState() {
|
|
50
|
-
connectionString = null;
|
|
51
|
-
client = null;
|
|
52
|
-
connectionPromise = null;
|
|
53
|
-
disconnectionPromise = null;
|
|
54
|
-
activeUsers = 0;
|
|
55
|
-
sessionLock = Promise.resolve();
|
|
10
|
+
function loadPg() {
|
|
11
|
+
pgModule ?? (pgModule = import('pg'));
|
|
12
|
+
return pgModule;
|
|
56
13
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Resolves the connection string from `db.connectionString`, awaiting it when
|
|
59
|
-
* it was given as a function.
|
|
60
|
-
*/
|
|
61
14
|
async function resolveConnectionString(db) {
|
|
62
15
|
const configured = await resolveConfigValue(db.connectionString);
|
|
63
16
|
if (configured)
|
|
@@ -66,165 +19,100 @@ async function resolveConnectionString(db) {
|
|
|
66
19
|
'to a connection string, or to a function returning one (e.g. reading a ' +
|
|
67
20
|
'Hyperdrive binding off `getCloudflareContext().env`).');
|
|
68
21
|
}
|
|
69
|
-
// A single `pg.Client` is not safe for concurrent queries; Next.js fires many
|
|
70
|
-
// in parallel per request. Serializing every `query` through one promise chain
|
|
71
|
-
// keeps one connection correct instead of paying for a pool on top of
|
|
72
|
-
// Hyperdrive's own pooling.
|
|
73
|
-
function serializeQueries(raw) {
|
|
74
|
-
const originalQuery = raw.query;
|
|
75
|
-
if (typeof originalQuery !== 'function')
|
|
76
|
-
return raw;
|
|
77
|
-
let last = Promise.resolve();
|
|
78
|
-
raw.query = (...args) => {
|
|
79
|
-
const run = () => originalQuery.apply(raw, args);
|
|
80
|
-
const next = last.then(run, run);
|
|
81
|
-
last = next.catch(() => undefined);
|
|
82
|
-
return next;
|
|
83
|
-
};
|
|
84
|
-
return raw;
|
|
85
|
-
}
|
|
86
22
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `pg` client — and then every call **must** be paired with a
|
|
93
|
-
* `disconnectPostgres` call, or the connection is never released.
|
|
94
|
-
*
|
|
95
|
-
* @param config Your routing config; `config.db` must be set.
|
|
96
|
-
* @param resolved A connection string already resolved by the caller (e.g.
|
|
97
|
-
* `resolveDbMode`, which has to call `db.connectionString` itself to decide
|
|
98
|
-
* the transport) — pass it to skip resolving `db.connectionString` a second
|
|
99
|
-
* time. Omit it to have this function resolve it itself, as before.
|
|
100
|
-
* @returns The connected, shared client.
|
|
101
|
-
* @throws If `db` is not set, or no connection string can be resolved from
|
|
102
|
-
* `db.connectionString`.
|
|
23
|
+
* Runs `queryFn` on a Postgres client scoped to this single call: one
|
|
24
|
+
* `connect()`, your callback, then a guaranteed `end()`. Each call gets its own
|
|
25
|
+
* client, so concurrent renders in the same isolate can never share session
|
|
26
|
+
* state (role, `request.jwt.claims`, an open transaction) with each other.
|
|
27
|
+
* Hyperdrive pools the server-side connection behind this.
|
|
103
28
|
*/
|
|
104
|
-
export
|
|
29
|
+
export async function withDbClient(config, queryFn) {
|
|
105
30
|
const db = config.db;
|
|
106
31
|
requireDbConfig(db);
|
|
107
|
-
await
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// starting its own.
|
|
113
|
-
if (connectionPromise === null) {
|
|
114
|
-
connectionPromise = (async () => {
|
|
115
|
-
connectionString = resolved ?? await resolveConnectionString(db);
|
|
116
|
-
const { Client } = await import('pg');
|
|
117
|
-
const created = serializeQueries(new Client({ connectionString }));
|
|
118
|
-
// The shared client outlives a single request (see the module doc)
|
|
119
|
-
// and Hyperdrive/Postgres can close its idle socket at any time. `pg`
|
|
120
|
-
// surfaces that as an `'error'` event on the `Client`, which is an
|
|
121
|
-
// `EventEmitter` — with no listener, Node treats it as unhandled and
|
|
122
|
-
// throws, crashing whatever unrelated request happens to be running
|
|
123
|
-
// in the isolate at that moment. Listening here converts it into a
|
|
124
|
-
// clean reset so the next call reconnects instead.
|
|
125
|
-
created.on('error', (error) => {
|
|
126
|
-
if (client === created) {
|
|
127
|
-
client = null;
|
|
128
|
-
connectionString = null;
|
|
129
|
-
connectionPromise = null;
|
|
130
|
-
}
|
|
131
|
-
// `pg` emits "Connection terminated"/"Connection terminated
|
|
132
|
-
// unexpectedly" (lib/client.js) — never "Connection closed" —
|
|
133
|
-
// when the idle socket dies outside a query. That's the
|
|
134
|
-
// expected shape of this event (Hyperdrive/Postgres recycling
|
|
135
|
-
// the connection, not a query failure): swallow it entirely,
|
|
136
|
-
// don't report or log it.
|
|
137
|
-
if (/(connection terminated|connection closed)/i.test(error.message))
|
|
138
|
-
return;
|
|
139
|
-
void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.connectToPostgres.clientError' });
|
|
140
|
-
});
|
|
141
|
-
client = created;
|
|
142
|
-
await created.connect();
|
|
143
|
-
return created;
|
|
144
|
-
})();
|
|
145
|
-
}
|
|
146
|
-
activeUsers++;
|
|
32
|
+
const connectionString = await resolveConnectionString(db);
|
|
33
|
+
const { Client: PgClient } = await loadPg();
|
|
34
|
+
const client = new PgClient({ connectionString });
|
|
35
|
+
let result;
|
|
36
|
+
let connected = false;
|
|
147
37
|
try {
|
|
148
|
-
|
|
38
|
+
try {
|
|
39
|
+
await client.connect();
|
|
40
|
+
connected = true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error ?? '');
|
|
44
|
+
if (!/(connection terminated|connection closed|socket closed|unexpected eof)/i.test(message)) {
|
|
45
|
+
void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.withDbClient.connectError' });
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
result = await queryFn(client);
|
|
149
50
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
51
|
+
finally {
|
|
52
|
+
const endPromise = connected ? client.end() : Promise.resolve();
|
|
53
|
+
const getContext = config.generate?.getCloudflareContext;
|
|
54
|
+
if (!getContext || db.disconnectAfterRequest === false) {
|
|
55
|
+
await endPromise.catch(() => undefined);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
try {
|
|
59
|
+
const context = await getContext({ async: true });
|
|
60
|
+
if (typeof context?.ctx?.waitUntil === 'function') {
|
|
61
|
+
context.ctx.waitUntil(endPromise.catch(() => undefined));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await endPromise.catch(() => undefined);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
await endPromise.catch(() => undefined);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
159
71
|
}
|
|
72
|
+
return result;
|
|
160
73
|
}
|
|
161
74
|
/**
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* `connectToPostgres` call.
|
|
75
|
+
* No-op kept for backward compatibility. There is no cached connection state
|
|
76
|
+
* to reset now that every {@link withDbClient} call owns its client.
|
|
165
77
|
*
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
78
|
+
* @deprecated Connection state is per-call; this does nothing.
|
|
79
|
+
*/
|
|
80
|
+
export function resetConnectionState() {
|
|
81
|
+
// no cached state to reset
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Runs `fn` directly. Kept for backward compatibility: session state can no
|
|
85
|
+
* longer leak between callers, so there is nothing left to serialize.
|
|
171
86
|
*
|
|
172
|
-
* @
|
|
87
|
+
* @deprecated Clients are per-call now; no lock is needed.
|
|
173
88
|
*/
|
|
174
|
-
export function
|
|
89
|
+
export async function withSessionLock(fn) {
|
|
90
|
+
return await fn();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Opens a Postgres client the caller owns and must close with
|
|
94
|
+
* {@link disconnectPostgres}. Prefer {@link withDbClient}, which closes the
|
|
95
|
+
* client for you even when the callback throws.
|
|
96
|
+
*
|
|
97
|
+
* @deprecated Use {@link withDbClient} instead.
|
|
98
|
+
*/
|
|
99
|
+
export async function connectToPostgres(config) {
|
|
175
100
|
const db = config.db;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
client
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout closing postgres client')), timeoutMs)),
|
|
194
|
-
]);
|
|
195
|
-
}
|
|
196
|
-
catch (error) {
|
|
197
|
-
await reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.disconnectPostgres' });
|
|
198
|
-
}
|
|
199
|
-
finally {
|
|
200
|
-
if (disconnectionPromise === endPromise)
|
|
201
|
-
disconnectionPromise = null;
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
const getContext = config.generate?.getCloudflareContext;
|
|
205
|
-
if (!getContext) {
|
|
206
|
-
void settle();
|
|
207
|
-
return;
|
|
208
|
-
}
|
|
209
|
-
void (async () => {
|
|
210
|
-
// No matter what happens resolving the context/waitUntil below,
|
|
211
|
-
// settle() must always run — it's the only place disconnectionPromise
|
|
212
|
-
// gets cleared, and connectToPostgres awaits that promise on every
|
|
213
|
-
// call. A rejected/throwing getContext must still fall through to
|
|
214
|
-
// settle() directly instead of leaving the disconnect stuck forever.
|
|
215
|
-
let waitUntil;
|
|
216
|
-
try {
|
|
217
|
-
const context = await getContext({ async: true });
|
|
218
|
-
if (typeof context?.ctx?.waitUntil === 'function') {
|
|
219
|
-
waitUntil = context.ctx.waitUntil.bind(context.ctx);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
waitUntil = undefined;
|
|
224
|
-
}
|
|
225
|
-
if (waitUntil)
|
|
226
|
-
waitUntil(settle());
|
|
227
|
-
else
|
|
228
|
-
await settle();
|
|
229
|
-
})();
|
|
101
|
+
requireDbConfig(db);
|
|
102
|
+
const connectionString = await resolveConnectionString(db);
|
|
103
|
+
const { Client: PgClient } = await loadPg();
|
|
104
|
+
const client = new PgClient({ connectionString });
|
|
105
|
+
client.on('error', (error) => {
|
|
106
|
+
void reportError({ errorHandling: config.errorHandling, generate: config.generate }, { error, classOrMethodName: 'db.connectToPostgres.clientError' });
|
|
107
|
+
});
|
|
108
|
+
await client.connect();
|
|
109
|
+
return client;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Closes a client from {@link connectToPostgres}.
|
|
113
|
+
*
|
|
114
|
+
* @deprecated Use {@link withDbClient} instead.
|
|
115
|
+
*/
|
|
116
|
+
export async function disconnectPostgres(client) {
|
|
117
|
+
await client?.end().catch(() => undefined);
|
|
230
118
|
}
|
package/dist/src/db/context.d.ts
CHANGED
|
@@ -9,64 +9,20 @@ export type DrizzleDb = NodePgDatabase<Record<string, never>>;
|
|
|
9
9
|
* Runs a query as the **anonymous** role: no transaction, no role switch, no
|
|
10
10
|
* user identity attached. Use this for data any visitor may read.
|
|
11
11
|
*
|
|
12
|
-
* Because no user id is set, RLS policies that test `auth.jwt()->>'sub'` see
|
|
13
|
-
* no user and will deny access — reach for {@link withUserDb} whenever the
|
|
14
|
-
* rows depend on who is asking.
|
|
15
|
-
*
|
|
16
|
-
* In connection-string mode the connection is taken from the request's
|
|
17
|
-
* shared client and released when `fn` settles, even if it throws. In
|
|
18
|
-
* Supabase mode there is no connection to release — each call is one
|
|
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
|
-
*
|
|
23
12
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
24
13
|
* @returns Whatever `fn` resolves to.
|
|
25
14
|
* @throws If `db` is not set on your `RoutingConfig`, or the connection fails.
|
|
26
|
-
*
|
|
27
|
-
* @example
|
|
28
|
-
* const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
29
15
|
*/
|
|
30
16
|
export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>): Promise<T>;
|
|
31
17
|
/**
|
|
32
|
-
* Runs a query as the **signed-in user
|
|
18
|
+
* Runs a query as the **signed-in user**, with `request.jwt.claims` and the
|
|
19
|
+
* authenticated role set on the session so RLS policies apply to their id.
|
|
33
20
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
49
|
-
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
50
|
-
* the wrapper to use for anything user-owned.
|
|
51
|
-
*
|
|
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).
|
|
56
|
-
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
57
|
-
* pass `null`, in normal use — either way the id then comes from
|
|
58
|
-
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
59
|
-
* `firebaseAuth` is configured. `null` is accepted alongside `undefined` so a
|
|
60
|
-
* caller's own lookup (which may itself come back empty) can be passed
|
|
61
|
-
* straight through without an extra check. Ignored in Supabase mode, which
|
|
62
|
-
* resolves identity via `db.getAccessToken`/Firebase instead — see
|
|
63
|
-
* {@link resolveAccessToken}.
|
|
64
|
-
* @returns Whatever `fn` resolves to.
|
|
65
|
-
* @throws If `db` is not set on your `RoutingConfig`, if no user id/access
|
|
66
|
-
* token can be resolved, or the connection fails.
|
|
21
|
+
* The session lives on a client scoped to this call and closed when it ends,
|
|
22
|
+
* so the role and claims can never be observed by another caller.
|
|
67
23
|
*
|
|
68
|
-
* @
|
|
69
|
-
*
|
|
24
|
+
* @param fn Receives the Drizzle handle
|
|
25
|
+
* @param uid Overrides the user ID for authenticated calls
|
|
70
26
|
*/
|
|
71
27
|
export declare function withUserDb<T>(fn: (db: DrizzleDb) => Promise<T>, uid?: string | null): Promise<T>;
|
|
72
28
|
/** One statement's `{rows, rowCount}` result from a Supabase-mode `db.transaction()` batch. */
|
package/dist/src/db/context.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import config from '../config/intl_config';
|
|
2
2
|
import requireDbConfig from './require_config';
|
|
3
|
-
import
|
|
3
|
+
import { withDbClient } from './connection';
|
|
4
4
|
import resolveDbMode from './resolve_mode';
|
|
5
5
|
import resolveSupabaseEndpoint from './supabase_config';
|
|
6
6
|
import createSupabaseTransport from './supabase_transport';
|
|
@@ -73,11 +73,8 @@ async function postgresDb(drizzleHandle, rawClient) {
|
|
|
73
73
|
* via an inline-parameterised `query()` call, wrapped in a real
|
|
74
74
|
* `BEGIN`/`COMMIT` for atomicity, and returns `ExecResult[]`.
|
|
75
75
|
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
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.
|
|
76
|
+
* The client is scoped to one `withDbClient` call, so no other caller can
|
|
77
|
+
* interleave statements into this transaction.
|
|
81
78
|
*/
|
|
82
79
|
async function runPostgresTransaction(rawClient, build) {
|
|
83
80
|
const queries = await build(buildOnlyDb());
|
|
@@ -93,7 +90,7 @@ async function runPostgresTransaction(rawClient, build) {
|
|
|
93
90
|
return results;
|
|
94
91
|
}
|
|
95
92
|
catch (error) {
|
|
96
|
-
await rawClient.query('rollback');
|
|
93
|
+
await rawClient.query('rollback').catch(() => undefined);
|
|
97
94
|
throw error;
|
|
98
95
|
}
|
|
99
96
|
}
|
|
@@ -118,23 +115,9 @@ function buildOnlyDb() {
|
|
|
118
115
|
* Runs a query as the **anonymous** role: no transaction, no role switch, no
|
|
119
116
|
* user identity attached. Use this for data any visitor may read.
|
|
120
117
|
*
|
|
121
|
-
* Because no user id is set, RLS policies that test `auth.jwt()->>'sub'` see
|
|
122
|
-
* no user and will deny access — reach for {@link withUserDb} whenever the
|
|
123
|
-
* rows depend on who is asking.
|
|
124
|
-
*
|
|
125
|
-
* In connection-string mode the connection is taken from the request's
|
|
126
|
-
* shared client and released when `fn` settles, even if it throws. In
|
|
127
|
-
* Supabase mode there is no connection to release — each call is one
|
|
128
|
-
* PostgREST round-trip authenticated as the anon key. Either way, call
|
|
129
|
-
* `.transaction(...)` on the handle `fn` receives for atomicity across more
|
|
130
|
-
* than one statement — see the module doc for the shape that takes in each mode.
|
|
131
|
-
*
|
|
132
118
|
* @param fn Receives the Drizzle handle; return whatever the caller needs.
|
|
133
119
|
* @returns Whatever `fn` resolves to.
|
|
134
120
|
* @throws If `db` is not set on your `RoutingConfig`, or the connection fails.
|
|
135
|
-
*
|
|
136
|
-
* @example
|
|
137
|
-
* const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
138
121
|
*/
|
|
139
122
|
export async function withPublicDb(fn) {
|
|
140
123
|
const db = config.db;
|
|
@@ -144,58 +127,22 @@ export async function withPublicDb(fn) {
|
|
|
144
127
|
const { anonKey } = await resolveSupabaseEndpoint(resolved.supabase);
|
|
145
128
|
return fn(await supabaseDb(resolved.supabase, anonKey));
|
|
146
129
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
return await fn(await postgresDb(drizzleHandle, client));
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
finally {
|
|
157
|
-
disconnectPostgres(config);
|
|
158
|
-
}
|
|
130
|
+
return await withDbClient(config, async (client) => {
|
|
131
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
132
|
+
const drizzleHandle = drizzle(client);
|
|
133
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
134
|
+
return await fn(await postgresDb(drizzleHandle, client));
|
|
135
|
+
});
|
|
159
136
|
}
|
|
160
137
|
/**
|
|
161
|
-
* Runs a query as the **signed-in user
|
|
138
|
+
* Runs a query as the **signed-in user**, with `request.jwt.claims` and the
|
|
139
|
+
* authenticated role set on the session so RLS policies apply to their id.
|
|
162
140
|
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
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
|
|
178
|
-
* `cfni_exec_batch` call instead — see the module doc). Either way this is
|
|
179
|
-
* the wrapper to use for anything user-owned.
|
|
141
|
+
* The session lives on a client scoped to this call and closed when it ends,
|
|
142
|
+
* so the role and claims can never be observed by another caller.
|
|
180
143
|
*
|
|
181
|
-
* @param fn Receives the Drizzle handle
|
|
182
|
-
*
|
|
183
|
-
* atomicity across statements (build-and-return shape in both modes, not a
|
|
184
|
-
* live session — see the module doc).
|
|
185
|
-
* @param uid Connection-string mode only: overrides the user id. Omit it, or
|
|
186
|
-
* pass `null`, in normal use — either way the id then comes from
|
|
187
|
-
* `db.getUserId()` when set, otherwise from the signed-in Firebase user when
|
|
188
|
-
* `firebaseAuth` is configured. `null` is accepted alongside `undefined` so a
|
|
189
|
-
* caller's own lookup (which may itself come back empty) can be passed
|
|
190
|
-
* straight through without an extra check. Ignored in Supabase mode, which
|
|
191
|
-
* resolves identity via `db.getAccessToken`/Firebase instead — see
|
|
192
|
-
* {@link resolveAccessToken}.
|
|
193
|
-
* @returns Whatever `fn` resolves to.
|
|
194
|
-
* @throws If `db` is not set on your `RoutingConfig`, if no user id/access
|
|
195
|
-
* token can be resolved, or the connection fails.
|
|
196
|
-
*
|
|
197
|
-
* @example
|
|
198
|
-
* const mine = await withUserDb((db) => db.select().from(orders));
|
|
144
|
+
* @param fn Receives the Drizzle handle
|
|
145
|
+
* @param uid Overrides the user ID for authenticated calls
|
|
199
146
|
*/
|
|
200
147
|
export async function withUserDb(fn, uid) {
|
|
201
148
|
const db = config.db;
|
|
@@ -206,59 +153,19 @@ export async function withUserDb(fn, uid) {
|
|
|
206
153
|
return fn(await supabaseDb(resolved.supabase, token));
|
|
207
154
|
}
|
|
208
155
|
const userId = await resolveUserId(uid);
|
|
209
|
-
const client = await connectToPostgres(config, resolved.connectionString);
|
|
210
156
|
const role = db.authenticatedRole ?? DEFAULT_ROLE;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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}"`);
|
|
230
|
-
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
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
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
finally {
|
|
244
|
-
disconnectPostgres(config);
|
|
245
|
-
}
|
|
157
|
+
return await withDbClient(config, async (client) => {
|
|
158
|
+
const rawClient = client;
|
|
159
|
+
await rawClient.query(`select set_config('request.jwt.claims', $1, false)`, [JSON.stringify({ sub: userId })]);
|
|
160
|
+
await rawClient.query(`set role "${role}"`);
|
|
161
|
+
const { drizzle } = await import('drizzle-orm/node-postgres');
|
|
162
|
+
const drizzleHandle = drizzle(client);
|
|
163
|
+
return await fn(await postgresDb(drizzleHandle, rawClient));
|
|
164
|
+
});
|
|
246
165
|
}
|
|
247
166
|
/**
|
|
248
167
|
* Runs several statements atomically over `cfni_exec_batch`, backing
|
|
249
|
-
* Supabase-mode `db.transaction()`.
|
|
250
|
-
* it **builds** them and returns the array; call `.toSQL()` on each Drizzle
|
|
251
|
-
* query instead of `await`ing it (`await`ing throws immediately; see
|
|
252
|
-
* {@link buildOnlyDb}). Every query is inlined and sent as one round trip:
|
|
253
|
-
* the Postgres function runs them in order inside a single plpgsql call,
|
|
254
|
-
* which is itself an implicit transaction, so a failure on any statement
|
|
255
|
-
* rolls back every statement before it.
|
|
256
|
-
*
|
|
257
|
-
* @param supabase The `db.supabase` config block.
|
|
258
|
-
* @param bearerToken The anon key or user JWT.
|
|
259
|
-
* @param build Returns the queries to run, via `.toSQL()` — never executes them directly.
|
|
260
|
-
* @returns One result per query, in the same order as `build`'s array.
|
|
261
|
-
* @throws If `db.supabase.rawSql` is `false` — `cfni_exec_batch` needs `cfni_exec`, which is disabled too.
|
|
168
|
+
* Supabase-mode `db.transaction()`.
|
|
262
169
|
*/
|
|
263
170
|
async function runTransaction(supabase, bearerToken, build) {
|
|
264
171
|
if (supabase.rawSql === false) {
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -38,5 +38,5 @@
|
|
|
38
38
|
*/
|
|
39
39
|
export { withPublicDb, withUserDb } from './context';
|
|
40
40
|
export type { DrizzleDb, TransactionResult } from './context';
|
|
41
|
-
export {
|
|
41
|
+
export { withDbClient, connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
|
|
42
42
|
export type { DbRoutingConfig } from '../types/types';
|
package/dist/src/db/index.js
CHANGED
|
@@ -37,4 +37,4 @@
|
|
|
37
37
|
* the separate `cloudflare-next-intl/dbHelpers` entry point.
|
|
38
38
|
*/
|
|
39
39
|
export { withPublicDb, withUserDb } from './context';
|
|
40
|
-
export {
|
|
40
|
+
export { withDbClient, connectToPostgres, disconnectPostgres, resetConnectionState } from './connection';
|
|
@@ -884,10 +884,12 @@ export interface DbRoutingConfig {
|
|
|
884
884
|
/**
|
|
885
885
|
* Whether the pooled client is closed once the last in-flight
|
|
886
886
|
* `withPublicDb`/`withUserDb` call of the request finishes.
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
*
|
|
890
|
-
*
|
|
887
|
+
*
|
|
888
|
+
* @deprecated Ignored since 0.8.23. Every `withPublicDb`/`withUserDb` call
|
|
889
|
+
* now opens and closes its own client, so no connection survives a call to
|
|
890
|
+
* be kept open. `true` and `false` behave identically; the only difference
|
|
891
|
+
* is that `false` awaits the close instead of deferring it to
|
|
892
|
+
* `ctx.waitUntil`. Hyperdrive pools the server-side connection.
|
|
891
893
|
*/
|
|
892
894
|
disconnectAfterRequest?: boolean;
|
|
893
895
|
/**
|
|
@@ -902,7 +904,12 @@ export interface DbRoutingConfig {
|
|
|
902
904
|
* to use a different auth source (or when `firebaseAuth` is absent).
|
|
903
905
|
*/
|
|
904
906
|
getUserId?: () => Promise<string | null> | string | null;
|
|
905
|
-
/**
|
|
907
|
+
/**
|
|
908
|
+
* Milliseconds `disconnectPostgres` waits for `client.end()` before giving up.
|
|
909
|
+
*
|
|
910
|
+
* @deprecated Ignored since 0.8.23. Client teardown is awaited or deferred
|
|
911
|
+
* to `ctx.waitUntil` without a timeout.
|
|
912
|
+
*/
|
|
906
913
|
disconnectTimeoutMs?: number;
|
|
907
914
|
/**
|
|
908
915
|
* Reaches Postgres through the Supabase Data API instead of a direct
|
package/llms.txt
CHANGED
|
@@ -53,11 +53,11 @@ Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@sup
|
|
|
53
53
|
|
|
54
54
|
- Direct Postgres (wins if configured): `db.connectionString` — Postgres connection string, or a sync/async function returning one (resolved on each connect). The function form is the way to read a value unavailable at module scope, e.g. a Cloudflare Hyperdrive binding: `connectionString: async () => (await getCloudflareContext({ async: true })).env.HYPERDRIVE.connectionString`. There is no separate `hyperdriveBinding` option.
|
|
55
55
|
- Supabase Data API (used only when neither of the above is set): `db.supabase` — `{ url?, anonKey?, execFunction?, rawSql? }` where `url`/`anonKey` each accept a string or a sync/async function returning one, defaulting `url`/`anonKey` to `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`. Statements are translated to PostgREST REST calls first; unsupported statements fall back to `supabase/cfni_exec.sql` (a `security invoker` SQL-exec function) in your database — `cfni-db-codegen`/`cfni-db-install-exec` can install it for you (see below). No multi-statement transactions — each statement in a `withUserDb` callback is its own round-trip; `.transaction()` throws instead of running non-atomically. `rawSql: false` disables `cfni_exec` fallback, throwing an informative error when a query cannot be served over REST.
|
|
56
|
-
- `db.disconnectAfterRequest` —
|
|
56
|
+
- `db.disconnectAfterRequest` — deprecated, ignored since 0.8.23. Each `withPublicDb`/`withUserDb` call opens and closes its own client; Hyperdrive pools the server-side connection.
|
|
57
57
|
- `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Defaults to `'authenticated'` (Supabase RLS convention).
|
|
58
58
|
- `db.getUserId` — direct-Postgres mode only: resolves the user id injected as `request.jwt.claims->>'sub'` in `withUserDb`. Omit when `firebaseAuth` is configured — the uid is then taken automatically from the signed-in Firebase user via this package's own `getAuthUser()`.
|
|
59
59
|
- `db.getAccessToken` — Supabase mode only: resolves the JWT sent as `Authorization: Bearer` in `withUserDb`, which is what makes PostgREST resolve `authenticated` and apply RLS. Omit when `firebaseAuth` is configured — the signed-in user's Firebase ID token is used automatically.
|
|
60
|
-
- `db.disconnectTimeoutMs` —
|
|
60
|
+
- `db.disconnectTimeoutMs` — deprecated, ignored since 0.8.23. Teardown is awaited or deferred to `ctx.waitUntil` without a timeout.
|
|
61
61
|
- `withPublicDb(fn)` — anonymous role. Direct-Postgres mode: the request's pooled connection, no transaction, no role switch. Supabase mode: the anon key as the PostgREST bearer token. Either way, no user id is attached — RLS keyed on `auth.jwt()` denies access.
|
|
62
62
|
- `withUserDb(fn, uid?)` — signed-in-user role. Direct-Postgres mode: a transaction with `set_config('request.jwt.claims', ...)` + `set local role`, `uid` resolution order explicit arg → `db.getUserId()` → Firebase auth uid → throws. Supabase mode: identity rides on the JWT from `db.getAccessToken`/Firebase instead (`uid` param is ignored), no transaction wraps the call.
|
|
63
63
|
- `./dbHelpers` functions are plain Drizzle `sql`-building utilities with no config dependency — usable standalone.
|