@@ -28,16 +28,37 @@ using (published_at is not null);
2828-- drop table if exists comment cascade;
2929create table if not exists comment (
3030 comment_id uuid primary key default uuidv7(),
31- author text not null check (trim (author) <> ' ' ),
32- content text not null check (length (content) <= 10000 and trim (content) <> ' ' ),
31+ author text not null check (trim (author) <> ' ' and length(author) <= 100 ),
32+ content text not null check (trim (content) <> ' ' and length (content) <= 10000 ),
3333 post_id uuid not null references post (post_id) on delete cascade ,
34- published_at timestamptz default now()
34+ published_at timestamptz default now(),
35+ approved_at timestamptz default null
3536);
3637
3738create index if not exists post_id on comment (post_id);
3839
3940grant select , insert on table comment to anon;
4041
42+ alter table comment enable row level security;
43+
44+ drop policy if exists " moderated_select" on comment;
45+ create policy " moderated_select" on comment for all to anon
46+ using ((
47+ with query (query) as (
48+ select nullif(current_setting(' httpg.query' , true), ' ' )::jsonb
49+ )
50+ select case when query- > ' qs' ? ' include_unmoderated' or comment .comment_id = coalesce(query- > ' qs' - >> ' comment_id' , query- > ' body' - > ' params' - >> 0 )::uuid
51+ then true
52+ else approved_at is not null
53+ end
54+ from query
55+ ))
56+ with check (true);
57+
58+ -- drop policy if exists "moderated_insert" on comment;
59+ -- create policy "moderated_insert" on comment as restrictive for insert to anon
60+ -- with check (true);
61+
4162truncate post cascade;
4263insert into post (title, content, published_at)
4364select i::text , xmlelement(name h3, ' hello ' || i)::text , case when i > 6 then null else now() end
@@ -62,53 +83,86 @@ from generate_series(1, 5) i, post;
6283-- drop view if exists post_html cascade;
6384create or replace view post_html (post_id, body)
6485with (security_invoker)
65- as with entry (post_id, xml) as (
86+ as with httpg (params, comment_id, include_unmoderated) as (
87+ with query (query) as (
88+ select nullif(current_setting(' httpg.query' , true), ' ' )::jsonb
89+ )
90+ select query- > ' body' - > ' params' , (query- > ' qs' - >> ' comment_id' )::uuid, query- > ' qs' ? ' include_unmoderated'
91+ from query
92+ ),
93+ entry (post_id, xml) as (
6694 select post_id, xmlelement(name div,
6795 xmlelement(name article, xmlattributes(' card' as class),
68- xmlelement(name h2, post .title ),
96+ xmlelement(name a, xmlattributes(
97+ url(' /blog/query' , jsonb_build_object(
98+ ' sql' , ' select * from blog.head union all select body::text from blog.post_html where post_id = $1::uuid' ,
99+ ' params[]' , post_id
100+ )) as href
101+ ),
102+ xmlelement(name h2, post .title )
103+ ),
69104 post .content ::xml,
70105 xmlelement(name hr),
106+ xmlelement(name h4, ' Comments' ),
71107 xmlelement(name form, xmlattributes(
72108 ' POST' as method,
73109 ' /blog/query' as action
74110 ),
75111 xmlelement(name input, xmlattributes(
76112 ' hidden' as type,
77113 ' sql' as name,
78- ' insert into blog.comment (author, content, post_id) values ($1, $2, $3::uuid)' as value
114+ $$
115+ insert into blog .comment (comment_id, author, content, post_id) values ($1 ::uuid, $2 , $3 , $4 ::uuid)
116+ returning 303 status, hstore(' Location' , url .url (' /blog/query' , jsonb_build_object(
117+ ' sql' , ' select * from blog.head union all select body::text from blog.post_html where post_id = $1::uuid' ,
118+ ' params[0]' , post_id,
119+ ' comment_id' , comment_id
120+ ))) header
121+ $$ as value
79122 )),
80123 xmlelement(name input, xmlattributes(
81124 ' hidden' as type,
82125 ' on_error' as name,
83- ' select * from blog.blog' as value
126+ ' select * from blog.head union all select body::text from blog.post_html where post_id = $4::uuid ' as value
84127 )),
85128 xmlelement(name input, xmlattributes(
86129 ' hidden' as type,
87- ' redirect' as name,
88- url(' /blog/query' , jsonb_build_object(' sql' , ' select * from blog.blog' )) as value
130+ ' params[0]' as name,
131+ ' author' as placeholder,
132+ coalesce(params- >> 0 , uuidv7()::text ) as value
89133 )),
90134 xmlelement(name input, xmlattributes(
91135 ' text' as type,
92- ' params[0]' as name,
93- ' author' as placeholder
136+ ' params[1]' as name,
137+ ' author' as placeholder,
138+ params- >> 1 as value
94139 )),
95140 xmlelement(name textarea, xmlattributes(
96- ' params[1]' as name,
97- ' comment' as placeholder
98- ), ' ' ),
141+ ' params[2]' as name,
142+ ' comment' as placeholder,
143+ 7 as rows
144+ ), coalesce(params- >> 2 , ' ' )),
99145 xmlelement(name input, xmlattributes(
100146 ' hidden' as type,
101- ' params[2 ]' as name,
147+ ' params[3 ]' as name,
102148 post_id as value
103149 )),
104150 xmlelement(name input, xmlattributes(
105151 ' submit' as type,
106152 ' Comment' as value
107153 ))
108154 ),
155+ xmlelement(name a, xmlattributes(
156+ url(' /blog/query' , jsonb_build_object(
157+ ' sql' , ' select * from blog.head union all select body::text from blog.post_html where post_id = $1::uuid' ,
158+ ' params[]' , post_id,
159+ ' include_unmoderated' , null
160+ )) as href
161+ ), ' Include unmoderated' ),
109162 xmlelement(name div, xmlattributes(' messages' as class), xmlagg(
110163 xmlelement(name article, xmlattributes(' card' as class),
111- comment .content ,
164+ xmlelement(name address, comment .author ),
165+ comment .content
112166 -- (
113167 -- with recursive n (comment_id, n, i, ordinality) as (
114168 -- select comment_id, r.n, 0, ordinality
@@ -123,15 +177,14 @@ as with entry (post_id, xml) as (
123177 -- group by comment_id
124178 -- -- order by i, ordinality
125179 -- )),
126- xmlelement(name address, comment .author )
127180 )
128- order by comment .published_at
181+ order by comment .published_at desc
129182 ))
130183 )
131184 )
132- from post
185+ from httpg, post
133186 left join comment using (post_id)
134- group by post_id
187+ group by post_id, params
135188)
136189select post_id, xml
137190from entry
@@ -153,33 +206,45 @@ select $html$<!DOCTYPE html>
153206 <meta http-equiv="Content-Security-Policy" content="default-src ' self' ; base-uri ' self' ; form-action ' self' ; " />
154207 <link rel="stylesheet" href="/cpres/index.css?v=4" />
155208</head>
156- $html$;
157-
158- grant select on table head to anon;
159-
160- -- drop view if exists blog cascade;
161- create or replace view blog (html)
162- with (security_invoker)
163- as
164- with httpg (error) as (
165- select nullif(current_setting(' httpg .errors ' , true), ' ' )::jsonb->>' error'
166- )
167- table head
209+ $html$
168210union all
169- select xmlelement(name h1, ' docteurklein' ' s blog' )::text
211+ select xmlelement(name a, xmlattributes(
212+ url(' / blog/ query' , jsonb_build_object(
213+ ' sql' , ' select * from blog .blog '
214+ )) as href
215+ ),
216+ xmlelement(name h1, ' docteurklein' ' s blog' )
217+ )::text
170218union all
219+ (with httpg (error) as (
220+ select nullif(current_setting(' httpg .errors ' , true), ' ' )::jsonb->>' error'
221+ )
171222select xmlelement(name article, xmlattributes(
172223 ' card error' as class
173224), coalesce(
174- pg_get_constraintdef((
175- select oid
176- from pg_constraint
177- where conname = substring(error, ' violates check constraint " (\w+)" ' )
178- )),
225+ (
226+ with c (oid, name) as (
227+ select c.oid, a.attname
228+ from pg_constraint c
229+ join pg_attribute a on (a.attnum = any(c.conkey) and a.attrelid = c.conrelid)
230+ where conname = substring(error, ' violates check constraint " (\w+)" ' )
231+ and connamespace = to_regnamespace(' blog' )
232+ )
233+ select string_agg(format(' %s: %s' , name, pg_get_constraintdef(oid)), ' , ' )
234+ from c
235+ ),
179236 error
180237))::text
181238from httpg
182- where error is not null
239+ where error is not null)
240+ ;
241+
242+ grant select on table head to anon;
243+
244+ -- drop view if exists blog cascade;
245+ create or replace view blog (html)
246+ with (security_invoker)
247+ as table head
183248union all
184249select body::text
185250from post_html
0 commit comments