cloudflare-next-intl 0.7.8 → 0.8.1

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.
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Test double for {@link DrizzleDb} — the handle `withPublicDb`/`withUserDb`
3
+ * pass to your callback. Use it in repository/unit tests to avoid a real
4
+ * Postgres connection: build one with {@link makeFakeDb}, queue canned
5
+ * results with {@link rowsResult}/{@link executeResult}, then pass it
6
+ * directly wherever your code expects a `DrizzleDb`.
7
+ *
8
+ * Real repositories call chains like `db.select({...}).from(t).where(w)...`
9
+ * where every intermediate call returns `this` and the final awaited value
10
+ * resolves to a canned row array — plus `db.execute(sql...)` resolving to
11
+ * `{ rows: [...] }`, and `db.$with(name).as(builder)` / `db.with(...).select(...)`
12
+ * for CTE-based queries.
13
+ *
14
+ * Each fake db instance is given an ordered queue of results (`resultQueue`)
15
+ * consumed one-per-terminal-call in the exact sequence your code issues
16
+ * them — select/insert/update/delete/execute all share one queue, so call
17
+ * order in your source is what determines which fixture a call gets.
18
+ *
19
+ * Every intermediate chain call (`.where(...)`, `.values(...)`, `.set(...)`,
20
+ * `.orderBy(...)`, etc.) is also recorded with its exact arguments on the
21
+ * chain object returned from `select`/`insert`/`update`/`delete`, and that
22
+ * chain object is attached to the matching entry in `db.calls` — so a test
23
+ * can assert not just "select was called twice" but "the second select's
24
+ * `.where(...)` argument was X", closing the gap where a fake that only
25
+ * echoes canned rows back can't detect a broken predicate/sort/value.
26
+ */
27
+ type Row = Record<string, unknown>;
28
+ interface QueuedResult {
29
+ kind: 'rows' | 'execute';
30
+ rows: Row[];
31
+ }
32
+ /** Queues a result for a `select`/`insert`/`update`/`delete` chain. */
33
+ declare function rowsResult(rows: Row[]): QueuedResult;
34
+ /** Queues a result for an `execute(...)` call. */
35
+ declare function executeResult(rows: Row[]): QueuedResult;
36
+ interface ChainCall {
37
+ method: string;
38
+ args: unknown[];
39
+ }
40
+ /**
41
+ * Lazy like real drizzle query builders: intermediate calls just record
42
+ * themselves and return `this`; the queue is only consumed when the chain
43
+ * is actually awaited (`.then()`), which is also when a CTE built via
44
+ * `db.$with(name).as(builder)` is skipped — a CTE definition is never
45
+ * itself awaited standalone, only the final `db.with(...).select(...)`
46
+ * chain that references it is.
47
+ */
48
+ declare class ChainableQuery implements PromiseLike<Row[]> {
49
+ private readonly takeRows;
50
+ /** Every intermediate call this chain received, in call order, with its exact arguments. */
51
+ readonly chainCalls: ChainCall[];
52
+ constructor(takeRows: () => Row[]);
53
+ private record;
54
+ /** Returns the arguments of this chain's first call to `method` (e.g. `"where"`, `"values"`). */
55
+ argsOf(method: string): unknown[] | undefined;
56
+ from(...args: unknown[]): this;
57
+ leftJoin(...args: unknown[]): this;
58
+ innerJoin(...args: unknown[]): this;
59
+ where(...args: unknown[]): this;
60
+ orderBy(...args: unknown[]): this;
61
+ groupBy(...args: unknown[]): this;
62
+ limit(...args: unknown[]): this;
63
+ offset(...args: unknown[]): this;
64
+ values(...args: unknown[]): this;
65
+ set(...args: unknown[]): this;
66
+ returning(...args: unknown[]): this;
67
+ as(...args: unknown[]): this;
68
+ then<TResult1 = Row[], TResult2 = never>(onfulfilled?: ((value: Row[]) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
69
+ }
70
+ /**
71
+ * Fake {@link DrizzleDb} backed by a queue of canned results. Build one with
72
+ * {@link makeFakeDb} rather than calling this constructor directly.
73
+ */
74
+ declare class FakeDrizzleDb {
75
+ private readonly queue;
76
+ /** Top-level calls captured for assertions — each entry's `chain` is the same `ChainableQuery` returned to the caller, so its recorded `.where()`/`.values()`/etc. arguments are inspectable via `chain.argsOf(...)`. */
77
+ readonly calls: {
78
+ method: string;
79
+ args: unknown[];
80
+ chain?: ChainableQuery;
81
+ }[];
82
+ constructor(queue: QueuedResult[]);
83
+ private takeRows;
84
+ private makeChain;
85
+ select(...args: unknown[]): ChainableQuery;
86
+ insert(...args: unknown[]): ChainableQuery;
87
+ update(...args: unknown[]): ChainableQuery;
88
+ delete(...args: unknown[]): ChainableQuery;
89
+ execute(...args: unknown[]): Promise<{
90
+ rows: Row[];
91
+ }>;
92
+ /** CTE definition — never awaited on its own, so it must not touch the queue. */
93
+ $with(_name: string): {
94
+ as: (builder: unknown) => unknown;
95
+ };
96
+ with(..._ctes: unknown[]): {
97
+ select: (...args: unknown[]) => ChainableQuery;
98
+ };
99
+ }
100
+ /**
101
+ * Builds a fake {@link DrizzleDb} that resolves each terminal call
102
+ * (`select`/`insert`/`update`/`delete`/`execute`) to the next entry in
103
+ * `results`, in the exact order your code under test calls them.
104
+ *
105
+ * @param results Queued results, one per terminal call, built with
106
+ * {@link rowsResult}/{@link executeResult}.
107
+ * @returns A fake db to pass wherever your code expects a `DrizzleDb`.
108
+ *
109
+ * @example
110
+ * const db = makeFakeDb([rowsResult([{ id: 1 }])]);
111
+ * const rows = await db.select().from(bonds);
112
+ */
113
+ export declare function makeFakeDb(results: QueuedResult[]): FakeDrizzleDb;
114
+ export { rowsResult, executeResult };
115
+ export type { Row, ChainableQuery, FakeDrizzleDb };
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Test double for {@link DrizzleDb} — the handle `withPublicDb`/`withUserDb`
3
+ * pass to your callback. Use it in repository/unit tests to avoid a real
4
+ * Postgres connection: build one with {@link makeFakeDb}, queue canned
5
+ * results with {@link rowsResult}/{@link executeResult}, then pass it
6
+ * directly wherever your code expects a `DrizzleDb`.
7
+ *
8
+ * Real repositories call chains like `db.select({...}).from(t).where(w)...`
9
+ * where every intermediate call returns `this` and the final awaited value
10
+ * resolves to a canned row array — plus `db.execute(sql...)` resolving to
11
+ * `{ rows: [...] }`, and `db.$with(name).as(builder)` / `db.with(...).select(...)`
12
+ * for CTE-based queries.
13
+ *
14
+ * Each fake db instance is given an ordered queue of results (`resultQueue`)
15
+ * consumed one-per-terminal-call in the exact sequence your code issues
16
+ * them — select/insert/update/delete/execute all share one queue, so call
17
+ * order in your source is what determines which fixture a call gets.
18
+ *
19
+ * Every intermediate chain call (`.where(...)`, `.values(...)`, `.set(...)`,
20
+ * `.orderBy(...)`, etc.) is also recorded with its exact arguments on the
21
+ * chain object returned from `select`/`insert`/`update`/`delete`, and that
22
+ * chain object is attached to the matching entry in `db.calls` — so a test
23
+ * can assert not just "select was called twice" but "the second select's
24
+ * `.where(...)` argument was X", closing the gap where a fake that only
25
+ * echoes canned rows back can't detect a broken predicate/sort/value.
26
+ */
27
+ /** Queues a result for a `select`/`insert`/`update`/`delete` chain. */
28
+ function rowsResult(rows) {
29
+ return { kind: 'rows', rows };
30
+ }
31
+ /** Queues a result for an `execute(...)` call. */
32
+ function executeResult(rows) {
33
+ return { kind: 'execute', rows };
34
+ }
35
+ /**
36
+ * Lazy like real drizzle query builders: intermediate calls just record
37
+ * themselves and return `this`; the queue is only consumed when the chain
38
+ * is actually awaited (`.then()`), which is also when a CTE built via
39
+ * `db.$with(name).as(builder)` is skipped — a CTE definition is never
40
+ * itself awaited standalone, only the final `db.with(...).select(...)`
41
+ * chain that references it is.
42
+ */
43
+ class ChainableQuery {
44
+ constructor(takeRows) {
45
+ this.takeRows = takeRows;
46
+ /** Every intermediate call this chain received, in call order, with its exact arguments. */
47
+ this.chainCalls = [];
48
+ }
49
+ record(method, args) {
50
+ this.chainCalls.push({ method, args });
51
+ return this;
52
+ }
53
+ /** Returns the arguments of this chain's first call to `method` (e.g. `"where"`, `"values"`). */
54
+ argsOf(method) {
55
+ return this.chainCalls.find((c) => c.method === method)?.args;
56
+ }
57
+ from(...args) { return this.record('from', args); }
58
+ leftJoin(...args) { return this.record('leftJoin', args); }
59
+ innerJoin(...args) { return this.record('innerJoin', args); }
60
+ where(...args) { return this.record('where', args); }
61
+ orderBy(...args) { return this.record('orderBy', args); }
62
+ groupBy(...args) { return this.record('groupBy', args); }
63
+ limit(...args) { return this.record('limit', args); }
64
+ offset(...args) { return this.record('offset', args); }
65
+ values(...args) { return this.record('values', args); }
66
+ set(...args) { return this.record('set', args); }
67
+ returning(...args) { return this.record('returning', args); }
68
+ as(...args) { return this.record('as', args); }
69
+ then(onfulfilled, onrejected) {
70
+ return Promise.resolve().then(() => this.takeRows()).then(onfulfilled, onrejected);
71
+ }
72
+ }
73
+ /**
74
+ * Fake {@link DrizzleDb} backed by a queue of canned results. Build one with
75
+ * {@link makeFakeDb} rather than calling this constructor directly.
76
+ */
77
+ class FakeDrizzleDb {
78
+ constructor(queue) {
79
+ /** Top-level calls captured for assertions — each entry's `chain` is the same `ChainableQuery` returned to the caller, so its recorded `.where()`/`.values()`/etc. arguments are inspectable via `chain.argsOf(...)`. */
80
+ this.calls = [];
81
+ this.queue = [...queue];
82
+ }
83
+ takeRows(method) {
84
+ const next = this.queue.shift();
85
+ if (!next)
86
+ throw new Error(`FakeDrizzleDb: no queued result left for ${method}()`);
87
+ return next.rows;
88
+ }
89
+ makeChain(method, args) {
90
+ const chain = new ChainableQuery(() => this.takeRows(method));
91
+ this.calls.push({ method, args, chain });
92
+ return chain;
93
+ }
94
+ select(...args) {
95
+ return this.makeChain('select', args);
96
+ }
97
+ insert(...args) {
98
+ return this.makeChain('insert', args);
99
+ }
100
+ update(...args) {
101
+ return this.makeChain('update', args);
102
+ }
103
+ delete(...args) {
104
+ return this.makeChain('delete', args);
105
+ }
106
+ async execute(...args) {
107
+ this.calls.push({ method: 'execute', args });
108
+ const next = this.queue.shift();
109
+ if (!next)
110
+ throw new Error('FakeDrizzleDb: no queued result left for execute()');
111
+ return { rows: next.rows };
112
+ }
113
+ /** CTE definition — never awaited on its own, so it must not touch the queue. */
114
+ $with(_name) {
115
+ return { as: (builder) => builder };
116
+ }
117
+ with(..._ctes) {
118
+ return { select: (...args) => this.select(...args) };
119
+ }
120
+ }
121
+ /**
122
+ * Builds a fake {@link DrizzleDb} that resolves each terminal call
123
+ * (`select`/`insert`/`update`/`delete`/`execute`) to the next entry in
124
+ * `results`, in the exact order your code under test calls them.
125
+ *
126
+ * @param results Queued results, one per terminal call, built with
127
+ * {@link rowsResult}/{@link executeResult}.
128
+ * @returns A fake db to pass wherever your code expects a `DrizzleDb`.
129
+ *
130
+ * @example
131
+ * const db = makeFakeDb([rowsResult([{ id: 1 }])]);
132
+ * const rows = await db.select().from(bonds);
133
+ */
134
+ export function makeFakeDb(results) {
135
+ return new FakeDrizzleDb(results);
136
+ }
137
+ export { rowsResult, executeResult };
@@ -4,3 +4,4 @@ export * from './server';
4
4
  export * from './client';
5
5
  export * from './theme_switcher';
6
6
  export * from './types';
7
+ export * from './db';
package/dist/src/index.js CHANGED
@@ -4,3 +4,4 @@ export * from './server';
4
4
  export * from './client';
5
5
  export * from './theme_switcher';
6
6
  export * from './types';
7
+ export * from './db';
@@ -85,6 +85,14 @@ export interface RoutingConfig<AppLocales extends Locales, AppLocalePrefixMode e
85
85
  * if this field is missing at call time rather than silently no-op'ing.
86
86
  */
87
87
  firebaseAuth?: FirebaseAuthRoutingConfig;
88
+ /**
89
+ * Configures the optional `db` submodule (Postgres/Drizzle access over a
90
+ * Cloudflare Hyperdrive or plain connection string). Omit entirely to keep
91
+ * it fully disabled — no file in this package imports `pg`/`drizzle-orm`
92
+ * unless a `db` export is actually called, and every such export throws a
93
+ * clear error if this field is missing at call time.
94
+ */
95
+ db?: DbRoutingConfig;
88
96
  /**
89
97
  * Configures the optional `cookie_consent` submodule (cookie-consent +
90
98
  * privacy-policy-update banners). Omit entirely to keep it disabled —
@@ -809,3 +817,86 @@ export interface IntlSitemap {
809
817
  lastModified: Date | string | undefined;
810
818
  videos?: Videos[] | undefined;
811
819
  }
820
+ export interface SupabaseDbConfig {
821
+ /**
822
+ * Supabase project URL, e.g. `https://abc.supabase.co`. Defaults to
823
+ * `process.env.NEXT_PUBLIC_SUPABASE_URL`.
824
+ */
825
+ url?: string;
826
+ /**
827
+ * Supabase anon (publishable) key. Defaults to
828
+ * `process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY`. This is the only key the
829
+ * `db` module ever needs — never put a service-role key here.
830
+ */
831
+ anonKey?: string;
832
+ /**
833
+ * Name of the Postgres function that runs the generated SQL. Defaults to
834
+ * `'cfni_exec'` — the function shipped in `supabase/cfni_exec.sql`.
835
+ */
836
+ execFunction?: string;
837
+ }
838
+ export interface DbRoutingConfig {
839
+ /**
840
+ * Postgres connection string. Omit to resolve it from the Cloudflare
841
+ * Hyperdrive binding named by `hyperdriveBinding` instead (the normal
842
+ * production setup); a value here always wins over the binding, which is
843
+ * what makes local dev / build-time evaluation work.
844
+ */
845
+ connectionString?: string;
846
+ /**
847
+ * Name of the Hyperdrive binding on `env` whose `connectionString` is used
848
+ * when `connectionString` is not set. Defaults to `'HYPERDRIVE'`. Requires
849
+ * `generate.getCloudflareContext` to be configured.
850
+ */
851
+ hyperdriveBinding?: string;
852
+ /**
853
+ * Whether the pooled client is closed once the last in-flight
854
+ * `withPublicDb`/`withUserDb` call of the request finishes.
855
+ * Defaults to `true` (one connection per request, released to Hyperdrive
856
+ * immediately). Set `false` to keep the connection open for the lifetime
857
+ * of the isolate — faster for a long-lived server, but it holds a
858
+ * Hyperdrive connection slot between requests.
859
+ */
860
+ disconnectAfterRequest?: boolean;
861
+ /**
862
+ * Postgres role assumed inside `withUserDb`'s transaction. Defaults
863
+ * to `'authenticated'` (the Supabase RLS convention).
864
+ */
865
+ authenticatedRole?: string;
866
+ /**
867
+ * Resolves the user id injected as `request.jwt.claims->>'sub'` inside
868
+ * `withUserDb`. Omit when `firebaseAuth` is configured — the uid then
869
+ * comes from this package's own `getAuthUser()` automatically. Provide it
870
+ * to use a different auth source (or when `firebaseAuth` is absent).
871
+ */
872
+ getUserId?: () => Promise<string | null> | string | null;
873
+ /** Milliseconds `disconnectPostgres` waits for `client.end()` before giving up. Defaults to `2000`. */
874
+ disconnectTimeoutMs?: number;
875
+ /**
876
+ * Reaches Postgres through the Supabase Data API instead of a direct
877
+ * connection, using only your project URL and anon key. Set this when you
878
+ * have no Postgres password to give the package — `withPublicDb` and
879
+ * `withUserDb` behave the same either way, so switching is a config change
880
+ * with no app-code change.
881
+ *
882
+ * Ignored when `connectionString` or `hyperdriveBinding` is set: a direct
883
+ * connection always wins, so adding this block cannot silently reroute
884
+ * live traffic. Requires the `cfni_exec` function from
885
+ * `supabase/cfni_exec.sql` to be installed in your database.
886
+ *
887
+ * No multi-statement transactions: unlike connection-string mode, each
888
+ * statement inside a `withUserDb` callback is its own round-trip. Do not
889
+ * rely on multi-statement atomicity in this mode.
890
+ */
891
+ supabase?: SupabaseDbConfig;
892
+ /**
893
+ * Resolves the JWT sent as `Authorization: Bearer` for `withUserDb` in
894
+ * Supabase mode, which is what makes PostgREST resolve the caller as
895
+ * `authenticated` and apply RLS. Omit when `firebaseAuth` is configured —
896
+ * the signed-in user's Firebase ID token is then used automatically.
897
+ *
898
+ * Unused in connection-string mode, which identifies the user with
899
+ * `getUserId` and `set_config` instead.
900
+ */
901
+ getAccessToken?: () => Promise<string | null> | string | null;
902
+ }
package/llms.txt CHANGED
@@ -23,6 +23,8 @@ other subpath can be used.
23
23
  - `./localeStaticParams` — `generateStaticParams` helper for locale segments.
24
24
  - `./use` — `useLocale`/`useTranslations`; resolves to the RSC or client implementation automatically via the `react-server` export condition. Both throw `"... must be used within an IntlProvider"` if called outside one.
25
25
  - `./ThemeSwitcher` — optional light/dark toggle component.
26
+ - `./db` — `withPublicDb(fn)` / `withUserDb(fn, uid?)` server-side Postgres/Drizzle context helpers (require `db` set on your `RoutingConfig`; direct Postgres or Supabase Data API, see below).
27
+ - `./dbHelpers` — generic Drizzle SQL helper functions (`excluded`, `onConflictSet`, `ago`, `currentDate`, `windowCount`, `unnestLateral`, `ascNullsLast`, `alwaysTrue`, `lateral`, `aliasColumn`, `minOf`, `maxOf`, `roundReal`, `multiply`, `scalarFromCte`) for use with `./db`.
26
28
 
27
29
  ## `firebaseAuth*` subpaths (require `firebaseAuth` set on your `RoutingConfig`)
28
30
 
@@ -44,6 +46,38 @@ other subpath can be used.
44
46
  - `./cookieConsentAnalytics` — `CookieConsentAnalytics`: gates Cloudflare Web Analytics / Google Ads / Google Analytics / AdSense / Microsoft Clarity behind consent; rendered automatically by `IntlProvider` when `cookieConsent.secrets` or `getSecrets` is set (and `autoWireAnalytics !== false`). Never renders in local dev (`NODE_ENV === 'development'`) unless `cookieConsent.enableAnalyticsInDevMode` is `true`.
45
47
  - Country-based gating (`cookieConsent.getCountryCode` / `getCloudflareContext` + `gdprCountries`): resolved server-side by `IntlProvider` into a `requiresConsent` boolean passed to `CookieConsentProvider`. Neither getter set → gating off, consent always implicitly granted. `getCountryCode` (direct country resolver) takes precedence over `getCloudflareContext` (reads `cf.country`) when both are set. `getCloudflareContext` accepts `@opennextjs/cloudflare`'s `getCloudflareContext` function directly (its exact overloaded signature — `CookieConsentGetCloudflareContext`), called internally with `{ async: true }`. Unresolved country (or a `null` context) always requires consent (fail-safe). `./cookieConsent` also exports `defaultGdprCountries` (EU/EEA + UK + Switzerland).
46
48
 
49
+ ## `db*` subpaths (require `db` set on your `RoutingConfig`)
50
+
51
+ Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@supabase/supabase-js` ship as dependencies and load via dynamic `import()`, so nothing bundles unless a `db` export is called.
52
+
53
+ - Direct Postgres (wins if configured): `db.connectionString` — Postgres connection string; when omitted, resolved from the Hyperdrive binding named by `db.hyperdriveBinding` (default `'HYPERDRIVE'`) via `generate.getCloudflareContext`. An explicit `connectionString` always wins (enables local dev / build-time evaluation).
54
+ - Supabase Data API (used only when neither of the above is set): `db.supabase` — `{ url?, anonKey?, execFunction? }`, defaulting `url`/`anonKey` to `NEXT_PUBLIC_SUPABASE_URL`/`NEXT_PUBLIC_SUPABASE_ANON_KEY`. Requires installing `supabase/cfni_exec.sql` (a `security invoker` SQL-exec function) in your database. No multi-statement transactions — each statement in a `withUserDb` callback is its own round-trip.
55
+ - `db.disconnectAfterRequest` — direct-Postgres mode only: closes the pooled client once the last in-flight `withPublicDb`/`withUserDb` call of the request finishes. Defaults to `true`; set `false` to keep the connection open for the isolate's lifetime (holds a Hyperdrive slot between requests).
56
+ - `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Defaults to `'authenticated'` (Supabase RLS convention).
57
+ - `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()`.
58
+ - `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.
59
+ - `db.disconnectTimeoutMs` — direct-Postgres mode only: ms `disconnectPostgres` waits for `client.end()` before giving up. Defaults to `2000`.
60
+ - `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.
61
+ - `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.
62
+ - `./dbHelpers` functions are plain Drizzle `sql`-building utilities with no config dependency — usable standalone.
63
+
64
+ ```typescript
65
+ // src/i18n/intl_config.ts
66
+ import { getCloudflareContext } from "@opennextjs/cloudflare";
67
+
68
+ export default setIntlConfig({
69
+ locales: ["en", "uk"] as const,
70
+ defaultLocale: "en",
71
+ generate: { getCloudflareContext },
72
+ db: { hyperdriveBinding: "HYPERDRIVE" },
73
+ });
74
+
75
+ // anywhere on the server
76
+ import { withPublicDb } from "cloudflare-next-intl/db";
77
+
78
+ const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
79
+ ```
80
+
47
81
  ## Conventions
48
82
 
49
83
  - Every exported function/component has a JSDoc comment with an `@example` where usage isn't obvious from the signature alone.
@@ -56,3 +90,4 @@ other subpath can be used.
56
90
  - Missing `firebaseAuth` on `RoutingConfig` throws immediately from any `firebaseAuth*` export, naming the missing config field.
57
91
  - `useLocale`/`useTranslations` (from `./use`) and `useAuthUser` (from `./useFirebaseAuthUser`) resolve to a Server- or Client-Component implementation automatically via the `react-server` export condition — always import from the subpath, never from an internal file path.
58
92
  - No unified `useFirebaseAuthUser` re-export inside `cloudflare-next-intl/firebaseAuth`'s barrel — that barrel can't replicate the `react-server` condition split, so import `cloudflare-next-intl/useFirebaseAuthUser` directly for the hook.
93
+ - Missing `db` on `RoutingConfig` throws immediately from `withPublicDb`/`withUserDb`, naming the missing config field. `pg`, `drizzle-orm`, and `@supabase/supabase-js` ship as dependencies and load via dynamic `import()` — nothing to install yourself, and nothing bundles unless a `db` export is called.
package/package.json CHANGED
@@ -1,13 +1,17 @@
1
1
  {
2
2
  "name": "cloudflare-next-intl",
3
- "version": "0.7.8",
3
+ "version": "0.8.1",
4
4
  "description": "Optimized Next Intl Package Special for App Router and Cloudflare",
5
-
6
5
  "main": "dist/index.js",
7
6
  "types": "dist/index.d.ts",
8
7
  "sideEffects": false,
8
+ "bin": {
9
+ "cfni-db-codegen": "bin/db_codegen.mjs"
10
+ },
9
11
  "files": [
10
12
  "dist",
13
+ "bin",
14
+ "supabase",
11
15
  "LICENSE",
12
16
  "README.md",
13
17
  "llms.txt"
@@ -172,6 +176,18 @@
172
176
  "./createServerErrorAction": {
173
177
  "types": "./dist/src/error_handling/create_server_error_action.d.ts",
174
178
  "import": "./dist/src/error_handling/create_server_error_action.js"
179
+ },
180
+ "./db": {
181
+ "types": "./dist/src/db/index.d.ts",
182
+ "import": "./dist/src/db/index.js"
183
+ },
184
+ "./dbHelpers": {
185
+ "types": "./dist/src/db/helpers.d.ts",
186
+ "import": "./dist/src/db/helpers.js"
187
+ },
188
+ "./dbTesting": {
189
+ "types": "./dist/src/db/testing.d.ts",
190
+ "import": "./dist/src/db/testing.js"
175
191
  }
176
192
  },
177
193
  "scripts": {
@@ -216,8 +232,11 @@
216
232
  "homepage": "https://github.com/demian-ilnytskyi/cloudflare-next-intl#readme",
217
233
  "dependencies": {
218
234
  "@microsoft/clarity": "^1.0.2",
235
+ "@supabase/supabase-js": "^2.112.3",
236
+ "drizzle-orm": "^0.45.2",
219
237
  "firebase": "^12.17.0",
220
- "jose": "^6.2.8"
238
+ "jose": "^6.2.8",
239
+ "pg": "^8.23.0"
221
240
  },
222
241
  "peerDependencies": {
223
242
  "next": ">=12.0.0",
@@ -236,16 +255,18 @@
236
255
  "@testing-library/jest-dom": "^7.0.0",
237
256
  "@testing-library/react": "^16.3.2",
238
257
  "@types/node": "^20.14.5",
258
+ "@types/pg": "^8.23.1",
239
259
  "@types/react": "^19.0.0",
240
260
  "@types/react-dom": "^19.0.0",
241
261
  "@vitest/coverage-v8": "^3.2.7",
262
+ "drizzle-kit": "^0.31.10",
242
263
  "eslint": "^9.28.0",
243
264
  "eslint-config-next": "15.3.3",
244
265
  "eslint-config-prettier": "^10.1.2",
245
266
  "jsdom": "^29.1.1",
246
267
  "next": "^15.3.0",
247
- "react": "^19.0.0",
248
- "react-dom": "^19.0.0",
268
+ "react": "19.2.0",
269
+ "react-dom": "19.2.0",
249
270
  "tsup": "^8.5.1",
250
271
  "typescript": "^5.5.3",
251
272
  "typescript-eslint": "^8.33.1",
@@ -0,0 +1,34 @@
1
+ -- Executes a statement generated by cloudflare-next-intl's `db` module and
2
+ -- returns its rows as positional JSON arrays, which is the shape
3
+ -- drizzle-orm/pg-proxy maps result columns from.
4
+ --
5
+ -- SECURITY INVOKER is load-bearing: the statement runs with the privileges of
6
+ -- the caller (anon or authenticated, per the request's JWT), so row-level
7
+ -- security still applies exactly as it does over the REST API. Do not change
8
+ -- this to SECURITY DEFINER, and do not add a role parameter — either would
9
+ -- turn this into a privilege-escalation primitive.
10
+ create or replace function public.cfni_exec(statement text, params jsonb default '[]'::jsonb)
11
+ returns json
12
+ language plpgsql
13
+ security invoker
14
+ as $$
15
+ declare
16
+ result json;
17
+ args text[];
18
+ begin
19
+ select coalesce(array_agg(value #>> '{}' order by ordinality), '{}')
20
+ into args
21
+ from jsonb_array_elements(params) with ordinality as t(value, ordinality);
22
+
23
+ execute format('select coalesce(json_agg(json_build_array(r.*)), ''[]''::json) from (%s) r', statement)
24
+ into result
25
+ using args;
26
+
27
+ return result;
28
+ end;
29
+ $$;
30
+
31
+ revoke all on function public.cfni_exec(text, jsonb) from public;
32
+ grant execute on function public.cfni_exec(text, jsonb) to authenticated;
33
+ -- Grant to anon only if your app calls withPublicDb:
34
+ grant execute on function public.cfni_exec(text, jsonb) to anon;