This commit is contained in:
140
server/migrations/001_initial_schema.sql
Normal file
140
server/migrations/001_initial_schema.sql
Normal file
@@ -0,0 +1,140 @@
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
create type user_role as enum ('student', 'instructor', 'admin');
|
||||
create type course_status as enum ('draft', 'published', 'archived');
|
||||
create type lesson_access_level as enum ('public', 'enrolled');
|
||||
create type media_status as enum ('processing', 'ready', 'failed');
|
||||
create type asset_kind as enum ('document', 'spreadsheet', 'archive', 'image', 'link');
|
||||
|
||||
create table users (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
email text not null unique,
|
||||
password_hash text not null,
|
||||
display_name text not null,
|
||||
role user_role not null default 'student',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table courses (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
slug text not null unique,
|
||||
title text not null,
|
||||
description text not null default '',
|
||||
category text not null,
|
||||
cover_image_url text,
|
||||
status course_status not null default 'draft',
|
||||
instructor_id uuid not null references users(id),
|
||||
published_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint courses_published_at_check check (
|
||||
(status = 'published' and published_at is not null) or status <> 'published'
|
||||
)
|
||||
);
|
||||
|
||||
create table lessons (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
course_id uuid not null references courses(id) on delete cascade,
|
||||
title text not null,
|
||||
description text not null default '',
|
||||
position integer not null check (position > 0),
|
||||
duration_seconds integer check (duration_seconds is null or duration_seconds >= 0),
|
||||
access_level lesson_access_level not null default 'enrolled',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (course_id, position)
|
||||
);
|
||||
|
||||
-- Video delivery is intentionally provider-neutral. A lesson can be moved from
|
||||
-- one provider to another without changing the lesson itself.
|
||||
create table lesson_media (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
lesson_id uuid not null references lessons(id) on delete cascade,
|
||||
provider text not null,
|
||||
external_id text not null,
|
||||
playback_url text,
|
||||
embed_url text,
|
||||
status media_status not null default 'processing',
|
||||
duration_seconds integer check (duration_seconds is null or duration_seconds >= 0),
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (provider, external_id)
|
||||
);
|
||||
|
||||
create table course_enrollments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
course_id uuid not null references courses(id) on delete cascade,
|
||||
user_id uuid not null references users(id) on delete cascade,
|
||||
enrolled_at timestamptz not null default now(),
|
||||
revoked_at timestamptz,
|
||||
unique (course_id, user_id)
|
||||
);
|
||||
|
||||
create table lesson_progress (
|
||||
lesson_id uuid not null references lessons(id) on delete cascade,
|
||||
user_id uuid not null references users(id) on delete cascade,
|
||||
watched_seconds integer not null default 0 check (watched_seconds >= 0),
|
||||
completed_at timestamptz,
|
||||
updated_at timestamptz not null default now(),
|
||||
primary key (lesson_id, user_id)
|
||||
);
|
||||
|
||||
create table assets (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
course_id uuid references courses(id) on delete cascade,
|
||||
lesson_id uuid references lessons(id) on delete cascade,
|
||||
name text not null,
|
||||
kind asset_kind not null,
|
||||
url text not null,
|
||||
size_bytes bigint check (size_bytes is null or size_bytes >= 0),
|
||||
description text not null default '',
|
||||
access_level lesson_access_level not null default 'enrolled',
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
constraint assets_parent_check check (
|
||||
(course_id is not null and lesson_id is null) or (course_id is null and lesson_id is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create table comments (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
course_id uuid not null references courses(id) on delete cascade,
|
||||
lesson_id uuid references lessons(id) on delete set null,
|
||||
author_id uuid not null references users(id),
|
||||
body text not null check (char_length(body) between 1 and 4000),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table comment_replies (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
comment_id uuid not null unique references comments(id) on delete cascade,
|
||||
author_id uuid not null references users(id),
|
||||
body text not null check (char_length(body) between 1 and 4000),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index courses_published_index on courses (published_at desc) where status = 'published';
|
||||
create index lessons_course_position_index on lessons (course_id, position);
|
||||
create index lesson_media_lesson_index on lesson_media (lesson_id);
|
||||
create index enrollments_active_index on course_enrollments (user_id, course_id) where revoked_at is null;
|
||||
create index comments_course_created_index on comments (course_id, created_at desc);
|
||||
|
||||
create function set_updated_at() returns trigger language plpgsql as $$
|
||||
begin
|
||||
new.updated_at = now();
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
create trigger users_set_updated_at before update on users for each row execute function set_updated_at();
|
||||
create trigger courses_set_updated_at before update on courses for each row execute function set_updated_at();
|
||||
create trigger lessons_set_updated_at before update on lessons for each row execute function set_updated_at();
|
||||
create trigger lesson_media_set_updated_at before update on lesson_media for each row execute function set_updated_at();
|
||||
create trigger lesson_progress_set_updated_at before update on lesson_progress for each row execute function set_updated_at();
|
||||
create trigger assets_set_updated_at before update on assets for each row execute function set_updated_at();
|
||||
create trigger comments_set_updated_at before update on comments for each row execute function set_updated_at();
|
||||
create trigger comment_replies_set_updated_at before update on comment_replies for each row execute function set_updated_at();
|
||||
Reference in New Issue
Block a user