cloudflare-next-intl 0.10.18 → 0.10.19
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 +2 -2
- package/llms.txt +2 -2
- package/package.json +3 -3
- package/supabase/cfni_exec.sql +0 -192
- package/supabase/tests/cfni_exec.sql +0 -193
package/README.md
CHANGED
|
@@ -914,7 +914,7 @@ const rows = await withPublicDb((db) => db.select().from(bonds).limit(10));
|
|
|
914
914
|
```
|
|
915
915
|
|
|
916
916
|
Supabase mode requires one function in your database, shipped at
|
|
917
|
-
`node_modules/cloudflare-next-intl/supabase/cfni_exec.sql
|
|
917
|
+
`node_modules/cloudflare-next-intl-db-codegen/supabase/cfni_exec.sql` (installed as a dependency). Run it once (via
|
|
918
918
|
`supabase db push`, a migration, or the SQL editor) — the file starts with a
|
|
919
919
|
`drop function if exists` so re-running it to upgrade is always safe. It is
|
|
920
920
|
`security invoker`, so statements execute with the caller's own privileges and
|
|
@@ -1248,7 +1248,7 @@ tested two ways in this package's own repo, and both are shipped so you can
|
|
|
1248
1248
|
reuse them against your own database rather than trusting the function
|
|
1249
1249
|
untested:
|
|
1250
1250
|
|
|
1251
|
-
- `supabase/tests/cfni_exec.sql` — a [pgTAP](https://pgtap.org/) suite,
|
|
1251
|
+
- `node_modules/cloudflare-next-intl-db-codegen/supabase/tests/cfni_exec.sql` — a [pgTAP](https://pgtap.org/) suite,
|
|
1252
1252
|
runnable with `supabase test db` (or `pg_prove`) once both this file and
|
|
1253
1253
|
`cfni_exec.sql` are installed in a database. It checks the SQL function
|
|
1254
1254
|
directly: every statement shape `cfni_exec` classifies (plain `SELECT`,
|
package/llms.txt
CHANGED
|
@@ -73,7 +73,7 @@ Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@sup
|
|
|
73
73
|
- 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.
|
|
74
74
|
- `db.autoHyperdrive` — when `true` (default) and `db.connectionString` is unset, `env.HYPERDRIVE.connectionString` is read automatically before falling through to `db.supabase`. Set `false` to skip this and go straight to `supabase` (or the "no connection string" error).
|
|
75
75
|
- `db.autoHyperdriveSkipUrls` — connection strings from `env.HYPERDRIVE.connectionString` treated as "no connection" by auto-Hyperdrive (e.g. `wrangler dev`'s unconfigured placeholder). Defaults to `['postgresql://user:pass@localhost:5432/db']`.
|
|
76
|
-
- 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
|
|
76
|
+
- 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 `cfni_exec` (shipped as `cloudflare-next-intl-db-codegen/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.
|
|
77
77
|
- `db.disconnectAfterRequest` — deprecated, ignored since 0.8.23. Each `withPublicDb`/`withUserDb` call opens and closes its own client; Hyperdrive pools the server-side connection.
|
|
78
78
|
- `db.authenticatedRole` — direct-Postgres mode only: Postgres role assumed inside `withUserDb`'s transaction. Accepts a string or a sync/async function returning one. Defaults to `'authenticated'` (Supabase RLS convention).
|
|
79
79
|
- `db.authenticatedRoleClaim` — direct-Postgres mode only: Firebase custom claim field (default `'role'`) read to determine the Postgres role in `withUserDb` when `firebaseAuth` is enabled. Set `false` to disable.
|
|
@@ -83,7 +83,7 @@ Two transports, picked by which `db` fields are set — `pg`/`drizzle-orm`/`@sup
|
|
|
83
83
|
- `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.
|
|
84
84
|
- `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.
|
|
85
85
|
- `./dbHelpers` functions are plain Drizzle `sql`-building utilities with no config dependency — usable standalone.
|
|
86
|
-
- `cfni-db-codegen` binary — regenerates Drizzle models via `drizzle-kit pull`. By default, prefers `embedded-postgres` (zero setup, no Docker needed) to load DDL from `--ddl-dir` and introspect it. If an explicit `--db-url`/`CODEGEN_DATABASE_URL` is set, connects to that live DB (and falls back to `embedded-postgres` with a warning if unreachable). Flags/env: `--ddl-dir`/`CFNI_DB_DDL_DIR`, `--out-dir`/`CFNI_DB_OUT_DIR`, `--out-file`/`CFNI_DB_OUT_FILE`, `--db-url`/`CODEGEN_DATABASE_URL`, `--drizzle-config`/`CFNI_DB_DRIZZLE_CONFIG`, `--rpc-dir`/`CFNI_DB_RPC_DIR`, `--tests-dir`/`CFNI_DB_TESTS_DIR`, `--force`/`CFNI_DB_FORCE_EXEC`, `--skip-exec`/`CFNI_DB_SKIP_EXEC`, `--check`. `--out-dir` is repeatable and accepts a comma-separated list, so one run generates the same schema (and `manifest.json`) into several projects; `--check` verifies every target. After a successful run it also copies `supabase/cfni_exec.sql` + its pgTAP test file into `--rpc-dir`/`--tests-dir` (siblings of `--ddl-dir` by default), gated on `db.supabase.rawSql` (read from `next.config.*`'s `@intl-config` alias; unknown → warns and assumes `true`) — skips if `rawSql: false`, skips if `--skip-exec`, skips-with-warning on a differing existing file unless `--force`. `cfni-db-install-exec` runs only this copy step, same flags, no Postgres/drizzle-kit needed.
|
|
86
|
+
- `cfni-db-codegen` binary — regenerates Drizzle models via `drizzle-kit pull`. By default, prefers `embedded-postgres` (zero setup, no Docker needed) to load DDL from `--ddl-dir` and introspect it. If an explicit `--db-url`/`CODEGEN_DATABASE_URL` is set, connects to that live DB (and falls back to `embedded-postgres` with a warning if unreachable). Flags/env: `--ddl-dir`/`CFNI_DB_DDL_DIR`, `--out-dir`/`CFNI_DB_OUT_DIR`, `--out-file`/`CFNI_DB_OUT_FILE`, `--db-url`/`CODEGEN_DATABASE_URL`, `--drizzle-config`/`CFNI_DB_DRIZZLE_CONFIG`, `--rpc-dir`/`CFNI_DB_RPC_DIR`, `--tests-dir`/`CFNI_DB_TESTS_DIR`, `--force`/`CFNI_DB_FORCE_EXEC`, `--skip-exec`/`CFNI_DB_SKIP_EXEC`, `--check`. `--out-dir` is repeatable and accepts a comma-separated list, so one run generates the same schema (and `manifest.json`) into several projects; `--check` verifies every target. After a successful run it also copies `cloudflare-next-intl-db-codegen/supabase/cfni_exec.sql` + its pgTAP test file into `--rpc-dir`/`--tests-dir` (siblings of `--ddl-dir` by default), gated on `db.supabase.rawSql` (read from `next.config.*`'s `@intl-config` alias; unknown → warns and assumes `true`) — skips if `rawSql: false`, skips if `--skip-exec`, skips-with-warning on a differing existing file unless `--force`. `cfni-db-install-exec` runs only this copy step, same flags, no Postgres/drizzle-kit needed.
|
|
87
87
|
|
|
88
88
|
```typescript
|
|
89
89
|
// src/i18n/intl_config.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cloudflare-next-intl",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.19",
|
|
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",
|
|
@@ -358,8 +358,8 @@
|
|
|
358
358
|
"homepage": "https://github.com/demian-ilnytskyi/cloudflare-next-intl#readme",
|
|
359
359
|
"dependencies": {
|
|
360
360
|
"@microsoft/clarity": "^1.0.2",
|
|
361
|
-
"cloudflare-next-intl-db": "^0.
|
|
362
|
-
"cloudflare-next-intl-db-codegen": "^0.1.
|
|
361
|
+
"cloudflare-next-intl-db": "^0.3.0",
|
|
362
|
+
"cloudflare-next-intl-db-codegen": "^0.1.3",
|
|
363
363
|
"jose": "^6.2.8",
|
|
364
364
|
"sharp": "^0.34.5 || ^0.35.0"
|
|
365
365
|
},
|
package/supabase/cfni_exec.sql
DELETED
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
-- Executes a statement generated by cloudflare-next-intl's `db` module and
|
|
2
|
-
-- returns its rows/rowCount as jsonb, in the shape drizzle-orm/pg-proxy expects.
|
|
3
|
-
--
|
|
4
|
-
-- v2: statements arrive with parameters already inlined as literals by the
|
|
5
|
-
-- `db` transport (see `inline_params.ts`), so this function takes no
|
|
6
|
-
-- parameters and never binds anything itself. It also now supports
|
|
7
|
-
-- INSERT/UPDATE/DELETE, with or without RETURNING.
|
|
8
|
-
--
|
|
9
|
-
-- Each output row is cast with `r::text`, Postgres' own composite-literal
|
|
10
|
-
-- text format: `(field1,field2,...)`, fields comma-separated, quoted with
|
|
11
|
-
-- `"…"` (doubling embedded quotes) whenever a field's own text contains a
|
|
12
|
-
-- comma/quote/paren/backslash/whitespace, and empty-unquoted for NULL. Every
|
|
13
|
-
-- field is already in that column's normal `::text` form — arrays as
|
|
14
|
-
-- `{1,2}`, booleans as `t`/`f`, timestamps without a `T`, full `numeric`
|
|
15
|
-
-- precision — the same representation the `pg` wire protocol sends in text
|
|
16
|
-
-- mode, and what Drizzle's column mappers decode. Unlike `row_to_json`/
|
|
17
|
-
-- `hstore(record)`, this stays correct with duplicate column names (a common
|
|
18
|
-
-- shape after a join with `select a.*, b.*`), since it is purely positional
|
|
19
|
-
-- and never keys fields by name. The transport (`supabase_transport.ts`)
|
|
20
|
-
-- parses this literal back into a positional array with `parse_composite.ts`.
|
|
21
|
-
--
|
|
22
|
-
-- SECURITY INVOKER is load-bearing: the statement runs with the privileges of
|
|
23
|
-
-- the caller (anon or authenticated, per the request's JWT), so row-level
|
|
24
|
-
-- security still applies exactly as it does over the REST API. Do not change
|
|
25
|
-
-- this to SECURITY DEFINER, and do not add a role parameter — either would
|
|
26
|
-
-- turn this into a privilege-escalation primitive.
|
|
27
|
-
--
|
|
28
|
-
-- The drop below is only needed when upgrading from the old `json`-returning
|
|
29
|
-
-- v1 of this function — `create or replace` cannot change a function's
|
|
30
|
-
-- return type, and is a no-op (safe to re-run) when this is a fresh install.
|
|
31
|
-
drop function if exists public.cfni_exec(text);
|
|
32
|
-
|
|
33
|
-
create or replace function public.cfni_exec(statement text)
|
|
34
|
-
returns jsonb
|
|
35
|
-
language plpgsql
|
|
36
|
-
security invoker
|
|
37
|
-
set search_path = public
|
|
38
|
-
as $$
|
|
39
|
-
declare
|
|
40
|
-
top_level_verb text;
|
|
41
|
-
has_returning boolean;
|
|
42
|
-
has_inner_writable_cte boolean;
|
|
43
|
-
rows_json jsonb := '[]'::jsonb;
|
|
44
|
-
row_count bigint := 0;
|
|
45
|
-
row_value record;
|
|
46
|
-
row_texts text[] := '{}';
|
|
47
|
-
begin
|
|
48
|
-
top_level_verb := public.cfni_top_level_verb(statement);
|
|
49
|
-
has_returning := statement ~* '\yreturning\y';
|
|
50
|
-
|
|
51
|
-
if top_level_verb in ('insert', 'update', 'delete') and not has_returning then
|
|
52
|
-
execute statement;
|
|
53
|
-
get diagnostics row_count = row_count;
|
|
54
|
-
return jsonb_build_object('rows', '[]'::jsonb, 'rowCount', row_count);
|
|
55
|
-
end if;
|
|
56
|
-
|
|
57
|
-
if top_level_verb in ('insert', 'update', 'delete') then
|
|
58
|
-
-- INSERT/UPDATE/DELETE with RETURNING cannot be wrapped as
|
|
59
|
-
-- `from (<stmt>) r` — Postgres rejects DML directly inside FROM —
|
|
60
|
-
-- so run it as a CTE instead and select its rows out of that.
|
|
61
|
-
execute format(
|
|
62
|
-
'with cfni_dml_result as (%s) select coalesce(jsonb_agg(r::text), ''[]''::jsonb), count(*) from cfni_dml_result r',
|
|
63
|
-
statement
|
|
64
|
-
) into rows_json, row_count;
|
|
65
|
-
return jsonb_build_object('rows', rows_json, 'rowCount', row_count);
|
|
66
|
-
end if;
|
|
67
|
-
|
|
68
|
-
-- Top-level statement is a SELECT (plain, or `with ... select ...`).
|
|
69
|
-
has_inner_writable_cte := statement ~* '\y(with|,)\s+[A-Za-z_][A-Za-z0-9_]*\s+as\s*\(\s*(insert|update|delete)\y';
|
|
70
|
-
if has_inner_writable_cte then
|
|
71
|
-
-- Postgres forbids nesting a WITH that contains a data-modifying
|
|
72
|
-
-- statement inside another query (`from (with u as (update ...
|
|
73
|
-
-- returning ...) select ... from u) r` is rejected even though the
|
|
74
|
-
-- statement is legal standalone) — so this shape must run exactly
|
|
75
|
-
-- as given, not wrapped in a further subquery. Row values are
|
|
76
|
-
-- collected by iterating the statement's own result set instead,
|
|
77
|
-
-- casting each row to its composite-literal text form directly.
|
|
78
|
-
for row_value in execute statement loop
|
|
79
|
-
row_texts := row_texts || (row_value::text);
|
|
80
|
-
row_count := row_count + 1;
|
|
81
|
-
end loop;
|
|
82
|
-
return jsonb_build_object('rows', to_jsonb(row_texts), 'rowCount', row_count);
|
|
83
|
-
end if;
|
|
84
|
-
|
|
85
|
-
-- Plain SELECT with no inner writable CTE: wrap as a subquery and emit
|
|
86
|
-
-- each row's composite-literal text form; the transport parses it back
|
|
87
|
-
-- into a positional array of column values.
|
|
88
|
-
execute format(
|
|
89
|
-
'select coalesce(jsonb_agg(r::text), ''[]''::jsonb), count(*) from (%s) r',
|
|
90
|
-
statement
|
|
91
|
-
) into rows_json, row_count;
|
|
92
|
-
|
|
93
|
-
return jsonb_build_object('rows', rows_json, 'rowCount', row_count);
|
|
94
|
-
end;
|
|
95
|
-
$$;
|
|
96
|
-
|
|
97
|
-
-- Finds the leading keyword of a statement's *outermost* clause, skipping
|
|
98
|
-
-- past any leading CTE chain (`with x as (...), y as (...) <verb> ...`) by
|
|
99
|
-
-- tracking paren depth rather than a single non-greedy regex match, which
|
|
100
|
-
-- breaks on multiple CTEs or parens nested inside a CTE body. Returns one of
|
|
101
|
-
-- 'select', 'insert', 'update', 'delete', or 'other' (lowercased).
|
|
102
|
-
create or replace function public.cfni_top_level_verb(statement text)
|
|
103
|
-
returns text
|
|
104
|
-
language plpgsql
|
|
105
|
-
immutable
|
|
106
|
-
set search_path = ''
|
|
107
|
-
as $$
|
|
108
|
-
declare
|
|
109
|
-
stripped text;
|
|
110
|
-
depth int := 0;
|
|
111
|
-
i int := 1;
|
|
112
|
-
len int;
|
|
113
|
-
ch text;
|
|
114
|
-
begin
|
|
115
|
-
stripped := regexp_replace(statement, '^(\s*--[^\n]*\n|\s*/\*.*?\*/)*\s*', '', 'sn');
|
|
116
|
-
|
|
117
|
-
if stripped !~* '^with\y' then
|
|
118
|
-
return lower((regexp_match(stripped, '^([A-Za-z]+)'))[1]);
|
|
119
|
-
end if;
|
|
120
|
-
|
|
121
|
-
len := length(stripped);
|
|
122
|
-
while i <= len loop
|
|
123
|
-
ch := substr(stripped, i, 1);
|
|
124
|
-
if ch = '(' then
|
|
125
|
-
depth := depth + 1;
|
|
126
|
-
elsif ch = ')' then
|
|
127
|
-
depth := depth - 1;
|
|
128
|
-
elsif depth = 0 and ch = ',' then
|
|
129
|
-
-- Between CTE definitions at depth 0; keep scanning for the
|
|
130
|
-
-- statement that follows the whole `with ... ,` chain.
|
|
131
|
-
null;
|
|
132
|
-
elsif depth = 0 and substr(stripped, i) ~* '^(select|insert|update|delete)\y' then
|
|
133
|
-
return lower((regexp_match(substr(stripped, i), '^([A-Za-z]+)'))[1]);
|
|
134
|
-
end if;
|
|
135
|
-
i := i + 1;
|
|
136
|
-
end loop;
|
|
137
|
-
|
|
138
|
-
return 'other';
|
|
139
|
-
end;
|
|
140
|
-
$$;
|
|
141
|
-
|
|
142
|
-
-- Runs several statements as one atomic unit: `execute`d one at a time via
|
|
143
|
-
-- `cfni_exec`, all inside this single function body, so a failure on any
|
|
144
|
-
-- statement rolls back everything the batch already did — a plpgsql function
|
|
145
|
-
-- call is itself an implicit transaction, which is what makes this atomic
|
|
146
|
-
-- even though `pg-proxy` (the Supabase-mode transport) can open no
|
|
147
|
-
-- transaction of its own: each statement it sends is normally its own
|
|
148
|
-
-- independent PostgREST round-trip with no shared session. This is the
|
|
149
|
-
-- explicit batch API for that gap — see `withUserTransaction`/
|
|
150
|
-
-- `withPublicTransaction` on the TypeScript side, which render every query
|
|
151
|
-
-- in the callback with `.toSQL()` up front (nothing executes until the
|
|
152
|
-
-- batch is sent) and pass the resulting array here as one call.
|
|
153
|
-
--
|
|
154
|
-
-- Returns a jsonb array with one `cfni_exec`-shaped `{rows, rowCount}` result
|
|
155
|
-
-- per input statement, in the same order — so the caller can read each
|
|
156
|
-
-- statement's own result the way it would outside a batch.
|
|
157
|
-
--
|
|
158
|
-
-- SECURITY INVOKER for the same reason as `cfni_exec`: the whole batch runs
|
|
159
|
-
-- with the caller's own privileges, so RLS applies per statement exactly as
|
|
160
|
-
-- it would if each ran on its own.
|
|
161
|
-
create or replace function public.cfni_exec_batch(statements text[])
|
|
162
|
-
returns jsonb
|
|
163
|
-
language plpgsql
|
|
164
|
-
security invoker
|
|
165
|
-
set search_path = public
|
|
166
|
-
as $$
|
|
167
|
-
declare
|
|
168
|
-
results jsonb := '[]'::jsonb;
|
|
169
|
-
statement text;
|
|
170
|
-
begin
|
|
171
|
-
foreach statement in array statements loop
|
|
172
|
-
results := results || jsonb_build_array(public.cfni_exec(statement));
|
|
173
|
-
end loop;
|
|
174
|
-
return results;
|
|
175
|
-
end;
|
|
176
|
-
$$;
|
|
177
|
-
|
|
178
|
-
revoke all on function public.cfni_exec(text) from public;
|
|
179
|
-
grant execute on function public.cfni_exec(text) to authenticated;
|
|
180
|
-
-- Grant to anon only if your app calls withPublicDb:
|
|
181
|
-
grant execute on function public.cfni_exec(text) to anon;
|
|
182
|
-
grant execute on function public.cfni_exec(text) to service_role;
|
|
183
|
-
|
|
184
|
-
revoke all on function public.cfni_exec_batch(text[]) from public;
|
|
185
|
-
grant execute on function public.cfni_exec_batch(text[]) to authenticated;
|
|
186
|
-
grant execute on function public.cfni_exec_batch(text[]) to anon;
|
|
187
|
-
grant execute on function public.cfni_exec_batch(text[]) to service_role;
|
|
188
|
-
|
|
189
|
-
revoke all on function public.cfni_top_level_verb(text) from public;
|
|
190
|
-
grant execute on function public.cfni_top_level_verb(text) to authenticated;
|
|
191
|
-
grant execute on function public.cfni_top_level_verb(text) to anon;
|
|
192
|
-
grant execute on function public.cfni_top_level_verb(text) to service_role;
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
-- pgTAP tests for cfni_exec.sql. Run with `supabase test db` (or
|
|
2
|
-
-- `pg_prove`) from a project that has installed both this file and
|
|
3
|
-
-- `cfni_exec.sql`. Mirrors the scenarios in
|
|
4
|
-
-- `src/db/cfni_exec.integration.test.ts` on the TypeScript side — this file
|
|
5
|
-
-- checks the SQL function itself and its RLS interaction directly in
|
|
6
|
-
-- Postgres, without going through the transport/JS parsing layer.
|
|
7
|
-
begin;
|
|
8
|
-
select plan(26);
|
|
9
|
-
|
|
10
|
-
do $$
|
|
11
|
-
begin
|
|
12
|
-
if not exists (select 1 from pg_roles where rolname = 'anon') then
|
|
13
|
-
create role anon;
|
|
14
|
-
end if;
|
|
15
|
-
if not exists (select 1 from pg_roles where rolname = 'authenticated') then
|
|
16
|
-
create role authenticated;
|
|
17
|
-
end if;
|
|
18
|
-
end
|
|
19
|
-
$$;
|
|
20
|
-
|
|
21
|
-
create table cfni_test_a (id int primary key, name text);
|
|
22
|
-
create table cfni_test_b (id int primary key, name text);
|
|
23
|
-
insert into cfni_test_a values (1, 'a1');
|
|
24
|
-
insert into cfni_test_b values (2, 'b2');
|
|
25
|
-
|
|
26
|
-
select is(pg_typeof(cfni_exec('select 1'))::text, 'jsonb', 'cfni_exec returns jsonb');
|
|
27
|
-
|
|
28
|
-
select is(
|
|
29
|
-
cfni_exec('select id, name from cfni_test_a where id = 1'),
|
|
30
|
-
'{"rows": ["(1,a1)"], "rowCount": 1}'::jsonb,
|
|
31
|
-
'plain select returns rows and rowCount'
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
select is(
|
|
35
|
-
cfni_exec('select a.*, b.* from cfni_test_a a, cfni_test_b b'),
|
|
36
|
-
'{"rows": ["(1,a1,2,b2)"], "rowCount": 1}'::jsonb,
|
|
37
|
-
'duplicate column names from a join are preserved positionally'
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
select is(
|
|
41
|
-
cfni_exec('select array[1,2,3] as arr, true as b, 1.10::numeric as n, null as z'),
|
|
42
|
-
'{"rows": ["(\"{1,2,3}\",t,1.10,)"], "rowCount": 1}'::jsonb,
|
|
43
|
-
'array/boolean/numeric/null values use pg text form, not JSON re-encoding'
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
select is(
|
|
47
|
-
cfni_exec('insert into cfni_test_a (id, name) values (10, ''x'')'),
|
|
48
|
-
'{"rows": [], "rowCount": 1}'::jsonb,
|
|
49
|
-
'insert without returning reports rowCount with no rows'
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
select is(
|
|
53
|
-
cfni_exec('insert into cfni_test_a (id, name) values (11, ''y'') returning id, name'),
|
|
54
|
-
'{"rows": ["(11,y)"], "rowCount": 1}'::jsonb,
|
|
55
|
-
'insert with returning reports rows'
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
select is(
|
|
59
|
-
cfni_exec('update cfni_test_a set name = ''z'' where id = 1 returning id, name'),
|
|
60
|
-
'{"rows": ["(1,z)"], "rowCount": 1}'::jsonb,
|
|
61
|
-
'update with returning reports rows'
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
select is(
|
|
65
|
-
cfni_exec('update cfni_test_a set name = ''z2'' where id = 1'),
|
|
66
|
-
'{"rows": [], "rowCount": 1}'::jsonb,
|
|
67
|
-
'update without returning reports rowCount with no rows'
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
select is(
|
|
71
|
-
cfni_exec('delete from cfni_test_a where id = 10 returning id'),
|
|
72
|
-
'{"rows": ["(10)"], "rowCount": 1}'::jsonb,
|
|
73
|
-
'delete with returning reports rows'
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
select is(
|
|
77
|
-
cfni_exec('delete from cfni_test_a where id = 11'),
|
|
78
|
-
'{"rows": [], "rowCount": 1}'::jsonb,
|
|
79
|
-
'delete without returning reports rowCount with no rows'
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
alter table cfni_test_a add constraint cfni_test_a_id_uq unique (id);
|
|
83
|
-
select is(
|
|
84
|
-
cfni_exec('insert into cfni_test_a (id, name) values (1, ''dup'') on conflict (id) do update set name = excluded.name returning id, name'),
|
|
85
|
-
'{"rows": ["(1,dup)"], "rowCount": 1}'::jsonb,
|
|
86
|
-
'on conflict do update via excluded works'
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
select is(
|
|
90
|
-
cfni_exec('with x as (select 1 as v) select v from x'),
|
|
91
|
-
'{"rows": ["(1)"], "rowCount": 1}'::jsonb,
|
|
92
|
-
'plain with-select CTE works'
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
select is(
|
|
96
|
-
cfni_exec('with src as (select 20 as id, ''q'' as name) insert into cfni_test_a (id, name) select id, name from src returning id, name'),
|
|
97
|
-
'{"rows": ["(20,q)"], "rowCount": 1}'::jsonb,
|
|
98
|
-
'writable CTE (insert ... select ... returning) at the top level works'
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
select is(
|
|
102
|
-
cfni_exec('with u as (update cfni_test_a set name = ''zz'' where id = 1 returning id) select id from u'),
|
|
103
|
-
'{"rows": ["(1)"], "rowCount": 1}'::jsonb,
|
|
104
|
-
'writable CTE wrapped in an outer select works'
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
select is(
|
|
108
|
-
cfni_exec('select ''the returning hero'' as s'),
|
|
109
|
-
'{"rows": ["(\"the returning hero\")"], "rowCount": 1}'::jsonb,
|
|
110
|
-
'a select whose literal text contains the word "returning" is not misclassified as DML'
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
select is(
|
|
114
|
-
cfni_exec('select 1 where false'),
|
|
115
|
-
'{"rows": [], "rowCount": 0}'::jsonb,
|
|
116
|
-
'no matching rows returns an empty result set'
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
select is(pg_proc.prosecdef, false, 'cfni_exec is security invoker, not security definer')
|
|
120
|
-
from pg_proc where proname = 'cfni_exec';
|
|
121
|
-
|
|
122
|
-
-- Re-running the install file must not error (create-or-replace + a
|
|
123
|
-
-- return-type-changing drop guard) — exercised by re-declaring the function
|
|
124
|
-
-- with an identical body here, standing in for `\i cfni_exec.sql` a second
|
|
125
|
-
-- time, since re-sourcing the actual file mid-transaction isn't expressible
|
|
126
|
-
-- from within a single pgTAP script.
|
|
127
|
-
select isnt(cfni_exec('select 1'), null, 'cfni_exec remains callable after a hypothetical reinstall');
|
|
128
|
-
|
|
129
|
-
alter table cfni_test_a enable row level security;
|
|
130
|
-
create policy cfni_test_a_select on cfni_test_a for select to authenticated using (true);
|
|
131
|
-
grant select on cfni_test_a to anon, authenticated;
|
|
132
|
-
|
|
133
|
-
set role anon;
|
|
134
|
-
select is(
|
|
135
|
-
cfni_exec('select * from cfni_test_a'),
|
|
136
|
-
'{"rows": [], "rowCount": 0}'::jsonb,
|
|
137
|
-
'anon sees no rows with no matching RLS policy'
|
|
138
|
-
);
|
|
139
|
-
reset role;
|
|
140
|
-
|
|
141
|
-
set role authenticated;
|
|
142
|
-
select is(
|
|
143
|
-
(cfni_exec('select * from cfni_test_a') ->> 'rowCount')::int,
|
|
144
|
-
(select count(*)::int from cfni_test_a),
|
|
145
|
-
'authenticated sees all rows under its permissive select policy'
|
|
146
|
-
);
|
|
147
|
-
reset role;
|
|
148
|
-
|
|
149
|
-
-- cfni_exec_batch
|
|
150
|
-
|
|
151
|
-
select is(
|
|
152
|
-
cfni_exec_batch(array[
|
|
153
|
-
'insert into cfni_test_a (id, name) values (30, ''batch1'') returning id, name',
|
|
154
|
-
'insert into cfni_test_a (id, name) values (31, ''batch2'') returning id, name'
|
|
155
|
-
]),
|
|
156
|
-
'[{"rows": ["(30,batch1)"], "rowCount": 1}, {"rows": ["(31,batch2)"], "rowCount": 1}]'::jsonb,
|
|
157
|
-
'batch runs every statement and returns one cfni_exec-shaped result per statement, in order'
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
select is(
|
|
161
|
-
(select count(*) from cfni_test_a where id in (30, 31)),
|
|
162
|
-
2::bigint,
|
|
163
|
-
'both batched inserts committed'
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
select throws_ok(
|
|
167
|
-
$$select cfni_exec_batch(array[
|
|
168
|
-
'insert into cfni_test_a (id, name) values (40, ''ok'')',
|
|
169
|
-
'insert into cfni_test_a (id, name) values (invalid syntax'
|
|
170
|
-
])$$,
|
|
171
|
-
null, null,
|
|
172
|
-
'a later statement failing raises out of the batch call'
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
select is(
|
|
176
|
-
(select count(*) from cfni_test_a where id = 40),
|
|
177
|
-
0::bigint,
|
|
178
|
-
'an earlier statement in a failed batch is rolled back — the batch is atomic'
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
select is(pg_proc.prosecdef, false, 'cfni_exec_batch is security invoker, not security definer')
|
|
182
|
-
from pg_proc where proname = 'cfni_exec_batch';
|
|
183
|
-
|
|
184
|
-
set role anon;
|
|
185
|
-
select is(
|
|
186
|
-
cfni_exec_batch(array['select * from cfni_test_a where id = 30']),
|
|
187
|
-
'[{"rows": [], "rowCount": 0}]'::jsonb,
|
|
188
|
-
'cfni_exec_batch applies RLS per statement just like cfni_exec — anon sees no rows here'
|
|
189
|
-
);
|
|
190
|
-
reset role;
|
|
191
|
-
|
|
192
|
-
select * from finish();
|
|
193
|
-
rollback;
|