Fitaru
Fitaru Supabase Initial Migration
SQL migration awal untuk schema database Fitaru di Supabase/PostgreSQL.
57 dari 57 materi Project fitarusupabasesqlmigration
Fitaru Supabase Initial Migration
File ini adalah SQL migration awal untuk schema database Fitaru.
-- Fitaru MVP initial database schema
-- Target: Supabase PostgreSQL
create extension if not exists "pgcrypto";
-- Enums
do $$ begin
create type account_status as enum ('active', 'suspended', 'deleted');
exception when duplicate_object then null;
end $$;
do $$ begin
create type admin_status as enum ('active', 'inactive');
exception when duplicate_object then null;
end $$;
do $$ begin
create type admin_role as enum ('super_admin', 'content_admin', 'support_admin');
exception when duplicate_object then null;
end $$;
do $$ begin
create type auth_provider as enum ('google', 'apple', 'email', 'phone');
exception when duplicate_object then null;
end $$;
do $$ begin
create type gender_type as enum ('male', 'female', 'other');
exception when duplicate_object then null;
end $$;
do $$ begin
create type user_goal as enum ('lose_weight', 'maintain_weight', 'gain_muscle', 'healthier');
exception when duplicate_object then null;
end $$;
do $$ begin
create type diet_style as enum ('relaxed', 'calorie_deficit', 'high_protein', 'low_sugar', 'low_fried', 'fasting', 'custom');
exception when duplicate_object then null;
end $$;
do $$ begin
create type activity_level as enum ('sedentary', 'light', 'active', 'very_active');
exception when duplicate_object then null;
end $$;
do $$ begin
create type meal_time as enum ('breakfast', 'lunch', 'dinner', 'snack', 'drink');
exception when duplicate_object then null;
end $$;
do $$ begin
create type portion_size as enum ('small', 'medium', 'large');
exception when duplicate_object then null;
end $$;
do $$ begin
create type intensity_level as enum ('light', 'medium', 'heavy');
exception when duplicate_object then null;
end $$;
do $$ begin
create type record_status as enum ('active', 'inactive');
exception when duplicate_object then null;
end $$;
do $$ begin
create type article_status as enum ('draft', 'review', 'published', 'archived');
exception when duplicate_object then null;
end $$;
do $$ begin
create type notification_status as enum ('draft', 'scheduled', 'sent', 'cancelled');
exception when duplicate_object then null;
end $$;
do $$ begin
create type notification_segment as enum ('all', 'active', 'inactive_7_days', 'lose_weight', 'no_meal_today');
exception when duplicate_object then null;
end $$;
do $$ begin
create type delivery_status as enum ('pending', 'sent', 'failed', 'opened');
exception when duplicate_object then null;
end $$;
do $$ begin
create type feedback_type as enum ('bug', 'suggestion', 'content_request', 'other');
exception when duplicate_object then null;
end $$;
do $$ begin
create type feedback_status as enum ('open', 'reviewed', 'resolved', 'archived');
exception when duplicate_object then null;
end $$;
do $$ begin
create type audit_action as enum ('create', 'update', 'delete', 'publish', 'suspend');
exception when duplicate_object then null;
end $$;
-- Utility
create or replace function set_updated_at()
returns trigger as $$
begin
new.updated_at = now();
return new;
end;
$$ language plpgsql;
-- Users
create table if not exists users (
id uuid primary key default gen_random_uuid(),
email text unique,
phone text unique,
auth_provider auth_provider not null default 'email',
status account_status not null default 'active',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint users_contact_required check (email is not null or phone is not null)
);
create trigger users_set_updated_at
before update on users
for each row execute function set_updated_at();
create table if not exists user_profiles (
id uuid primary key default gen_random_uuid(),
user_id uuid not null unique references users(id) on delete cascade,
display_name text not null,
gender gender_type,
birth_year int,
height_cm numeric(5, 2),
current_weight_kg numeric(5, 2),
target_weight_kg numeric(5, 2),
goal user_goal not null default 'healthier',
diet_style diet_style not null default 'relaxed',
activity_level activity_level not null default 'light',
timezone text not null default 'Asia/Jakarta',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint user_profiles_birth_year_check check (birth_year is null or birth_year between 1900 and 2100),
constraint user_profiles_height_check check (height_cm is null or height_cm > 0),
constraint user_profiles_weight_check check (current_weight_kg is null or current_weight_kg > 0),
constraint user_profiles_target_weight_check check (target_weight_kg is null or target_weight_kg > 0)
);
create trigger user_profiles_set_updated_at
before update on user_profiles
for each row execute function set_updated_at();
create table if not exists daily_targets (
id uuid primary key default gen_random_uuid(),
user_id uuid not null unique references users(id) on delete cascade,
water_glasses_target int not null default 8,
exercise_weekly_target int not null default 3,
calorie_target int,
weigh_in_days text[] not null default array['monday', 'thursday'],
is_calorie_tracking_enabled boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint daily_targets_water_check check (water_glasses_target between 1 and 30),
constraint daily_targets_exercise_check check (exercise_weekly_target between 0 and 14),
constraint daily_targets_calorie_check check (calorie_target is null or calorie_target > 0)
);
create trigger daily_targets_set_updated_at
before update on daily_targets
for each row execute function set_updated_at();
-- Admin CMS
create table if not exists admin_users (
id uuid primary key default gen_random_uuid(),
name text not null,
email text not null unique,
role admin_role not null default 'support_admin',
status admin_status not null default 'active',
last_login_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create trigger admin_users_set_updated_at
before update on admin_users
for each row execute function set_updated_at();
-- Reference data
create table if not exists food_items (
id uuid primary key default gen_random_uuid(),
name text not null,
category text not null default 'other',
default_portion portion_size not null default 'medium',
calories_per_portion int,
protein_g numeric(6, 2),
carbs_g numeric(6, 2),
fat_g numeric(6, 2),
notes text,
status record_status not null default 'active',
created_by uuid references admin_users(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint food_items_calories_check check (calories_per_portion is null or calories_per_portion >= 0),
constraint food_items_macro_check check (
(protein_g is null or protein_g >= 0)
and (carbs_g is null or carbs_g >= 0)
and (fat_g is null or fat_g >= 0)
)
);
create trigger food_items_set_updated_at
before update on food_items
for each row execute function set_updated_at();
create table if not exists exercise_items (
id uuid primary key default gen_random_uuid(),
name text not null,
category text not null default 'other',
default_intensity intensity_level not null default 'medium',
default_duration_minutes int not null default 30,
calories_per_30_minutes int,
notes text,
status record_status not null default 'active',
created_by uuid references admin_users(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint exercise_items_duration_check check (default_duration_minutes > 0),
constraint exercise_items_calories_check check (calories_per_30_minutes is null or calories_per_30_minutes >= 0)
);
create trigger exercise_items_set_updated_at
before update on exercise_items
for each row execute function set_updated_at();
-- Activity logs
create table if not exists meal_logs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade,
food_item_id uuid references food_items(id) on delete set null,
meal_time meal_time not null,
food_name text not null,
portion_size portion_size not null default 'medium',
estimated_calories int,
photo_url text,
note text,
logged_at timestamptz not null default now(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint meal_logs_calories_check check (estimated_calories is null or estimated_calories >= 0)
);
create trigger meal_logs_set_updated_at
before update on meal_logs
for each row execute function set_updated_at();
create table if not exists exercise_logs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade,
exercise_item_id uuid references exercise_items(id) on delete set null,
exercise_name text not null,
duration_minutes int not null,
intensity intensity_level not null default 'medium',
estimated_calories_burned int,
note text,
logged_at timestamptz not null default now(),
created_at timestamptz not null default now(),
updated_at timestamptz not null default now(),
constraint exercise_logs_duration_check check (duration_minutes > 0),
constraint exercise_logs_calories_check check (estimated_calories_burned is null or estimated_calories_burned >= 0)
);
create trigger exercise_logs_set_updated_at
before update on exercise_logs
for each row execute function set_updated_at();
create table if not exists weight_logs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade,
weight_kg numeric(5, 2) not null,
progress_photo_url text,
note text,
logged_at timestamptz not null default now(),
created_at timestamptz not null default now(),
constraint weight_logs_weight_check check (weight_kg > 0)
);
create table if not exists water_logs (
id uuid primary key default gen_random_uuid(),
user_id uuid not null references users(id) on delete cascade,
glasses int not null default 1,
logged_at timestamptz not null default now(),
created_at timestamptz not null default now(),
constraint water_logs_glasses_check check (glasses > 0)
);
-- Content CMS
create table if not exists content_categories (
id uuid primary key default gen_random_uuid(),
name text not null,
slug text not null unique,
description text,
status record_status not null default 'active',
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create trigger content_categories_set_updated_at
before update on content_categories
for each row execute function set_updated_at();
create table if not exists articles (
id uuid primary key default gen_random_uuid(),
category_id uuid references content_categories(id) on delete set null,
title text not null,
slug text not null unique,
summary text not null,
content text not null,
thumbnail_url text,
status article_status not null default 'draft',
author_id uuid references admin_users(id) on delete set null,
published_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create trigger articles_set_updated_at
before update on articles
for each row execute function set_updated_at();
-- Notifications
create table if not exists notifications (
id uuid primary key default gen_random_uuid(),
title text not null,
message text not null,
target_segment notification_segment not null default 'all',
scheduled_at timestamptz,
sent_at timestamptz,
status notification_status not null default 'draft',
created_by uuid references admin_users(id) on delete set null,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create trigger notifications_set_updated_at
before update on notifications
for each row execute function set_updated_at();
create table if not exists notification_deliveries (
id uuid primary key default gen_random_uuid(),
notification_id uuid not null references notifications(id) on delete cascade,
user_id uuid not null references users(id) on delete cascade,
delivery_status delivery_status not null default 'pending',
sent_at timestamptz,
opened_at timestamptz,
created_at timestamptz not null default now(),
unique (notification_id, user_id)
);
-- Feedback
create table if not exists feedback (
id uuid primary key default gen_random_uuid(),
user_id uuid references users(id) on delete set null,
type feedback_type not null default 'other',
subject text not null,
message text not null,
status feedback_status not null default 'open',
admin_note text,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
create trigger feedback_set_updated_at
before update on feedback
for each row execute function set_updated_at();
-- Audit logs
create table if not exists audit_logs (
id uuid primary key default gen_random_uuid(),
admin_user_id uuid references admin_users(id) on delete set null,
action audit_action not null,
resource_type text not null,
resource_id uuid,
metadata jsonb not null default '{}'::jsonb,
created_at timestamptz not null default now()
);
-- Indexes
create index if not exists idx_users_status_created_at on users(status, created_at);
create index if not exists idx_user_profiles_goal on user_profiles(goal);
create index if not exists idx_meal_logs_user_logged_at on meal_logs(user_id, logged_at desc);
create index if not exists idx_meal_logs_food_name on meal_logs(food_name);
create index if not exists idx_exercise_logs_user_logged_at on exercise_logs(user_id, logged_at desc);
create index if not exists idx_weight_logs_user_logged_at on weight_logs(user_id, logged_at desc);
create index if not exists idx_water_logs_user_logged_at on water_logs(user_id, logged_at desc);
create index if not exists idx_food_items_name on food_items(name);
create index if not exists idx_food_items_status_category on food_items(status, category);
create index if not exists idx_exercise_items_name on exercise_items(name);
create index if not exists idx_exercise_items_status_category on exercise_items(status, category);
create index if not exists idx_content_categories_status on content_categories(status);
create index if not exists idx_articles_status_published_at on articles(status, published_at desc);
create index if not exists idx_articles_slug on articles(slug);
create index if not exists idx_notifications_status_scheduled_at on notifications(status, scheduled_at);
create index if not exists idx_notification_deliveries_user_status on notification_deliveries(user_id, delivery_status);
create index if not exists idx_feedback_status_created_at on feedback(status, created_at desc);
create index if not exists idx_audit_logs_admin_created_at on audit_logs(admin_user_id, created_at desc);
create index if not exists idx_audit_logs_resource on audit_logs(resource_type, resource_id);
-- Seed content categories
insert into content_categories (name, slug, description)
values
('Makan santai', 'makan-santai', 'Tips makan lebih sadar tanpa diet ekstrem.'),
('Olahraga pemula', 'olahraga-pemula', 'Panduan gerak ringan untuk pemula.'),
('Kurangi gula', 'kurangi-gula', 'Tips mengurangi gula secara realistis.'),
('Meal prep', 'meal-prep', 'Ide persiapan makanan sederhana.'),
('Tidur dan recovery', 'tidur-recovery', 'Tips tidur, recovery, dan ritme tubuh.')
on conflict (slug) do nothing;