@nextblock-cms/db 0.17.0 → 0.17.2
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 +1 -1
- package/supabase/migrations/02000_catchup_gen1.sql +5880 -0
- package/supabase/migrations/{00000000000000_baseline_schema.sql → 02001_baseline_schema.sql} +305 -25
- package/supabase/migrations/{00000000000001_baseline_constraints_and_indexes.sql → 02002_baseline_constraints_and_indexes.sql} +194 -3
- package/supabase/migrations/{00000000000002_baseline_security_and_grants.sql → 02003_baseline_security_and_grants.sql} +403 -161
- package/supabase/migrations/02004_baseline_seed.sql +565 -0
- package/supabase/migrations/02005_home_live_demo_promo_cadence_15min.sql +53 -0
- package/supabase/migrations/00000000000003_baseline_seed.sql +0 -468
- package/supabase/migrations/00000000000004_default_logo_email_safe.sql +0 -24
- package/supabase/migrations/00000000000005_add_custom_canonical.sql +0 -13
- package/supabase/migrations/00000000000006_home_live_demo_promo.sql +0 -67
- package/supabase/migrations/00000000000007_home_live_demo_promo_copy_fix.sql +0 -49
- package/supabase/migrations/00000000000008_setup_article_reorder.sql +0 -30
- package/supabase/migrations/00000000000009_home_live_demo_promo_contrast.sql +0 -52
- package/supabase/migrations/00000000000010_drop_github_username.sql +0 -79
- package/supabase/migrations/00000000000011_language_detection_admin_only.sql +0 -21
- package/supabase/migrations/00000000000012_cortex_ai_stock_photo_settings.sql +0 -25
- package/supabase/migrations/00000000000013_youtube_nocookie_embeds.sql +0 -136
- package/supabase/migrations/00000000000014_site_themes.sql +0 -254
- package/supabase/migrations/00000000000015_scheduled_publishing.sql +0 -43
- package/supabase/migrations/00000000000016_product_revisions_and_revision_baseline.sql +0 -310
- package/supabase/migrations/00000000000017_cortex_ai_mcp_server.sql +0 -91
- package/supabase/migrations/00000000000018_site_scripts.sql +0 -96
- package/supabase/migrations/00000000000019_site_script_revisions.sql +0 -85
- package/supabase/migrations/00000000000020_updating_article.sql +0 -162
- package/supabase/migrations/00000000000021_updating_article_git_merge.sql +0 -91
- package/supabase/migrations/00000000000022_updating_article_accuracy.sql +0 -94
- package/supabase/migrations/00000000000023_updating_article_layout_not_host.sql +0 -87
- package/supabase/migrations/00000000000024_updating_article_newline_fix.sql +0 -70
- package/supabase/migrations/00000000000025_rebrand_nextblock_dev.sql +0 -53
- package/supabase/migrations/00000000000026_product_inquiries.sql +0 -118
- package/supabase/migrations/00000000000027_message_threads.sql +0 -391
- package/supabase/migrations/00000000000028_interaction_replies.sql +0 -86
- package/supabase/migrations/00000000000029_form_endpoints_default_empty.sql +0 -52
- package/supabase/migrations/00000000000030_seo_redirects_and_robots.sql +0 -149
- package/supabase/migrations/00000000000031_seo_robots_settings_admin_only.sql +0 -35
- package/supabase/migrations/00000000000032_seed_seo_score_optimizations.sql +0 -1172
- package/supabase/migrations/00000000000033_seed_seo_score_optimizations_p2.sql +0 -353
- package/supabase/migrations/00000000000034_seed_seo_french_legal_readability.sql +0 -101
- package/supabase/migrations/00000000000035_digital_products_seo_optimizations.sql +0 -134
- package/supabase/migrations/00000000000036_seed_posts_missing_seo_metadata.sql +0 -37
- package/supabase/migrations/00000000000037_reposition_marketing_and_cortex_mcp.sql +0 -501
- package/supabase/migrations/00000000000038_mcp_guide_visual_refresh.sql +0 -64
- package/supabase/migrations/00000000000039_mcp_guide_diagram_dimensions.sql +0 -21
- package/supabase/migrations/00000000000040_home_comparison_chart_free_cms.sql +0 -81
- package/supabase/migrations/00000000000041_marketing_review_free_cms_trial.sql +0 -141
- package/supabase/migrations/00000000000042_how_nextblock_works_css_diagram.sql +0 -66
package/supabase/migrations/{00000000000000_baseline_schema.sql → 02001_baseline_schema.sql}
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
-- AUTO-GENERATED baseline (
|
|
2
|
-
--
|
|
3
|
-
-- Regenerate via tools/scripts/rebaseline-transform.mjs
|
|
1
|
+
-- AUTO-GENERATED baseline: squash generation 2 (of migrations 00000000000000..00000000000042).
|
|
2
|
+
-- 001 · schema: enums, functions, tables, sequences, defaults
|
|
3
|
+
-- Idempotent; safe to replay. Regenerate via tools/scripts/rebaseline-transform.mjs — do not hand-edit.
|
|
4
|
+
-- Naming: GGNNN_name.sql (GG = squash generation, NNN = sequence). See docs/04-DATABASE-AND-AUTH.md.
|
|
4
5
|
|
|
5
6
|
SET check_function_bodies = false;
|
|
6
7
|
|
|
@@ -473,6 +474,20 @@ BEGIN
|
|
|
473
474
|
END;
|
|
474
475
|
$$;
|
|
475
476
|
|
|
477
|
+
CREATE OR REPLACE FUNCTION public.handle_default_theme_change() RETURNS trigger
|
|
478
|
+
LANGUAGE plpgsql
|
|
479
|
+
SET search_path TO ''
|
|
480
|
+
AS $$
|
|
481
|
+
BEGIN
|
|
482
|
+
IF NEW.is_default THEN
|
|
483
|
+
UPDATE public.site_themes
|
|
484
|
+
SET is_default = false
|
|
485
|
+
WHERE id <> NEW.id AND is_default;
|
|
486
|
+
END IF;
|
|
487
|
+
RETURN NEW;
|
|
488
|
+
END;
|
|
489
|
+
$$;
|
|
490
|
+
|
|
476
491
|
CREATE OR REPLACE FUNCTION public.handle_inventory_item_change() RETURNS trigger
|
|
477
492
|
LANGUAGE plpgsql
|
|
478
493
|
SET search_path TO 'public'
|
|
@@ -534,8 +549,6 @@ DECLARE
|
|
|
534
549
|
user_role public.user_role;
|
|
535
550
|
v_full_name text;
|
|
536
551
|
v_avatar_url text;
|
|
537
|
-
v_github_username text;
|
|
538
|
-
v_provider text;
|
|
539
552
|
BEGIN
|
|
540
553
|
INSERT INTO public.site_settings (key, value)
|
|
541
554
|
VALUES ('is_admin_created', 'false'::jsonb)
|
|
@@ -559,35 +572,22 @@ BEGIN
|
|
|
559
572
|
|
|
560
573
|
v_full_name := NEW.raw_user_meta_data->>'full_name';
|
|
561
574
|
v_avatar_url := NEW.raw_user_meta_data->>'avatar_url';
|
|
562
|
-
v_provider := NEW.raw_app_meta_data->>'provider';
|
|
563
|
-
|
|
564
|
-
IF v_provider = 'github' OR (NEW.raw_user_meta_data->>'iss') LIKE '%github%' THEN
|
|
565
|
-
v_github_username := COALESCE(
|
|
566
|
-
NEW.raw_user_meta_data->>'user_name',
|
|
567
|
-
NEW.raw_user_meta_data->>'preferred_username'
|
|
568
|
-
);
|
|
569
|
-
ELSE
|
|
570
|
-
v_github_username := NULL;
|
|
571
|
-
END IF;
|
|
572
575
|
|
|
573
576
|
INSERT INTO public.profiles (
|
|
574
577
|
id,
|
|
575
578
|
role,
|
|
576
579
|
full_name,
|
|
577
|
-
avatar_url
|
|
578
|
-
github_username
|
|
580
|
+
avatar_url
|
|
579
581
|
)
|
|
580
582
|
VALUES (
|
|
581
583
|
NEW.id,
|
|
582
584
|
user_role,
|
|
583
585
|
v_full_name,
|
|
584
|
-
v_avatar_url
|
|
585
|
-
v_github_username
|
|
586
|
+
v_avatar_url
|
|
586
587
|
)
|
|
587
588
|
ON CONFLICT (id) DO UPDATE SET
|
|
588
589
|
full_name = EXCLUDED.full_name,
|
|
589
|
-
avatar_url = EXCLUDED.avatar_url
|
|
590
|
-
github_username = EXCLUDED.github_username;
|
|
590
|
+
avatar_url = EXCLUDED.avatar_url;
|
|
591
591
|
|
|
592
592
|
RETURN NEW;
|
|
593
593
|
END;
|
|
@@ -752,6 +752,49 @@ CREATE OR REPLACE FUNCTION public.normalize_currency_amount_map(amounts jsonb) R
|
|
|
752
752
|
END;
|
|
753
753
|
$_$;
|
|
754
754
|
|
|
755
|
+
CREATE OR REPLACE FUNCTION public.prevent_site_script_revision_rewrite() RETURNS trigger
|
|
756
|
+
LANGUAGE plpgsql
|
|
757
|
+
SET search_path TO ''
|
|
758
|
+
AS $$
|
|
759
|
+
BEGIN
|
|
760
|
+
RAISE EXCEPTION 'site_script_revisions is append-only; % is not permitted', TG_OP
|
|
761
|
+
USING ERRCODE = 'restrict_violation';
|
|
762
|
+
END;
|
|
763
|
+
$$;
|
|
764
|
+
|
|
765
|
+
CREATE OR REPLACE FUNCTION public.prevent_system_theme_delete() RETURNS trigger
|
|
766
|
+
LANGUAGE plpgsql
|
|
767
|
+
SET search_path TO ''
|
|
768
|
+
AS $$
|
|
769
|
+
BEGIN
|
|
770
|
+
IF OLD.is_system THEN
|
|
771
|
+
RAISE EXCEPTION 'Theme "%" is a system theme and cannot be deleted', OLD.slug
|
|
772
|
+
USING ERRCODE = 'restrict_violation';
|
|
773
|
+
END IF;
|
|
774
|
+
RETURN OLD;
|
|
775
|
+
END;
|
|
776
|
+
$$;
|
|
777
|
+
|
|
778
|
+
CREATE OR REPLACE FUNCTION public.prevent_thread_message_rewrite() RETURNS trigger
|
|
779
|
+
LANGUAGE plpgsql
|
|
780
|
+
SET search_path TO ''
|
|
781
|
+
AS $$
|
|
782
|
+
BEGIN
|
|
783
|
+
IF TG_OP = 'DELETE' THEN
|
|
784
|
+
RAISE EXCEPTION 'thread_messages is append-only; DELETE is not permitted'
|
|
785
|
+
USING ERRCODE = 'restrict_violation';
|
|
786
|
+
END IF;
|
|
787
|
+
IF NEW.body IS DISTINCT FROM OLD.body
|
|
788
|
+
OR NEW.direction IS DISTINCT FROM OLD.direction
|
|
789
|
+
OR NEW.thread_id IS DISTINCT FROM OLD.thread_id
|
|
790
|
+
OR NEW.created_at IS DISTINCT FROM OLD.created_at THEN
|
|
791
|
+
RAISE EXCEPTION 'thread_messages content is append-only; only delivery flags may change'
|
|
792
|
+
USING ERRCODE = 'restrict_violation';
|
|
793
|
+
END IF;
|
|
794
|
+
RETURN NEW;
|
|
795
|
+
END;
|
|
796
|
+
$$;
|
|
797
|
+
|
|
755
798
|
CREATE OR REPLACE FUNCTION public.set_currency_defaults() RETURNS trigger
|
|
756
799
|
LANGUAGE plpgsql
|
|
757
800
|
SET search_path TO ''
|
|
@@ -1434,8 +1477,10 @@ CREATE TABLE IF NOT EXISTS public.cms_interactions (
|
|
|
1434
1477
|
reactions jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1435
1478
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1436
1479
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1480
|
+
parent_id uuid,
|
|
1437
1481
|
CONSTRAINT check_product_or_post CHECK ((((product_id IS NOT NULL) AND (post_id IS NULL)) OR ((post_id IS NOT NULL) AND (product_id IS NULL)))),
|
|
1438
|
-
CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL))))
|
|
1482
|
+
CONSTRAINT check_rating_only_for_review CHECK ((((type = 'review'::public.interaction_type) AND (rating IS NOT NULL) AND (rating >= 1) AND (rating <= 5)) OR ((type = 'comment'::public.interaction_type) AND (rating IS NULL)))),
|
|
1483
|
+
CONSTRAINT cms_interactions_reply_check CHECK (((parent_id IS NULL) OR ((parent_id <> id) AND (rating IS NULL) AND (type = 'comment'::public.interaction_type))))
|
|
1439
1484
|
);
|
|
1440
1485
|
|
|
1441
1486
|
COMMENT ON TABLE public.cms_interactions IS 'Stores user-submitted product reviews and blog post comments.';
|
|
@@ -1446,6 +1491,32 @@ COMMENT ON COLUMN public.cms_interactions.user_id IS 'References public.profiles
|
|
|
1446
1491
|
|
|
1447
1492
|
COMMENT ON COLUMN public.cms_interactions.reactions IS 'JSONB structure tracking counts of reactions (likes, etc.).';
|
|
1448
1493
|
|
|
1494
|
+
COMMENT ON COLUMN public.cms_interactions.parent_id IS 'Set on a staff reply: points at the review or comment being answered. Replies are always type=comment with a NULL rating, and carry the parent''s product_id/post_id so check_product_or_post still holds.';
|
|
1495
|
+
|
|
1496
|
+
CREATE TABLE IF NOT EXISTS public.cms_redirects (
|
|
1497
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1498
|
+
source_path text NOT NULL,
|
|
1499
|
+
destination_path text NOT NULL,
|
|
1500
|
+
status_code integer DEFAULT 301 NOT NULL,
|
|
1501
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
1502
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1503
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1504
|
+
CONSTRAINT cms_redirects_destination_path_check CHECK (((destination_path ~ '^/'::text) OR (destination_path ~ '^https://'::text))),
|
|
1505
|
+
CONSTRAINT cms_redirects_no_self_redirect_check CHECK ((source_path <> destination_path)),
|
|
1506
|
+
CONSTRAINT cms_redirects_source_path_check CHECK ((source_path ~ '^/'::text)),
|
|
1507
|
+
CONSTRAINT cms_redirects_status_code_check CHECK ((status_code = ANY (ARRAY[301, 302])))
|
|
1508
|
+
);
|
|
1509
|
+
|
|
1510
|
+
COMMENT ON TABLE public.cms_redirects IS 'Operator-managed 301/302 redirects resolved by apps/nextblock/proxy.ts before rendering. Only is_active rows are publicly readable; only ADMIN may write, because a redirect rule is an open-redirect surface.';
|
|
1511
|
+
|
|
1512
|
+
COMMENT ON COLUMN public.cms_redirects.source_path IS 'Exact site-relative path to match, normalized to a leading slash and no trailing slash (except root). No wildcards, by design.';
|
|
1513
|
+
|
|
1514
|
+
COMMENT ON COLUMN public.cms_redirects.destination_path IS 'Site-relative path or absolute https URL to send the visitor to.';
|
|
1515
|
+
|
|
1516
|
+
COMMENT ON COLUMN public.cms_redirects.status_code IS '301 permanent or 302 temporary. 307/308 are not offered by the admin UI.';
|
|
1517
|
+
|
|
1518
|
+
COMMENT ON COLUMN public.cms_redirects.is_active IS 'Only active rows are readable by anon, and only active rows are matched by the proxy.';
|
|
1519
|
+
|
|
1449
1520
|
CREATE TABLE IF NOT EXISTS public.content_drafts (
|
|
1450
1521
|
id bigint NOT NULL,
|
|
1451
1522
|
parent_type text NOT NULL,
|
|
@@ -1644,6 +1715,25 @@ CREATE TABLE IF NOT EXISTS public.email_2fa_challenges (
|
|
|
1644
1715
|
|
|
1645
1716
|
COMMENT ON TABLE public.email_2fa_challenges IS 'Short-lived SHA-256 hashes of 6-digit email verification codes. Readable/writable only by the service role.';
|
|
1646
1717
|
|
|
1718
|
+
CREATE TABLE IF NOT EXISTS public.form_endpoints (
|
|
1719
|
+
form_key uuid NOT NULL,
|
|
1720
|
+
label text DEFAULT 'Contact form'::text NOT NULL,
|
|
1721
|
+
recipient_email text,
|
|
1722
|
+
fields jsonb DEFAULT '[]'::jsonb NOT NULL,
|
|
1723
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1724
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1725
|
+
CONSTRAINT form_endpoints_fields_array CHECK ((jsonb_typeof(fields) = 'array'::text)),
|
|
1726
|
+
CONSTRAINT form_endpoints_label_not_blank CHECK ((char_length(btrim(label)) > 0))
|
|
1727
|
+
);
|
|
1728
|
+
|
|
1729
|
+
COMMENT ON TABLE public.form_endpoints IS 'Server-side destination and field manifest for a contact-form block, keyed by the non-secret form_key carried in block content. The address never reaches the browser.';
|
|
1730
|
+
|
|
1731
|
+
COMMENT ON COLUMN public.form_endpoints.form_key IS 'Non-secret opaque handle. Safe in the RSC payload: it grants nothing, it is only a lookup key.';
|
|
1732
|
+
|
|
1733
|
+
COMMENT ON COLUMN public.form_endpoints.recipient_email IS 'Per-form override. NULL means "use the site contact address" (CMS -> Messages, falling back to the first admin), which is the default and the usual case.';
|
|
1734
|
+
|
|
1735
|
+
COMMENT ON COLUMN public.form_endpoints.fields IS 'Snapshot of [{temp_id,label,field_type}] written by the CMS editor, so labels in emails and the inbox are server-trusted.';
|
|
1736
|
+
|
|
1647
1737
|
CREATE TABLE IF NOT EXISTS public.freemius_plans (
|
|
1648
1738
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1649
1739
|
product_id uuid NOT NULL,
|
|
@@ -1722,6 +1812,21 @@ COMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the
|
|
|
1722
1812
|
|
|
1723
1813
|
COMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';
|
|
1724
1814
|
|
|
1815
|
+
CREATE TABLE IF NOT EXISTS public.mcp_access_tokens (
|
|
1816
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1817
|
+
name text NOT NULL,
|
|
1818
|
+
token_hash text NOT NULL,
|
|
1819
|
+
token_prefix text NOT NULL,
|
|
1820
|
+
scopes text[] DEFAULT ARRAY['read'::text, 'write'::text] NOT NULL,
|
|
1821
|
+
created_by uuid,
|
|
1822
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1823
|
+
last_used_at timestamp with time zone,
|
|
1824
|
+
expires_at timestamp with time zone,
|
|
1825
|
+
revoked_at timestamp with time zone
|
|
1826
|
+
);
|
|
1827
|
+
|
|
1828
|
+
COMMENT ON TABLE public.mcp_access_tokens IS 'Bearer tokens for the Cortex AI MCP server at /api/mcp. Stores SHA-256 hashes only; plaintext is displayed once at mint time.';
|
|
1829
|
+
|
|
1725
1830
|
CREATE TABLE IF NOT EXISTS public.media (
|
|
1726
1831
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1727
1832
|
uploader_id uuid,
|
|
@@ -1756,6 +1861,40 @@ COMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storag
|
|
|
1756
1861
|
|
|
1757
1862
|
COMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';
|
|
1758
1863
|
|
|
1864
|
+
CREATE TABLE IF NOT EXISTS public.message_threads (
|
|
1865
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1866
|
+
source text NOT NULL,
|
|
1867
|
+
subject_id uuid,
|
|
1868
|
+
form_key uuid,
|
|
1869
|
+
subject_label text DEFAULT 'Message'::text NOT NULL,
|
|
1870
|
+
sender_name text,
|
|
1871
|
+
sender_email text,
|
|
1872
|
+
locale text,
|
|
1873
|
+
fields jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
1874
|
+
status text DEFAULT 'open'::text NOT NULL,
|
|
1875
|
+
unread_for_admin boolean DEFAULT true NOT NULL,
|
|
1876
|
+
unread_for_visitor boolean DEFAULT false NOT NULL,
|
|
1877
|
+
token_hash text,
|
|
1878
|
+
token_expires_at timestamp with time zone,
|
|
1879
|
+
token_revoked_at timestamp with time zone,
|
|
1880
|
+
token_last_used_at timestamp with time zone,
|
|
1881
|
+
last_message_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1882
|
+
ip_masked text,
|
|
1883
|
+
user_agent text,
|
|
1884
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1885
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1886
|
+
CONSTRAINT message_threads_fields_object CHECK ((jsonb_typeof(fields) = 'object'::text)),
|
|
1887
|
+
CONSTRAINT message_threads_source_check CHECK ((source = ANY (ARRAY['product_inquiry'::text, 'contact_form'::text]))),
|
|
1888
|
+
CONSTRAINT message_threads_status_check CHECK ((status = ANY (ARRAY['open'::text, 'closed'::text]))),
|
|
1889
|
+
CONSTRAINT message_threads_subject_check CHECK ((((source = 'product_inquiry'::text) AND (subject_id IS NOT NULL)) OR ((source = 'contact_form'::text) AND (form_key IS NOT NULL))))
|
|
1890
|
+
);
|
|
1891
|
+
|
|
1892
|
+
COMMENT ON TABLE public.message_threads IS 'Private conversations with anonymous visitors (product enquiries and contact-form submissions). Written by the service role from public server actions; read by ADMINs only. Public reviews and comments are NOT here - they live in cms_interactions.';
|
|
1893
|
+
|
|
1894
|
+
COMMENT ON COLUMN public.message_threads.subject_id IS 'product_inquiries.id. Plain uuid, no FK: a conversation outlives the enquiry record it grew from.';
|
|
1895
|
+
|
|
1896
|
+
COMMENT ON COLUMN public.message_threads.token_hash IS 'SHA-256 hex of the visitor thread token. The raw token is never stored; it is minted on the first admin reply and mailed once.';
|
|
1897
|
+
|
|
1759
1898
|
CREATE TABLE IF NOT EXISTS public.navigation_items (
|
|
1760
1899
|
id bigint NOT NULL,
|
|
1761
1900
|
language_id bigint NOT NULL,
|
|
@@ -1926,7 +2065,9 @@ CREATE TABLE IF NOT EXISTS public.pages (
|
|
|
1926
2065
|
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1927
2066
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1928
2067
|
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1929
|
-
feature_image_id uuid
|
|
2068
|
+
feature_image_id uuid,
|
|
2069
|
+
custom_canonical text,
|
|
2070
|
+
published_at timestamp with time zone
|
|
1930
2071
|
);
|
|
1931
2072
|
|
|
1932
2073
|
COMMENT ON TABLE public.pages IS 'Stores static pages for the website.';
|
|
@@ -1939,6 +2080,8 @@ COMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different languag
|
|
|
1939
2080
|
|
|
1940
2081
|
COMMENT ON COLUMN public.pages.feature_image_id IS 'ID of the media item to be used as the page feature image.';
|
|
1941
2082
|
|
|
2083
|
+
COMMENT ON COLUMN public.pages.published_at IS 'Optional go-live moment. NULL = live as soon as status is published. A future value withholds the page from public reads until it passes (status stays "published"; the CMS renders that pair as "Scheduled").';
|
|
2084
|
+
|
|
1942
2085
|
DO $rb$ BEGIN
|
|
1943
2086
|
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.pages'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
1944
2087
|
ALTER TABLE public.pages ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
@@ -1996,7 +2139,8 @@ CREATE TABLE IF NOT EXISTS public.posts (
|
|
|
1996
2139
|
version integer DEFAULT 1 NOT NULL,
|
|
1997
2140
|
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
1998
2141
|
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
1999
|
-
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2142
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2143
|
+
custom_canonical text
|
|
2000
2144
|
);
|
|
2001
2145
|
|
|
2002
2146
|
COMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';
|
|
@@ -2128,12 +2272,66 @@ CREATE TABLE IF NOT EXISTS public.product_freemius_sale_coupons (
|
|
|
2128
2272
|
|
|
2129
2273
|
COMMENT ON TABLE public.product_freemius_sale_coupons IS 'Auto-generated, time-bounded Freemius coupons that enforce a scheduled sale on a Freemius product at Freemius-hosted checkout.';
|
|
2130
2274
|
|
|
2275
|
+
CREATE TABLE IF NOT EXISTS public.product_inquiries (
|
|
2276
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2277
|
+
product_id uuid,
|
|
2278
|
+
product_slug text,
|
|
2279
|
+
product_title text,
|
|
2280
|
+
sender_name text NOT NULL,
|
|
2281
|
+
sender_email text NOT NULL,
|
|
2282
|
+
message text NOT NULL,
|
|
2283
|
+
locale text,
|
|
2284
|
+
ip_masked text,
|
|
2285
|
+
user_agent text,
|
|
2286
|
+
email_delivered boolean DEFAULT false NOT NULL,
|
|
2287
|
+
is_resolved boolean DEFAULT false NOT NULL,
|
|
2288
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2289
|
+
CONSTRAINT product_inquiries_message_not_blank CHECK ((char_length(btrim(message)) > 0)),
|
|
2290
|
+
CONSTRAINT product_inquiries_sender_email_not_blank CHECK ((char_length(btrim(sender_email)) > 0)),
|
|
2291
|
+
CONSTRAINT product_inquiries_sender_name_not_blank CHECK ((char_length(btrim(sender_name)) > 0))
|
|
2292
|
+
);
|
|
2293
|
+
|
|
2294
|
+
COMMENT ON TABLE public.product_inquiries IS 'Visitor purchase enquiries raised when the store cannot take payment. Written by the service role from a public server action; read by ADMINs only.';
|
|
2295
|
+
|
|
2296
|
+
COMMENT ON COLUMN public.product_inquiries.product_id IS 'Plain uuid, no FK: an enquiry outlives the product it was about.';
|
|
2297
|
+
|
|
2298
|
+
COMMENT ON COLUMN public.product_inquiries.ip_masked IS 'Partially masked IP (e.g. 203.0.113.x) - never store a full address. Also backs the per-IP submission throttle.';
|
|
2299
|
+
|
|
2300
|
+
COMMENT ON COLUMN public.product_inquiries.email_delivered IS 'False when the owner notification could not be sent (e.g. SMTP unconfigured); the stored row is then the only record.';
|
|
2301
|
+
|
|
2131
2302
|
CREATE TABLE IF NOT EXISTS public.product_media (
|
|
2132
2303
|
product_id uuid NOT NULL,
|
|
2133
2304
|
media_id uuid NOT NULL,
|
|
2134
2305
|
sort_order integer DEFAULT 0
|
|
2135
2306
|
);
|
|
2136
2307
|
|
|
2308
|
+
CREATE TABLE IF NOT EXISTS public.product_revisions (
|
|
2309
|
+
id bigint NOT NULL,
|
|
2310
|
+
product_id uuid NOT NULL,
|
|
2311
|
+
author_id uuid,
|
|
2312
|
+
version integer NOT NULL,
|
|
2313
|
+
revision_type public.revision_type NOT NULL,
|
|
2314
|
+
content jsonb NOT NULL,
|
|
2315
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL
|
|
2316
|
+
);
|
|
2317
|
+
|
|
2318
|
+
COMMENT ON TABLE public.product_revisions IS 'Hybrid (snapshot/diff) revisions for products.';
|
|
2319
|
+
|
|
2320
|
+
COMMENT ON COLUMN public.product_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
2321
|
+
|
|
2322
|
+
DO $rb$ BEGIN
|
|
2323
|
+
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid = 'public.product_revisions'::regclass AND attname = 'id' AND attidentity <> '') THEN
|
|
2324
|
+
ALTER TABLE public.product_revisions ALTER COLUMN id ADD GENERATED BY DEFAULT AS IDENTITY (
|
|
2325
|
+
SEQUENCE NAME public.product_revisions_id_seq
|
|
2326
|
+
START WITH 1
|
|
2327
|
+
INCREMENT BY 1
|
|
2328
|
+
NO MINVALUE
|
|
2329
|
+
NO MAXVALUE
|
|
2330
|
+
CACHE 1
|
|
2331
|
+
);
|
|
2332
|
+
END IF;
|
|
2333
|
+
END $rb$;
|
|
2334
|
+
|
|
2137
2335
|
CREATE TABLE IF NOT EXISTS public.product_variants (
|
|
2138
2336
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2139
2337
|
product_id uuid NOT NULL,
|
|
@@ -2207,6 +2405,9 @@ CREATE TABLE IF NOT EXISTS public.products (
|
|
|
2207
2405
|
scheduled_price_at timestamp with time zone,
|
|
2208
2406
|
average_rating numeric(3,2) DEFAULT 0.00 NOT NULL,
|
|
2209
2407
|
total_reviews integer DEFAULT 0 NOT NULL,
|
|
2408
|
+
custom_canonical text,
|
|
2409
|
+
published_at timestamp with time zone,
|
|
2410
|
+
version integer DEFAULT 1 NOT NULL,
|
|
2210
2411
|
CONSTRAINT products_payment_provider_check CHECK ((payment_provider = ANY (ARRAY['stripe'::text, 'freemius'::text]))),
|
|
2211
2412
|
CONSTRAINT products_prices_is_valid CHECK (public.is_valid_currency_amount_map(prices)),
|
|
2212
2413
|
CONSTRAINT products_product_type_check CHECK ((product_type = ANY (ARRAY['physical'::text, 'digital'::text]))),
|
|
@@ -2237,13 +2438,16 @@ COMMENT ON COLUMN public.products.average_rating IS 'Aggregated average 1-5 rati
|
|
|
2237
2438
|
|
|
2238
2439
|
COMMENT ON COLUMN public.products.total_reviews IS 'Total count of approved reviews for this product.';
|
|
2239
2440
|
|
|
2441
|
+
COMMENT ON COLUMN public.products.published_at IS 'Optional go-live moment. NULL = live as soon as status is active. A future value withholds the product from public reads until it passes (status stays "active"; the CMS renders that pair as "Scheduled").';
|
|
2442
|
+
|
|
2443
|
+
COMMENT ON COLUMN public.products.version IS 'Monotonic version number for hybrid revisions.';
|
|
2444
|
+
|
|
2240
2445
|
CREATE TABLE IF NOT EXISTS public.profiles (
|
|
2241
2446
|
id uuid NOT NULL,
|
|
2242
2447
|
updated_at timestamp with time zone,
|
|
2243
2448
|
full_name text,
|
|
2244
2449
|
avatar_url text,
|
|
2245
2450
|
website text,
|
|
2246
|
-
github_username text,
|
|
2247
2451
|
phone text,
|
|
2248
2452
|
role public.user_role DEFAULT 'USER'::public.user_role NOT NULL
|
|
2249
2453
|
);
|
|
@@ -2308,6 +2512,43 @@ CREATE TABLE IF NOT EXISTS public.shipping_zones (
|
|
|
2308
2512
|
updated_at timestamp with time zone DEFAULT now()
|
|
2309
2513
|
);
|
|
2310
2514
|
|
|
2515
|
+
CREATE TABLE IF NOT EXISTS public.site_script_revisions (
|
|
2516
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2517
|
+
script_id uuid,
|
|
2518
|
+
script_name text NOT NULL,
|
|
2519
|
+
revision_type text NOT NULL,
|
|
2520
|
+
actor_user_id uuid,
|
|
2521
|
+
source text DEFAULT 'cms'::text NOT NULL,
|
|
2522
|
+
summary text,
|
|
2523
|
+
snapshot jsonb NOT NULL,
|
|
2524
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2525
|
+
CONSTRAINT site_script_revisions_snapshot_is_object_check CHECK ((jsonb_typeof(snapshot) = 'object'::text)),
|
|
2526
|
+
CONSTRAINT site_script_revisions_source_check CHECK ((source = ANY (ARRAY['cms'::text, 'mcp'::text]))),
|
|
2527
|
+
CONSTRAINT site_script_revisions_type_check CHECK ((revision_type = ANY (ARRAY['create'::text, 'update'::text, 'delete'::text, 'revert'::text])))
|
|
2528
|
+
);
|
|
2529
|
+
|
|
2530
|
+
COMMENT ON TABLE public.site_script_revisions IS 'Append-only audit log and undo history for site_scripts. Each row is a restorable snapshot. UPDATE and DELETE are blocked by trigger, including for the service role.';
|
|
2531
|
+
|
|
2532
|
+
CREATE TABLE IF NOT EXISTS public.site_scripts (
|
|
2533
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2534
|
+
name text NOT NULL,
|
|
2535
|
+
description text,
|
|
2536
|
+
code text DEFAULT ''::text NOT NULL,
|
|
2537
|
+
src text,
|
|
2538
|
+
placement text DEFAULT 'body_end'::text NOT NULL,
|
|
2539
|
+
load_strategy text DEFAULT 'default'::text NOT NULL,
|
|
2540
|
+
is_active boolean DEFAULT false NOT NULL,
|
|
2541
|
+
sort_order integer DEFAULT 0 NOT NULL,
|
|
2542
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2543
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2544
|
+
CONSTRAINT site_scripts_has_payload_check CHECK (((src IS NOT NULL) OR (length(btrim(code)) > 0))),
|
|
2545
|
+
CONSTRAINT site_scripts_load_strategy_check CHECK ((load_strategy = ANY (ARRAY['default'::text, 'defer'::text, 'async'::text]))),
|
|
2546
|
+
CONSTRAINT site_scripts_placement_check CHECK ((placement = ANY (ARRAY['head'::text, 'body_start'::text, 'body_end'::text]))),
|
|
2547
|
+
CONSTRAINT site_scripts_src_scheme_check CHECK (((src IS NULL) OR (src ~ '^https://'::text)))
|
|
2548
|
+
);
|
|
2549
|
+
|
|
2550
|
+
COMMENT ON TABLE public.site_scripts IS 'Admin-authored JavaScript injected into the public site by the root layout, with the request CSP nonce applied. Only is_active rows are publicly readable; only ADMIN may write. Distinct from privacy_settings.custom_scripts, which is consent-gated marketing tags.';
|
|
2551
|
+
|
|
2311
2552
|
CREATE TABLE IF NOT EXISTS public.site_settings (
|
|
2312
2553
|
key text NOT NULL,
|
|
2313
2554
|
value jsonb
|
|
@@ -2315,6 +2556,28 @@ CREATE TABLE IF NOT EXISTS public.site_settings (
|
|
|
2315
2556
|
|
|
2316
2557
|
COMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings. Sensitive keys (Cortex AI BYOK, Bot Protection Secret, Email secret, Payment secret) hold encrypted envelopes and are restricted to ADMIN via row-level policies.';
|
|
2317
2558
|
|
|
2559
|
+
CREATE TABLE IF NOT EXISTS public.site_themes (
|
|
2560
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2561
|
+
slug text NOT NULL,
|
|
2562
|
+
name text NOT NULL,
|
|
2563
|
+
description text,
|
|
2564
|
+
icon text DEFAULT 'Palette'::text NOT NULL,
|
|
2565
|
+
color_scheme text DEFAULT 'light'::text NOT NULL,
|
|
2566
|
+
tokens jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
2567
|
+
extra_css text,
|
|
2568
|
+
is_system boolean DEFAULT false NOT NULL,
|
|
2569
|
+
is_default boolean DEFAULT false NOT NULL,
|
|
2570
|
+
is_active boolean DEFAULT true NOT NULL,
|
|
2571
|
+
sort_order integer DEFAULT 0 NOT NULL,
|
|
2572
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2573
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2574
|
+
CONSTRAINT site_themes_color_scheme_check CHECK ((color_scheme = ANY (ARRAY['light'::text, 'dark'::text]))),
|
|
2575
|
+
CONSTRAINT site_themes_slug_format_check CHECK ((slug ~ '^[a-z][a-z0-9-]{0,38}[a-z0-9]$'::text)),
|
|
2576
|
+
CONSTRAINT site_themes_tokens_is_object_check CHECK ((jsonb_typeof(tokens) = 'object'::text))
|
|
2577
|
+
);
|
|
2578
|
+
|
|
2579
|
+
COMMENT ON TABLE public.site_themes IS 'Editable colour themes. Each row renders to a `:root.<slug>` CSS rule injected by the root layout. Publicly readable (anonymous visitors need the palette); only ADMIN may write.';
|
|
2580
|
+
|
|
2318
2581
|
CREATE TABLE IF NOT EXISTS public.system_alerts (
|
|
2319
2582
|
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2320
2583
|
alert_type text NOT NULL,
|
|
@@ -2368,6 +2631,23 @@ COMMENT ON COLUMN public.tax_rates.tax_name IS 'Display name for the tax compone
|
|
|
2368
2631
|
|
|
2369
2632
|
COMMENT ON COLUMN public.tax_rates.tax_rate IS 'Percent value, not decimal fraction. Example: 5.0000 means 5%.';
|
|
2370
2633
|
|
|
2634
|
+
CREATE TABLE IF NOT EXISTS public.thread_messages (
|
|
2635
|
+
id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
2636
|
+
thread_id uuid NOT NULL,
|
|
2637
|
+
direction text NOT NULL,
|
|
2638
|
+
body text NOT NULL,
|
|
2639
|
+
author_id uuid,
|
|
2640
|
+
author_name text,
|
|
2641
|
+
email_delivered boolean DEFAULT false NOT NULL,
|
|
2642
|
+
email_error text,
|
|
2643
|
+
ip_masked text,
|
|
2644
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
2645
|
+
CONSTRAINT thread_messages_body_not_blank CHECK ((char_length(btrim(body)) > 0)),
|
|
2646
|
+
CONSTRAINT thread_messages_direction_check CHECK ((direction = ANY (ARRAY['inbound'::text, 'outbound'::text])))
|
|
2647
|
+
);
|
|
2648
|
+
|
|
2649
|
+
COMMENT ON TABLE public.thread_messages IS 'Turns of a private conversation. Content is append-only; only the delivery flags may change after insert.';
|
|
2650
|
+
|
|
2371
2651
|
CREATE TABLE IF NOT EXISTS public.translations (
|
|
2372
2652
|
key text NOT NULL,
|
|
2373
2653
|
translations jsonb NOT NULL,
|