@nextblock-cms/db 0.16.1 → 0.16.3

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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/supabase/migrations/00000000000003_baseline_seed.sql +468 -468
  3. package/supabase/migrations/00000000000006_home_live_demo_promo.sql +67 -67
  4. package/supabase/migrations/00000000000007_home_live_demo_promo_copy_fix.sql +49 -49
  5. package/supabase/migrations/00000000000009_home_live_demo_promo_contrast.sql +52 -52
  6. package/supabase/migrations/00000000000014_site_themes.sql +254 -254
  7. package/supabase/migrations/00000000000015_scheduled_publishing.sql +43 -43
  8. package/supabase/migrations/00000000000016_product_revisions_and_revision_baseline.sql +310 -310
  9. package/supabase/migrations/00000000000017_cortex_ai_mcp_server.sql +91 -91
  10. package/supabase/migrations/00000000000018_site_scripts.sql +96 -96
  11. package/supabase/migrations/00000000000019_site_script_revisions.sql +85 -85
  12. package/supabase/migrations/00000000000020_updating_article.sql +162 -162
  13. package/supabase/migrations/00000000000021_updating_article_git_merge.sql +91 -91
  14. package/supabase/migrations/00000000000022_updating_article_accuracy.sql +94 -94
  15. package/supabase/migrations/00000000000023_updating_article_layout_not_host.sql +87 -87
  16. package/supabase/migrations/00000000000024_updating_article_newline_fix.sql +70 -70
  17. package/supabase/migrations/00000000000025_rebrand_nextblock_dev.sql +53 -53
  18. package/supabase/migrations/00000000000026_product_inquiries.sql +118 -118
  19. package/supabase/migrations/00000000000027_message_threads.sql +391 -391
  20. package/supabase/migrations/00000000000028_interaction_replies.sql +86 -86
  21. package/supabase/migrations/00000000000029_form_endpoints_default_empty.sql +52 -52
  22. package/supabase/migrations/00000000000030_seo_redirects_and_robots.sql +149 -149
  23. package/supabase/migrations/00000000000031_seo_robots_settings_admin_only.sql +35 -35
  24. package/supabase/migrations/00000000000032_seed_seo_score_optimizations.sql +1172 -0
  25. package/supabase/migrations/00000000000033_seed_seo_score_optimizations_p2.sql +353 -0
  26. package/supabase/migrations/00000000000034_seed_seo_french_legal_readability.sql +101 -0
