764 lines
36 KiB
PL/PgSQL
764 lines
36 KiB
PL/PgSQL
-- ============================================================
|
|
-- Classroom Copilot — Application Schema
|
|
-- Migration 002: All application tables (non-GAIS)
|
|
-- Run after: 001_gais_seed.sql
|
|
-- ============================================================
|
|
|
|
-- Extensions
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
|
|
|
-- ============================================================
|
|
-- 1. Core user & school tables
|
|
-- ============================================================
|
|
|
|
-- 1.1 Profiles — mirrors auth.users, extended user data
|
|
CREATE TABLE IF NOT EXISTS profiles (
|
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
email TEXT NOT NULL,
|
|
user_type TEXT NOT NULL CHECK (user_type IN ('teacher','student','admin')),
|
|
username TEXT NOT NULL UNIQUE,
|
|
full_name TEXT,
|
|
display_name TEXT,
|
|
school_id UUID, -- FK to institutes added below (circular avoidance)
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
neo4j_sync_status TEXT DEFAULT 'pending', -- tracks Neo4j knowledge-graph sync
|
|
neo4j_synced_at TIMESTAMPTZ,
|
|
last_login TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 1.2 Admin profiles — separate table for system-level admins
|
|
CREATE TABLE IF NOT EXISTS admin_profiles (
|
|
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
email TEXT NOT NULL,
|
|
display_name TEXT,
|
|
admin_role TEXT NOT NULL DEFAULT 'admin',
|
|
is_super_admin BOOLEAN NOT NULL DEFAULT false,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 1.3 Institutes (schools)
|
|
CREATE TABLE IF NOT EXISTS institutes (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name TEXT NOT NULL,
|
|
urn TEXT UNIQUE,
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
address JSONB NOT NULL DEFAULT '{}',
|
|
website TEXT,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
geo_coordinates JSONB NOT NULL DEFAULT '{}',
|
|
neo4j_uuid_string TEXT,
|
|
neo4j_public_sync_status TEXT DEFAULT 'pending',
|
|
neo4j_public_sync_at TIMESTAMPTZ,
|
|
neo4j_private_sync_status TEXT DEFAULT 'not_started',
|
|
neo4j_private_sync_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Deferred FK: profiles.school_id → institutes
|
|
ALTER TABLE profiles
|
|
DROP CONSTRAINT IF EXISTS profiles_school_id_fkey;
|
|
ALTER TABLE profiles
|
|
ADD CONSTRAINT profiles_school_id_fkey
|
|
FOREIGN KEY (school_id) REFERENCES institutes(id) ON DELETE SET NULL;
|
|
|
|
-- 1.4 Institute memberships
|
|
CREATE TABLE IF NOT EXISTS institute_memberships (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL CHECK (role IN ('school_admin','teacher','student')),
|
|
tldraw_preferences JSONB NOT NULL DEFAULT '{}',
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (profile_id, institute_id)
|
|
);
|
|
|
|
-- 1.5 Institute membership requests (teacher invite / student join flow)
|
|
CREATE TABLE IF NOT EXISTS institute_membership_requests (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
|
requested_role TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 2. TLDraw whiteboard rooms
|
|
-- ============================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS whiteboard_rooms (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
institute_id UUID REFERENCES institutes(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL DEFAULT 'My Workspace',
|
|
context_type TEXT NOT NULL DEFAULT 'profile',
|
|
context_id TEXT,
|
|
is_default BOOLEAN NOT NULL DEFAULT false,
|
|
storage_path TEXT,
|
|
neo4j_node_id TEXT,
|
|
neo4j_db_name TEXT,
|
|
node_type TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 3. File cabinet system
|
|
-- ============================================================
|
|
|
|
-- 3.1 Cabinets — top-level containers owned by a user
|
|
CREATE TABLE IF NOT EXISTS file_cabinets (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 3.2 Files — records for files stored in Supabase Storage
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
cabinet_id UUID NOT NULL REFERENCES file_cabinets(id) ON DELETE CASCADE,
|
|
uploaded_by UUID REFERENCES profiles(id) ON DELETE SET NULL,
|
|
name TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
bucket TEXT NOT NULL DEFAULT 'cc.users',
|
|
mime_type TEXT,
|
|
size_bytes BIGINT,
|
|
size TEXT,
|
|
category TEXT,
|
|
source TEXT DEFAULT 'uploader-web',
|
|
is_directory BOOLEAN NOT NULL DEFAULT false,
|
|
parent_directory_id UUID REFERENCES files(id) ON DELETE SET NULL,
|
|
relative_path TEXT,
|
|
directory_manifest JSONB,
|
|
upload_session_id UUID,
|
|
processing_status TEXT NOT NULL DEFAULT 'uploaded',
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 3.3 Cabinet memberships — share a cabinet with other users
|
|
CREATE TABLE IF NOT EXISTS cabinet_memberships (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
cabinet_id UUID NOT NULL REFERENCES file_cabinets(id) ON DELETE CASCADE,
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
role TEXT NOT NULL DEFAULT 'viewer' CHECK (role IN ('viewer','editor','owner')),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (cabinet_id, profile_id)
|
|
);
|
|
|
|
-- 3.4 Document artefacts — processed outputs from files (Docling, Tika, etc.)
|
|
CREATE TABLE IF NOT EXISTS document_artefacts (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
type TEXT NOT NULL,
|
|
rel_path TEXT NOT NULL,
|
|
page_number INTEGER NOT NULL DEFAULT 0,
|
|
chunk_index INTEGER,
|
|
size_tag TEXT,
|
|
language TEXT,
|
|
extra JSONB,
|
|
status TEXT NOT NULL DEFAULT 'completed',
|
|
started_at TIMESTAMPTZ DEFAULT now(),
|
|
completed_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 4. Knowledge banks (brains) — Phase G: RAG over file collections
|
|
-- ============================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS brains (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
user_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
purpose TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS brain_files (
|
|
brain_id UUID NOT NULL REFERENCES brains(id) ON DELETE CASCADE,
|
|
file_id UUID NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (brain_id, file_id)
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 5. Class system
|
|
-- ============================================================
|
|
|
|
-- 5.1 Classes
|
|
CREATE TABLE IF NOT EXISTS classes (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
|
name VARCHAR NOT NULL,
|
|
class_code TEXT, -- MIS identifier e.g. '9YO/Bi', '10Da'
|
|
subject VARCHAR,
|
|
key_stage TEXT, -- '3', '4', '5'
|
|
year_group VARCHAR,
|
|
academic_year VARCHAR,
|
|
description TEXT,
|
|
type VARCHAR NOT NULL DEFAULT 'standard',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_by UUID NOT NULL REFERENCES profiles(id),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 5.2 Class teachers
|
|
CREATE TABLE IF NOT EXISTS class_teachers (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
|
teacher_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
is_primary BOOLEAN NOT NULL DEFAULT false,
|
|
can_edit BOOLEAN NOT NULL DEFAULT true,
|
|
assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
assigned_by UUID REFERENCES profiles(id),
|
|
UNIQUE (class_id, teacher_id)
|
|
);
|
|
|
|
-- 5.3 Class students
|
|
CREATE TABLE IF NOT EXISTS class_students (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
|
student_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
status VARCHAR NOT NULL DEFAULT 'active' CHECK (status IN ('active','inactive','pending')),
|
|
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
enrolled_by UUID REFERENCES profiles(id),
|
|
UNIQUE (class_id, student_id)
|
|
);
|
|
|
|
-- 5.4 Enrollment requests — student self-enrollment flow (Phase D)
|
|
CREATE TABLE IF NOT EXISTS enrollment_requests (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
|
student_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
status VARCHAR NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','approved','rejected')),
|
|
request_message TEXT,
|
|
requested_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
responded_at TIMESTAMPTZ,
|
|
responded_by UUID REFERENCES profiles(id),
|
|
response_message TEXT
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 6. Curriculum reference (flat Supabase lookup — full graph in Neo4j Phase G)
|
|
-- ============================================================
|
|
|
|
-- 6.0 Curriculum topics — importable from curriculum.xlsx, referenced by planned_lessons
|
|
CREATE TABLE IF NOT EXISTS curriculum_topics (
|
|
id TEXT PRIMARY KEY, -- e.g. '7B1', '10P10' — matches xlsx TopicID
|
|
title TEXT NOT NULL,
|
|
subject TEXT,
|
|
key_stage TEXT, -- '3', '4', '5'
|
|
year_group TEXT,
|
|
topic_type TEXT, -- 'Standard', 'Assessment', etc.
|
|
total_lessons INTEGER,
|
|
department TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- ============================================================
|
|
-- 7. Lesson planning — Phase C
|
|
-- ============================================================
|
|
|
|
-- 7.1 Planned lessons — teacher-authored lesson plans
|
|
CREATE TABLE IF NOT EXISTS planned_lessons (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
created_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
institute_id UUID NOT NULL REFERENCES institutes(id),
|
|
class_id UUID REFERENCES classes(id) ON DELETE SET NULL,
|
|
whiteboard_room_id UUID REFERENCES whiteboard_rooms(id) ON DELETE SET NULL,
|
|
topic_code TEXT REFERENCES curriculum_topics(id) ON DELETE SET NULL,
|
|
timetable_period_id TEXT, -- Neo4j period node reference (e.g. 'AMon1')
|
|
title TEXT NOT NULL,
|
|
subject TEXT,
|
|
year_group TEXT,
|
|
estimated_duration_minutes INTEGER,
|
|
objectives JSONB NOT NULL DEFAULT '[]',
|
|
activities JSONB NOT NULL DEFAULT '[]',
|
|
status TEXT NOT NULL DEFAULT 'draft'
|
|
CHECK (status IN ('draft','ready','archived')),
|
|
tags TEXT[] NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 6.2 Lesson collaborators — co-planning: additional teachers on a lesson
|
|
CREATE TABLE IF NOT EXISTS lesson_collaborators (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
planned_lesson_id UUID NOT NULL REFERENCES planned_lessons(id) ON DELETE CASCADE,
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
|
can_edit BOOLEAN NOT NULL DEFAULT true,
|
|
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (planned_lesson_id, profile_id)
|
|
);
|
|
|
|
-- 6.3 Lesson deliveries — records of when a planned lesson is actually taught
|
|
CREATE TABLE IF NOT EXISTS lesson_deliveries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
planned_lesson_id UUID REFERENCES planned_lessons(id) ON DELETE SET NULL,
|
|
delivered_by UUID NOT NULL REFERENCES profiles(id),
|
|
class_id UUID REFERENCES classes(id) ON DELETE SET NULL,
|
|
institute_id UUID NOT NULL REFERENCES institutes(id),
|
|
whiteboard_room_id UUID REFERENCES whiteboard_rooms(id) ON DELETE SET NULL,
|
|
transcription_session_id UUID, -- FK to transcription_sessions added after CIS tables
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ended_at TIMESTAMPTZ,
|
|
notes TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 8. Exam board reference (Phase F)
|
|
-- ============================================================
|
|
|
|
CREATE TABLE IF NOT EXISTS eb_specifications (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
spec_code TEXT UNIQUE,
|
|
exam_board_code TEXT,
|
|
award_code TEXT,
|
|
subject_code TEXT,
|
|
first_teach TEXT,
|
|
spec_ver TEXT,
|
|
storage_loc TEXT,
|
|
doc_type TEXT,
|
|
doc_details JSONB NOT NULL DEFAULT '{}',
|
|
docling_docs JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS eb_exams (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
exam_code TEXT,
|
|
spec_code TEXT REFERENCES eb_specifications(spec_code),
|
|
paper_code TEXT,
|
|
tier TEXT,
|
|
session TEXT,
|
|
type_code TEXT,
|
|
storage_loc TEXT,
|
|
doc_type TEXT,
|
|
doc_details JSONB NOT NULL DEFAULT '{}',
|
|
docling_docs JSONB NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
|
|
-- ============================================================
|
|
-- 9. CIS: Transcription & Canvas event system
|
|
-- ============================================================
|
|
|
|
-- 8.1 Transcription sessions
|
|
CREATE TABLE IF NOT EXISTS transcription_sessions (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
title TEXT,
|
|
canvas_type TEXT DEFAULT 'tldraw',
|
|
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
ended_at TIMESTAMPTZ,
|
|
duration_seconds INTEGER,
|
|
timetable_period_id TEXT,
|
|
timetable_event_type TEXT,
|
|
timetable_event_label TEXT,
|
|
auto_tagged BOOLEAN NOT NULL DEFAULT false,
|
|
llm_provider TEXT,
|
|
llm_model TEXT,
|
|
word_count INTEGER NOT NULL DEFAULT 0,
|
|
segment_count INTEGER NOT NULL DEFAULT 0,
|
|
metadata JSONB NOT NULL DEFAULT '{}',
|
|
deleted_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 8.2 Transcription segments
|
|
CREATE TABLE IF NOT EXISTS transcription_segments (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
session_id UUID NOT NULL REFERENCES transcription_sessions(id) ON DELETE CASCADE,
|
|
sequence_index INTEGER NOT NULL,
|
|
text TEXT NOT NULL,
|
|
start_seconds REAL NOT NULL DEFAULT 0,
|
|
end_seconds REAL NOT NULL DEFAULT 0,
|
|
is_final BOOLEAN NOT NULL DEFAULT true,
|
|
speaker_label TEXT,
|
|
keyword_matches TEXT[] NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 8.3 Canvas events (TLDraw interactions during a session)
|
|
CREATE TABLE IF NOT EXISTS canvas_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
session_id UUID REFERENCES transcription_sessions(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
timestamp TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
session_elapsed_seconds REAL,
|
|
event_type TEXT NOT NULL,
|
|
event_payload JSONB NOT NULL DEFAULT '{}',
|
|
canvas_snapshot_url TEXT,
|
|
tldraw_page_id TEXT,
|
|
tldraw_shape_ids TEXT[] NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 8.4 AI-generated summaries
|
|
CREATE TABLE IF NOT EXISTS transcription_summaries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
session_id UUID NOT NULL REFERENCES transcription_sessions(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
summary_type TEXT NOT NULL,
|
|
content TEXT NOT NULL,
|
|
prompt_used TEXT,
|
|
llm_provider TEXT,
|
|
llm_model TEXT,
|
|
input_tokens INTEGER,
|
|
output_tokens INTEGER,
|
|
segment_range_start INTEGER,
|
|
segment_range_end INTEGER,
|
|
canvas_snapshot_urls TEXT[] NOT NULL DEFAULT '{}',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- 8.5 Keyword watch list
|
|
CREATE TABLE IF NOT EXISTS keyword_watches (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
keyword TEXT NOT NULL,
|
|
match_type TEXT NOT NULL DEFAULT 'contains',
|
|
action TEXT NOT NULL DEFAULT 'notify',
|
|
is_active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
UNIQUE (user_id, keyword)
|
|
);
|
|
|
|
-- 8.6 Keyword events
|
|
CREATE TABLE IF NOT EXISTS keyword_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
session_id UUID NOT NULL REFERENCES transcription_sessions(id) ON DELETE CASCADE,
|
|
segment_id UUID REFERENCES transcription_segments(id) ON DELETE SET NULL,
|
|
keyword_watch_id UUID REFERENCES keyword_watches(id) ON DELETE SET NULL,
|
|
keyword_text TEXT NOT NULL,
|
|
matched_in_text TEXT NOT NULL,
|
|
session_elapsed_seconds REAL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Deferred FK: lesson_deliveries.transcription_session_id
|
|
ALTER TABLE lesson_deliveries
|
|
DROP CONSTRAINT IF EXISTS lesson_deliveries_transcription_session_id_fkey;
|
|
ALTER TABLE lesson_deliveries
|
|
ADD CONSTRAINT lesson_deliveries_transcription_session_id_fkey
|
|
FOREIGN KEY (transcription_session_id)
|
|
REFERENCES transcription_sessions(id) ON DELETE SET NULL;
|
|
|
|
|
|
-- ============================================================
|
|
-- 9. Updated_at trigger
|
|
-- ============================================================
|
|
|
|
CREATE OR REPLACE FUNCTION set_updated_at()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
NEW.updated_at = now();
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DO $$ DECLARE
|
|
t TEXT;
|
|
BEGIN
|
|
FOREACH t IN ARRAY ARRAY[
|
|
'profiles', 'admin_profiles', 'institutes',
|
|
'institute_memberships', 'institute_membership_requests',
|
|
'whiteboard_rooms', 'cabinet_memberships',
|
|
'classes', 'planned_lessons', 'lesson_deliveries',
|
|
'transcription_sessions', 'keyword_watches',
|
|
'eb_specifications', 'eb_exams'
|
|
] LOOP
|
|
EXECUTE format(
|
|
'DROP TRIGGER IF EXISTS trg_updated_at ON %I;
|
|
CREATE TRIGGER trg_updated_at
|
|
BEFORE UPDATE ON %I
|
|
FOR EACH ROW EXECUTE FUNCTION set_updated_at();',
|
|
t, t
|
|
);
|
|
END LOOP;
|
|
END $$;
|
|
|
|
|
|
-- ============================================================
|
|
-- 10. Indexes
|
|
-- ============================================================
|
|
|
|
-- Profiles
|
|
CREATE INDEX IF NOT EXISTS idx_profiles_school_id ON profiles(school_id);
|
|
CREATE INDEX IF NOT EXISTS idx_profiles_email ON profiles(email);
|
|
CREATE INDEX IF NOT EXISTS idx_profiles_username ON profiles(username);
|
|
|
|
-- Institutes
|
|
CREATE INDEX IF NOT EXISTS idx_institutes_urn ON institutes(urn);
|
|
|
|
-- Institute memberships
|
|
CREATE INDEX IF NOT EXISTS idx_im_profile ON institute_memberships(profile_id);
|
|
CREATE INDEX IF NOT EXISTS idx_im_institute ON institute_memberships(institute_id);
|
|
|
|
-- Whiteboard rooms
|
|
CREATE INDEX IF NOT EXISTS idx_wr_user ON whiteboard_rooms(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_wr_context ON whiteboard_rooms(context_type, context_id);
|
|
|
|
-- Files
|
|
CREATE INDEX IF NOT EXISTS idx_files_cabinet ON files(cabinet_id);
|
|
CREATE INDEX IF NOT EXISTS idx_files_uploaded_by ON files(uploaded_by);
|
|
CREATE INDEX IF NOT EXISTS idx_files_status ON files(processing_status);
|
|
CREATE INDEX IF NOT EXISTS idx_files_parent ON files(parent_directory_id);
|
|
|
|
-- Document artefacts
|
|
CREATE INDEX IF NOT EXISTS idx_artefacts_file ON document_artefacts(file_id);
|
|
CREATE INDEX IF NOT EXISTS idx_artefacts_type ON document_artefacts(type);
|
|
|
|
-- Cabinet memberships
|
|
CREATE INDEX IF NOT EXISTS idx_cabinet_mb_profile ON cabinet_memberships(profile_id);
|
|
|
|
-- Brains
|
|
CREATE INDEX IF NOT EXISTS idx_brains_user ON brains(user_id);
|
|
|
|
-- Curriculum topics
|
|
CREATE INDEX IF NOT EXISTS idx_ct_subject ON curriculum_topics(subject);
|
|
CREATE INDEX IF NOT EXISTS idx_ct_key_stage ON curriculum_topics(key_stage);
|
|
CREATE INDEX IF NOT EXISTS idx_ct_year_group ON curriculum_topics(year_group);
|
|
|
|
-- Classes
|
|
CREATE INDEX IF NOT EXISTS idx_classes_institute ON classes(institute_id);
|
|
CREATE INDEX IF NOT EXISTS idx_classes_class_code ON classes(class_code);
|
|
CREATE INDEX IF NOT EXISTS idx_classes_created_by ON classes(created_by);
|
|
CREATE INDEX IF NOT EXISTS idx_class_teachers_class ON class_teachers(class_id);
|
|
CREATE INDEX IF NOT EXISTS idx_class_students_class ON class_students(class_id);
|
|
|
|
-- Planned lessons
|
|
CREATE INDEX IF NOT EXISTS idx_pl_created_by ON planned_lessons(created_by);
|
|
CREATE INDEX IF NOT EXISTS idx_pl_institute ON planned_lessons(institute_id);
|
|
CREATE INDEX IF NOT EXISTS idx_pl_class ON planned_lessons(class_id);
|
|
CREATE INDEX IF NOT EXISTS idx_pl_status ON planned_lessons(status);
|
|
CREATE INDEX IF NOT EXISTS idx_pl_topic_code ON planned_lessons(topic_code);
|
|
CREATE INDEX IF NOT EXISTS idx_pl_timetable_period ON planned_lessons(timetable_period_id);
|
|
|
|
-- Lesson deliveries
|
|
CREATE INDEX IF NOT EXISTS idx_ld_delivered_by ON lesson_deliveries(delivered_by);
|
|
CREATE INDEX IF NOT EXISTS idx_ld_planned_lesson ON lesson_deliveries(planned_lesson_id);
|
|
CREATE INDEX IF NOT EXISTS idx_ld_started_at ON lesson_deliveries(started_at DESC);
|
|
|
|
-- CIS
|
|
CREATE INDEX IF NOT EXISTS idx_ts_user ON transcription_sessions(user_id);
|
|
CREATE INDEX IF NOT EXISTS idx_ts_started ON transcription_sessions(started_at DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_ts_deleted ON transcription_sessions(deleted_at) WHERE deleted_at IS NULL;
|
|
CREATE INDEX IF NOT EXISTS idx_seg_session ON transcription_segments(session_id, sequence_index);
|
|
CREATE INDEX IF NOT EXISTS idx_ce_session ON canvas_events(session_id, timestamp);
|
|
CREATE INDEX IF NOT EXISTS idx_ce_user ON canvas_events(user_id, timestamp DESC);
|
|
CREATE INDEX IF NOT EXISTS idx_kw_user ON keyword_watches(user_id) WHERE is_active = true;
|
|
CREATE INDEX IF NOT EXISTS idx_ke_session ON keyword_events(session_id);
|
|
|
|
|
|
-- ============================================================
|
|
-- 11. Row Level Security
|
|
-- ============================================================
|
|
|
|
-- Enable RLS
|
|
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE admin_profiles ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE institutes ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE institute_memberships ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE institute_membership_requests ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE whiteboard_rooms ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE file_cabinets ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE files ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE cabinet_memberships ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE document_artefacts ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE brains ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE brain_files ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE classes ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE class_teachers ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE class_students ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE enrollment_requests ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE planned_lessons ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE lesson_collaborators ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE lesson_deliveries ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE curriculum_topics ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE eb_specifications ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE eb_exams ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE transcription_sessions ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE transcription_segments ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE canvas_events ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE transcription_summaries ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE keyword_watches ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE keyword_events ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- Profiles: own row + service role full access
|
|
DROP POLICY IF EXISTS "profiles_own" ON profiles;
|
|
DROP POLICY IF EXISTS "profiles_service_role" ON profiles;
|
|
CREATE POLICY "profiles_own" ON profiles FOR ALL USING (id = auth.uid());
|
|
CREATE POLICY "profiles_service_role" ON profiles FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Admin profiles: super admins only
|
|
DROP POLICY IF EXISTS "admin_profiles_service_role" ON admin_profiles;
|
|
CREATE POLICY "admin_profiles_service_role" ON admin_profiles FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Institutes: members can read, school_admin can write, service role full access
|
|
DROP POLICY IF EXISTS "institutes_member_read" ON institutes;
|
|
DROP POLICY IF EXISTS "institutes_service_role" ON institutes;
|
|
CREATE POLICY "institutes_member_read" ON institutes FOR SELECT
|
|
USING (id IN (SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()));
|
|
CREATE POLICY "institutes_service_role" ON institutes FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Institute memberships
|
|
DROP POLICY IF EXISTS "im_own" ON institute_memberships;
|
|
DROP POLICY IF EXISTS "im_service_role" ON institute_memberships;
|
|
CREATE POLICY "im_own" ON institute_memberships FOR ALL USING (profile_id = auth.uid());
|
|
CREATE POLICY "im_service_role" ON institute_memberships FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Institute membership requests
|
|
DROP POLICY IF EXISTS "imr_own" ON institute_membership_requests;
|
|
DROP POLICY IF EXISTS "imr_service_role" ON institute_membership_requests;
|
|
CREATE POLICY "imr_own" ON institute_membership_requests FOR ALL USING (profile_id = auth.uid());
|
|
CREATE POLICY "imr_service_role" ON institute_membership_requests FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Whiteboard rooms: own rooms
|
|
DROP POLICY IF EXISTS "wr_own" ON whiteboard_rooms;
|
|
DROP POLICY IF EXISTS "wr_service_role" ON whiteboard_rooms;
|
|
CREATE POLICY "wr_own" ON whiteboard_rooms FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "wr_service_role" ON whiteboard_rooms FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- File cabinets: own cabinets
|
|
DROP POLICY IF EXISTS "fc_own" ON file_cabinets;
|
|
DROP POLICY IF EXISTS "fc_service_role" ON file_cabinets;
|
|
CREATE POLICY "fc_own" ON file_cabinets FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "fc_service_role" ON file_cabinets FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Files: own via cabinet
|
|
DROP POLICY IF EXISTS "files_own" ON files;
|
|
DROP POLICY IF EXISTS "files_service_role" ON files;
|
|
CREATE POLICY "files_own" ON files FOR ALL
|
|
USING (cabinet_id IN (SELECT id FROM file_cabinets WHERE user_id = auth.uid()));
|
|
CREATE POLICY "files_service_role" ON files FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Cabinet memberships
|
|
DROP POLICY IF EXISTS "cm_own" ON cabinet_memberships;
|
|
DROP POLICY IF EXISTS "cm_service_role" ON cabinet_memberships;
|
|
CREATE POLICY "cm_own" ON cabinet_memberships FOR ALL USING (profile_id = auth.uid());
|
|
CREATE POLICY "cm_service_role" ON cabinet_memberships FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Document artefacts: via file ownership
|
|
DROP POLICY IF EXISTS "da_own" ON document_artefacts;
|
|
DROP POLICY IF EXISTS "da_service_role" ON document_artefacts;
|
|
CREATE POLICY "da_own" ON document_artefacts FOR ALL
|
|
USING (file_id IN (
|
|
SELECT f.id FROM files f
|
|
JOIN file_cabinets fc ON fc.id = f.cabinet_id
|
|
WHERE fc.user_id = auth.uid()
|
|
));
|
|
CREATE POLICY "da_service_role" ON document_artefacts FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Brains
|
|
DROP POLICY IF EXISTS "brains_own" ON brains;
|
|
DROP POLICY IF EXISTS "brains_service_role" ON brains;
|
|
CREATE POLICY "brains_own" ON brains FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "brains_service_role" ON brains FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Brain files
|
|
DROP POLICY IF EXISTS "brain_files_own" ON brain_files;
|
|
DROP POLICY IF EXISTS "brain_files_service_role" ON brain_files;
|
|
CREATE POLICY "brain_files_own" ON brain_files FOR ALL
|
|
USING (brain_id IN (SELECT id FROM brains WHERE user_id = auth.uid()));
|
|
CREATE POLICY "brain_files_service_role" ON brain_files FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Classes: members of institute can read; teachers can write
|
|
DROP POLICY IF EXISTS "classes_institute_read" ON classes;
|
|
DROP POLICY IF EXISTS "classes_service_role" ON classes;
|
|
CREATE POLICY "classes_institute_read" ON classes FOR SELECT
|
|
USING (institute_id IN (SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()));
|
|
CREATE POLICY "classes_service_role" ON classes FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Class teachers / students
|
|
DROP POLICY IF EXISTS "ct_service_role" ON class_teachers;
|
|
DROP POLICY IF EXISTS "cs_service_role" ON class_students;
|
|
CREATE POLICY "ct_service_role" ON class_teachers FOR ALL USING (auth.role() = 'service_role');
|
|
CREATE POLICY "cs_service_role" ON class_students FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Enrollment requests
|
|
DROP POLICY IF EXISTS "er_own" ON enrollment_requests;
|
|
DROP POLICY IF EXISTS "er_service_role" ON enrollment_requests;
|
|
CREATE POLICY "er_own" ON enrollment_requests FOR ALL USING (student_id = auth.uid());
|
|
CREATE POLICY "er_service_role" ON enrollment_requests FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Planned lessons
|
|
DROP POLICY IF EXISTS "pl_own" ON planned_lessons;
|
|
DROP POLICY IF EXISTS "pl_collab_read" ON planned_lessons;
|
|
DROP POLICY IF EXISTS "pl_service_role" ON planned_lessons;
|
|
CREATE POLICY "pl_own" ON planned_lessons FOR ALL USING (created_by = auth.uid());
|
|
CREATE POLICY "pl_collab_read" ON planned_lessons FOR SELECT
|
|
USING (id IN (SELECT planned_lesson_id FROM lesson_collaborators WHERE profile_id = auth.uid()));
|
|
CREATE POLICY "pl_service_role" ON planned_lessons FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Lesson collaborators
|
|
DROP POLICY IF EXISTS "lc_own" ON lesson_collaborators;
|
|
DROP POLICY IF EXISTS "lc_service_role" ON lesson_collaborators;
|
|
CREATE POLICY "lc_own" ON lesson_collaborators FOR ALL USING (profile_id = auth.uid());
|
|
CREATE POLICY "lc_service_role" ON lesson_collaborators FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Lesson deliveries
|
|
DROP POLICY IF EXISTS "ld_own" ON lesson_deliveries;
|
|
DROP POLICY IF EXISTS "ld_service_role" ON lesson_deliveries;
|
|
CREATE POLICY "ld_own" ON lesson_deliveries FOR ALL USING (delivered_by = auth.uid());
|
|
CREATE POLICY "ld_service_role" ON lesson_deliveries FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Curriculum topics: public read, service role write
|
|
DROP POLICY IF EXISTS "ct_public_read" ON curriculum_topics;
|
|
DROP POLICY IF EXISTS "ct_service_role" ON curriculum_topics;
|
|
CREATE POLICY "ct_public_read" ON curriculum_topics FOR SELECT USING (true);
|
|
CREATE POLICY "ct_service_role" ON curriculum_topics FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- Exam boards: public read
|
|
DROP POLICY IF EXISTS "eb_spec_public_read" ON eb_specifications;
|
|
DROP POLICY IF EXISTS "eb_spec_service_role" ON eb_specifications;
|
|
DROP POLICY IF EXISTS "eb_exam_public_read" ON eb_exams;
|
|
DROP POLICY IF EXISTS "eb_exam_service_role" ON eb_exams;
|
|
CREATE POLICY "eb_spec_public_read" ON eb_specifications FOR SELECT USING (true);
|
|
CREATE POLICY "eb_spec_service_role" ON eb_specifications FOR ALL USING (auth.role() = 'service_role');
|
|
CREATE POLICY "eb_exam_public_read" ON eb_exams FOR SELECT USING (true);
|
|
CREATE POLICY "eb_exam_service_role" ON eb_exams FOR ALL USING (auth.role() = 'service_role');
|
|
|
|
-- CIS: users own their sessions
|
|
DROP POLICY IF EXISTS "ts_own" ON transcription_sessions;
|
|
DROP POLICY IF EXISTS "seg_own" ON transcription_segments;
|
|
DROP POLICY IF EXISTS "ce_own" ON canvas_events;
|
|
DROP POLICY IF EXISTS "sum_own" ON transcription_summaries;
|
|
DROP POLICY IF EXISTS "kw_own" ON keyword_watches;
|
|
DROP POLICY IF EXISTS "ke_own" ON keyword_events;
|
|
CREATE POLICY "ts_own" ON transcription_sessions FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "seg_own" ON transcription_segments FOR ALL
|
|
USING (session_id IN (SELECT id FROM transcription_sessions WHERE user_id = auth.uid()));
|
|
CREATE POLICY "ce_own" ON canvas_events FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "sum_own" ON transcription_summaries FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "kw_own" ON keyword_watches FOR ALL USING (user_id = auth.uid());
|
|
CREATE POLICY "ke_own" ON keyword_events FOR ALL
|
|
USING (session_id IN (SELECT id FROM transcription_sessions WHERE user_id = auth.uid()));
|