@nextblock-cms/db 0.2.22 → 0.2.24
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/00000000000000_setup_extensions_and_roles.sql +54 -0
- package/supabase/migrations/00000000000001_setup_site_settings.sql +19 -0
- package/supabase/migrations/00000000000002_setup_profiles.sql +87 -0
- package/supabase/migrations/00000000000003_setup_languages.sql +43 -0
- package/supabase/migrations/00000000000004_setup_media.sql +54 -0
- package/supabase/migrations/00000000000005_setup_posts.sql +56 -0
- package/supabase/migrations/00000000000006_setup_pages.sql +51 -0
- package/supabase/migrations/00000000000007_setup_blocks.sql +47 -0
- package/supabase/migrations/00000000000008_setup_navigation.sql +51 -0
- package/supabase/migrations/00000000000009_setup_logos.sql +13 -0
- package/supabase/migrations/00000000000010_setup_translations.sql +29 -0
- package/supabase/migrations/00000000000011_setup_revisions.sql +38 -0
- package/supabase/migrations/00000000000012_setup_rls_policies.sql +239 -0
- package/supabase/migrations/00000000000013_seed_data.sql +139 -0
- package/supabase/migrations/00000000000014_seed_homepage_blocks.sql +648 -0
- package/supabase/migrations/00000000000015_seed_how_it_works.sql +89 -0
package/package.json
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- 00000000000000_setup_extensions_and_roles.sql
|
|
2
|
+
-- Base setup: Extensions, Enums, Helper Functions, and Grants
|
|
3
|
+
|
|
4
|
+
-- 1. Grants
|
|
5
|
+
GRANT USAGE ON SCHEMA public TO postgres;
|
|
6
|
+
GRANT USAGE ON SCHEMA public TO anon;
|
|
7
|
+
GRANT USAGE ON SCHEMA public TO authenticated;
|
|
8
|
+
GRANT USAGE ON SCHEMA public TO service_role;
|
|
9
|
+
|
|
10
|
+
-- 2. Enums
|
|
11
|
+
DO $$
|
|
12
|
+
BEGIN
|
|
13
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_role') THEN
|
|
14
|
+
CREATE TYPE public.user_role AS ENUM ('ADMIN', 'WRITER', 'USER');
|
|
15
|
+
END IF;
|
|
16
|
+
|
|
17
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'page_status') THEN
|
|
18
|
+
CREATE TYPE public.page_status AS ENUM ('draft', 'published', 'archived');
|
|
19
|
+
END IF;
|
|
20
|
+
|
|
21
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'menu_location') THEN
|
|
22
|
+
CREATE TYPE public.menu_location AS ENUM ('HEADER', 'FOOTER', 'SIDEBAR');
|
|
23
|
+
END IF;
|
|
24
|
+
|
|
25
|
+
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'revision_type') THEN
|
|
26
|
+
CREATE TYPE public.revision_type AS ENUM ('snapshot', 'diff');
|
|
27
|
+
END IF;
|
|
28
|
+
END
|
|
29
|
+
$$;
|
|
30
|
+
|
|
31
|
+
-- 3. Helper Functions
|
|
32
|
+
|
|
33
|
+
-- Function: get_my_claim
|
|
34
|
+
-- Description: Helper to read JWT claims safely
|
|
35
|
+
CREATE OR REPLACE FUNCTION get_my_claim(claim TEXT)
|
|
36
|
+
RETURNS JSONB AS $$
|
|
37
|
+
SET search_path = '';
|
|
38
|
+
SELECT COALESCE(current_setting('request.jwt.claims', true)::JSONB ->> claim, NULL)::JSONB
|
|
39
|
+
$$ LANGUAGE SQL STABLE;
|
|
40
|
+
|
|
41
|
+
-- Function: get_current_user_role
|
|
42
|
+
-- Description: Fetches the role of the currently authenticated user.
|
|
43
|
+
-- SECURITY DEFINER to prevent RLS recursion issues when used in policies.
|
|
44
|
+
-- Note: This depends on the 'profiles' table which will be created in the next migration.
|
|
45
|
+
-- However, since functions are just definitions, this CREATE statement will succeed
|
|
46
|
+
-- as long as the table exists when the function is *called*.
|
|
47
|
+
-- To be safe and avoid "relation does not exist" errors during creation if validation runs,
|
|
48
|
+
-- we will defer the creation of this specific function to the profiles migration
|
|
49
|
+
-- OR just ensure profiles is created immediately after.
|
|
50
|
+
-- Actually, let's put it here but be aware it needs profiles table to run.
|
|
51
|
+
-- Postgres allows creating functions referring to non-existent tables? No, it usually checks.
|
|
52
|
+
-- So I will move `get_current_user_role` to `setup_profiles.sql` or create a placeholder table?
|
|
53
|
+
-- Better: I'll put it in `setup_profiles.sql` after the table is created.
|
|
54
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- 00000000000001_setup_site_settings.sql
|
|
2
|
+
-- Setup site_settings table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.site_settings (
|
|
5
|
+
key TEXT PRIMARY KEY,
|
|
6
|
+
value JSONB
|
|
7
|
+
);
|
|
8
|
+
|
|
9
|
+
COMMENT ON TABLE public.site_settings IS 'Key-value store for global site settings.';
|
|
10
|
+
|
|
11
|
+
-- Seed initial copyright setting
|
|
12
|
+
INSERT INTO public.site_settings (key, value)
|
|
13
|
+
VALUES ('footer_copyright', '{"en": "© {year} Nextblock CMS. All rights reserved.", "fr": "© {year} Nextblock CMS. Tous droits réservés."}')
|
|
14
|
+
ON CONFLICT (key) DO NOTHING;
|
|
15
|
+
|
|
16
|
+
-- Seed initial admin created flag (default false, will be updated by trigger)
|
|
17
|
+
INSERT INTO public.site_settings (key, value)
|
|
18
|
+
VALUES ('is_admin_created', 'false'::jsonb)
|
|
19
|
+
ON CONFLICT (key) DO NOTHING;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
-- 00000000000002_setup_profiles.sql
|
|
2
|
+
-- Setup profiles table and auto-create trigger
|
|
3
|
+
|
|
4
|
+
-- 1. Create profiles table
|
|
5
|
+
CREATE TABLE public.profiles (
|
|
6
|
+
id uuid NOT NULL PRIMARY KEY, -- references auth.users(id)
|
|
7
|
+
updated_at timestamp with time zone,
|
|
8
|
+
username text UNIQUE,
|
|
9
|
+
full_name text,
|
|
10
|
+
avatar_url text,
|
|
11
|
+
website text,
|
|
12
|
+
role public.user_role NOT NULL DEFAULT 'USER',
|
|
13
|
+
|
|
14
|
+
CONSTRAINT username_length CHECK (char_length(username) >= 3)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
-- Foreign key to auth.users
|
|
18
|
+
ALTER TABLE public.profiles
|
|
19
|
+
ADD CONSTRAINT profiles_id_fkey
|
|
20
|
+
FOREIGN KEY (id)
|
|
21
|
+
REFERENCES auth.users (id)
|
|
22
|
+
ON DELETE CASCADE;
|
|
23
|
+
|
|
24
|
+
COMMENT ON TABLE public.profiles IS 'Profile information for each user, extending auth.users.';
|
|
25
|
+
COMMENT ON COLUMN public.profiles.id IS 'References auth.users.id';
|
|
26
|
+
COMMENT ON COLUMN public.profiles.role IS 'User role for RBAC.';
|
|
27
|
+
|
|
28
|
+
-- 2. Helper Function: get_current_user_role
|
|
29
|
+
-- Now that profiles table exists, we can define this function.
|
|
30
|
+
CREATE OR REPLACE FUNCTION public.get_current_user_role()
|
|
31
|
+
RETURNS public.user_role
|
|
32
|
+
LANGUAGE sql
|
|
33
|
+
STABLE
|
|
34
|
+
SECURITY DEFINER
|
|
35
|
+
SET search_path = public
|
|
36
|
+
AS $$
|
|
37
|
+
SELECT role FROM public.profiles WHERE id = auth.uid();
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
COMMENT ON FUNCTION public.get_current_user_role() IS 'Fetches the role of the currently authenticated user. SECURITY DEFINER to prevent RLS recursion issues.';
|
|
41
|
+
|
|
42
|
+
-- 3. Trigger: handle_new_user
|
|
43
|
+
-- Automatically creates a profile when a new user signs up.
|
|
44
|
+
-- Assigns 'ADMIN' to the first user, 'USER' to subsequent users.
|
|
45
|
+
CREATE OR REPLACE FUNCTION public.handle_new_user()
|
|
46
|
+
RETURNS TRIGGER
|
|
47
|
+
LANGUAGE plpgsql
|
|
48
|
+
SECURITY DEFINER
|
|
49
|
+
SET search_path = 'public'
|
|
50
|
+
AS $$
|
|
51
|
+
DECLARE
|
|
52
|
+
admin_flag_set BOOLEAN := FALSE;
|
|
53
|
+
user_role public.user_role;
|
|
54
|
+
BEGIN
|
|
55
|
+
-- Ensure the admin flag row exists (redundant if seeded, but safe)
|
|
56
|
+
INSERT INTO public.site_settings (key, value)
|
|
57
|
+
VALUES ('is_admin_created', 'false'::jsonb)
|
|
58
|
+
ON CONFLICT (key) DO NOTHING;
|
|
59
|
+
|
|
60
|
+
-- Lock and read the flag
|
|
61
|
+
SELECT COALESCE((value)::jsonb::boolean, FALSE)
|
|
62
|
+
INTO admin_flag_set
|
|
63
|
+
FROM public.site_settings
|
|
64
|
+
WHERE key = 'is_admin_created'
|
|
65
|
+
FOR UPDATE;
|
|
66
|
+
|
|
67
|
+
IF admin_flag_set = FALSE THEN
|
|
68
|
+
user_role := 'ADMIN'::public.user_role;
|
|
69
|
+
UPDATE public.site_settings
|
|
70
|
+
SET value = 'true'::jsonb
|
|
71
|
+
WHERE key = 'is_admin_created';
|
|
72
|
+
ELSE
|
|
73
|
+
user_role := 'USER'::public.user_role;
|
|
74
|
+
END IF;
|
|
75
|
+
|
|
76
|
+
INSERT INTO public.profiles (id, role)
|
|
77
|
+
VALUES (NEW.id, user_role);
|
|
78
|
+
|
|
79
|
+
RETURN NEW;
|
|
80
|
+
END;
|
|
81
|
+
$$;
|
|
82
|
+
|
|
83
|
+
-- Attach trigger to auth.users
|
|
84
|
+
DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users;
|
|
85
|
+
CREATE TRIGGER on_auth_user_created
|
|
86
|
+
AFTER INSERT ON auth.users
|
|
87
|
+
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- 00000000000003_setup_languages.sql
|
|
2
|
+
-- Setup languages table
|
|
3
|
+
|
|
4
|
+
-- 1. Create languages table
|
|
5
|
+
CREATE TABLE public.languages (
|
|
6
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
7
|
+
code text NOT NULL UNIQUE, -- e.g., 'en', 'fr'
|
|
8
|
+
name text NOT NULL, -- e.g., 'English', 'Français'
|
|
9
|
+
is_default boolean NOT NULL DEFAULT false,
|
|
10
|
+
is_active boolean DEFAULT true, -- Added from later migration
|
|
11
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
12
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
COMMENT ON TABLE public.languages IS 'Stores supported languages for the CMS.';
|
|
16
|
+
COMMENT ON COLUMN public.languages.code IS 'BCP 47 language code.';
|
|
17
|
+
|
|
18
|
+
-- 2. Indexes
|
|
19
|
+
CREATE UNIQUE INDEX ensure_single_default_language_idx
|
|
20
|
+
ON public.languages (is_default)
|
|
21
|
+
WHERE (is_default = true);
|
|
22
|
+
|
|
23
|
+
-- 3. Seed initial languages
|
|
24
|
+
INSERT INTO public.languages (code, name, is_default, is_active)
|
|
25
|
+
VALUES
|
|
26
|
+
('en', 'English', true, true),
|
|
27
|
+
('fr', 'Français', false, true);
|
|
28
|
+
|
|
29
|
+
-- 4. Trigger: handle_languages_update
|
|
30
|
+
CREATE OR REPLACE FUNCTION public.handle_languages_update()
|
|
31
|
+
RETURNS TRIGGER
|
|
32
|
+
LANGUAGE plpgsql
|
|
33
|
+
AS $$
|
|
34
|
+
BEGIN
|
|
35
|
+
NEW.updated_at = now();
|
|
36
|
+
RETURN NEW;
|
|
37
|
+
END;
|
|
38
|
+
$$;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER on_languages_update
|
|
41
|
+
BEFORE UPDATE ON public.languages
|
|
42
|
+
FOR EACH ROW
|
|
43
|
+
EXECUTE PROCEDURE public.handle_languages_update();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
-- 00000000000004_setup_media.sql
|
|
2
|
+
-- Setup media table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.media (
|
|
5
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
6
|
+
uploader_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
7
|
+
file_name text NOT NULL,
|
|
8
|
+
object_key text NOT NULL UNIQUE,
|
|
9
|
+
file_type text,
|
|
10
|
+
size_bytes bigint,
|
|
11
|
+
description text,
|
|
12
|
+
|
|
13
|
+
-- Added columns
|
|
14
|
+
width integer,
|
|
15
|
+
height integer,
|
|
16
|
+
blur_data_url text,
|
|
17
|
+
variants jsonb,
|
|
18
|
+
file_path text,
|
|
19
|
+
folder text,
|
|
20
|
+
|
|
21
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
22
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
COMMENT ON TABLE public.media IS 'Stores information about uploaded media assets.';
|
|
26
|
+
COMMENT ON COLUMN public.media.object_key IS 'Unique key (path) in Cloudflare R2.';
|
|
27
|
+
COMMENT ON COLUMN public.media.width IS 'Width of the image in pixels.';
|
|
28
|
+
COMMENT ON COLUMN public.media.height IS 'Height of the image in pixels.';
|
|
29
|
+
COMMENT ON COLUMN public.media.blur_data_url IS 'Base64 encoded string for image blur placeholders.';
|
|
30
|
+
COMMENT ON COLUMN public.media.variants IS 'Array of image variant objects.';
|
|
31
|
+
COMMENT ON COLUMN public.media.file_path IS 'Full path to the file in the storage bucket.';
|
|
32
|
+
COMMENT ON COLUMN public.media.folder IS 'Folder path prefix for the R2 object.';
|
|
33
|
+
|
|
34
|
+
-- Indexes
|
|
35
|
+
CREATE INDEX idx_media_uploader_id ON public.media(uploader_id);
|
|
36
|
+
CREATE INDEX media_folder_idx ON public.media(folder);
|
|
37
|
+
|
|
38
|
+
-- Trigger: handle_media_update
|
|
39
|
+
CREATE OR REPLACE FUNCTION public.handle_media_update()
|
|
40
|
+
RETURNS TRIGGER
|
|
41
|
+
LANGUAGE plpgsql
|
|
42
|
+
SECURITY DEFINER
|
|
43
|
+
SET search_path = public
|
|
44
|
+
AS $$
|
|
45
|
+
BEGIN
|
|
46
|
+
NEW.updated_at = now();
|
|
47
|
+
RETURN NEW;
|
|
48
|
+
END;
|
|
49
|
+
$$;
|
|
50
|
+
|
|
51
|
+
CREATE TRIGGER on_media_update
|
|
52
|
+
BEFORE UPDATE ON public.media
|
|
53
|
+
FOR EACH ROW
|
|
54
|
+
EXECUTE PROCEDURE public.handle_media_update();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- 00000000000005_setup_posts.sql
|
|
2
|
+
-- Setup posts table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.posts (
|
|
5
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
6
|
+
language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
|
|
7
|
+
author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
8
|
+
title text NOT NULL,
|
|
9
|
+
slug text NOT NULL,
|
|
10
|
+
excerpt text,
|
|
11
|
+
status public.page_status NOT NULL DEFAULT 'draft',
|
|
12
|
+
published_at timestamp with time zone,
|
|
13
|
+
meta_title text,
|
|
14
|
+
meta_description text,
|
|
15
|
+
|
|
16
|
+
-- Added columns
|
|
17
|
+
feature_image_id uuid REFERENCES public.media(id) ON DELETE SET NULL,
|
|
18
|
+
version integer NOT NULL DEFAULT 1,
|
|
19
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
20
|
+
|
|
21
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
22
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
COMMENT ON TABLE public.posts IS 'Stores blog posts or news articles.';
|
|
26
|
+
COMMENT ON COLUMN public.posts.slug IS 'URL-friendly identifier, unique per language.';
|
|
27
|
+
COMMENT ON COLUMN public.posts.feature_image_id IS 'ID of the media item to be used as the feature image.';
|
|
28
|
+
COMMENT ON COLUMN public.posts.version IS 'Monotonic version number for hybrid revisions.';
|
|
29
|
+
COMMENT ON COLUMN public.posts.translation_group_id IS 'Groups different language versions of the same conceptual post.';
|
|
30
|
+
|
|
31
|
+
-- Constraints
|
|
32
|
+
ALTER TABLE public.posts
|
|
33
|
+
ADD CONSTRAINT posts_language_id_slug_key UNIQUE (language_id, slug);
|
|
34
|
+
|
|
35
|
+
-- Indexes
|
|
36
|
+
CREATE INDEX idx_posts_feature_image_id ON public.posts(feature_image_id);
|
|
37
|
+
CREATE INDEX idx_posts_author_id ON public.posts(author_id);
|
|
38
|
+
CREATE INDEX idx_posts_translation_group_id ON public.posts(translation_group_id);
|
|
39
|
+
|
|
40
|
+
-- Trigger: handle_posts_update
|
|
41
|
+
CREATE OR REPLACE FUNCTION public.handle_posts_update()
|
|
42
|
+
RETURNS TRIGGER
|
|
43
|
+
LANGUAGE plpgsql
|
|
44
|
+
SECURITY DEFINER
|
|
45
|
+
SET search_path = public
|
|
46
|
+
AS $$
|
|
47
|
+
BEGIN
|
|
48
|
+
NEW.updated_at = now();
|
|
49
|
+
RETURN NEW;
|
|
50
|
+
END;
|
|
51
|
+
$$;
|
|
52
|
+
|
|
53
|
+
CREATE TRIGGER on_posts_update
|
|
54
|
+
BEFORE UPDATE ON public.posts
|
|
55
|
+
FOR EACH ROW
|
|
56
|
+
EXECUTE PROCEDURE public.handle_posts_update();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- 00000000000006_setup_pages.sql
|
|
2
|
+
-- Setup pages table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.pages (
|
|
5
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
6
|
+
language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
|
|
7
|
+
author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
8
|
+
title text NOT NULL,
|
|
9
|
+
slug text NOT NULL,
|
|
10
|
+
status public.page_status NOT NULL DEFAULT 'draft',
|
|
11
|
+
meta_title text,
|
|
12
|
+
meta_description text,
|
|
13
|
+
|
|
14
|
+
-- Added columns
|
|
15
|
+
version integer NOT NULL DEFAULT 1,
|
|
16
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
17
|
+
|
|
18
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
19
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
COMMENT ON TABLE public.pages IS 'Stores static pages for the website.';
|
|
23
|
+
COMMENT ON COLUMN public.pages.slug IS 'URL-friendly identifier, unique per language.';
|
|
24
|
+
COMMENT ON COLUMN public.pages.version IS 'Monotonic version number for hybrid revisions.';
|
|
25
|
+
COMMENT ON COLUMN public.pages.translation_group_id IS 'Groups different language versions of the same conceptual page.';
|
|
26
|
+
|
|
27
|
+
-- Constraints
|
|
28
|
+
ALTER TABLE public.pages
|
|
29
|
+
ADD CONSTRAINT pages_language_id_slug_key UNIQUE (language_id, slug);
|
|
30
|
+
|
|
31
|
+
-- Indexes
|
|
32
|
+
CREATE INDEX idx_pages_author_id ON public.pages(author_id);
|
|
33
|
+
CREATE INDEX idx_pages_translation_group_id ON public.pages(translation_group_id);
|
|
34
|
+
|
|
35
|
+
-- Trigger: handle_pages_update
|
|
36
|
+
CREATE OR REPLACE FUNCTION public.handle_pages_update()
|
|
37
|
+
RETURNS TRIGGER
|
|
38
|
+
LANGUAGE plpgsql
|
|
39
|
+
SECURITY DEFINER
|
|
40
|
+
SET search_path = public
|
|
41
|
+
AS $$
|
|
42
|
+
BEGIN
|
|
43
|
+
NEW.updated_at = now();
|
|
44
|
+
RETURN NEW;
|
|
45
|
+
END;
|
|
46
|
+
$$;
|
|
47
|
+
|
|
48
|
+
CREATE TRIGGER on_pages_update
|
|
49
|
+
BEFORE UPDATE ON public.pages
|
|
50
|
+
FOR EACH ROW
|
|
51
|
+
EXECUTE PROCEDURE public.handle_pages_update();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- 00000000000007_setup_blocks.sql
|
|
2
|
+
-- Setup blocks table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.blocks (
|
|
5
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
6
|
+
page_id bigint REFERENCES public.pages(id) ON DELETE CASCADE,
|
|
7
|
+
post_id bigint REFERENCES public.posts(id) ON DELETE CASCADE,
|
|
8
|
+
language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
|
|
9
|
+
block_type text NOT NULL,
|
|
10
|
+
content jsonb,
|
|
11
|
+
"order" integer NOT NULL DEFAULT 0,
|
|
12
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
13
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
14
|
+
|
|
15
|
+
CONSTRAINT check_exactly_one_parent CHECK (
|
|
16
|
+
(page_id IS NOT NULL AND post_id IS NULL) OR
|
|
17
|
+
(post_id IS NOT NULL AND page_id IS NULL)
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
COMMENT ON TABLE public.blocks IS 'Stores content blocks for pages and posts.';
|
|
22
|
+
COMMENT ON COLUMN public.blocks.block_type IS 'Type of the block, e.g., "text", "image".';
|
|
23
|
+
COMMENT ON COLUMN public.blocks.content IS 'JSONB content specific to the block_type.';
|
|
24
|
+
COMMENT ON COLUMN public.blocks.order IS 'Sort order of the block.';
|
|
25
|
+
|
|
26
|
+
-- Indexes
|
|
27
|
+
CREATE INDEX idx_blocks_language_id ON public.blocks(language_id);
|
|
28
|
+
CREATE INDEX idx_blocks_page_id ON public.blocks(page_id);
|
|
29
|
+
CREATE INDEX idx_blocks_post_id ON public.blocks(post_id);
|
|
30
|
+
|
|
31
|
+
-- Trigger: handle_blocks_update
|
|
32
|
+
CREATE OR REPLACE FUNCTION public.handle_blocks_update()
|
|
33
|
+
RETURNS TRIGGER
|
|
34
|
+
LANGUAGE plpgsql
|
|
35
|
+
SECURITY DEFINER
|
|
36
|
+
SET search_path = public
|
|
37
|
+
AS $$
|
|
38
|
+
BEGIN
|
|
39
|
+
NEW.updated_at = now();
|
|
40
|
+
RETURN NEW;
|
|
41
|
+
END;
|
|
42
|
+
$$;
|
|
43
|
+
|
|
44
|
+
CREATE TRIGGER on_blocks_update
|
|
45
|
+
BEFORE UPDATE ON public.blocks
|
|
46
|
+
FOR EACH ROW
|
|
47
|
+
EXECUTE PROCEDURE public.handle_blocks_update();
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
-- 00000000000008_setup_navigation.sql
|
|
2
|
+
-- Setup navigation_items table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.navigation_items (
|
|
5
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
6
|
+
language_id bigint NOT NULL REFERENCES public.languages(id) ON DELETE CASCADE,
|
|
7
|
+
menu_key public.menu_location NOT NULL,
|
|
8
|
+
label text NOT NULL,
|
|
9
|
+
url text NOT NULL,
|
|
10
|
+
parent_id bigint REFERENCES public.navigation_items(id) ON DELETE CASCADE,
|
|
11
|
+
"order" integer NOT NULL DEFAULT 0,
|
|
12
|
+
page_id bigint REFERENCES public.pages(id) ON DELETE SET NULL,
|
|
13
|
+
|
|
14
|
+
-- Added columns (if any, checking previous files... none explicitly added but translation_group_id was mentioned in index drop?)
|
|
15
|
+
-- 20250520171900_add_translation_group_to_nav_items.sql was listed.
|
|
16
|
+
-- Let's assume we want it if it was there. I'll double check the file list.
|
|
17
|
+
-- Yes: 20250520171900_add_translation_group_to_nav_items.sql
|
|
18
|
+
-- I should add it.
|
|
19
|
+
translation_group_id uuid DEFAULT gen_random_uuid() NOT NULL,
|
|
20
|
+
|
|
21
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
22
|
+
updated_at timestamp with time zone NOT NULL DEFAULT now()
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
COMMENT ON TABLE public.navigation_items IS 'Stores navigation menu items.';
|
|
26
|
+
COMMENT ON COLUMN public.navigation_items.menu_key IS 'Identifies the menu this item belongs to.';
|
|
27
|
+
|
|
28
|
+
-- Indexes
|
|
29
|
+
CREATE INDEX idx_navigation_items_menu_lang_order ON public.navigation_items (menu_key, language_id, "order");
|
|
30
|
+
CREATE INDEX idx_navigation_items_language_id ON public.navigation_items(language_id);
|
|
31
|
+
CREATE INDEX idx_navigation_items_page_id ON public.navigation_items(page_id);
|
|
32
|
+
CREATE INDEX idx_navigation_items_parent_id ON public.navigation_items(parent_id);
|
|
33
|
+
-- Note: idx_navigation_items_translation_group_id was dropped in optimize_indexes.sql as unused, so I won't create it.
|
|
34
|
+
|
|
35
|
+
-- Trigger: handle_navigation_items_update
|
|
36
|
+
CREATE OR REPLACE FUNCTION public.handle_navigation_items_update()
|
|
37
|
+
RETURNS TRIGGER
|
|
38
|
+
LANGUAGE plpgsql
|
|
39
|
+
SECURITY DEFINER
|
|
40
|
+
SET search_path = public
|
|
41
|
+
AS $$
|
|
42
|
+
BEGIN
|
|
43
|
+
NEW.updated_at = now();
|
|
44
|
+
RETURN NEW;
|
|
45
|
+
END;
|
|
46
|
+
$$;
|
|
47
|
+
|
|
48
|
+
CREATE TRIGGER on_navigation_items_update
|
|
49
|
+
BEFORE UPDATE ON public.navigation_items
|
|
50
|
+
FOR EACH ROW
|
|
51
|
+
EXECUTE PROCEDURE public.handle_navigation_items_update();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-- 00000000000009_setup_logos.sql
|
|
2
|
+
-- Setup logos table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.logos (
|
|
5
|
+
id uuid NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
6
|
+
name text NOT NULL,
|
|
7
|
+
media_id uuid REFERENCES public.media(id) ON DELETE SET NULL,
|
|
8
|
+
created_at timestamp with time zone NOT NULL DEFAULT now()
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
COMMENT ON TABLE public.logos IS 'Stores company and brand logos.';
|
|
12
|
+
COMMENT ON COLUMN public.logos.name IS 'The name of the brand or company for the logo.';
|
|
13
|
+
COMMENT ON COLUMN public.logos.media_id IS 'Foreign key to the media table for the logo image.';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
-- 00000000000010_setup_translations.sql
|
|
2
|
+
-- Setup translations table
|
|
3
|
+
|
|
4
|
+
CREATE TABLE public.translations (
|
|
5
|
+
key text PRIMARY KEY,
|
|
6
|
+
translations jsonb NOT NULL,
|
|
7
|
+
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
|
8
|
+
updated_at timestamp with time zone DEFAULT now() NOT NULL
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
COMMENT ON COLUMN public.translations.key IS 'A unique, slugified identifier (e.g., "sign_in_button_text").';
|
|
12
|
+
COMMENT ON COLUMN public.translations.translations IS 'Stores translations as key-value pairs (e.g., {"en": "Sign In", "fr": "s''inscrire"}).';
|
|
13
|
+
|
|
14
|
+
-- Trigger: set_updated_at
|
|
15
|
+
CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
|
|
16
|
+
RETURNS TRIGGER AS $$
|
|
17
|
+
DECLARE
|
|
18
|
+
_new record;
|
|
19
|
+
BEGIN
|
|
20
|
+
_new := NEW;
|
|
21
|
+
_new."updated_at" = NOW();
|
|
22
|
+
RETURN _new;
|
|
23
|
+
END;
|
|
24
|
+
$$ LANGUAGE plpgsql;
|
|
25
|
+
|
|
26
|
+
CREATE TRIGGER set_updated_at
|
|
27
|
+
BEFORE UPDATE ON public.translations
|
|
28
|
+
FOR EACH ROW
|
|
29
|
+
EXECUTE FUNCTION public.set_current_timestamp_updated_at();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- 00000000000011_setup_revisions.sql
|
|
2
|
+
-- Setup revisions tables
|
|
3
|
+
|
|
4
|
+
-- 1. Page Revisions
|
|
5
|
+
CREATE TABLE public.page_revisions (
|
|
6
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
7
|
+
page_id bigint NOT NULL REFERENCES public.pages(id) ON DELETE CASCADE,
|
|
8
|
+
author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
9
|
+
version integer NOT NULL,
|
|
10
|
+
revision_type public.revision_type NOT NULL,
|
|
11
|
+
content jsonb NOT NULL,
|
|
12
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
13
|
+
CONSTRAINT page_revisions_page_version_key UNIQUE (page_id, version)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
COMMENT ON TABLE public.page_revisions IS 'Hybrid (snapshot/diff) revisions for pages.';
|
|
17
|
+
COMMENT ON COLUMN public.page_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
18
|
+
|
|
19
|
+
CREATE INDEX idx_page_revisions_page_id ON public.page_revisions(page_id);
|
|
20
|
+
CREATE INDEX idx_page_revisions_page_id_version ON public.page_revisions(page_id, version);
|
|
21
|
+
|
|
22
|
+
-- 2. Post Revisions
|
|
23
|
+
CREATE TABLE public.post_revisions (
|
|
24
|
+
id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
25
|
+
post_id bigint NOT NULL REFERENCES public.posts(id) ON DELETE CASCADE,
|
|
26
|
+
author_id uuid REFERENCES public.profiles(id) ON DELETE SET NULL,
|
|
27
|
+
version integer NOT NULL,
|
|
28
|
+
revision_type public.revision_type NOT NULL,
|
|
29
|
+
content jsonb NOT NULL,
|
|
30
|
+
created_at timestamp with time zone NOT NULL DEFAULT now(),
|
|
31
|
+
CONSTRAINT post_revisions_post_version_key UNIQUE (post_id, version)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
COMMENT ON TABLE public.post_revisions IS 'Hybrid (snapshot/diff) revisions for posts.';
|
|
35
|
+
COMMENT ON COLUMN public.post_revisions.content IS 'If snapshot: full content; if diff: JSON Patch array.';
|
|
36
|
+
|
|
37
|
+
CREATE INDEX idx_post_revisions_post_id ON public.post_revisions(post_id);
|
|
38
|
+
CREATE INDEX idx_post_revisions_post_id_version ON public.post_revisions(post_id, version);
|