cloudflare-next-intl 0.9.34 → 0.9.36
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 +32 -0
- package/dist/src/db/context.d.ts +7 -1
- package/dist/src/db/context.js +44 -6
- package/dist/src/db/index.d.ts +2 -1
- package/dist/src/db/index.js +1 -1
- package/dist/src/server/components/link.d.ts +1 -0
- package/dist/src/server/components/link.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -754,6 +754,38 @@ query code is identical either way — switching is a config change only.
|
|
|
754
754
|
A direct connection always wins if both are configured, so adding a `supabase`
|
|
755
755
|
block cannot silently reroute live traffic.
|
|
756
756
|
|
|
757
|
+
#### Reading as the user from a cached function
|
|
758
|
+
|
|
759
|
+
`withUserDb` normally resolves the caller from the request — cookies, the
|
|
760
|
+
Firebase session — every time it runs. Next forbids that inside a function
|
|
761
|
+
wrapped in `unstable_cache`, and the background revalidation it runs for one
|
|
762
|
+
happens after the response is gone, so both fail with ``` `cookies()` cannot be
|
|
763
|
+
called inside a function cached with `unstable_cache()` ```.
|
|
764
|
+
|
|
765
|
+
Resolve the caller once, while the request still exists, and pass the plain
|
|
766
|
+
values in:
|
|
767
|
+
|
|
768
|
+
```typescript
|
|
769
|
+
import { resolveUserDbCredentials, withUserDb } from "cloudflare-next-intl/db";
|
|
770
|
+
import { unstable_cache } from "next/cache";
|
|
771
|
+
|
|
772
|
+
const credentials = await resolveUserDbCredentials();
|
|
773
|
+
if (credentials.uid === null) return null;
|
|
774
|
+
|
|
775
|
+
const row = await unstable_cache(
|
|
776
|
+
() => withUserDb((db) => db.select().from(profiles).where(eq(profiles.id, credentials.uid!)), credentials),
|
|
777
|
+
["profile-row", credentials.uid],
|
|
778
|
+
{ tags: [`profile:${credentials.uid}`], revalidate: 60 },
|
|
779
|
+
)();
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
`resolveUserDbCredentials()` returns `{ uid, accessToken, role }`, each `null`
|
|
783
|
+
when it cannot be resolved — a signed-out visitor gets nulls rather than a
|
|
784
|
+
throw, so the check above is yours to make. Passing the object to `withUserDb`
|
|
785
|
+
in place of a bare `uid` makes it use those values and read nothing
|
|
786
|
+
request-scoped; it throws naming the missing field if the one that mode needs
|
|
787
|
+
is `null`.
|
|
788
|
+
|
|
757
789
|
#### Standalone usage (no Next.js / `@intl-config`)
|
|
758
790
|
|
|
759
791
|
`withPublicDb`/`withUserDb` normally read their `db` config from
|
package/dist/src/db/context.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
2
2
|
import type { DbRoutingConfig } from '../types/types.js';
|
|
3
3
|
export type DrizzleDb = NodePgDatabase<Record<string, never>>;
|
|
4
|
+
export interface UserDbCredentials {
|
|
5
|
+
uid: string | null;
|
|
6
|
+
accessToken: string | null;
|
|
7
|
+
role: string | null;
|
|
8
|
+
}
|
|
9
|
+
export declare function resolveUserDbCredentials(dbOverride?: DbRoutingConfig): Promise<UserDbCredentials>;
|
|
4
10
|
export declare function withPublicDb<T>(fn: (db: DrizzleDb) => Promise<T>, dbOverride?: DbRoutingConfig): Promise<T>;
|
|
5
|
-
export declare function withUserDb<T>(fn: (db: DrizzleDb) => Promise<T>,
|
|
11
|
+
export declare function withUserDb<T>(fn: (db: DrizzleDb) => Promise<T>, auth?: string | null | UserDbCredentials, dbOverride?: DbRoutingConfig): Promise<T>;
|
|
6
12
|
export type { ExecResult as TransactionResult } from './supabase_transport.js';
|
package/dist/src/db/context.js
CHANGED
|
@@ -8,6 +8,38 @@ import resolveAccessToken from './access_token.js';
|
|
|
8
8
|
import runTransactionBatch from './transaction_batch.js';
|
|
9
9
|
import inlineParams from './inline_params.js';
|
|
10
10
|
const DEFAULT_ROLE = 'authenticated';
|
|
11
|
+
function isCredentials(value) {
|
|
12
|
+
return typeof value === 'object' && value !== null;
|
|
13
|
+
}
|
|
14
|
+
function throwMissingCredential(what) {
|
|
15
|
+
throw new Error(`db: withUserDb was given credentials without ${what}. resolveUserDbCredentials() ` +
|
|
16
|
+
'returns nulls when nobody is signed in — check for that before calling withUserDb.');
|
|
17
|
+
}
|
|
18
|
+
export async function resolveUserDbCredentials(dbOverride) {
|
|
19
|
+
const config = await resolveDbConfig(dbOverride);
|
|
20
|
+
const db = config.db;
|
|
21
|
+
requireDbConfig(db);
|
|
22
|
+
const fromConfigUid = (await db.getUserId?.()) ?? null;
|
|
23
|
+
const fromConfigToken = (await db.getAccessToken?.()) ?? null;
|
|
24
|
+
let uid = fromConfigUid;
|
|
25
|
+
let accessToken = fromConfigToken;
|
|
26
|
+
let role = null;
|
|
27
|
+
if (config.firebaseAuth && (uid === null || accessToken === null || db.authenticatedRoleClaim !== false)) {
|
|
28
|
+
const { getAuthUser } = await import('../firebase_auth/server/use_auth_user_server.js');
|
|
29
|
+
const { user } = await getAuthUser();
|
|
30
|
+
if (user) {
|
|
31
|
+
uid ?? (uid = user.uid ?? null);
|
|
32
|
+
accessToken ?? (accessToken = (await user.getIdToken(false)) ?? null);
|
|
33
|
+
if (db.authenticatedRoleClaim !== false && typeof user.getIdTokenResult === 'function') {
|
|
34
|
+
const { claims } = await user.getIdTokenResult();
|
|
35
|
+
const claimValue = claims[db.authenticatedRoleClaim ?? 'role'];
|
|
36
|
+
if (typeof claimValue === 'string' && claimValue)
|
|
37
|
+
role = claimValue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { uid, accessToken, role };
|
|
42
|
+
}
|
|
11
43
|
async function resolveUserId(config, uid) {
|
|
12
44
|
if (uid)
|
|
13
45
|
return uid;
|
|
@@ -25,9 +57,11 @@ async function resolveUserId(config, uid) {
|
|
|
25
57
|
throw new Error('db: withUserDb could not resolve a user id. Pass one explicitly, set ' +
|
|
26
58
|
'`db.getUserId`, or configure `firebaseAuth` so the signed-in Firebase uid is used.');
|
|
27
59
|
}
|
|
28
|
-
async function resolveAuthenticatedRole(config, db) {
|
|
60
|
+
async function resolveAuthenticatedRole(config, db, claimed) {
|
|
61
|
+
if (claimed)
|
|
62
|
+
return claimed;
|
|
29
63
|
const claimField = db.authenticatedRoleClaim;
|
|
30
|
-
if (config.firebaseAuth && claimField !== false) {
|
|
64
|
+
if (config.firebaseAuth && claimField !== false && claimed === undefined) {
|
|
31
65
|
const { getAuthUser } = await import('../firebase_auth/server/use_auth_user_server.js');
|
|
32
66
|
const { user } = await getAuthUser();
|
|
33
67
|
if (user && typeof user.getIdTokenResult === 'function') {
|
|
@@ -127,17 +161,21 @@ function injectUidComment(sql, userId) {
|
|
|
127
161
|
}
|
|
128
162
|
return sql;
|
|
129
163
|
}
|
|
130
|
-
export async function withUserDb(fn,
|
|
164
|
+
export async function withUserDb(fn, auth, dbOverride) {
|
|
131
165
|
const config = await resolveDbConfig(dbOverride);
|
|
132
166
|
const db = config.db;
|
|
133
167
|
requireDbConfig(db);
|
|
168
|
+
const credentials = isCredentials(auth) ? auth : null;
|
|
169
|
+
const uid = credentials ? credentials.uid : auth ?? null;
|
|
134
170
|
const resolved = await resolveDbMode(db, config.generate);
|
|
135
171
|
if (resolved.mode === 'supabase') {
|
|
136
|
-
const token =
|
|
172
|
+
const token = credentials
|
|
173
|
+
? credentials.accessToken ?? throwMissingCredential('an access token')
|
|
174
|
+
: await resolveAccessToken(config);
|
|
137
175
|
return fn(await supabaseDb(resolved.supabase, token));
|
|
138
176
|
}
|
|
139
|
-
const userId = await resolveUserId(config, uid);
|
|
140
|
-
const role = await resolveAuthenticatedRole(config, db);
|
|
177
|
+
const userId = credentials ? uid ?? throwMissingCredential('a user id') : await resolveUserId(config, uid);
|
|
178
|
+
const role = await resolveAuthenticatedRole(config, db, credentials ? credentials.role : undefined);
|
|
141
179
|
return await withDbClient(config, async (client) => {
|
|
142
180
|
const rawClient = client;
|
|
143
181
|
const setSessionState = async () => {
|
package/dist/src/db/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export { withPublicDb, withUserDb } from './context.js';
|
|
1
|
+
export { withPublicDb, withUserDb, resolveUserDbCredentials } from './context.js';
|
|
2
|
+
export type { UserDbCredentials } from './context.js';
|
|
2
3
|
export type { DrizzleDb, TransactionResult } from './context.js';
|
|
3
4
|
export { withDbClient, connectToPostgres, disconnectPostgres, resetConnectionState } from './connection.js';
|
|
4
5
|
export type { DbRoutingConfig } from '../types/types.js';
|
package/dist/src/db/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { withPublicDb, withUserDb } from './context.js';
|
|
1
|
+
export { withPublicDb, withUserDb, resolveUserDbCredentials } from './context.js';
|
|
2
2
|
export { withDbClient, connectToPostgres, disconnectPostgres, resetConnectionState } from './connection.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type LinkProps } from 'next/link.js';
|
|
2
2
|
import { type ComponentProps } from 'react';
|
|
3
|
+
export declare const PENDING_NAVIGATION_EVENT = "cloudflare-next-intl:pending-navigation";
|
|
3
4
|
export type PrefetchType = 'custom' | 'eager' | 'default';
|
|
4
5
|
type NextLinkProps = Omit<ComponentProps<'a'>, keyof LinkProps> & Omit<LinkProps, 'locale'> & {
|
|
5
6
|
prefetchType?: PrefetchType;
|
|
@@ -5,6 +5,7 @@ import { forwardRef, useCallback, useEffect, useRef, useState, useTransition, }
|
|
|
5
5
|
import config from '../../config/intl_config.js';
|
|
6
6
|
import { getLocaleCache } from '../../general/cache_variables.js';
|
|
7
7
|
import { usePathname, useRouter } from 'next/navigation.js';
|
|
8
|
+
export const PENDING_NAVIGATION_EVENT = 'cloudflare-next-intl:pending-navigation';
|
|
8
9
|
const prefetchedRoutes = new Set();
|
|
9
10
|
function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', hoverPrefetchDelayMs = 100, onClick, onMouseEnter, onMouseLeave, onPointerDown, ...rest }, ref) {
|
|
10
11
|
const localeValue = getLocaleCache();
|
|
@@ -25,8 +26,14 @@ function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', hoverPref
|
|
|
25
26
|
const pathname = usePathname();
|
|
26
27
|
const [isPending, startTransition] = useTransition();
|
|
27
28
|
const [isNavigating, setIsNavigating] = useState(false);
|
|
29
|
+
const isFirstPathnameEffect = useRef(true);
|
|
28
30
|
useEffect(() => {
|
|
29
31
|
setIsNavigating(false);
|
|
32
|
+
if (isFirstPathnameEffect.current) {
|
|
33
|
+
isFirstPathnameEffect.current = false;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
window.dispatchEvent(new CustomEvent(PENDING_NAVIGATION_EVENT, { detail: null }));
|
|
30
37
|
}, [pathname]);
|
|
31
38
|
useEffect(() => {
|
|
32
39
|
if (!isNavigating)
|
|
@@ -84,6 +91,8 @@ function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', hoverPref
|
|
|
84
91
|
onClick?.(e);
|
|
85
92
|
if (e.defaultPrevented)
|
|
86
93
|
return;
|
|
94
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0)
|
|
95
|
+
return;
|
|
87
96
|
if (isCustom && (isNavigating || isPending)) {
|
|
88
97
|
e.preventDefault();
|
|
89
98
|
return;
|
|
@@ -92,6 +101,7 @@ function CustomLinkFunction({ href, prefetch, prefetchType = 'custom', hoverPref
|
|
|
92
101
|
const targetPath = typeof pathnames === 'string' ? pathnames : urlString;
|
|
93
102
|
if (pathname !== targetPath) {
|
|
94
103
|
setIsNavigating(true);
|
|
104
|
+
window.dispatchEvent(new CustomEvent(PENDING_NAVIGATION_EVENT, { detail: targetPath }));
|
|
95
105
|
}
|
|
96
106
|
startTransition(() => {
|
|
97
107
|
router.push(targetPath);
|