@marktiderman/genesis-sites 0.0.0-stage → 0.2.0
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/LICENSE +21 -0
- package/README.md +465 -2
- package/dist/assets/form-island.c0f6fac859.js +51 -0
- package/dist/assets/islands.77d11f5697.js +1 -0
- package/dist/assets/sites-forms.4e6aa42497.css +10 -0
- package/dist/assets/sites-forms.css +10 -0
- package/dist/assets/sites.css +2 -0
- package/dist/assets/sites.e1ffd2708b.css +2 -0
- package/dist/blocks.d.ts +13 -0
- package/dist/blocks.js +26 -0
- package/dist/blocks.js.map +1 -0
- package/dist/chunk-3VKTBE7S.js +69 -0
- package/dist/chunk-3VKTBE7S.js.map +1 -0
- package/dist/chunk-C375ZPU2.js +2241 -0
- package/dist/chunk-C375ZPU2.js.map +1 -0
- package/dist/chunk-IXMY4B56.js +877 -0
- package/dist/chunk-IXMY4B56.js.map +1 -0
- package/dist/chunk-VZUDRPFG.js +13 -0
- package/dist/chunk-VZUDRPFG.js.map +1 -0
- package/dist/chunk-ZGWGQGT2.js +595 -0
- package/dist/chunk-ZGWGQGT2.js.map +1 -0
- package/dist/import.d.ts +130 -0
- package/dist/import.js +330 -0
- package/dist/import.js.map +1 -0
- package/dist/index.d.ts +273 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/registry-D_DCWRTc.d.ts +1030 -0
- package/dist/sections.d.ts +802 -0
- package/dist/sections.js +6 -0
- package/dist/sections.js.map +1 -0
- package/dist/store-DkSycDhq.d.ts +443 -0
- package/dist/supabase.d.ts +41 -0
- package/dist/supabase.js +283 -0
- package/dist/supabase.js.map +1 -0
- package/dist/web.d.ts +136 -0
- package/dist/web.js +479 -0
- package/dist/web.js.map +1 -0
- package/migrations/0001_genesis_sites.sql +821 -0
- package/migrations/0002_genesis_sites_posts.sql +404 -0
- package/migrations/tests/0001_genesis_sites.test.sql +349 -0
- package/migrations/tests/0002_genesis_sites_posts.test.sql +242 -0
- package/package.json +152 -3
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
-- Genesis Sites 0002: blog posts (site_posts) and the post page
|
|
2
|
+
--
|
|
3
|
+
-- A post is its own document beside the pages, with a draft and a published
|
|
4
|
+
-- copy. publish_post copies the draft (at the revision the editor checked)
|
|
5
|
+
-- to the published copy; that copy is what visitors read, through
|
|
6
|
+
-- genesis_sites.live_posts and live_post, so a post goes live (and comes
|
|
7
|
+
-- down) on its own, without a new release of the site. A site's rollback
|
|
8
|
+
-- doesn't touch its posts.
|
|
9
|
+
--
|
|
10
|
+
-- The site's `post` page (a new page kind, at most one per site and never
|
|
11
|
+
-- the home page) is the template every post renders in: at its own path it
|
|
12
|
+
-- is the blog's index, at <path>/<slug> it shows that post.
|
|
13
|
+
--
|
|
14
|
+
-- Access is the same seam as 0001: reads where genesis_sites.can_view says
|
|
15
|
+
-- so, writes through the security definer functions below, each asking
|
|
16
|
+
-- genesis_sites.can_edit. Anon reads only published posts of a live site.
|
|
17
|
+
-- The checks a post passes before it is published (alt text, headings) run
|
|
18
|
+
-- in TypeScript (`checkPost`, in the stores); the database checks shape,
|
|
19
|
+
-- size, slugs and revisions.
|
|
20
|
+
--
|
|
21
|
+
-- This migration needs 0001. It creates no hook; it runs
|
|
22
|
+
-- genesis_sites.ensure_hook_stubs() so a hook is there, and never replaces
|
|
23
|
+
-- an app's own version.
|
|
24
|
+
--
|
|
25
|
+
-- Use: `genesis add sites` copies this file into supabase/migrations under a
|
|
26
|
+
-- new timestamped name. Never edit it once applied; change it with a new migration.
|
|
27
|
+
--
|
|
28
|
+
-- Verify: the pgTAP test beside this file (`tests/0002_genesis_sites_posts.test.sql`).
|
|
29
|
+
|
|
30
|
+
select genesis_sites.ensure_hook_stubs();
|
|
31
|
+
|
|
32
|
+
-- ── The post page ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
alter table genesis_sites.site_pages drop constraint site_pages_kind;
|
|
35
|
+
alter table genesis_sites.site_pages
|
|
36
|
+
add constraint site_pages_kind check (kind in ('page', 'landing', 'post')),
|
|
37
|
+
-- Posts live under the post page's path; at / they would take over the site's paths.
|
|
38
|
+
add constraint site_pages_post_not_home check (kind <> 'post' or path <> '/');
|
|
39
|
+
|
|
40
|
+
-- One post page per site: it is where every post renders.
|
|
41
|
+
create unique index site_pages_one_post_page on genesis_sites.site_pages (site_id) where kind = 'post' and deleted_at is null;
|
|
42
|
+
|
|
43
|
+
-- ── Posts ─────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
create table genesis_sites.site_posts (
|
|
46
|
+
id uuid primary key default gen_random_uuid(),
|
|
47
|
+
site_id uuid not null references genesis_sites.sites (id) on delete cascade,
|
|
48
|
+
slug text not null constraint site_posts_slug_format check (char_length(slug) <= 80 and slug ~ '^[a-z0-9]+(-[a-z0-9]+)*$'),
|
|
49
|
+
-- The post document (PostDoc): title, excerpt, body, cover, author, SEO.
|
|
50
|
+
draft jsonb not null constraint site_posts_draft_shape check (jsonb_typeof(draft) = 'object' and octet_length(draft::text) <= 524288),
|
|
51
|
+
draft_rev integer not null default 1,
|
|
52
|
+
-- What visitors read: a copy of the draft at its last publish.
|
|
53
|
+
published jsonb constraint site_posts_published_shape check (published is null or (jsonb_typeof(published) = 'object' and octet_length(published::text) <= 524288)),
|
|
54
|
+
status text not null default 'draft' constraint site_posts_status check (status in ('draft', 'published')),
|
|
55
|
+
-- When it was first published, kept when it is published again (an import keeps the old site's date).
|
|
56
|
+
published_at timestamptz,
|
|
57
|
+
-- When the published copy last changed.
|
|
58
|
+
published_updated_at timestamptz,
|
|
59
|
+
created_by uuid default auth.uid() references auth.users (id) on delete set null,
|
|
60
|
+
created_at timestamptz not null default now(),
|
|
61
|
+
updated_at timestamptz not null default now(),
|
|
62
|
+
deleted_at timestamptz,
|
|
63
|
+
constraint site_posts_published_state check (
|
|
64
|
+
(status = 'published' and published is not null and published_at is not null and published_updated_at is not null)
|
|
65
|
+
or (status = 'draft' and published is null)
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
create unique index site_posts_one_per_slug on genesis_sites.site_posts (site_id, slug) where deleted_at is null;
|
|
70
|
+
create index site_posts_live on genesis_sites.site_posts (site_id, published_at desc) where status = 'published' and deleted_at is null;
|
|
71
|
+
|
|
72
|
+
comment on table genesis_sites.site_posts is 'A site''s blog posts: a draft and the published copy visitors read. A deleted post keeps its row (deleted_at). Genesis Sites.';
|
|
73
|
+
|
|
74
|
+
create trigger genesis_touch before update on genesis_sites.site_posts for each row execute function genesis_sites.touch();
|
|
75
|
+
|
|
76
|
+
-- What a post serves changed (published, taken down, deleted): tell a listener to purge the site's cache.
|
|
77
|
+
create or replace function genesis_sites.notify_post_change()
|
|
78
|
+
returns trigger
|
|
79
|
+
language plpgsql
|
|
80
|
+
set search_path = ''
|
|
81
|
+
as $$
|
|
82
|
+
begin
|
|
83
|
+
perform pg_notify('genesis_sites', jsonb_build_object(
|
|
84
|
+
'site_id', new.site_id,
|
|
85
|
+
'post_id', new.id,
|
|
86
|
+
'slug', new.slug,
|
|
87
|
+
'status', new.status,
|
|
88
|
+
'action', 'post'
|
|
89
|
+
)::text);
|
|
90
|
+
return new;
|
|
91
|
+
end;
|
|
92
|
+
$$;
|
|
93
|
+
|
|
94
|
+
create trigger genesis_notify_post after update of status, published on genesis_sites.site_posts
|
|
95
|
+
for each row when (old.status is distinct from new.status or old.published is distinct from new.published)
|
|
96
|
+
execute function genesis_sites.notify_post_change();
|
|
97
|
+
create trigger genesis_notify_post_insert after insert on genesis_sites.site_posts
|
|
98
|
+
for each row when (new.status = 'published')
|
|
99
|
+
execute function genesis_sites.notify_post_change();
|
|
100
|
+
|
|
101
|
+
alter table genesis_sites.site_posts enable row level security;
|
|
102
|
+
|
|
103
|
+
create policy "viewers read its posts" on genesis_sites.site_posts
|
|
104
|
+
for select to authenticated using (deleted_at is null and genesis_sites.can_view(site_id));
|
|
105
|
+
|
|
106
|
+
-- ── Helpers ───────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
create or replace function genesis_sites.assert_post(slug text, doc jsonb)
|
|
109
|
+
returns void
|
|
110
|
+
language plpgsql
|
|
111
|
+
immutable
|
|
112
|
+
set search_path = ''
|
|
113
|
+
as $$
|
|
114
|
+
begin
|
|
115
|
+
if slug is null or char_length(slug) > 80 or slug !~ '^[a-z0-9]+(-[a-z0-9]+)*$' then
|
|
116
|
+
raise exception 'genesis_sites: "%" isn''t a post slug (lowercase letters, digits and single hyphens)', slug using errcode = '22023';
|
|
117
|
+
end if;
|
|
118
|
+
if doc is null or jsonb_typeof(doc) <> 'object' or octet_length(doc::text) > 524288 then
|
|
119
|
+
raise exception 'genesis_sites: the post document must be a JSON object of at most 512 KiB' using errcode = '22023';
|
|
120
|
+
end if;
|
|
121
|
+
if doc -> 'schemaVersion' is distinct from '1'::jsonb then
|
|
122
|
+
raise exception 'genesis_sites: the post document needs schemaVersion 1' using errcode = '22023';
|
|
123
|
+
end if;
|
|
124
|
+
if jsonb_typeof(doc -> 'title') is distinct from 'string' or jsonb_typeof(doc -> 'body') is distinct from 'object' or doc #>> '{body,type}' is distinct from 'doc' then
|
|
125
|
+
raise exception 'genesis_sites: a post document needs a title and a rich text body' using errcode = '22023';
|
|
126
|
+
end if;
|
|
127
|
+
end;
|
|
128
|
+
$$;
|
|
129
|
+
|
|
130
|
+
create or replace function genesis_sites.post_json(p genesis_sites.site_posts)
|
|
131
|
+
returns jsonb
|
|
132
|
+
language sql
|
|
133
|
+
stable
|
|
134
|
+
set search_path = ''
|
|
135
|
+
as $$
|
|
136
|
+
select jsonb_build_object(
|
|
137
|
+
'id', p.id, 'siteId', p.site_id, 'slug', p.slug, 'title', p.draft ->> 'title', 'status', p.status,
|
|
138
|
+
'rev', p.draft_rev, 'publishedAt', p.published_at, 'updatedAt', p.updated_at
|
|
139
|
+
)
|
|
140
|
+
$$;
|
|
141
|
+
|
|
142
|
+
-- ── Writes (security definer, each asking can_edit) ───────────────────────────
|
|
143
|
+
|
|
144
|
+
-- Create a post (post_id null) or save its draft if nobody saved since
|
|
145
|
+
-- `expected_rev`. Returns { id, rev }; raises GS409 (with the current
|
|
146
|
+
-- revision in DETAIL) when someone saved first. A post keeps its slug once
|
|
147
|
+
-- it has been published.
|
|
148
|
+
create or replace function genesis_sites.save_post(site_id uuid, post_id uuid, slug text, expected_rev integer, doc jsonb)
|
|
149
|
+
returns jsonb
|
|
150
|
+
language plpgsql
|
|
151
|
+
security definer
|
|
152
|
+
set search_path = ''
|
|
153
|
+
as $$
|
|
154
|
+
declare
|
|
155
|
+
post genesis_sites.site_posts;
|
|
156
|
+
new_id uuid;
|
|
157
|
+
begin
|
|
158
|
+
-- can_edit first, then any lock: someone who can't edit learns nothing and locks nothing.
|
|
159
|
+
perform genesis_sites.assert_can_edit(site_id);
|
|
160
|
+
perform genesis_sites.assert_post(slug, doc);
|
|
161
|
+
perform 1 from genesis_sites.sites s where s.id = save_post.site_id for update;
|
|
162
|
+
if not found then
|
|
163
|
+
raise exception 'genesis_sites: no site %', site_id using errcode = 'P0002';
|
|
164
|
+
end if;
|
|
165
|
+
if post_id is null then
|
|
166
|
+
if exists (select 1 from genesis_sites.site_posts p where p.site_id = save_post.site_id and p.slug = save_post.slug and p.deleted_at is null) then
|
|
167
|
+
raise exception 'genesis_sites: a post is already at %', slug using errcode = '23505';
|
|
168
|
+
end if;
|
|
169
|
+
insert into genesis_sites.site_posts (site_id, slug, draft) values (site_id, slug, doc) returning id into new_id;
|
|
170
|
+
return jsonb_build_object('id', new_id, 'rev', 1);
|
|
171
|
+
end if;
|
|
172
|
+
select * into post from genesis_sites.site_posts p where p.id = post_id and p.site_id = save_post.site_id and p.deleted_at is null for update;
|
|
173
|
+
if not found then
|
|
174
|
+
raise exception 'genesis_sites: no post %', post_id using errcode = 'P0002';
|
|
175
|
+
end if;
|
|
176
|
+
if post.draft_rev is distinct from expected_rev then
|
|
177
|
+
raise exception 'genesis_sites: someone saved this post first' using errcode = 'GS409', detail = post.draft_rev::text;
|
|
178
|
+
end if;
|
|
179
|
+
if post.slug <> slug then
|
|
180
|
+
if post.published_at is not null then
|
|
181
|
+
raise exception 'genesis_sites: a post keeps its slug once it has been published' using errcode = '22023';
|
|
182
|
+
end if;
|
|
183
|
+
if exists (select 1 from genesis_sites.site_posts p where p.site_id = save_post.site_id and p.slug = save_post.slug and p.deleted_at is null and p.id <> post_id) then
|
|
184
|
+
raise exception 'genesis_sites: a post is already at %', slug using errcode = '23505';
|
|
185
|
+
end if;
|
|
186
|
+
end if;
|
|
187
|
+
update genesis_sites.site_posts p set slug = save_post.slug, draft = doc, draft_rev = p.draft_rev + 1 where p.id = post_id;
|
|
188
|
+
return jsonb_build_object('id', post_id, 'rev', post.draft_rev + 1);
|
|
189
|
+
end;
|
|
190
|
+
$$;
|
|
191
|
+
|
|
192
|
+
-- Publish a post: the draft exactly as the caller checked it (`expected_rev`)
|
|
193
|
+
-- becomes the published copy. Raises GS409 if the draft changed since.
|
|
194
|
+
create or replace function genesis_sites.publish_post(post_id uuid, expected_rev integer)
|
|
195
|
+
returns jsonb
|
|
196
|
+
language plpgsql
|
|
197
|
+
security definer
|
|
198
|
+
set search_path = ''
|
|
199
|
+
as $$
|
|
200
|
+
declare
|
|
201
|
+
post genesis_sites.site_posts;
|
|
202
|
+
site_status text;
|
|
203
|
+
begin
|
|
204
|
+
perform genesis_sites.assert_can_edit((select p.site_id from genesis_sites.site_posts p where p.id = post_id));
|
|
205
|
+
select s.status into site_status from genesis_sites.sites s where s.id = (select p.site_id from genesis_sites.site_posts p where p.id = post_id) for update;
|
|
206
|
+
-- Archiving is a takedown: an editor can't put posts back up.
|
|
207
|
+
if site_status = 'archived' then
|
|
208
|
+
raise exception 'genesis_sites: an archived site can''t publish posts' using errcode = '22023';
|
|
209
|
+
end if;
|
|
210
|
+
select * into post from genesis_sites.site_posts p where p.id = post_id and p.deleted_at is null for update;
|
|
211
|
+
if not found then
|
|
212
|
+
raise exception 'genesis_sites: no post %', post_id using errcode = 'P0002';
|
|
213
|
+
end if;
|
|
214
|
+
if post.draft_rev is distinct from expected_rev then
|
|
215
|
+
raise exception 'genesis_sites: the post changed since it was checked' using errcode = 'GS409', detail = post.draft_rev::text;
|
|
216
|
+
end if;
|
|
217
|
+
perform genesis_sites.assert_post(post.slug, post.draft);
|
|
218
|
+
update genesis_sites.site_posts p
|
|
219
|
+
set published = p.draft, status = 'published', published_at = coalesce(p.published_at, now()), published_updated_at = now()
|
|
220
|
+
where p.id = post_id
|
|
221
|
+
returning * into post;
|
|
222
|
+
return genesis_sites.post_json(post);
|
|
223
|
+
end;
|
|
224
|
+
$$;
|
|
225
|
+
|
|
226
|
+
-- Take a post down. Its draft stays; its first publish date stays, for when it goes back up.
|
|
227
|
+
create or replace function genesis_sites.unpublish_post(post_id uuid)
|
|
228
|
+
returns jsonb
|
|
229
|
+
language plpgsql
|
|
230
|
+
security definer
|
|
231
|
+
set search_path = ''
|
|
232
|
+
as $$
|
|
233
|
+
declare
|
|
234
|
+
post genesis_sites.site_posts;
|
|
235
|
+
begin
|
|
236
|
+
perform genesis_sites.assert_can_edit((select p.site_id from genesis_sites.site_posts p where p.id = post_id));
|
|
237
|
+
update genesis_sites.site_posts p set status = 'draft', published = null, published_updated_at = null
|
|
238
|
+
where p.id = post_id and p.deleted_at is null
|
|
239
|
+
returning * into post;
|
|
240
|
+
if not found then
|
|
241
|
+
raise exception 'genesis_sites: no post %', post_id using errcode = 'P0002';
|
|
242
|
+
end if;
|
|
243
|
+
return genesis_sites.post_json(post);
|
|
244
|
+
end;
|
|
245
|
+
$$;
|
|
246
|
+
|
|
247
|
+
create or replace function genesis_sites.delete_post(post_id uuid)
|
|
248
|
+
returns void
|
|
249
|
+
language plpgsql
|
|
250
|
+
security definer
|
|
251
|
+
set search_path = ''
|
|
252
|
+
as $$
|
|
253
|
+
begin
|
|
254
|
+
perform genesis_sites.assert_can_edit((select p.site_id from genesis_sites.site_posts p where p.id = post_id));
|
|
255
|
+
update genesis_sites.site_posts p set deleted_at = now(), status = 'draft', published = null, published_updated_at = null
|
|
256
|
+
where p.id = post_id and p.deleted_at is null;
|
|
257
|
+
end;
|
|
258
|
+
$$;
|
|
259
|
+
|
|
260
|
+
-- A one-time import (a move from another site): `posts` is
|
|
261
|
+
-- [{ slug, doc, status, publishedAt }], at most 500. Each keeps its slug and
|
|
262
|
+
-- its date (never later than now); a slug the site already has, or a post
|
|
263
|
+
-- that isn't one, is skipped and reported. Returns { imported: [{ slug, id,
|
|
264
|
+
-- status }], skipped: [{ slug, reason }] }. A post is published only when it
|
|
265
|
+
-- asks for `published`; with no status it is a draft. The client runs the
|
|
266
|
+
-- checks first and asks for `draft` on a post that fails them.
|
|
267
|
+
create or replace function genesis_sites.import_posts(site_id uuid, posts jsonb)
|
|
268
|
+
returns jsonb
|
|
269
|
+
language plpgsql
|
|
270
|
+
security definer
|
|
271
|
+
set search_path = ''
|
|
272
|
+
as $$
|
|
273
|
+
declare
|
|
274
|
+
item jsonb;
|
|
275
|
+
item_slug text;
|
|
276
|
+
item_status text;
|
|
277
|
+
item_at timestamptz;
|
|
278
|
+
new_id uuid;
|
|
279
|
+
imported jsonb := '[]'::jsonb;
|
|
280
|
+
skipped jsonb := '[]'::jsonb;
|
|
281
|
+
begin
|
|
282
|
+
perform genesis_sites.assert_can_edit(site_id);
|
|
283
|
+
if posts is null or jsonb_typeof(posts) <> 'array' or jsonb_array_length(posts) > 500 then
|
|
284
|
+
raise exception 'genesis_sites: posts is an array of at most 500 posts' using errcode = '22023';
|
|
285
|
+
end if;
|
|
286
|
+
perform 1 from genesis_sites.sites s where s.id = import_posts.site_id and s.status <> 'archived' for update;
|
|
287
|
+
if not found then
|
|
288
|
+
raise exception 'genesis_sites: no site % to import into', site_id using errcode = 'P0002';
|
|
289
|
+
end if;
|
|
290
|
+
for item in select * from jsonb_array_elements(posts) loop
|
|
291
|
+
item_slug := item ->> 'slug';
|
|
292
|
+
begin
|
|
293
|
+
perform genesis_sites.assert_post(item_slug, item -> 'doc');
|
|
294
|
+
if exists (select 1 from genesis_sites.site_posts p where p.site_id = import_posts.site_id and p.slug = item_slug and p.deleted_at is null) then
|
|
295
|
+
skipped := skipped || jsonb_build_array(jsonb_build_object('slug', item_slug, 'reason', 'The site already has a post at this slug.'));
|
|
296
|
+
continue;
|
|
297
|
+
end if;
|
|
298
|
+
-- Published only when asked: a post with no status (or any other) comes in as a draft.
|
|
299
|
+
item_status := case when item ->> 'status' = 'published' then 'published' else 'draft' end;
|
|
300
|
+
item_at := least(coalesce((item ->> 'publishedAt')::timestamptz, now()), now());
|
|
301
|
+
-- A real date: not infinity, not before 1970 (the feeds and the sitemap print it).
|
|
302
|
+
if not isfinite(item_at) or item_at < '1970-01-01T00:00:00Z'::timestamptz then
|
|
303
|
+
raise exception 'genesis_sites: "%" isn''t a usable publish date', item ->> 'publishedAt' using errcode = '22008';
|
|
304
|
+
end if;
|
|
305
|
+
insert into genesis_sites.site_posts (site_id, slug, draft, published, status, published_at, published_updated_at)
|
|
306
|
+
values (
|
|
307
|
+
site_id, item_slug, item -> 'doc',
|
|
308
|
+
case when item_status = 'published' then item -> 'doc' end,
|
|
309
|
+
item_status,
|
|
310
|
+
case when item_status = 'published' then item_at end,
|
|
311
|
+
case when item_status = 'published' then item_at end
|
|
312
|
+
)
|
|
313
|
+
returning id into new_id;
|
|
314
|
+
imported := imported || jsonb_build_array(jsonb_build_object('slug', item_slug, 'id', new_id, 'status', item_status));
|
|
315
|
+
exception when sqlstate '22023' or sqlstate '22007' or sqlstate '22008' or sqlstate '23514' or sqlstate '23505' then
|
|
316
|
+
skipped := skipped || jsonb_build_array(jsonb_build_object('slug', coalesce(item_slug, '?'), 'reason', sqlerrm));
|
|
317
|
+
end;
|
|
318
|
+
end loop;
|
|
319
|
+
return jsonb_build_object('imported', imported, 'skipped', skipped);
|
|
320
|
+
end;
|
|
321
|
+
$$;
|
|
322
|
+
|
|
323
|
+
-- ── The public read ───────────────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
-- A live site's published posts, newest first, without their bodies (at
|
|
326
|
+
-- most 500): for the blog's index, the sitemap and the feed. Never drafts.
|
|
327
|
+
create or replace function genesis_sites.live_posts(site_id uuid)
|
|
328
|
+
returns jsonb
|
|
329
|
+
language sql
|
|
330
|
+
stable
|
|
331
|
+
security definer
|
|
332
|
+
set search_path = ''
|
|
333
|
+
as $$
|
|
334
|
+
select coalesce(jsonb_agg(item order by at desc, slug), '[]'::jsonb)
|
|
335
|
+
from (
|
|
336
|
+
select p.published_at as at, p.slug,
|
|
337
|
+
jsonb_strip_nulls(jsonb_build_object(
|
|
338
|
+
'slug', p.slug,
|
|
339
|
+
'title', p.published ->> 'title',
|
|
340
|
+
'excerpt', p.published -> 'excerpt',
|
|
341
|
+
'cover', p.published -> 'cover',
|
|
342
|
+
'author', p.published -> 'author',
|
|
343
|
+
'category', p.published -> 'category',
|
|
344
|
+
-- A JSON comparison, so a malformed stored value can't fail the whole list.
|
|
345
|
+
'noindex', case when p.published #> '{seo,noindex}' = 'true'::jsonb or p.published #> '{seo,canonical}' is not null then true end,
|
|
346
|
+
'publishedAt', p.published_at,
|
|
347
|
+
'updatedAt', p.published_updated_at
|
|
348
|
+
)) as item
|
|
349
|
+
from genesis_sites.sites s
|
|
350
|
+
join genesis_sites.site_posts p on p.site_id = s.id
|
|
351
|
+
where s.id = live_posts.site_id and s.status = 'live' and s.live_release_id is not null
|
|
352
|
+
and p.status = 'published' and p.deleted_at is null
|
|
353
|
+
order by p.published_at desc, p.slug
|
|
354
|
+
limit 500
|
|
355
|
+
) latest
|
|
356
|
+
$$;
|
|
357
|
+
|
|
358
|
+
comment on function genesis_sites.live_posts(uuid) is
|
|
359
|
+
'The published posts of a live site, newest first, without bodies (at most 500). Public: anon may call it. Never returns drafts. Genesis Sites.';
|
|
360
|
+
|
|
361
|
+
-- One published post of a live site: { slug, publishedAt, updatedAt, doc }, or null.
|
|
362
|
+
create or replace function genesis_sites.live_post(site_id uuid, slug text)
|
|
363
|
+
returns jsonb
|
|
364
|
+
language sql
|
|
365
|
+
stable
|
|
366
|
+
security definer
|
|
367
|
+
set search_path = ''
|
|
368
|
+
as $$
|
|
369
|
+
select jsonb_build_object('slug', p.slug, 'publishedAt', p.published_at, 'updatedAt', p.published_updated_at, 'doc', p.published)
|
|
370
|
+
from genesis_sites.sites s
|
|
371
|
+
join genesis_sites.site_posts p on p.site_id = s.id
|
|
372
|
+
where s.id = live_post.site_id and s.status = 'live' and s.live_release_id is not null
|
|
373
|
+
and p.slug = live_post.slug and p.status = 'published' and p.deleted_at is null
|
|
374
|
+
$$;
|
|
375
|
+
|
|
376
|
+
comment on function genesis_sites.live_post(uuid, text) is
|
|
377
|
+
'One published post of a live site, or null. Public: anon may call it. Never returns a draft. Genesis Sites.';
|
|
378
|
+
|
|
379
|
+
-- ── Privileges, by name ───────────────────────────────────────────────────────
|
|
380
|
+
|
|
381
|
+
revoke all on table genesis_sites.site_posts from public, anon, authenticated;
|
|
382
|
+
grant select on table genesis_sites.site_posts to authenticated;
|
|
383
|
+
revoke all on table genesis_sites.site_posts from service_role;
|
|
384
|
+
grant select, insert, update, delete on table genesis_sites.site_posts to service_role;
|
|
385
|
+
|
|
386
|
+
revoke execute on function genesis_sites.save_post(uuid, uuid, text, integer, jsonb) from public, anon;
|
|
387
|
+
grant execute on function genesis_sites.save_post(uuid, uuid, text, integer, jsonb) to authenticated;
|
|
388
|
+
revoke execute on function genesis_sites.publish_post(uuid, integer) from public, anon;
|
|
389
|
+
grant execute on function genesis_sites.publish_post(uuid, integer) to authenticated;
|
|
390
|
+
revoke execute on function genesis_sites.unpublish_post(uuid) from public, anon;
|
|
391
|
+
grant execute on function genesis_sites.unpublish_post(uuid) to authenticated;
|
|
392
|
+
revoke execute on function genesis_sites.delete_post(uuid) from public, anon;
|
|
393
|
+
grant execute on function genesis_sites.delete_post(uuid) to authenticated;
|
|
394
|
+
revoke execute on function genesis_sites.import_posts(uuid, jsonb) from public, anon;
|
|
395
|
+
grant execute on function genesis_sites.import_posts(uuid, jsonb) to authenticated;
|
|
396
|
+
revoke execute on function genesis_sites.live_posts(uuid) from public;
|
|
397
|
+
grant execute on function genesis_sites.live_posts(uuid) to anon, authenticated, service_role;
|
|
398
|
+
revoke execute on function genesis_sites.live_post(uuid, text) from public;
|
|
399
|
+
grant execute on function genesis_sites.live_post(uuid, text) to anon, authenticated, service_role;
|
|
400
|
+
|
|
401
|
+
-- Internal: only the functions above call these.
|
|
402
|
+
revoke execute on function genesis_sites.assert_post(text, jsonb) from public, anon, authenticated;
|
|
403
|
+
revoke execute on function genesis_sites.post_json(genesis_sites.site_posts) from public, anon, authenticated;
|
|
404
|
+
revoke execute on function genesis_sites.notify_post_change() from public, anon, authenticated;
|