@@ -1,310 +1,310 @@
1
- -- 00000000000016_product_revisions_and_revision_baseline.sql
2
- --
3
- -- Revision History, part 1 of 2 (schema). The application-side rewrite lives in
4
- -- apps/nextblock/app/cms/revisions/**.
5
- --
6
- -- Three things happen here:
7
- --
8
- -- 1. products.version — the monotonic counter the hybrid revision engine drives,
9
- -- mirroring pages.version / posts.version.
10
- --
11
- -- 2. product_revisions — a structural mirror of page_revisions / post_revisions.
12
- -- product_id is uuid (products.id is uuid, not bigint), and
13
- -- writes are gated on is_admin() to match products_*_policy
14
- -- rather than the ADMIN|WRITER pattern the page/post revision
15
- -- tables use. A WRITER who could insert a revision but not
16
- -- apply a restore would get a silent no-op restore, because
17
- -- PostgREST returns no error for an UPDATE matching zero rows.
18
- --
19
- -- 3. Revision baseline — every page, post and product gets a real `snapshot` row to
20
- -- restore to. Until now the CMS synthesised a fake "Initial
21
- -- Version" entry in the UI whose Restore button resolved to
22
- -- "current metadata + zero blocks" and wiped the content.
23
- -- There is now an actual stored baseline instead.
24
- --
25
- -- Case A (version = 1, no revisions at all): the live row IS
26
- -- version 1. This covers seeded content — 00000000000003
27
- -- inserts every page and post at version 1 and writes no
28
- -- revision rows — and everything authored since the CMS save
29
- -- path stopped recording revisions. Snapshotting it at
30
- -- version 1 is what makes "restore the original seeded page"
31
- -- real for the first time.
32
- --
33
- -- Case B (version > 1 but no snapshot at or below it): the
34
- -- true v1 is unrecoverable and is NOT fabricated. A snapshot
35
- -- of the current state is stored at the current version so the
36
- -- diff chain has a valid base and future restores resolve.
37
- --
38
- -- Forward-only, idempotent, and it modifies no existing row: every backfill is an
39
- -- INSERT ... WHERE NOT EXISTS ... ON CONFLICT DO NOTHING.
40
-
41
- -- ---------------------------------------------------------------------------
42
- -- 1. products.version
43
- -- ---------------------------------------------------------------------------
44
-
45
- ALTER TABLE public.products
46
- ADD COLUMN IF NOT EXISTS version integer DEFAULT 1 NOT NULL;
47
-
48
- COMMENT ON COLUMN public.products.version IS 'Monotonic version number for hybrid revisions.';
49
-
50
- -- ---------------------------------------------------------------------------
51
- -- 2. product_revisions
52
- -- ---------------------------------------------------------------------------
53
-
54
- CREATE TABLE IF NOT EXISTS public.product_revisions (
55
- id bigint NOT NULL,
56
- product_id uuid NOT NULL,
57
- author_id uuid,
58
- version integer NOT NULL,
59
- revision_type public.revision_type NOT NULL,
60
- content jsonb NOT NULL,
61
- created_at timestamp with time zone DEFAULT now() NOT NULL
62
- );
63
-
64
- COMMENT ON TABLE public.product_revisions IS 'Hybrid (snapshot/diff) revisions for products.';
65
- COMMENT ON COLUMN public.product_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
66
-
67
- DO $rb$
68
- BEGIN
69
- IF NOT EXISTS (
70
- SELECT 1 FROM pg_attribute
71
- WHERE attrelid = 'public.product_revisions'::regclass
72
- AND attname = 'id'
73
- AND attidentity <> ''
74
- ) THEN
75
- ALTER TABLE public.product_revisions
76
- ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
77
- SEQUENCE NAME public.product_revisions_id_seq
78
- START WITH 1
79
- INCREMENT BY 1
80
- NO MINVALUE
81
- NO MAXVALUE
82
- CACHE 1
83
- );
84
- END IF;
85
- END $rb$;
86
-
87
- DO $rb$
88
- BEGIN
89
- IF NOT EXISTS (SELECT 1 FROM pg_constraint
90
- WHERE conname = 'product_revisions_pkey'
91
- AND conrelid = 'public.product_revisions'::regclass) THEN
92
- ALTER TABLE ONLY public.product_revisions
93
- ADD CONSTRAINT product_revisions_pkey PRIMARY KEY (id);
94
- END IF;
95
- END $rb$;
96
-
97
- DO $rb$
98
- BEGIN
99
- IF NOT EXISTS (SELECT 1 FROM pg_constraint
100
- WHERE conname = 'product_revisions_product_version_key'
101
- AND conrelid = 'public.product_revisions'::regclass) THEN
102
- ALTER TABLE ONLY public.product_revisions
103
- ADD CONSTRAINT product_revisions_product_version_key UNIQUE (product_id, version);
104
- END IF;
105
- END $rb$;
106
-
107
- DO $rb$
108
- BEGIN
109
- IF NOT EXISTS (SELECT 1 FROM pg_constraint
110
- WHERE conname = 'product_revisions_author_id_fkey'
111
- AND conrelid = 'public.product_revisions'::regclass) THEN
112
- ALTER TABLE ONLY public.product_revisions
113
- ADD CONSTRAINT product_revisions_author_id_fkey
114
- FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;
115
- END IF;
116
- END $rb$;
117
-
118
- DO $rb$
119
- BEGIN
120
- IF NOT EXISTS (SELECT 1 FROM pg_constraint
121
- WHERE conname = 'product_revisions_product_id_fkey'
122
- AND conrelid = 'public.product_revisions'::regclass) THEN
123
- ALTER TABLE ONLY public.product_revisions
124
- ADD CONSTRAINT product_revisions_product_id_fkey
125
- FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;
126
- END IF;
127
- END $rb$;
128
-
129
- CREATE INDEX IF NOT EXISTS idx_product_revisions_author_id
130
- ON public.product_revisions USING btree (author_id);
131
-
132
- CREATE INDEX IF NOT EXISTS idx_product_revisions_product_id_version
133
- ON public.product_revisions USING btree (product_id, version);
134
-
135
- ALTER TABLE public.product_revisions ENABLE ROW LEVEL SECURITY;
136
-
137
- DROP POLICY IF EXISTS product_revisions_read_policy ON public.product_revisions;
138
- CREATE POLICY product_revisions_read_policy ON public.product_revisions
139
- FOR SELECT TO authenticated USING (true);
140
-
141
- DROP POLICY IF EXISTS product_revisions_insert_policy ON public.product_revisions;
142
- CREATE POLICY product_revisions_insert_policy ON public.product_revisions
143
- FOR INSERT TO authenticated
144
- WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));
145
-
146
- DROP POLICY IF EXISTS product_revisions_update_policy ON public.product_revisions;
147
- CREATE POLICY product_revisions_update_policy ON public.product_revisions
148
- FOR UPDATE TO authenticated
149
- USING (((SELECT public.is_admin() AS is_admin) IS TRUE))
150
- WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));
151
-
152
- DROP POLICY IF EXISTS product_revisions_delete_policy ON public.product_revisions;
153
- CREATE POLICY product_revisions_delete_policy ON public.product_revisions
154
- FOR DELETE TO authenticated
155
- USING (((SELECT public.is_admin() AS is_admin) IS TRUE));
156
-
157
- GRANT ALL ON TABLE public.product_revisions TO anon;
158
- GRANT ALL ON TABLE public.product_revisions TO authenticated;
159
- GRANT ALL ON TABLE public.product_revisions TO service_role;
160
- GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO anon;
161
- GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO authenticated;
162
- GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO service_role;
163
-
164
- -- ---------------------------------------------------------------------------
165
- -- 3. Revision baseline backfill
166
- --
167
- -- The JSON shape must match FullPageContent / FullPostContent / FullProductContent
168
- -- in apps/nextblock/app/cms/revisions/utils.ts exactly, or the first diff taken
169
- -- against a baseline row will be full of phantom operations.
170
- --
171
- -- Timestamps are rendered with an explicit millisecond-precision UTC format so they
172
- -- match JavaScript's Date#toISOString() ("2026-07-03T17:52:15.643Z"). Postgres'
173
- -- default jsonb rendering of timestamptz ("2026-07-03T17:52:15.643901+00:00") would
174
- -- differ from the value the application writes and produce a spurious diff on the
175
- -- very next save.
176
- -- ---------------------------------------------------------------------------
177
-
178
- -- 3a. Pages
179
- INSERT INTO public.page_revisions (page_id, author_id, version, revision_type, content)
180
- SELECT
181
- p.id,
182
- NULL::uuid,
183
- p.version,
184
- 'snapshot'::public.revision_type,
185
- jsonb_build_object(
186
- 'meta', jsonb_build_object(
187
- 'title', p.title,
188
- 'slug', p.slug,
189
- 'language_id', p.language_id,
190
- 'status', p.status,
191
- 'meta_title', p.meta_title,
192
- 'meta_description', p.meta_description,
193
- 'custom_canonical', p.custom_canonical,
194
- 'published_at', to_char(p.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
195
- 'feature_image_id', p.feature_image_id
196
- ),
197
- 'blocks', COALESCE((
198
- SELECT jsonb_agg(
199
- jsonb_build_object(
200
- 'language_id', b.language_id,
201
- 'block_type', b.block_type,
202
- 'content', b.content,
203
- 'order', b."order"
204
- ) ORDER BY b."order" ASC, b.id ASC
205
- )
206
- FROM public.blocks b
207
- WHERE b.page_id = p.id
208
- ), '[]'::jsonb)
209
- )
210
- FROM public.pages p
211
- WHERE NOT EXISTS (
212
- SELECT 1 FROM public.page_revisions r
213
- WHERE r.page_id = p.id
214
- AND r.revision_type = 'snapshot'
215
- AND r.version <= p.version
216
- )
217
- ON CONFLICT (page_id, version) DO NOTHING;
218
-
219
- -- 3b. Posts
220
- INSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)
221
- SELECT
222
- po.id,
223
- NULL::uuid,
224
- po.version,
225
- 'snapshot'::public.revision_type,
226
- jsonb_build_object(
227
- 'meta', jsonb_build_object(
228
- 'title', po.title,
229
- 'slug', po.slug,
230
- 'language_id', po.language_id,
231
- 'status', po.status,
232
- 'meta_title', po.meta_title,
233
- 'meta_description', po.meta_description,
234
- 'custom_canonical', po.custom_canonical,
235
- 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
236
- 'feature_image_id', po.feature_image_id,
237
- 'label', po.label,
238
- 'excerpt', po.excerpt,
239
- 'subtitle', po.subtitle
240
- ),
241
- 'blocks', COALESCE((
242
- SELECT jsonb_agg(
243
- jsonb_build_object(
244
- 'language_id', b.language_id,
245
- 'block_type', b.block_type,
246
- 'content', b.content,
247
- 'order', b."order"
248
- ) ORDER BY b."order" ASC, b.id ASC
249
- )
250
- FROM public.blocks b
251
- WHERE b.post_id = po.id
252
- ), '[]'::jsonb)
253
- )
254
- FROM public.posts po
255
- WHERE NOT EXISTS (
256
- SELECT 1 FROM public.post_revisions r
257
- WHERE r.post_id = po.id
258
- AND r.revision_type = 'snapshot'
259
- AND r.version <= po.version
260
- )
261
- ON CONFLICT (post_id, version) DO NOTHING;
262
-
263
- -- 3c. Products.
264
- --
265
- -- Content only. price/prices/sale_*/scheduled_*/stock/sku/average_rating/total_reviews
266
- -- are deliberately excluded from the snapshot: pricing and inventory are mutated from
267
- -- outside the editor (promotions, Freemius sync, order fulfilment), ratings are derived
268
- -- aggregates, and inventory_items is keyed by bare SKU text with no FK to products — so
269
- -- replaying commerce state on restore would reach rows the editor never touched.
270
- -- Restoring a product restores its content, not its commerce state.
271
- INSERT INTO public.product_revisions (product_id, author_id, version, revision_type, content)
272
- SELECT
273
- pr.id,
274
- NULL::uuid,
275
- pr.version,
276
- 'snapshot'::public.revision_type,
277
- jsonb_build_object(
278
- 'meta', jsonb_build_object(
279
- 'title', pr.title,
280
- 'slug', pr.slug,
281
- 'language_id', pr.language_id,
282
- 'status', pr.status,
283
- 'meta_title', pr.meta_title,
284
- 'meta_description', pr.meta_description,
285
- 'custom_canonical', pr.custom_canonical,
286
- 'published_at', to_char(pr.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
287
- 'short_description', pr.short_description,
288
- 'description_json', pr.description_json
289
- ),
290
- 'blocks', COALESCE((
291
- SELECT jsonb_agg(
292
- jsonb_build_object(
293
- 'language_id', b.language_id,
294
- 'block_type', b.block_type,
295
- 'content', b.content,
296
- 'order', b."order"
297
- ) ORDER BY b."order" ASC, b.id ASC
298
- )
299
- FROM public.blocks b
300
- WHERE b.product_id = pr.id
301
- ), '[]'::jsonb)
302
- )
303
- FROM public.products pr
304
- WHERE NOT EXISTS (
305
- SELECT 1 FROM public.product_revisions r
306
- WHERE r.product_id = pr.id
307
- AND r.revision_type = 'snapshot'
308
- AND r.version <= pr.version
309
- )
310
- ON CONFLICT (product_id, version) DO NOTHING;
1
+ -- 00000000000016_product_revisions_and_revision_baseline.sql
2
+ --
3
+ -- Revision History, part 1 of 2 (schema). The application-side rewrite lives in
4
+ -- apps/nextblock/app/cms/revisions/**.
5
+ --
6
+ -- Three things happen here:
7
+ --
8
+ -- 1. products.version — the monotonic counter the hybrid revision engine drives,
9
+ -- mirroring pages.version / posts.version.
10
+ --
11
+ -- 2. product_revisions — a structural mirror of page_revisions / post_revisions.
12
+ -- product_id is uuid (products.id is uuid, not bigint), and
13
+ -- writes are gated on is_admin() to match products_*_policy
14
+ -- rather than the ADMIN|WRITER pattern the page/post revision
15
+ -- tables use. A WRITER who could insert a revision but not
16
+ -- apply a restore would get a silent no-op restore, because
17
+ -- PostgREST returns no error for an UPDATE matching zero rows.
18
+ --
19
+ -- 3. Revision baseline — every page, post and product gets a real `snapshot` row to
20
+ -- restore to. Until now the CMS synthesised a fake "Initial
21
+ -- Version" entry in the UI whose Restore button resolved to
22
+ -- "current metadata + zero blocks" and wiped the content.
23
+ -- There is now an actual stored baseline instead.
24
+ --
25
+ -- Case A (version = 1, no revisions at all): the live row IS
26
+ -- version 1. This covers seeded content — 00000000000003
27
+ -- inserts every page and post at version 1 and writes no
28
+ -- revision rows — and everything authored since the CMS save
29
+ -- path stopped recording revisions. Snapshotting it at
30
+ -- version 1 is what makes "restore the original seeded page"
31
+ -- real for the first time.
32
+ --
33
+ -- Case B (version > 1 but no snapshot at or below it): the
34
+ -- true v1 is unrecoverable and is NOT fabricated. A snapshot
35
+ -- of the current state is stored at the current version so the
36
+ -- diff chain has a valid base and future restores resolve.
37
+ --
38
+ -- Forward-only, idempotent, and it modifies no existing row: every backfill is an
39
+ -- INSERT ... WHERE NOT EXISTS ... ON CONFLICT DO NOTHING.
40
+
41
+ -- ---------------------------------------------------------------------------
42
+ -- 1. products.version
43
+ -- ---------------------------------------------------------------------------
44
+
45
+ ALTER TABLE public.products
46
+ ADD COLUMN IF NOT EXISTS version integer DEFAULT 1 NOT NULL;
47
+
48
+ COMMENT ON COLUMN public.products.version IS 'Monotonic version number for hybrid revisions.';
49
+
50
+ -- ---------------------------------------------------------------------------
51
+ -- 2. product_revisions
52
+ -- ---------------------------------------------------------------------------
53
+
54
+ CREATE TABLE IF NOT EXISTS public.product_revisions (
55
+ id bigint NOT NULL,
56
+ product_id uuid NOT NULL,
57
+ author_id uuid,
58
+ version integer NOT NULL,
59
+ revision_type public.revision_type NOT NULL,
60
+ content jsonb NOT NULL,
61
+ created_at timestamp with time zone DEFAULT now() NOT NULL
62
+ );
63
+
64
+ COMMENT ON TABLE public.product_revisions IS 'Hybrid (snapshot/diff) revisions for products.';
65
+ COMMENT ON COLUMN public.product_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
66
+
67
+ DO $rb$
68
+ BEGIN
69
+ IF NOT EXISTS (
70
+ SELECT 1 FROM pg_attribute
71
+ WHERE attrelid = 'public.product_revisions'::regclass
72
+ AND attname = 'id'
73
+ AND attidentity <> ''
74
+ ) THEN
75
+ ALTER TABLE public.product_revisions
76
+ ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
77
+ SEQUENCE NAME public.product_revisions_id_seq
78
+ START WITH 1
79
+ INCREMENT BY 1
80
+ NO MINVALUE
81
+ NO MAXVALUE
82
+ CACHE 1
83
+ );
84
+ END IF;
85
+ END $rb$;
86
+
87
+ DO $rb$
88
+ BEGIN
89
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
90
+ WHERE conname = 'product_revisions_pkey'
91
+ AND conrelid = 'public.product_revisions'::regclass) THEN
92
+ ALTER TABLE ONLY public.product_revisions
93
+ ADD CONSTRAINT product_revisions_pkey PRIMARY KEY (id);
94
+ END IF;
95
+ END $rb$;
96
+
97
+ DO $rb$
98
+ BEGIN
99
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
100
+ WHERE conname = 'product_revisions_product_version_key'
101
+ AND conrelid = 'public.product_revisions'::regclass) THEN
102
+ ALTER TABLE ONLY public.product_revisions
103
+ ADD CONSTRAINT product_revisions_product_version_key UNIQUE (product_id, version);
104
+ END IF;
105
+ END $rb$;
106
+
107
+ DO $rb$
108
+ BEGIN
109
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
110
+ WHERE conname = 'product_revisions_author_id_fkey'
111
+ AND conrelid = 'public.product_revisions'::regclass) THEN
112
+ ALTER TABLE ONLY public.product_revisions
113
+ ADD CONSTRAINT product_revisions_author_id_fkey
114
+ FOREIGN KEY (author_id) REFERENCES public.profiles(id) ON DELETE SET NULL;
115
+ END IF;
116
+ END $rb$;
117
+
118
+ DO $rb$
119
+ BEGIN
120
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint
121
+ WHERE conname = 'product_revisions_product_id_fkey'
122
+ AND conrelid = 'public.product_revisions'::regclass) THEN
123
+ ALTER TABLE ONLY public.product_revisions
124
+ ADD CONSTRAINT product_revisions_product_id_fkey
125
+ FOREIGN KEY (product_id) REFERENCES public.products(id) ON DELETE CASCADE;
126
+ END IF;
127
+ END $rb$;
128
+
129
+ CREATE INDEX IF NOT EXISTS idx_product_revisions_author_id
130
+ ON public.product_revisions USING btree (author_id);
131
+
132
+ CREATE INDEX IF NOT EXISTS idx_product_revisions_product_id_version
133
+ ON public.product_revisions USING btree (product_id, version);
134
+
135
+ ALTER TABLE public.product_revisions ENABLE ROW LEVEL SECURITY;
136
+
137
+ DROP POLICY IF EXISTS product_revisions_read_policy ON public.product_revisions;
138
+ CREATE POLICY product_revisions_read_policy ON public.product_revisions
139
+ FOR SELECT TO authenticated USING (true);
140
+
141
+ DROP POLICY IF EXISTS product_revisions_insert_policy ON public.product_revisions;
142
+ CREATE POLICY product_revisions_insert_policy ON public.product_revisions
143
+ FOR INSERT TO authenticated
144
+ WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));
145
+
146
+ DROP POLICY IF EXISTS product_revisions_update_policy ON public.product_revisions;
147
+ CREATE POLICY product_revisions_update_policy ON public.product_revisions
148
+ FOR UPDATE TO authenticated
149
+ USING (((SELECT public.is_admin() AS is_admin) IS TRUE))
150
+ WITH CHECK (((SELECT public.is_admin() AS is_admin) IS TRUE));
151
+
152
+ DROP POLICY IF EXISTS product_revisions_delete_policy ON public.product_revisions;
153
+ CREATE POLICY product_revisions_delete_policy ON public.product_revisions
154
+ FOR DELETE TO authenticated
155
+ USING (((SELECT public.is_admin() AS is_admin) IS TRUE));
156
+
157
+ GRANT ALL ON TABLE public.product_revisions TO anon;
158
+ GRANT ALL ON TABLE public.product_revisions TO authenticated;
159
+ GRANT ALL ON TABLE public.product_revisions TO service_role;
160
+ GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO anon;
161
+ GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO authenticated;
162
+ GRANT ALL ON SEQUENCE public.product_revisions_id_seq TO service_role;
163
+
164
+ -- ---------------------------------------------------------------------------
165
+ -- 3. Revision baseline backfill
166
+ --
167
+ -- The JSON shape must match FullPageContent / FullPostContent / FullProductContent
168
+ -- in apps/nextblock/app/cms/revisions/utils.ts exactly, or the first diff taken
169
+ -- against a baseline row will be full of phantom operations.
170
+ --
171
+ -- Timestamps are rendered with an explicit millisecond-precision UTC format so they
172
+ -- match JavaScript's Date#toISOString() ("2026-07-03T17:52:15.643Z"). Postgres'
173
+ -- default jsonb rendering of timestamptz ("2026-07-03T17:52:15.643901+00:00") would
174
+ -- differ from the value the application writes and produce a spurious diff on the
175
+ -- very next save.
176
+ -- ---------------------------------------------------------------------------
177
+
178
+ -- 3a. Pages
179
+ INSERT INTO public.page_revisions (page_id, author_id, version, revision_type, content)
180
+ SELECT
181
+ p.id,
182
+ NULL::uuid,
183
+ p.version,
184
+ 'snapshot'::public.revision_type,
185
+ jsonb_build_object(
186
+ 'meta', jsonb_build_object(
187
+ 'title', p.title,
188
+ 'slug', p.slug,
189
+ 'language_id', p.language_id,
190
+ 'status', p.status,
191
+ 'meta_title', p.meta_title,
192
+ 'meta_description', p.meta_description,
193
+ 'custom_canonical', p.custom_canonical,
194
+ 'published_at', to_char(p.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
195
+ 'feature_image_id', p.feature_image_id
196
+ ),
197
+ 'blocks', COALESCE((
198
+ SELECT jsonb_agg(
199
+ jsonb_build_object(
200
+ 'language_id', b.language_id,
201
+ 'block_type', b.block_type,
202
+ 'content', b.content,
203
+ 'order', b."order"
204
+ ) ORDER BY b."order" ASC, b.id ASC
205
+ )
206
+ FROM public.blocks b
207
+ WHERE b.page_id = p.id
208
+ ), '[]'::jsonb)
209
+ )
210
+ FROM public.pages p
211
+ WHERE NOT EXISTS (
212
+ SELECT 1 FROM public.page_revisions r
213
+ WHERE r.page_id = p.id
214
+ AND r.revision_type = 'snapshot'
215
+ AND r.version <= p.version
216
+ )
217
+ ON CONFLICT (page_id, version) DO NOTHING;
218
+
219
+ -- 3b. Posts
220
+ INSERT INTO public.post_revisions (post_id, author_id, version, revision_type, content)
221
+ SELECT
222
+ po.id,
223
+ NULL::uuid,
224
+ po.version,
225
+ 'snapshot'::public.revision_type,
226
+ jsonb_build_object(
227
+ 'meta', jsonb_build_object(
228
+ 'title', po.title,
229
+ 'slug', po.slug,
230
+ 'language_id', po.language_id,
231
+ 'status', po.status,
232
+ 'meta_title', po.meta_title,
233
+ 'meta_description', po.meta_description,
234
+ 'custom_canonical', po.custom_canonical,
235
+ 'published_at', to_char(po.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
236
+ 'feature_image_id', po.feature_image_id,
237
+ 'label', po.label,
238
+ 'excerpt', po.excerpt,
239
+ 'subtitle', po.subtitle
240
+ ),
241
+ 'blocks', COALESCE((
242
+ SELECT jsonb_agg(
243
+ jsonb_build_object(
244
+ 'language_id', b.language_id,
245
+ 'block_type', b.block_type,
246
+ 'content', b.content,
247
+ 'order', b."order"
248
+ ) ORDER BY b."order" ASC, b.id ASC
249
+ )
250
+ FROM public.blocks b
251
+ WHERE b.post_id = po.id
252
+ ), '[]'::jsonb)
253
+ )
254
+ FROM public.posts po
255
+ WHERE NOT EXISTS (
256
+ SELECT 1 FROM public.post_revisions r
257
+ WHERE r.post_id = po.id
258
+ AND r.revision_type = 'snapshot'
259
+ AND r.version <= po.version
260
+ )
261
+ ON CONFLICT (post_id, version) DO NOTHING;
262
+
263
+ -- 3c. Products.
264
+ --
265
+ -- Content only. price/prices/sale_*/scheduled_*/stock/sku/average_rating/total_reviews
266
+ -- are deliberately excluded from the snapshot: pricing and inventory are mutated from
267
+ -- outside the editor (promotions, Freemius sync, order fulfilment), ratings are derived
268
+ -- aggregates, and inventory_items is keyed by bare SKU text with no FK to products — so
269
+ -- replaying commerce state on restore would reach rows the editor never touched.
270
+ -- Restoring a product restores its content, not its commerce state.
271
+ INSERT INTO public.product_revisions (product_id, author_id, version, revision_type, content)
272
+ SELECT
273
+ pr.id,
274
+ NULL::uuid,
275
+ pr.version,
276
+ 'snapshot'::public.revision_type,
277
+ jsonb_build_object(
278
+ 'meta', jsonb_build_object(
279
+ 'title', pr.title,
280
+ 'slug', pr.slug,
281
+ 'language_id', pr.language_id,
282
+ 'status', pr.status,
283
+ 'meta_title', pr.meta_title,
284
+ 'meta_description', pr.meta_description,
285
+ 'custom_canonical', pr.custom_canonical,
286
+ 'published_at', to_char(pr.published_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'),
287
+ 'short_description', pr.short_description,
288
+ 'description_json', pr.description_json
289
+ ),
290
+ 'blocks', COALESCE((
291
+ SELECT jsonb_agg(
292
+ jsonb_build_object(
293
+ 'language_id', b.language_id,
294
+ 'block_type', b.block_type,
295
+ 'content', b.content,
296
+ 'order', b."order"
297
+ ) ORDER BY b."order" ASC, b.id ASC
298
+ )
299
+ FROM public.blocks b
300
+ WHERE b.product_id = pr.id
301
+ ), '[]'::jsonb)
302
+ )
303
+ FROM public.products pr
304
+ WHERE NOT EXISTS (
305
+ SELECT 1 FROM public.product_revisions r
306
+ WHERE r.product_id = pr.id
307
+ AND r.revision_type = 'snapshot'
308
+ AND r.version <= pr.version
309
+ )
310
+ ON CONFLICT (product_id, version) DO NOTHING;