@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextblock-cms/db",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "main": "index.cjs.js",
5
5
  "module": "index.es.js",
6
6
  "types": "index.d.ts",
@@ -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);