create-smoodly-app 0.0.15 → 0.0.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-smoodly-app",
3
- "version": "0.0.15",
3
+ "version": "0.0.16",
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": {
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.15"
17
+ "smoodly": "0.0.16"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -4,7 +4,7 @@ import { text } from "./toolsets";
4
4
  // A collection: structured content with its own admin list, served at
5
5
  // `path` by its entry page (components/pages/Post.tsx). Fields marked
6
6
  // .localized() are stored per locale; the rest are shared across
7
- // locales. `versions: true` keeps a history per locale. To add a
7
+ // locales. To add a
8
8
  // collection: copy this block, add it to `collections`, write its view
9
9
  // into a new migration with `collectionSQL` from "smoodly", and apply
10
10
  // it (supabase db push, or supabase db reset locally).
@@ -14,7 +14,6 @@ export const posts = smoodly.collection({
14
14
  titleField: "title",
15
15
  path: "posts",
16
16
  order: { by: "date", direction: "desc" },
17
- versions: true,
18
17
  fields: {
19
18
  title: f.text().localized(),
20
19
  slug: f.slug({ from: "title" }),
@@ -37,6 +37,7 @@ create table if not exists "page_locales" (
37
37
  "draft_version_id" uuid,
38
38
  "published_version_id" uuid,
39
39
  "updated_at" timestamptz not null default now(),
40
+ "updated_by" text,
40
41
  primary key ("page_id", "locale")
41
42
  );
42
43
 
@@ -49,19 +50,20 @@ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void la
49
50
  $$;
50
51
 
51
52
  -- A version belongs to ONE locale of one page: each locale has its own
52
- -- draft, its own publish and its own history.
53
+ -- draft, its own publish and its own history. One row per publish plus
54
+ -- the live draft (spec 2026-09-13 history §1): the unpublished draft row
55
+ -- is rewritten in place and saved_at is when its content last changed.
53
56
  create table if not exists "page_versions" (
54
57
  "id" uuid primary key default gen_random_uuid(),
55
58
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
56
59
  "page_id" uuid not null references "pages" ("id") on delete cascade,
57
60
  "locale" text not null,
58
61
  "tree" jsonb not null,
59
- "created_by" text,
60
- "created_at" timestamptz not null default now()
62
+ "saved_at" timestamptz not null default now()
61
63
  );
62
64
 
63
65
  create index if not exists "page_versions_page_locale_idx"
64
- on "page_versions" ("page_id", "locale", "created_at");
66
+ on "page_versions" ("page_id", "locale", "saved_at");
65
67
 
66
68
  create table if not exists "entries" (
67
69
  "id" uuid primary key default gen_random_uuid(),
@@ -98,6 +100,7 @@ create table if not exists "entry_locales" (
98
100
  "draft_version_id" uuid,
99
101
  "published_version_id" uuid,
100
102
  "updated_at" timestamptz not null default now(),
103
+ "updated_by" text,
101
104
  primary key ("entry_id", "locale")
102
105
  );
103
106
 
@@ -107,19 +110,19 @@ create index if not exists "entry_locales_status_idx"
107
110
  -- A version belongs to ONE locale of one entry and stores the MERGED
108
111
  -- fields (shared + localized + slug) as they were at that save, so
109
112
  -- restoring an old Finnish version restores the shared fields too.
110
- -- Rows exist only for collections registered with `versions: true`.
113
+ -- Rows for every collection and every shared item (spec 2026-09-14 §1).
114
+ -- One row per publish plus the live draft (spec 2026-09-13 history §1).
111
115
  create table if not exists "entry_versions" (
112
116
  "id" uuid primary key default gen_random_uuid(),
113
117
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
114
118
  "entry_id" uuid not null references "entries" ("id") on delete cascade,
115
119
  "locale" text not null,
116
120
  "fields" jsonb not null,
117
- "created_by" text,
118
- "created_at" timestamptz not null default now()
121
+ "saved_at" timestamptz not null default now()
119
122
  );
120
123
 
121
124
  create index if not exists "entry_versions_entry_locale_idx"
122
- on "entry_versions" ("entry_id", "locale", "created_at");
125
+ on "entry_versions" ("entry_id", "locale", "saved_at");
123
126
 
124
127
  -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
125
128
  -- statement. Deliberately does not touch updated_at — a drag must not
@@ -506,12 +509,12 @@ select
506
509
  e."sort",
507
510
  e."created_at",
508
511
  l."updated_at",
509
- v."fields"->>'title' as "title",
510
- (v."fields"->>'date')::timestamptz as "date",
511
- v."fields"->'body' as "body"
512
+ coalesce(v."fields", e."fields" || l."fields")->>'title' as "title",
513
+ (coalesce(v."fields", e."fields" || l."fields")->>'date')::timestamptz as "date",
514
+ coalesce(v."fields", e."fields" || l."fields")->'body' as "body"
512
515
  from "entries" e
513
516
  join "entry_locales" l on l."entry_id" = e."id"
514
- join "entry_versions" v on v."id" = l."published_version_id"
517
+ left join "entry_versions" v on v."id" = l."published_version_id"
515
518
  where e."collection" = 'posts' and l."status" = 'published';
516
519
 
517
520
  create index if not exists "entries_posts_date_idx"
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.15",
17
+ "smoodly": "0.0.16",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -4,7 +4,7 @@ import { text } from "./toolsets";
4
4
  // A collection: structured content with its own admin list, served at
5
5
  // `path` by its entry page (components/pages/Post.tsx). Fields marked
6
6
  // .localized() are stored per locale; the rest are shared across
7
- // locales. `versions: true` keeps a history per locale. To add a
7
+ // locales. To add a
8
8
  // collection: copy this block, add it to `collections`, write its view
9
9
  // into a new migration with `collectionSQL` from "smoodly", and apply
10
10
  // it (supabase db push, or supabase db reset locally).
@@ -14,7 +14,6 @@ export const posts = smoodly.collection({
14
14
  titleField: "title",
15
15
  path: "posts",
16
16
  order: { by: "date", direction: "desc" },
17
- versions: true,
18
17
  fields: {
19
18
  title: f.text().localized(),
20
19
  slug: f.slug({ from: "title" }),
@@ -37,6 +37,7 @@ create table if not exists "page_locales" (
37
37
  "draft_version_id" uuid,
38
38
  "published_version_id" uuid,
39
39
  "updated_at" timestamptz not null default now(),
40
+ "updated_by" text,
40
41
  primary key ("page_id", "locale")
41
42
  );
42
43
 
@@ -49,19 +50,20 @@ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void la
49
50
  $$;
50
51
 
51
52
  -- A version belongs to ONE locale of one page: each locale has its own
52
- -- draft, its own publish and its own history.
53
+ -- draft, its own publish and its own history. One row per publish plus
54
+ -- the live draft (spec 2026-09-13 history §1): the unpublished draft row
55
+ -- is rewritten in place and saved_at is when its content last changed.
53
56
  create table if not exists "page_versions" (
54
57
  "id" uuid primary key default gen_random_uuid(),
55
58
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
56
59
  "page_id" uuid not null references "pages" ("id") on delete cascade,
57
60
  "locale" text not null,
58
61
  "tree" jsonb not null,
59
- "created_by" text,
60
- "created_at" timestamptz not null default now()
62
+ "saved_at" timestamptz not null default now()
61
63
  );
62
64
 
63
65
  create index if not exists "page_versions_page_locale_idx"
64
- on "page_versions" ("page_id", "locale", "created_at");
66
+ on "page_versions" ("page_id", "locale", "saved_at");
65
67
 
66
68
  create table if not exists "entries" (
67
69
  "id" uuid primary key default gen_random_uuid(),
@@ -98,6 +100,7 @@ create table if not exists "entry_locales" (
98
100
  "draft_version_id" uuid,
99
101
  "published_version_id" uuid,
100
102
  "updated_at" timestamptz not null default now(),
103
+ "updated_by" text,
101
104
  primary key ("entry_id", "locale")
102
105
  );
103
106
 
@@ -107,19 +110,19 @@ create index if not exists "entry_locales_status_idx"
107
110
  -- A version belongs to ONE locale of one entry and stores the MERGED
108
111
  -- fields (shared + localized + slug) as they were at that save, so
109
112
  -- restoring an old Finnish version restores the shared fields too.
110
- -- Rows exist only for collections registered with `versions: true`.
113
+ -- Rows for every collection and every shared item (spec 2026-09-14 §1).
114
+ -- One row per publish plus the live draft (spec 2026-09-13 history §1).
111
115
  create table if not exists "entry_versions" (
112
116
  "id" uuid primary key default gen_random_uuid(),
113
117
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
114
118
  "entry_id" uuid not null references "entries" ("id") on delete cascade,
115
119
  "locale" text not null,
116
120
  "fields" jsonb not null,
117
- "created_by" text,
118
- "created_at" timestamptz not null default now()
121
+ "saved_at" timestamptz not null default now()
119
122
  );
120
123
 
121
124
  create index if not exists "entry_versions_entry_locale_idx"
122
- on "entry_versions" ("entry_id", "locale", "created_at");
125
+ on "entry_versions" ("entry_id", "locale", "saved_at");
123
126
 
124
127
  -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
125
128
  -- statement. Deliberately does not touch updated_at — a drag must not
@@ -506,12 +509,12 @@ select
506
509
  e."sort",
507
510
  e."created_at",
508
511
  l."updated_at",
509
- v."fields"->>'title' as "title",
510
- (v."fields"->>'date')::timestamptz as "date",
511
- v."fields"->'body' as "body"
512
+ coalesce(v."fields", e."fields" || l."fields")->>'title' as "title",
513
+ (coalesce(v."fields", e."fields" || l."fields")->>'date')::timestamptz as "date",
514
+ coalesce(v."fields", e."fields" || l."fields")->'body' as "body"
512
515
  from "entries" e
513
516
  join "entry_locales" l on l."entry_id" = e."id"
514
- join "entry_versions" v on v."id" = l."published_version_id"
517
+ left join "entry_versions" v on v."id" = l."published_version_id"
515
518
  where e."collection" = 'posts' and l."status" = 'published';
516
519
 
517
520
  create index if not exists "entries_posts_date_idx"
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.15"
17
+ "smoodly": "0.0.16"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@next/env": "^16.3.4",
@@ -156,7 +156,7 @@ const body = doc(
156
156
  cta("Ready to talk?", "Sed ut perspiciatis unde omnis iste natus error sit voluptatem.", "Get in touch", paths(EN).about),
157
157
  );
158
158
 
159
- // Versioned (`versions: true` on the collection): every save below is a
159
+ // Every collection is versioned: every save below is a
160
160
  // snapshot. Title and body are Latin in both locales; tags and slug are
161
161
  // per locale, so the Finnish row reads Finnish where a reader would notice.
162
162
  const article = await seedEntry("articles", {
@@ -15,11 +15,8 @@ export const people = smoodly.collection({
15
15
  },
16
16
  });
17
17
 
18
- // Articles keep a version history per locale (`versions: true`): every
19
- // save is a snapshot, publish moves a pointer, and the published site
20
- // keeps serving the published snapshot while a draft moves on. The path
21
- // is per locale — /posts/<slug> and /fi/artikkelit/<slug> — and the
22
- // Articles fixed page (components/pages/Articles.tsx) claims the same
18
+ // Articles are served at a path that is per locale — /posts/<slug> and
19
+ // /fi/artikkelit/<slug> — and the Articles fixed page (components/pages/Articles.tsx) claims the same
23
20
  // segments, which makes it the collection's index page. Tags are a list
24
21
  // localized as a whole, so the Finnish row carries Finnish tags.
25
22
  export const articles = smoodly.collection({
@@ -28,7 +25,6 @@ export const articles = smoodly.collection({
28
25
  titleField: "title",
29
26
  path: "posts",
30
27
  order: { by: "date", direction: "desc" },
31
- versions: true,
32
28
  fields: {
33
29
  title: f.text().localized(),
34
30
  image: f.image({ aspect: "3:2" }),
@@ -9,7 +9,7 @@ import { Quote } from "@/components/sections/Quote";
9
9
 
10
10
  export const header = smoodly.shared({ name: "header", title: "Header", section: Header });
11
11
 
12
- export const footer = smoodly.shared({ name: "footer", title: "Footer", section: Footer, versions: true });
12
+ export const footer = smoodly.shared({ name: "footer", title: "Footer", section: Footer });
13
13
 
14
14
  export const contact = smoodly.shared({
15
15
  name: "contact",
@@ -37,6 +37,7 @@ create table if not exists "page_locales" (
37
37
  "draft_version_id" uuid,
38
38
  "published_version_id" uuid,
39
39
  "updated_at" timestamptz not null default now(),
40
+ "updated_by" text,
40
41
  primary key ("page_id", "locale")
41
42
  );
42
43
 
@@ -49,19 +50,20 @@ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void la
49
50
  $$;
50
51
 
51
52
  -- A version belongs to ONE locale of one page: each locale has its own
52
- -- draft, its own publish and its own history.
53
+ -- draft, its own publish and its own history. One row per publish plus
54
+ -- the live draft (spec 2026-09-13 history §1): the unpublished draft row
55
+ -- is rewritten in place and saved_at is when its content last changed.
53
56
  create table if not exists "page_versions" (
54
57
  "id" uuid primary key default gen_random_uuid(),
55
58
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
56
59
  "page_id" uuid not null references "pages" ("id") on delete cascade,
57
60
  "locale" text not null,
58
61
  "tree" jsonb not null,
59
- "created_by" text,
60
- "created_at" timestamptz not null default now()
62
+ "saved_at" timestamptz not null default now()
61
63
  );
62
64
 
63
65
  create index if not exists "page_versions_page_locale_idx"
64
- on "page_versions" ("page_id", "locale", "created_at");
66
+ on "page_versions" ("page_id", "locale", "saved_at");
65
67
 
66
68
  create table if not exists "entries" (
67
69
  "id" uuid primary key default gen_random_uuid(),
@@ -98,6 +100,7 @@ create table if not exists "entry_locales" (
98
100
  "draft_version_id" uuid,
99
101
  "published_version_id" uuid,
100
102
  "updated_at" timestamptz not null default now(),
103
+ "updated_by" text,
101
104
  primary key ("entry_id", "locale")
102
105
  );
103
106
 
@@ -107,19 +110,19 @@ create index if not exists "entry_locales_status_idx"
107
110
  -- A version belongs to ONE locale of one entry and stores the MERGED
108
111
  -- fields (shared + localized + slug) as they were at that save, so
109
112
  -- restoring an old Finnish version restores the shared fields too.
110
- -- Rows exist only for collections registered with `versions: true`.
113
+ -- Rows for every collection and every shared item (spec 2026-09-14 §1).
114
+ -- One row per publish plus the live draft (spec 2026-09-13 history §1).
111
115
  create table if not exists "entry_versions" (
112
116
  "id" uuid primary key default gen_random_uuid(),
113
117
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
114
118
  "entry_id" uuid not null references "entries" ("id") on delete cascade,
115
119
  "locale" text not null,
116
120
  "fields" jsonb not null,
117
- "created_by" text,
118
- "created_at" timestamptz not null default now()
121
+ "saved_at" timestamptz not null default now()
119
122
  );
120
123
 
121
124
  create index if not exists "entry_versions_entry_locale_idx"
122
- on "entry_versions" ("entry_id", "locale", "created_at");
125
+ on "entry_versions" ("entry_id", "locale", "saved_at");
123
126
 
124
127
  -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
125
128
  -- statement. Deliberately does not touch updated_at — a drag must not
@@ -506,10 +509,11 @@ select
506
509
  e."sort",
507
510
  e."created_at",
508
511
  l."updated_at",
509
- e."fields"->>'name' as "name",
510
- l."fields"->>'role' as "role"
512
+ coalesce(v."fields", e."fields" || l."fields")->>'name' as "name",
513
+ coalesce(v."fields", e."fields" || l."fields")->>'role' as "role"
511
514
  from "entries" e
512
515
  join "entry_locales" l on l."entry_id" = e."id"
516
+ left join "entry_versions" v on v."id" = l."published_version_id"
513
517
  where e."collection" = 'people' and l."status" = 'published';
514
518
 
515
519
  create or replace view "articles" with (security_invoker = true) as
@@ -520,16 +524,16 @@ select
520
524
  e."sort",
521
525
  e."created_at",
522
526
  l."updated_at",
523
- v."fields"->>'title' as "title",
524
- v."fields"->'image' as "image",
525
- (v."fields"->>'date')::timestamptz as "date",
526
- v."fields"->>'author' as "author",
527
- v."fields"->>'excerpt' as "excerpt",
528
- v."fields"->'body' as "body",
529
- v."fields"->'tags' as "tags"
527
+ coalesce(v."fields", e."fields" || l."fields")->>'title' as "title",
528
+ coalesce(v."fields", e."fields" || l."fields")->'image' as "image",
529
+ (coalesce(v."fields", e."fields" || l."fields")->>'date')::timestamptz as "date",
530
+ coalesce(v."fields", e."fields" || l."fields")->>'author' as "author",
531
+ coalesce(v."fields", e."fields" || l."fields")->>'excerpt' as "excerpt",
532
+ coalesce(v."fields", e."fields" || l."fields")->'body' as "body",
533
+ coalesce(v."fields", e."fields" || l."fields")->'tags' as "tags"
530
534
  from "entries" e
531
535
  join "entry_locales" l on l."entry_id" = e."id"
532
- join "entry_versions" v on v."id" = l."published_version_id"
536
+ left join "entry_versions" v on v."id" = l."published_version_id"
533
537
  where e."collection" = 'articles' and l."status" = 'published';
534
538
 
535
539
  create index if not exists "entries_articles_date_idx"
@@ -14,7 +14,7 @@
14
14
  "next": "^16.3.4",
15
15
  "react": "^19",
16
16
  "react-dom": "^19",
17
- "smoodly": "0.0.15",
17
+ "smoodly": "0.0.16",
18
18
  "next-intl": "^4.14.2"
19
19
  },
20
20
  "devDependencies": {
@@ -156,7 +156,7 @@ const body = doc(
156
156
  cta("Ready to talk?", "Sed ut perspiciatis unde omnis iste natus error sit voluptatem.", "Get in touch", paths(EN).about),
157
157
  );
158
158
 
159
- // Versioned (`versions: true` on the collection): every save below is a
159
+ // Every collection is versioned: every save below is a
160
160
  // snapshot. Title and body are Latin in both locales; tags and slug are
161
161
  // per locale, so the Finnish row reads Finnish where a reader would notice.
162
162
  const article = await seedEntry("articles", {
@@ -15,11 +15,8 @@ export const people = smoodly.collection({
15
15
  },
16
16
  });
17
17
 
18
- // Articles keep a version history per locale (`versions: true`): every
19
- // save is a snapshot, publish moves a pointer, and the published site
20
- // keeps serving the published snapshot while a draft moves on. The path
21
- // is per locale — /posts/<slug> and /fi/artikkelit/<slug> — and the
22
- // Articles fixed page (components/pages/Articles.tsx) claims the same
18
+ // Articles are served at a path that is per locale — /posts/<slug> and
19
+ // /fi/artikkelit/<slug> — and the Articles fixed page (components/pages/Articles.tsx) claims the same
23
20
  // segments, which makes it the collection's index page. Tags are a list
24
21
  // localized as a whole, so the Finnish row carries Finnish tags.
25
22
  export const articles = smoodly.collection({
@@ -28,7 +25,6 @@ export const articles = smoodly.collection({
28
25
  titleField: "title",
29
26
  path: { en: "posts", fi: "artikkelit" },
30
27
  order: { by: "date", direction: "desc" },
31
- versions: true,
32
28
  fields: {
33
29
  title: f.text().localized(),
34
30
  image: f.image({ aspect: "3:2" }),
@@ -9,7 +9,7 @@ import { Quote } from "@/components/sections/Quote";
9
9
 
10
10
  export const header = smoodly.shared({ name: "header", title: "Header", section: Header });
11
11
 
12
- export const footer = smoodly.shared({ name: "footer", title: "Footer", section: Footer, versions: true });
12
+ export const footer = smoodly.shared({ name: "footer", title: "Footer", section: Footer });
13
13
 
14
14
  export const contact = smoodly.shared({
15
15
  name: "contact",
@@ -37,6 +37,7 @@ create table if not exists "page_locales" (
37
37
  "draft_version_id" uuid,
38
38
  "published_version_id" uuid,
39
39
  "updated_at" timestamptz not null default now(),
40
+ "updated_by" text,
40
41
  primary key ("page_id", "locale")
41
42
  );
42
43
 
@@ -49,19 +50,20 @@ create or replace function "smoodly_reorder_pages"(p_ids uuid[]) returns void la
49
50
  $$;
50
51
 
51
52
  -- A version belongs to ONE locale of one page: each locale has its own
52
- -- draft, its own publish and its own history.
53
+ -- draft, its own publish and its own history. One row per publish plus
54
+ -- the live draft (spec 2026-09-13 history §1): the unpublished draft row
55
+ -- is rewritten in place and saved_at is when its content last changed.
53
56
  create table if not exists "page_versions" (
54
57
  "id" uuid primary key default gen_random_uuid(),
55
58
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
56
59
  "page_id" uuid not null references "pages" ("id") on delete cascade,
57
60
  "locale" text not null,
58
61
  "tree" jsonb not null,
59
- "created_by" text,
60
- "created_at" timestamptz not null default now()
62
+ "saved_at" timestamptz not null default now()
61
63
  );
62
64
 
63
65
  create index if not exists "page_versions_page_locale_idx"
64
- on "page_versions" ("page_id", "locale", "created_at");
66
+ on "page_versions" ("page_id", "locale", "saved_at");
65
67
 
66
68
  create table if not exists "entries" (
67
69
  "id" uuid primary key default gen_random_uuid(),
@@ -98,6 +100,7 @@ create table if not exists "entry_locales" (
98
100
  "draft_version_id" uuid,
99
101
  "published_version_id" uuid,
100
102
  "updated_at" timestamptz not null default now(),
103
+ "updated_by" text,
101
104
  primary key ("entry_id", "locale")
102
105
  );
103
106
 
@@ -107,19 +110,19 @@ create index if not exists "entry_locales_status_idx"
107
110
  -- A version belongs to ONE locale of one entry and stores the MERGED
108
111
  -- fields (shared + localized + slug) as they were at that save, so
109
112
  -- restoring an old Finnish version restores the shared fields too.
110
- -- Rows exist only for collections registered with `versions: true`.
113
+ -- Rows for every collection and every shared item (spec 2026-09-14 §1).
114
+ -- One row per publish plus the live draft (spec 2026-09-13 history §1).
111
115
  create table if not exists "entry_versions" (
112
116
  "id" uuid primary key default gen_random_uuid(),
113
117
  "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
114
118
  "entry_id" uuid not null references "entries" ("id") on delete cascade,
115
119
  "locale" text not null,
116
120
  "fields" jsonb not null,
117
- "created_by" text,
118
- "created_at" timestamptz not null default now()
121
+ "saved_at" timestamptz not null default now()
119
122
  );
120
123
 
121
124
  create index if not exists "entry_versions_entry_locale_idx"
122
- on "entry_versions" ("entry_id", "locale", "created_at");
125
+ on "entry_versions" ("entry_id", "locale", "saved_at");
123
126
 
124
127
  -- v1 reorder (DESIGN.md §3 Manual ordering): rewrite sort 1..N in ONE
125
128
  -- statement. Deliberately does not touch updated_at — a drag must not
@@ -506,10 +509,11 @@ select
506
509
  e."sort",
507
510
  e."created_at",
508
511
  l."updated_at",
509
- e."fields"->>'name' as "name",
510
- l."fields"->>'role' as "role"
512
+ coalesce(v."fields", e."fields" || l."fields")->>'name' as "name",
513
+ coalesce(v."fields", e."fields" || l."fields")->>'role' as "role"
511
514
  from "entries" e
512
515
  join "entry_locales" l on l."entry_id" = e."id"
516
+ left join "entry_versions" v on v."id" = l."published_version_id"
513
517
  where e."collection" = 'people' and l."status" = 'published';
514
518
 
515
519
  create or replace view "articles" with (security_invoker = true) as
@@ -520,16 +524,16 @@ select
520
524
  e."sort",
521
525
  e."created_at",
522
526
  l."updated_at",
523
- v."fields"->>'title' as "title",
524
- v."fields"->'image' as "image",
525
- (v."fields"->>'date')::timestamptz as "date",
526
- v."fields"->>'author' as "author",
527
- v."fields"->>'excerpt' as "excerpt",
528
- v."fields"->'body' as "body",
529
- v."fields"->'tags' as "tags"
527
+ coalesce(v."fields", e."fields" || l."fields")->>'title' as "title",
528
+ coalesce(v."fields", e."fields" || l."fields")->'image' as "image",
529
+ (coalesce(v."fields", e."fields" || l."fields")->>'date')::timestamptz as "date",
530
+ coalesce(v."fields", e."fields" || l."fields")->>'author' as "author",
531
+ coalesce(v."fields", e."fields" || l."fields")->>'excerpt' as "excerpt",
532
+ coalesce(v."fields", e."fields" || l."fields")->'body' as "body",
533
+ coalesce(v."fields", e."fields" || l."fields")->'tags' as "tags"
530
534
  from "entries" e
531
535
  join "entry_locales" l on l."entry_id" = e."id"
532
- join "entry_versions" v on v."id" = l."published_version_id"
536
+ left join "entry_versions" v on v."id" = l."published_version_id"
533
537
  where e."collection" = 'articles' and l."status" = 'published';
534
538
 
535
539
  create index if not exists "entries_articles_date_idx"