create-smoodly-app 0.0.5 → 0.0.7
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 +23 -1
- package/dist/index.js +3 -2
- package/package.json +2 -2
- package/templates/next/package.json +1 -1
- package/templates/next/smoodly/collections.ts +10 -3
- package/templates/next/supabase/migrations/00000000000000_smoodly.sql +152 -58
- package/templates/next-intl/package.json +1 -1
- package/templates/next-intl/smoodly/collections.ts +10 -3
- package/templates/next-intl/supabase/migrations/00000000000000_smoodly.sql +152 -58
package/README.md
CHANGED
|
@@ -14,7 +14,10 @@ sign in. Spec: `docs/superpowers/specs/2026-09-04-create-smoodly-app-design.md`.
|
|
|
14
14
|
scaffolds the next-intl layout: the site under `app/[locale]/`, `i18n/`,
|
|
15
15
|
`proxy.ts`, translated URLs per locale. Flag: `--locales en,fi`.
|
|
16
16
|
3. Where content lives: a **local Supabase stack** (`supabase init` +
|
|
17
|
-
`supabase start` inside the app
|
|
17
|
+
`supabase start` inside the app, with the services Smoodly never uses
|
|
18
|
+
excluded — storage, image proxy, edge functions, analytics, realtime,
|
|
19
|
+
the pooler — so the stack stays small beside any other you run; needs
|
|
20
|
+
the Supabase CLI and Docker; when
|
|
18
21
|
another project holds the default `543xx` ports the new one moves to the
|
|
19
22
|
next free block, `553xx` and so on, and says so) or a
|
|
20
23
|
**hosted project** (URL, publishable key, secret key; then an offer to
|
|
@@ -61,6 +64,25 @@ multilingual one with `--locales en,de`.
|
|
|
61
64
|
|
|
62
65
|
## Follow-ups (logged 2026-09-06)
|
|
63
66
|
|
|
67
|
+
- Local mode on a fresh multilingual install (0.0.5, 2026-09-06) failed at
|
|
68
|
+
`supabase start` with `supabase_storage_<app> container is not ready:
|
|
69
|
+
unhealthy` — the storage container logged `Started Successfully` and the
|
|
70
|
+
CLI still stopped the stack. Five local stacks (57 containers) were
|
|
71
|
+
sharing 7.8 GB of Docker memory at the time, the same pressure that
|
|
72
|
+
made the repo's live suites flake on 2026-09-05. Smoodly never uses
|
|
73
|
+
storage, so the CLI could start the app's stack with
|
|
74
|
+
`supabase start -x storage-api,imgproxy,edge-runtime,logflare,vector,realtime`
|
|
75
|
+
(the container names `supabase start --help` lists; CI already excludes
|
|
76
|
+
most of these for the repo stack), and the "Finish by hand" text could
|
|
77
|
+
mention `supabase stop` on other projects. Retried the same day with
|
|
78
|
+
the other four stacks stopped (`supabase stop --project-id <name>`):
|
|
79
|
+
`npx create-smoodly-app@0.0.5 --supabase local --locales en,fi --yes`
|
|
80
|
+
came up with every container healthy, storage included, and the app
|
|
81
|
+
built and served `/admin` 200, `/` and `/fi` 404 — so the cause is
|
|
82
|
+
memory pressure, not the CLI. Fixed the same day: `run.ts` starts the
|
|
83
|
+
app's stack with `-x storage-api,imgproxy,edge-runtime,logflare,vector,realtime,supavisor`
|
|
84
|
+
(`LOCAL_START_ARGS`), and the "Finish by hand" text carries the same
|
|
85
|
+
flags. Kept: db, kong, auth, PostgREST, Studio and pg-meta, mailpit.
|
|
64
86
|
- **A non-interactive run without `--locales` needs `--yes`.** The
|
|
65
87
|
"Multilingual site?" prompt is asked whenever `--locales` is absent, and
|
|
66
88
|
with no TTY (a CI step, stdin at EOF) the CLI exits 13 having created
|
package/dist/index.js
CHANGED
|
@@ -378,6 +378,7 @@ function parseStatusEnv(output) {
|
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
// src/run.ts
|
|
381
|
+
var LOCAL_START_ARGS = ["start", "-x", "storage-api,imgproxy,edge-runtime,logflare,vector,realtime,supavisor"];
|
|
381
382
|
async function run(answers2, deps) {
|
|
382
383
|
if (answers2.supabase === "hosted" && !answers2.hosted) {
|
|
383
384
|
throw new Error("hosted answers need the project's keys");
|
|
@@ -420,7 +421,7 @@ async function run(answers2, deps) {
|
|
|
420
421
|
await deps.fs.writeFile(configPath, shiftBlock(toml, base));
|
|
421
422
|
deps.log(`Ports ${DEFAULT_BASE / 100}xx are busy (another Supabase project running?), using ${base / 100}xx.`);
|
|
422
423
|
}
|
|
423
|
-
await sh("supabase",
|
|
424
|
+
await sh("supabase", LOCAL_START_ARGS);
|
|
424
425
|
ctx.keys = parseStatusEnv((await sh("supabase", ["status", "-o", "env"], { capture: true })).stdout);
|
|
425
426
|
break;
|
|
426
427
|
}
|
|
@@ -466,7 +467,7 @@ ${renderEnv(keys).split("\n").filter((l) => !l.startsWith("#")).join("\n").trimE
|
|
|
466
467
|
].join("\n");
|
|
467
468
|
switch (step) {
|
|
468
469
|
case "supabaseLocal":
|
|
469
|
-
return [`cd ${answers2.name} && supabase init && supabase
|
|
470
|
+
return [`cd ${answers2.name} && supabase init && supabase ${LOCAL_START_ARGS.join(" ")}`, "", envLines, "", editorLines].join("\n");
|
|
470
471
|
case "supabaseInit": {
|
|
471
472
|
const commands = answers2.hosted?.linkAndPush ? [`cd ${answers2.name} && supabase init && ${linkCommand(answers2)} && supabase db push`] : [`cd ${answers2.name} && supabase init`, "", `When you are ready: ${linkCommand(answers2)} && supabase db push`];
|
|
472
473
|
return [...commands, "", envLines, "", editorLines].join("\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-smoodly-app",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Create a Smoodly site: a fresh Next.js app with the admin mounted, a Supabase schema, an env file and a first editor.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"bin": {
|
|
13
|
-
"create-smoodly-app": "
|
|
13
|
+
"create-smoodly-app": "dist/index.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { f, smoodly } from "smoodly";
|
|
2
2
|
|
|
3
|
+
// A person's name is the same in every language; the role is translated.
|
|
4
|
+
// Shared fields live on the entry, .localized() ones on its locale rows
|
|
5
|
+
// (spec 2026-09-06 §7).
|
|
3
6
|
export const people = smoodly.collection({
|
|
4
7
|
name: "people",
|
|
5
8
|
title: "People",
|
|
6
9
|
titleField: "name",
|
|
7
10
|
fields: {
|
|
8
11
|
name: f.text(),
|
|
9
|
-
role: f.text().optional(),
|
|
12
|
+
role: f.text().optional().localized(),
|
|
10
13
|
},
|
|
11
14
|
});
|
|
12
15
|
|
|
16
|
+
// Articles keep a version history per locale (`versions: true`): every
|
|
17
|
+
// save is a snapshot, publish moves a pointer, and the published site
|
|
18
|
+
// keeps serving the published snapshot while a draft moves on.
|
|
13
19
|
export const articles = smoodly.collection({
|
|
14
20
|
name: "articles",
|
|
15
21
|
title: "Articles",
|
|
16
22
|
titleField: "title",
|
|
17
23
|
path: "posts",
|
|
24
|
+
versions: true,
|
|
18
25
|
fields: {
|
|
19
|
-
title: f.text(),
|
|
26
|
+
title: f.text().localized(),
|
|
20
27
|
slug: f.slug({ from: "title" }),
|
|
21
|
-
excerpt: f.text().optional(),
|
|
28
|
+
excerpt: f.text().optional().localized(),
|
|
22
29
|
author: f.ref(() => people),
|
|
23
30
|
},
|
|
24
31
|
});
|
|
@@ -12,28 +12,34 @@ create table if not exists "pages" (
|
|
|
12
12
|
"id" uuid primary key default gen_random_uuid(),
|
|
13
13
|
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
14
14
|
"parent_id" uuid references "pages" ("id") on delete restrict,
|
|
15
|
-
"slug" text not null,
|
|
16
|
-
"title" text not null,
|
|
17
|
-
"i18n" jsonb,
|
|
18
15
|
"template" text,
|
|
19
|
-
"status" text not null default 'draft',
|
|
20
16
|
"sort" numeric,
|
|
21
|
-
"locales" text[],
|
|
22
|
-
"draft_version_id" uuid,
|
|
23
|
-
"published_version_id" uuid,
|
|
24
17
|
"created_at" timestamptz not null default now(),
|
|
25
18
|
"updated_at" timestamptz not null default now()
|
|
26
19
|
);
|
|
27
20
|
|
|
28
|
-
-- The tree is the URL (spec 2026-09-05 §1): a node stores its
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
-- The tree is the URL (spec 2026-09-05 §1): a node stores its parent and
|
|
22
|
+
-- its position; template null = folder. Everything a locale owns — the
|
|
23
|
+
-- node's own segment, title, publish state and version pointers — lives
|
|
24
|
+
-- in page_locales, one row per locale the page exists in (spec 2026-09-06
|
|
25
|
+
-- §2). There is no sibling-slug index: URL uniqueness per locale is the
|
|
26
|
+
-- primary key of "paths", which the stores write inside the same operation.
|
|
34
27
|
create index if not exists "pages_parent_idx"
|
|
35
28
|
on "pages" ("space_id", "parent_id", "sort");
|
|
36
29
|
|
|
30
|
+
create table if not exists "page_locales" (
|
|
31
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
32
|
+
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
33
|
+
"locale" text not null,
|
|
34
|
+
"slug" text not null,
|
|
35
|
+
"title" text not null,
|
|
36
|
+
"status" text not null default 'draft',
|
|
37
|
+
"draft_version_id" uuid,
|
|
38
|
+
"published_version_id" uuid,
|
|
39
|
+
"updated_at" timestamptz not null default now(),
|
|
40
|
+
primary key ("page_id", "locale")
|
|
41
|
+
);
|
|
42
|
+
|
|
37
43
|
-- Sibling reorder for pages: same shape as smoodly_reorder for entries.
|
|
38
44
|
create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void language sql as $$
|
|
39
45
|
update "pages" pg set "sort" = u.ord
|
|
@@ -42,15 +48,21 @@ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void la
|
|
|
42
48
|
and pg."space_id" = "smoodly_current_space"();
|
|
43
49
|
$$;
|
|
44
50
|
|
|
51
|
+
-- A version belongs to ONE locale of one page: each locale has its own
|
|
52
|
+
-- draft, its own publish and its own history.
|
|
45
53
|
create table if not exists "page_versions" (
|
|
46
54
|
"id" uuid primary key default gen_random_uuid(),
|
|
47
55
|
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
48
56
|
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
57
|
+
"locale" text not null,
|
|
49
58
|
"tree" jsonb not null,
|
|
50
59
|
"created_by" text,
|
|
51
60
|
"created_at" timestamptz not null default now()
|
|
52
61
|
);
|
|
53
62
|
|
|
63
|
+
create index if not exists "page_versions_page_locale_idx"
|
|
64
|
+
on "page_versions" ("page_id", "locale", "created_at");
|
|
65
|
+
|
|
54
66
|
create table if not exists "saved_sections" (
|
|
55
67
|
"id" uuid primary key default gen_random_uuid(),
|
|
56
68
|
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
@@ -73,40 +85,58 @@ create table if not exists "entries" (
|
|
|
73
85
|
"collection" text not null,
|
|
74
86
|
"parent_id" uuid references "entries" ("id") on delete restrict,
|
|
75
87
|
"fields" jsonb not null default '{}'::jsonb,
|
|
76
|
-
"i18n" jsonb,
|
|
77
|
-
"status" text not null default 'draft',
|
|
78
88
|
"sort" numeric,
|
|
79
|
-
"locales" text[],
|
|
80
89
|
"created_at" timestamptz not null default now(),
|
|
81
90
|
"updated_at" timestamptz not null default now()
|
|
82
91
|
);
|
|
83
92
|
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
-- a number, boolean or null slug would otherwise print as non-empty
|
|
93
|
-
-- text and claim the index; only a non-empty string slug claims a URL,
|
|
94
|
-
-- absent, null, empty or non-string slugs never conflict — the same
|
|
95
|
-
-- rule as slugOf in paths.ts. A flat collection (every entry a root)
|
|
96
|
-
-- therefore behaves exactly as per-collection uniqueness did.
|
|
97
|
-
create unique index if not exists "entries_sibling_slug_key"
|
|
98
|
-
on "entries" ("space_id", "collection", "parent_id", ("fields"->>'slug')) nulls not distinct
|
|
99
|
-
where (jsonb_typeof("fields"->'slug') = 'string' and "fields"->>'slug' <> '');
|
|
100
|
-
|
|
101
|
-
create index if not exists "entries_collection_status_idx"
|
|
102
|
-
on "entries" ("space_id", "collection", "status");
|
|
103
|
-
|
|
93
|
+
-- The node holds identity, tree position and the SHARED fields — every
|
|
94
|
+
-- field without .localized() (spec 2026-09-06 §7): a Person's photo is
|
|
95
|
+
-- the same in every language. Everything a locale owns — its segment,
|
|
96
|
+
-- publish state, the .localized() fields and the version pointers —
|
|
97
|
+
-- lives in entry_locales, one row per locale the entry exists in. There
|
|
98
|
+
-- is no sibling-slug index any more: URL uniqueness per locale is the
|
|
99
|
+
-- primary key of "paths", which the stores write inside the same
|
|
100
|
+
-- operation, and both adapters pre-check siblings per locale in code.
|
|
104
101
|
create index if not exists "entries_collection_sort_idx"
|
|
105
102
|
on "entries" ("space_id", "collection", "sort");
|
|
106
103
|
|
|
107
104
|
create index if not exists "entries_parent_idx"
|
|
108
105
|
on "entries" ("space_id", "collection", "parent_id");
|
|
109
106
|
|
|
107
|
+
create table if not exists "entry_locales" (
|
|
108
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
109
|
+
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
110
|
+
"locale" text not null,
|
|
111
|
+
"slug" text,
|
|
112
|
+
"status" text not null default 'draft',
|
|
113
|
+
"fields" jsonb not null default '{}'::jsonb,
|
|
114
|
+
"draft_version_id" uuid,
|
|
115
|
+
"published_version_id" uuid,
|
|
116
|
+
"updated_at" timestamptz not null default now(),
|
|
117
|
+
primary key ("entry_id", "locale")
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
create index if not exists "entry_locales_status_idx"
|
|
121
|
+
on "entry_locales" ("space_id", "locale", "status");
|
|
122
|
+
|
|
123
|
+
-- A version belongs to ONE locale of one entry and stores the MERGED
|
|
124
|
+
-- fields (shared + localized + slug) as they were at that save, so
|
|
125
|
+
-- restoring an old Finnish version restores the shared fields too.
|
|
126
|
+
-- Rows exist only for collections registered with `versions: true`.
|
|
127
|
+
create table if not exists "entry_versions" (
|
|
128
|
+
"id" uuid primary key default gen_random_uuid(),
|
|
129
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
130
|
+
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
131
|
+
"locale" text not null,
|
|
132
|
+
"fields" jsonb not null,
|
|
133
|
+
"created_by" text,
|
|
134
|
+
"created_at" timestamptz not null default now()
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
create index if not exists "entry_versions_entry_locale_idx"
|
|
138
|
+
on "entry_versions" ("entry_id", "locale", "created_at");
|
|
139
|
+
|
|
110
140
|
-- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
|
|
111
141
|
-- statement. Deliberately does not touch updated_at — a drag must not
|
|
112
142
|
-- make the whole collection look freshly edited. Scoped to the current
|
|
@@ -172,10 +202,13 @@ $$;
|
|
|
172
202
|
-- (collectionSQL) run with security_invoker = true, so they are subject
|
|
173
203
|
-- to this same smoodly_read policy rather than their owner's privileges.
|
|
174
204
|
alter table "pages" enable row level security;
|
|
205
|
+
alter table "page_locales" enable row level security;
|
|
175
206
|
alter table "page_versions" enable row level security;
|
|
176
207
|
alter table "saved_sections" enable row level security;
|
|
177
208
|
alter table "global_sections" enable row level security;
|
|
178
209
|
alter table "entries" enable row level security;
|
|
210
|
+
alter table "entry_locales" enable row level security;
|
|
211
|
+
alter table "entry_versions" enable row level security;
|
|
179
212
|
alter table "refs" enable row level security;
|
|
180
213
|
alter table "paths" enable row level security;
|
|
181
214
|
|
|
@@ -272,8 +305,14 @@ begin
|
|
|
272
305
|
end;
|
|
273
306
|
$$;
|
|
274
307
|
|
|
308
|
+
create index if not exists "page_locales_space_idx" on "page_locales" ("space_id");
|
|
309
|
+
|
|
275
310
|
create index if not exists "page_versions_space_idx" on "page_versions" ("space_id");
|
|
276
311
|
|
|
312
|
+
create index if not exists "entry_locales_space_idx" on "entry_locales" ("space_id");
|
|
313
|
+
|
|
314
|
+
create index if not exists "entry_versions_space_idx" on "entry_versions" ("space_id");
|
|
315
|
+
|
|
277
316
|
create index if not exists "saved_sections_space_idx" on "saved_sections" ("space_id");
|
|
278
317
|
|
|
279
318
|
create index if not exists "global_sections_space_idx" on "global_sections" ("space_id");
|
|
@@ -284,6 +323,10 @@ drop trigger if exists "smoodly_a_set_space" on "pages";
|
|
|
284
323
|
create trigger "smoodly_a_set_space" before insert on "pages"
|
|
285
324
|
for each row execute function "smoodly_set_space"();
|
|
286
325
|
|
|
326
|
+
drop trigger if exists "smoodly_a_set_space" on "page_locales";
|
|
327
|
+
create trigger "smoodly_a_set_space" before insert on "page_locales"
|
|
328
|
+
for each row execute function "smoodly_set_space"();
|
|
329
|
+
|
|
287
330
|
drop trigger if exists "smoodly_a_set_space" on "page_versions";
|
|
288
331
|
create trigger "smoodly_a_set_space" before insert on "page_versions"
|
|
289
332
|
for each row execute function "smoodly_set_space"();
|
|
@@ -300,6 +343,14 @@ drop trigger if exists "smoodly_a_set_space" on "entries";
|
|
|
300
343
|
create trigger "smoodly_a_set_space" before insert on "entries"
|
|
301
344
|
for each row execute function "smoodly_set_space"();
|
|
302
345
|
|
|
346
|
+
drop trigger if exists "smoodly_a_set_space" on "entry_locales";
|
|
347
|
+
create trigger "smoodly_a_set_space" before insert on "entry_locales"
|
|
348
|
+
for each row execute function "smoodly_set_space"();
|
|
349
|
+
|
|
350
|
+
drop trigger if exists "smoodly_a_set_space" on "entry_versions";
|
|
351
|
+
create trigger "smoodly_a_set_space" before insert on "entry_versions"
|
|
352
|
+
for each row execute function "smoodly_set_space"();
|
|
353
|
+
|
|
303
354
|
drop trigger if exists "smoodly_a_set_space" on "refs";
|
|
304
355
|
create trigger "smoodly_a_set_space" before insert on "refs"
|
|
305
356
|
for each row execute function "smoodly_set_space"();
|
|
@@ -316,6 +367,10 @@ drop trigger if exists "smoodly_b_row_cap" on "pages";
|
|
|
316
367
|
create trigger "smoodly_b_row_cap" before insert on "pages"
|
|
317
368
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
318
369
|
|
|
370
|
+
drop trigger if exists "smoodly_b_row_cap" on "page_locales";
|
|
371
|
+
create trigger "smoodly_b_row_cap" before insert on "page_locales"
|
|
372
|
+
for each row execute function "smoodly_enforce_row_cap"();
|
|
373
|
+
|
|
319
374
|
drop trigger if exists "smoodly_b_row_cap" on "page_versions";
|
|
320
375
|
create trigger "smoodly_b_row_cap" before insert on "page_versions"
|
|
321
376
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
@@ -332,6 +387,14 @@ drop trigger if exists "smoodly_b_row_cap" on "entries";
|
|
|
332
387
|
create trigger "smoodly_b_row_cap" before insert on "entries"
|
|
333
388
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
334
389
|
|
|
390
|
+
drop trigger if exists "smoodly_b_row_cap" on "entry_locales";
|
|
391
|
+
create trigger "smoodly_b_row_cap" before insert on "entry_locales"
|
|
392
|
+
for each row execute function "smoodly_enforce_row_cap"();
|
|
393
|
+
|
|
394
|
+
drop trigger if exists "smoodly_b_row_cap" on "entry_versions";
|
|
395
|
+
create trigger "smoodly_b_row_cap" before insert on "entry_versions"
|
|
396
|
+
for each row execute function "smoodly_enforce_row_cap"();
|
|
397
|
+
|
|
335
398
|
drop trigger if exists "smoodly_b_row_cap" on "refs";
|
|
336
399
|
create trigger "smoodly_b_row_cap" before insert on "refs"
|
|
337
400
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
@@ -347,6 +410,15 @@ create policy "smoodly_write" on "pages" for all
|
|
|
347
410
|
|
|
348
411
|
drop policy if exists "smoodly_read" on "pages";
|
|
349
412
|
create policy "smoodly_read" on "pages" for select
|
|
413
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "page_locales" l where l."page_id" = "pages"."id" and l."status" = 'published'));
|
|
414
|
+
|
|
415
|
+
drop policy if exists "smoodly_write" on "page_locales";
|
|
416
|
+
create policy "smoodly_write" on "page_locales" for all
|
|
417
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
418
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
419
|
+
|
|
420
|
+
drop policy if exists "smoodly_read" on "page_locales";
|
|
421
|
+
create policy "smoodly_read" on "page_locales" for select
|
|
350
422
|
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
|
|
351
423
|
|
|
352
424
|
drop policy if exists "smoodly_write" on "page_versions";
|
|
@@ -356,7 +428,7 @@ create policy "smoodly_write" on "page_versions" for all
|
|
|
356
428
|
|
|
357
429
|
drop policy if exists "smoodly_read" on "page_versions";
|
|
358
430
|
create policy "smoodly_read" on "page_versions" for select
|
|
359
|
-
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "
|
|
431
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "page_locales" l where l."published_version_id" = "page_versions"."id"));
|
|
360
432
|
|
|
361
433
|
drop policy if exists "smoodly_write" on "saved_sections";
|
|
362
434
|
create policy "smoodly_write" on "saved_sections" for all
|
|
@@ -379,8 +451,26 @@ create policy "smoodly_write" on "entries" for all
|
|
|
379
451
|
|
|
380
452
|
drop policy if exists "smoodly_read" on "entries";
|
|
381
453
|
create policy "smoodly_read" on "entries" for select
|
|
454
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "entry_locales" l where l."entry_id" = "entries"."id" and l."status" = 'published'));
|
|
455
|
+
|
|
456
|
+
drop policy if exists "smoodly_write" on "entry_locales";
|
|
457
|
+
create policy "smoodly_write" on "entry_locales" for all
|
|
458
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
459
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
460
|
+
|
|
461
|
+
drop policy if exists "smoodly_read" on "entry_locales";
|
|
462
|
+
create policy "smoodly_read" on "entry_locales" for select
|
|
382
463
|
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
|
|
383
464
|
|
|
465
|
+
drop policy if exists "smoodly_write" on "entry_versions";
|
|
466
|
+
create policy "smoodly_write" on "entry_versions" for all
|
|
467
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
468
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
469
|
+
|
|
470
|
+
drop policy if exists "smoodly_read" on "entry_versions";
|
|
471
|
+
create policy "smoodly_read" on "entry_versions" for select
|
|
472
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "entry_locales" l where l."published_version_id" = "entry_versions"."id"));
|
|
473
|
+
|
|
384
474
|
drop policy if exists "smoodly_write" on "refs";
|
|
385
475
|
create policy "smoodly_write" on "refs" for all
|
|
386
476
|
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
@@ -393,7 +483,7 @@ create policy "smoodly_write" on "paths" for all
|
|
|
393
483
|
|
|
394
484
|
drop policy if exists "smoodly_read" on "paths";
|
|
395
485
|
create policy "smoodly_read" on "paths" for select
|
|
396
|
-
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (("kind" = 'page' and exists (select 1 from "
|
|
486
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (("kind" = 'page' and exists (select 1 from "page_locales" l where l."page_id" = "paths"."target_id" and l."locale" = "paths"."locale" and l."status" = 'published')) or ("kind" = 'entry' and exists (select 1 from "entry_locales" l where l."entry_id" = "paths"."target_id" and l."locale" = "paths"."locale" and l."status" = 'published'))));
|
|
397
487
|
|
|
398
488
|
drop policy if exists "smoodly_write" on "editors";
|
|
399
489
|
create policy "smoodly_write" on "editors" for all
|
|
@@ -402,26 +492,30 @@ create policy "smoodly_write" on "editors" for all
|
|
|
402
492
|
|
|
403
493
|
create or replace view "people" with (security_invoker = true) as
|
|
404
494
|
select
|
|
405
|
-
"id",
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"
|
|
410
|
-
"
|
|
411
|
-
"fields"->>'
|
|
412
|
-
|
|
413
|
-
|
|
495
|
+
e."id",
|
|
496
|
+
l."locale",
|
|
497
|
+
l."slug",
|
|
498
|
+
e."sort",
|
|
499
|
+
e."created_at",
|
|
500
|
+
l."updated_at",
|
|
501
|
+
e."fields"->>'name' as "name",
|
|
502
|
+
l."fields"->>'role' as "role"
|
|
503
|
+
from "entries" e
|
|
504
|
+
join "entry_locales" l on l."entry_id" = e."id"
|
|
505
|
+
where e."collection" = 'people' and l."status" = 'published';
|
|
414
506
|
|
|
415
507
|
create or replace view "articles" with (security_invoker = true) as
|
|
416
508
|
select
|
|
417
|
-
"id",
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
"
|
|
423
|
-
"fields"->>'
|
|
424
|
-
"fields"->>'excerpt' as "excerpt",
|
|
425
|
-
"fields"->>'author' as "author"
|
|
426
|
-
from "entries"
|
|
427
|
-
|
|
509
|
+
e."id",
|
|
510
|
+
l."locale",
|
|
511
|
+
l."slug",
|
|
512
|
+
e."sort",
|
|
513
|
+
e."created_at",
|
|
514
|
+
l."updated_at",
|
|
515
|
+
v."fields"->>'title' as "title",
|
|
516
|
+
v."fields"->>'excerpt' as "excerpt",
|
|
517
|
+
v."fields"->>'author' as "author"
|
|
518
|
+
from "entries" e
|
|
519
|
+
join "entry_locales" l on l."entry_id" = e."id"
|
|
520
|
+
join "entry_versions" v on v."id" = l."published_version_id"
|
|
521
|
+
where e."collection" = 'articles' and l."status" = 'published';
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
import { f, smoodly } from "smoodly";
|
|
2
2
|
|
|
3
|
+
// A person's name is the same in every language; the role is translated.
|
|
4
|
+
// Shared fields live on the entry, .localized() ones on its locale rows
|
|
5
|
+
// (spec 2026-09-06 §7).
|
|
3
6
|
export const people = smoodly.collection({
|
|
4
7
|
name: "people",
|
|
5
8
|
title: "People",
|
|
6
9
|
titleField: "name",
|
|
7
10
|
fields: {
|
|
8
11
|
name: f.text(),
|
|
9
|
-
role: f.text().optional(),
|
|
12
|
+
role: f.text().optional().localized(),
|
|
10
13
|
},
|
|
11
14
|
});
|
|
12
15
|
|
|
16
|
+
// Articles keep a version history per locale (`versions: true`): every
|
|
17
|
+
// save is a snapshot, publish moves a pointer, and the published site
|
|
18
|
+
// keeps serving the published snapshot while a draft moves on.
|
|
13
19
|
export const articles = smoodly.collection({
|
|
14
20
|
name: "articles",
|
|
15
21
|
title: "Articles",
|
|
16
22
|
titleField: "title",
|
|
17
23
|
path: "posts",
|
|
24
|
+
versions: true,
|
|
18
25
|
fields: {
|
|
19
|
-
title: f.text(),
|
|
26
|
+
title: f.text().localized(),
|
|
20
27
|
slug: f.slug({ from: "title" }),
|
|
21
|
-
excerpt: f.text().optional(),
|
|
28
|
+
excerpt: f.text().optional().localized(),
|
|
22
29
|
author: f.ref(() => people),
|
|
23
30
|
},
|
|
24
31
|
});
|
|
@@ -12,28 +12,34 @@ create table if not exists "pages" (
|
|
|
12
12
|
"id" uuid primary key default gen_random_uuid(),
|
|
13
13
|
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
14
14
|
"parent_id" uuid references "pages" ("id") on delete restrict,
|
|
15
|
-
"slug" text not null,
|
|
16
|
-
"title" text not null,
|
|
17
|
-
"i18n" jsonb,
|
|
18
15
|
"template" text,
|
|
19
|
-
"status" text not null default 'draft',
|
|
20
16
|
"sort" numeric,
|
|
21
|
-
"locales" text[],
|
|
22
|
-
"draft_version_id" uuid,
|
|
23
|
-
"published_version_id" uuid,
|
|
24
17
|
"created_at" timestamptz not null default now(),
|
|
25
18
|
"updated_at" timestamptz not null default now()
|
|
26
19
|
);
|
|
27
20
|
|
|
28
|
-
-- The tree is the URL (spec 2026-09-05 §1): a node stores its
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
21
|
+
-- The tree is the URL (spec 2026-09-05 §1): a node stores its parent and
|
|
22
|
+
-- its position; template null = folder. Everything a locale owns — the
|
|
23
|
+
-- node's own segment, title, publish state and version pointers — lives
|
|
24
|
+
-- in page_locales, one row per locale the page exists in (spec 2026-09-06
|
|
25
|
+
-- §2). There is no sibling-slug index: URL uniqueness per locale is the
|
|
26
|
+
-- primary key of "paths", which the stores write inside the same operation.
|
|
34
27
|
create index if not exists "pages_parent_idx"
|
|
35
28
|
on "pages" ("space_id", "parent_id", "sort");
|
|
36
29
|
|
|
30
|
+
create table if not exists "page_locales" (
|
|
31
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
32
|
+
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
33
|
+
"locale" text not null,
|
|
34
|
+
"slug" text not null,
|
|
35
|
+
"title" text not null,
|
|
36
|
+
"status" text not null default 'draft',
|
|
37
|
+
"draft_version_id" uuid,
|
|
38
|
+
"published_version_id" uuid,
|
|
39
|
+
"updated_at" timestamptz not null default now(),
|
|
40
|
+
primary key ("page_id", "locale")
|
|
41
|
+
);
|
|
42
|
+
|
|
37
43
|
-- Sibling reorder for pages: same shape as smoodly_reorder for entries.
|
|
38
44
|
create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void language sql as $$
|
|
39
45
|
update "pages" pg set "sort" = u.ord
|
|
@@ -42,15 +48,21 @@ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void la
|
|
|
42
48
|
and pg."space_id" = "smoodly_current_space"();
|
|
43
49
|
$$;
|
|
44
50
|
|
|
51
|
+
-- A version belongs to ONE locale of one page: each locale has its own
|
|
52
|
+
-- draft, its own publish and its own history.
|
|
45
53
|
create table if not exists "page_versions" (
|
|
46
54
|
"id" uuid primary key default gen_random_uuid(),
|
|
47
55
|
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
48
56
|
"page_id" uuid not null references "pages" ("id") on delete cascade,
|
|
57
|
+
"locale" text not null,
|
|
49
58
|
"tree" jsonb not null,
|
|
50
59
|
"created_by" text,
|
|
51
60
|
"created_at" timestamptz not null default now()
|
|
52
61
|
);
|
|
53
62
|
|
|
63
|
+
create index if not exists "page_versions_page_locale_idx"
|
|
64
|
+
on "page_versions" ("page_id", "locale", "created_at");
|
|
65
|
+
|
|
54
66
|
create table if not exists "saved_sections" (
|
|
55
67
|
"id" uuid primary key default gen_random_uuid(),
|
|
56
68
|
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
@@ -73,40 +85,58 @@ create table if not exists "entries" (
|
|
|
73
85
|
"collection" text not null,
|
|
74
86
|
"parent_id" uuid references "entries" ("id") on delete restrict,
|
|
75
87
|
"fields" jsonb not null default '{}'::jsonb,
|
|
76
|
-
"i18n" jsonb,
|
|
77
|
-
"status" text not null default 'draft',
|
|
78
88
|
"sort" numeric,
|
|
79
|
-
"locales" text[],
|
|
80
89
|
"created_at" timestamptz not null default now(),
|
|
81
90
|
"updated_at" timestamptz not null default now()
|
|
82
91
|
);
|
|
83
92
|
|
|
84
|
-
--
|
|
85
|
-
--
|
|
86
|
-
--
|
|
87
|
-
--
|
|
88
|
-
--
|
|
89
|
-
--
|
|
90
|
-
--
|
|
91
|
-
--
|
|
92
|
-
-- a number, boolean or null slug would otherwise print as non-empty
|
|
93
|
-
-- text and claim the index; only a non-empty string slug claims a URL,
|
|
94
|
-
-- absent, null, empty or non-string slugs never conflict — the same
|
|
95
|
-
-- rule as slugOf in paths.ts. A flat collection (every entry a root)
|
|
96
|
-
-- therefore behaves exactly as per-collection uniqueness did.
|
|
97
|
-
create unique index if not exists "entries_sibling_slug_key"
|
|
98
|
-
on "entries" ("space_id", "collection", "parent_id", ("fields"->>'slug')) nulls not distinct
|
|
99
|
-
where (jsonb_typeof("fields"->'slug') = 'string' and "fields"->>'slug' <> '');
|
|
100
|
-
|
|
101
|
-
create index if not exists "entries_collection_status_idx"
|
|
102
|
-
on "entries" ("space_id", "collection", "status");
|
|
103
|
-
|
|
93
|
+
-- The node holds identity, tree position and the SHARED fields — every
|
|
94
|
+
-- field without .localized() (spec 2026-09-06 §7): a Person's photo is
|
|
95
|
+
-- the same in every language. Everything a locale owns — its segment,
|
|
96
|
+
-- publish state, the .localized() fields and the version pointers —
|
|
97
|
+
-- lives in entry_locales, one row per locale the entry exists in. There
|
|
98
|
+
-- is no sibling-slug index any more: URL uniqueness per locale is the
|
|
99
|
+
-- primary key of "paths", which the stores write inside the same
|
|
100
|
+
-- operation, and both adapters pre-check siblings per locale in code.
|
|
104
101
|
create index if not exists "entries_collection_sort_idx"
|
|
105
102
|
on "entries" ("space_id", "collection", "sort");
|
|
106
103
|
|
|
107
104
|
create index if not exists "entries_parent_idx"
|
|
108
105
|
on "entries" ("space_id", "collection", "parent_id");
|
|
109
106
|
|
|
107
|
+
create table if not exists "entry_locales" (
|
|
108
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
109
|
+
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
110
|
+
"locale" text not null,
|
|
111
|
+
"slug" text,
|
|
112
|
+
"status" text not null default 'draft',
|
|
113
|
+
"fields" jsonb not null default '{}'::jsonb,
|
|
114
|
+
"draft_version_id" uuid,
|
|
115
|
+
"published_version_id" uuid,
|
|
116
|
+
"updated_at" timestamptz not null default now(),
|
|
117
|
+
primary key ("entry_id", "locale")
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
create index if not exists "entry_locales_status_idx"
|
|
121
|
+
on "entry_locales" ("space_id", "locale", "status");
|
|
122
|
+
|
|
123
|
+
-- A version belongs to ONE locale of one entry and stores the MERGED
|
|
124
|
+
-- fields (shared + localized + slug) as they were at that save, so
|
|
125
|
+
-- restoring an old Finnish version restores the shared fields too.
|
|
126
|
+
-- Rows exist only for collections registered with `versions: true`.
|
|
127
|
+
create table if not exists "entry_versions" (
|
|
128
|
+
"id" uuid primary key default gen_random_uuid(),
|
|
129
|
+
"space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
|
|
130
|
+
"entry_id" uuid not null references "entries" ("id") on delete cascade,
|
|
131
|
+
"locale" text not null,
|
|
132
|
+
"fields" jsonb not null,
|
|
133
|
+
"created_by" text,
|
|
134
|
+
"created_at" timestamptz not null default now()
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
create index if not exists "entry_versions_entry_locale_idx"
|
|
138
|
+
on "entry_versions" ("entry_id", "locale", "created_at");
|
|
139
|
+
|
|
110
140
|
-- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
|
|
111
141
|
-- statement. Deliberately does not touch updated_at — a drag must not
|
|
112
142
|
-- make the whole collection look freshly edited. Scoped to the current
|
|
@@ -172,10 +202,13 @@ $$;
|
|
|
172
202
|
-- (collectionSQL) run with security_invoker = true, so they are subject
|
|
173
203
|
-- to this same smoodly_read policy rather than their owner's privileges.
|
|
174
204
|
alter table "pages" enable row level security;
|
|
205
|
+
alter table "page_locales" enable row level security;
|
|
175
206
|
alter table "page_versions" enable row level security;
|
|
176
207
|
alter table "saved_sections" enable row level security;
|
|
177
208
|
alter table "global_sections" enable row level security;
|
|
178
209
|
alter table "entries" enable row level security;
|
|
210
|
+
alter table "entry_locales" enable row level security;
|
|
211
|
+
alter table "entry_versions" enable row level security;
|
|
179
212
|
alter table "refs" enable row level security;
|
|
180
213
|
alter table "paths" enable row level security;
|
|
181
214
|
|
|
@@ -272,8 +305,14 @@ begin
|
|
|
272
305
|
end;
|
|
273
306
|
$$;
|
|
274
307
|
|
|
308
|
+
create index if not exists "page_locales_space_idx" on "page_locales" ("space_id");
|
|
309
|
+
|
|
275
310
|
create index if not exists "page_versions_space_idx" on "page_versions" ("space_id");
|
|
276
311
|
|
|
312
|
+
create index if not exists "entry_locales_space_idx" on "entry_locales" ("space_id");
|
|
313
|
+
|
|
314
|
+
create index if not exists "entry_versions_space_idx" on "entry_versions" ("space_id");
|
|
315
|
+
|
|
277
316
|
create index if not exists "saved_sections_space_idx" on "saved_sections" ("space_id");
|
|
278
317
|
|
|
279
318
|
create index if not exists "global_sections_space_idx" on "global_sections" ("space_id");
|
|
@@ -284,6 +323,10 @@ drop trigger if exists "smoodly_a_set_space" on "pages";
|
|
|
284
323
|
create trigger "smoodly_a_set_space" before insert on "pages"
|
|
285
324
|
for each row execute function "smoodly_set_space"();
|
|
286
325
|
|
|
326
|
+
drop trigger if exists "smoodly_a_set_space" on "page_locales";
|
|
327
|
+
create trigger "smoodly_a_set_space" before insert on "page_locales"
|
|
328
|
+
for each row execute function "smoodly_set_space"();
|
|
329
|
+
|
|
287
330
|
drop trigger if exists "smoodly_a_set_space" on "page_versions";
|
|
288
331
|
create trigger "smoodly_a_set_space" before insert on "page_versions"
|
|
289
332
|
for each row execute function "smoodly_set_space"();
|
|
@@ -300,6 +343,14 @@ drop trigger if exists "smoodly_a_set_space" on "entries";
|
|
|
300
343
|
create trigger "smoodly_a_set_space" before insert on "entries"
|
|
301
344
|
for each row execute function "smoodly_set_space"();
|
|
302
345
|
|
|
346
|
+
drop trigger if exists "smoodly_a_set_space" on "entry_locales";
|
|
347
|
+
create trigger "smoodly_a_set_space" before insert on "entry_locales"
|
|
348
|
+
for each row execute function "smoodly_set_space"();
|
|
349
|
+
|
|
350
|
+
drop trigger if exists "smoodly_a_set_space" on "entry_versions";
|
|
351
|
+
create trigger "smoodly_a_set_space" before insert on "entry_versions"
|
|
352
|
+
for each row execute function "smoodly_set_space"();
|
|
353
|
+
|
|
303
354
|
drop trigger if exists "smoodly_a_set_space" on "refs";
|
|
304
355
|
create trigger "smoodly_a_set_space" before insert on "refs"
|
|
305
356
|
for each row execute function "smoodly_set_space"();
|
|
@@ -316,6 +367,10 @@ drop trigger if exists "smoodly_b_row_cap" on "pages";
|
|
|
316
367
|
create trigger "smoodly_b_row_cap" before insert on "pages"
|
|
317
368
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
318
369
|
|
|
370
|
+
drop trigger if exists "smoodly_b_row_cap" on "page_locales";
|
|
371
|
+
create trigger "smoodly_b_row_cap" before insert on "page_locales"
|
|
372
|
+
for each row execute function "smoodly_enforce_row_cap"();
|
|
373
|
+
|
|
319
374
|
drop trigger if exists "smoodly_b_row_cap" on "page_versions";
|
|
320
375
|
create trigger "smoodly_b_row_cap" before insert on "page_versions"
|
|
321
376
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
@@ -332,6 +387,14 @@ drop trigger if exists "smoodly_b_row_cap" on "entries";
|
|
|
332
387
|
create trigger "smoodly_b_row_cap" before insert on "entries"
|
|
333
388
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
334
389
|
|
|
390
|
+
drop trigger if exists "smoodly_b_row_cap" on "entry_locales";
|
|
391
|
+
create trigger "smoodly_b_row_cap" before insert on "entry_locales"
|
|
392
|
+
for each row execute function "smoodly_enforce_row_cap"();
|
|
393
|
+
|
|
394
|
+
drop trigger if exists "smoodly_b_row_cap" on "entry_versions";
|
|
395
|
+
create trigger "smoodly_b_row_cap" before insert on "entry_versions"
|
|
396
|
+
for each row execute function "smoodly_enforce_row_cap"();
|
|
397
|
+
|
|
335
398
|
drop trigger if exists "smoodly_b_row_cap" on "refs";
|
|
336
399
|
create trigger "smoodly_b_row_cap" before insert on "refs"
|
|
337
400
|
for each row execute function "smoodly_enforce_row_cap"();
|
|
@@ -347,6 +410,15 @@ create policy "smoodly_write" on "pages" for all
|
|
|
347
410
|
|
|
348
411
|
drop policy if exists "smoodly_read" on "pages";
|
|
349
412
|
create policy "smoodly_read" on "pages" for select
|
|
413
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "page_locales" l where l."page_id" = "pages"."id" and l."status" = 'published'));
|
|
414
|
+
|
|
415
|
+
drop policy if exists "smoodly_write" on "page_locales";
|
|
416
|
+
create policy "smoodly_write" on "page_locales" for all
|
|
417
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
418
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
419
|
+
|
|
420
|
+
drop policy if exists "smoodly_read" on "page_locales";
|
|
421
|
+
create policy "smoodly_read" on "page_locales" for select
|
|
350
422
|
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
|
|
351
423
|
|
|
352
424
|
drop policy if exists "smoodly_write" on "page_versions";
|
|
@@ -356,7 +428,7 @@ create policy "smoodly_write" on "page_versions" for all
|
|
|
356
428
|
|
|
357
429
|
drop policy if exists "smoodly_read" on "page_versions";
|
|
358
430
|
create policy "smoodly_read" on "page_versions" for select
|
|
359
|
-
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "
|
|
431
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "page_locales" l where l."published_version_id" = "page_versions"."id"));
|
|
360
432
|
|
|
361
433
|
drop policy if exists "smoodly_write" on "saved_sections";
|
|
362
434
|
create policy "smoodly_write" on "saved_sections" for all
|
|
@@ -379,8 +451,26 @@ create policy "smoodly_write" on "entries" for all
|
|
|
379
451
|
|
|
380
452
|
drop policy if exists "smoodly_read" on "entries";
|
|
381
453
|
create policy "smoodly_read" on "entries" for select
|
|
454
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "entry_locales" l where l."entry_id" = "entries"."id" and l."status" = 'published'));
|
|
455
|
+
|
|
456
|
+
drop policy if exists "smoodly_write" on "entry_locales";
|
|
457
|
+
create policy "smoodly_write" on "entry_locales" for all
|
|
458
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
459
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
460
|
+
|
|
461
|
+
drop policy if exists "smoodly_read" on "entry_locales";
|
|
462
|
+
create policy "smoodly_read" on "entry_locales" for select
|
|
382
463
|
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and "status" = 'published');
|
|
383
464
|
|
|
465
|
+
drop policy if exists "smoodly_write" on "entry_versions";
|
|
466
|
+
create policy "smoodly_write" on "entry_versions" for all
|
|
467
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
468
|
+
with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
|
|
469
|
+
|
|
470
|
+
drop policy if exists "smoodly_read" on "entry_versions";
|
|
471
|
+
create policy "smoodly_read" on "entry_versions" for select
|
|
472
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and exists (select 1 from "entry_locales" l where l."published_version_id" = "entry_versions"."id"));
|
|
473
|
+
|
|
384
474
|
drop policy if exists "smoodly_write" on "refs";
|
|
385
475
|
create policy "smoodly_write" on "refs" for all
|
|
386
476
|
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
|
|
@@ -393,7 +483,7 @@ create policy "smoodly_write" on "paths" for all
|
|
|
393
483
|
|
|
394
484
|
drop policy if exists "smoodly_read" on "paths";
|
|
395
485
|
create policy "smoodly_read" on "paths" for select
|
|
396
|
-
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (("kind" = 'page' and exists (select 1 from "
|
|
486
|
+
using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (("kind" = 'page' and exists (select 1 from "page_locales" l where l."page_id" = "paths"."target_id" and l."locale" = "paths"."locale" and l."status" = 'published')) or ("kind" = 'entry' and exists (select 1 from "entry_locales" l where l."entry_id" = "paths"."target_id" and l."locale" = "paths"."locale" and l."status" = 'published'))));
|
|
397
487
|
|
|
398
488
|
drop policy if exists "smoodly_write" on "editors";
|
|
399
489
|
create policy "smoodly_write" on "editors" for all
|
|
@@ -402,26 +492,30 @@ create policy "smoodly_write" on "editors" for all
|
|
|
402
492
|
|
|
403
493
|
create or replace view "people" with (security_invoker = true) as
|
|
404
494
|
select
|
|
405
|
-
"id",
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"
|
|
410
|
-
"
|
|
411
|
-
"fields"->>'
|
|
412
|
-
|
|
413
|
-
|
|
495
|
+
e."id",
|
|
496
|
+
l."locale",
|
|
497
|
+
l."slug",
|
|
498
|
+
e."sort",
|
|
499
|
+
e."created_at",
|
|
500
|
+
l."updated_at",
|
|
501
|
+
e."fields"->>'name' as "name",
|
|
502
|
+
l."fields"->>'role' as "role"
|
|
503
|
+
from "entries" e
|
|
504
|
+
join "entry_locales" l on l."entry_id" = e."id"
|
|
505
|
+
where e."collection" = 'people' and l."status" = 'published';
|
|
414
506
|
|
|
415
507
|
create or replace view "articles" with (security_invoker = true) as
|
|
416
508
|
select
|
|
417
|
-
"id",
|
|
418
|
-
"
|
|
419
|
-
"
|
|
420
|
-
"
|
|
421
|
-
"
|
|
422
|
-
"
|
|
423
|
-
"fields"->>'
|
|
424
|
-
"fields"->>'excerpt' as "excerpt",
|
|
425
|
-
"fields"->>'author' as "author"
|
|
426
|
-
from "entries"
|
|
427
|
-
|
|
509
|
+
e."id",
|
|
510
|
+
l."locale",
|
|
511
|
+
l."slug",
|
|
512
|
+
e."sort",
|
|
513
|
+
e."created_at",
|
|
514
|
+
l."updated_at",
|
|
515
|
+
v."fields"->>'title' as "title",
|
|
516
|
+
v."fields"->>'excerpt' as "excerpt",
|
|
517
|
+
v."fields"->>'author' as "author"
|
|
518
|
+
from "entries" e
|
|
519
|
+
join "entry_locales" l on l."entry_id" = e."id"
|
|
520
|
+
join "entry_versions" v on v."id" = l."published_version_id"
|
|
521
|
+
where e."collection" = 'articles' and l."status" = 'published';
|