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 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
@@ -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>, uid?: string | null, dbOverride?: DbRoutingConfig): 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';
@@ -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, uid, dbOverride) {
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 = await resolveAccessToken(config);
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 () => {
@@ -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';
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.9.34",
3
+ "version": "0.9.36",
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",