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.
- package/README.md +172 -0
- package/bin/db_codegen.mjs +110 -0
- package/dist/src/db/access_token.d.ts +14 -0
- package/dist/src/db/access_token.js +30 -0
- package/dist/src/db/codegen_paths.d.ts +13 -0
- package/dist/src/db/codegen_paths.js +32 -0
- package/dist/src/db/connection.d.ts +42 -0
- package/dist/src/db/connection.js +178 -0
- package/dist/src/db/context.d.ts +57 -0
- package/dist/src/db/context.js +125 -0
- package/dist/src/db/helpers.d.ts +64 -0
- package/dist/src/db/helpers.js +119 -0
- package/dist/src/db/index.d.ts +24 -0
- package/dist/src/db/index.js +22 -0
- package/dist/src/db/require_config.d.ts +15 -0
- package/dist/src/db/require_config.js +20 -0
- package/dist/src/db/resolve_mode.d.ts +17 -0
- package/dist/src/db/resolve_mode.js +18 -0
- package/dist/src/db/supabase_config.d.ts +18 -0
- package/dist/src/db/supabase_config.js +22 -0
- package/dist/src/db/supabase_transport.d.ts +29 -0
- package/dist/src/db/supabase_transport.js +49 -0
- package/dist/src/db/testing.d.ts +115 -0
- package/dist/src/db/testing.js +137 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/types.d.ts +91 -0
- package/llms.txt +35 -0
- package/package.json +26 -5
- package/supabase/cfni_exec.sql +34 -0
|
@@ -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 };
|
package/dist/src/index.d.ts
CHANGED
package/dist/src/index.js
CHANGED
|
@@ -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.
|
|
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": "
|
|
248
|
-
"react-dom": "
|
|
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;
|