Consolidate Supabase migrations and dev seed
This commit is contained in:
parent
eab7c01f46
commit
02f68387e8
@ -355,17 +355,16 @@ services:
|
||||
- ./volumes/db/logs.sql:/docker-entrypoint-initdb.d/migrations/59-logs.sql:Z
|
||||
# Changes required for Pooler support
|
||||
- ./volumes/db/pooler.sql:/docker-entrypoint-initdb.d/migrations/59-pooler.sql:Z
|
||||
# ClassroomCopilot changes
|
||||
- ./volumes/db/cc/61-core-schema.sql:/docker-entrypoint-initdb.d/migrations/61-core-schema.sql:Z
|
||||
- ./volumes/db/cc/62-functions-triggers.sql:/docker-entrypoint-initdb.d/migrations/62-functions-triggers.sql:Z
|
||||
- ./volumes/db/cc/63-storage-policies.sql:/docker-entrypoint-initdb.d/migrations/63-storage-policies.sql:Z
|
||||
- ./volumes/db/cc/64-initial-admin.sql:/docker-entrypoint-initdb.d/migrations/64-initial-admin.sql:Z
|
||||
- ./volumes/db/cc/65-filesystem-augments.sql:/docker-entrypoint-initdb.d/migrations/65-filesystem-augments.sql:Z
|
||||
- ./volumes/db/cc/66-rls-policies.sql:/docker-entrypoint-initdb.d/migrations/66-rls-policies.sql:Z
|
||||
- ./volumes/db/cc/67-vectors.sql:/docker-entrypoint-initdb.d/migrations/67-vectors.sql:Z
|
||||
- ./volumes/db/cc/68-cabinet-memberships.sql:/docker-entrypoint-initdb.d/migrations/68-cabinet-memberships.sql:Z
|
||||
- ./volumes/db/cc/69-gc-prefix-cleanup.sql:/docker-entrypoint-initdb.d/migrations/69-gc-prefix-cleanup.sql:Z
|
||||
- ./volumes/db/cc/70-add-directory-support.sql:/docker-entrypoint-initdb.d/migrations/70-add-directory-support.sql:Z
|
||||
# Classroom Copilot consolidated application schema and deterministic dev seed.
|
||||
# Keep this chain ordered; GAIS reference tables are schema-only here, while
|
||||
# full GAIS open-data bulk loads remain outside the small dev seed.
|
||||
- ./volumes/db/cc/61-gais-reference.sql:/docker-entrypoint-initdb.d/migrations/61-gais-reference.sql:Z
|
||||
- ./volumes/db/cc/62-application-schema.sql:/docker-entrypoint-initdb.d/migrations/62-application-schema.sql:Z
|
||||
- ./volumes/db/cc/63-academic-calendar.sql:/docker-entrypoint-initdb.d/migrations/63-academic-calendar.sql:Z
|
||||
- ./volumes/db/cc/64-extended-schema.sql:/docker-entrypoint-initdb.d/migrations/64-extended-schema.sql:Z
|
||||
- ./volumes/db/cc/65-phase-c.sql:/docker-entrypoint-initdb.d/migrations/65-phase-c.sql:Z
|
||||
- ./volumes/db/cc/66-taught-lessons-nullable.sql:/docker-entrypoint-initdb.d/migrations/66-taught-lessons-nullable.sql:Z
|
||||
- ./volumes/db/cc/67-dev-seed.sql:/docker-entrypoint-initdb.d/migrations/67-dev-seed.sql:Z
|
||||
# PGDATA directory - persists database files between restarts
|
||||
- ./volumes/db-data:/var/lib/postgresql/data:Z
|
||||
# Use named volume to persist pgsodium decryption key between restarts
|
||||
|
||||
105
docs/migrations-and-dev-seed.md
Normal file
105
docs/migrations-and-dev-seed.md
Normal file
@ -0,0 +1,105 @@
|
||||
# Classroom Copilot Supabase migrations and deterministic dev seed
|
||||
|
||||
Status: branch implementation validated on Supabase dev host 192.168.0.94 using an isolated throwaway database.
|
||||
|
||||
## Consolidated init chain
|
||||
|
||||
The Docker Compose `db` service now mounts a single ordered Classroom Copilot chain:
|
||||
|
||||
1. `volumes/db/cc/61-gais-reference.sql` — GAIS reference table schema and open-data read policies only.
|
||||
2. `volumes/db/cc/62-application-schema.sql` — canonical app schema, storage/file metadata tables, class/lesson/CIS tables, indexes, and base RLS.
|
||||
3. `volumes/db/cc/63-academic-calendar.sql` — school timetable, academic year/term/week/day, teacher timetable, and teacher slot tables.
|
||||
4. `volumes/db/cc/64-extended-schema.sql` — term breaks, academic periods, invitations, taught lessons, and week-cycle slot uniqueness.
|
||||
5. `volumes/db/cc/65-phase-c.sql` — Phase C cleanup after taught lessons exist; links lesson deliveries to taught lessons.
|
||||
6. `volumes/db/cc/66-taught-lessons-nullable.sql` — nullable taught lesson `class_id` and teacher slot class FK.
|
||||
7. `volumes/db/cc/67-dev-seed.sql` — deterministic, non-sensitive dev fixtures.
|
||||
|
||||
The old `61-core-schema.sql` through `70-add-directory-support.sql` bootstrap files were removed from the active chain because they represented an older ClassConcepts/filesystem schema and stale role vocabulary. The Git history remains the archive.
|
||||
|
||||
## Deterministic dev seed contents
|
||||
|
||||
`67-dev-seed.sql` creates only fixture data:
|
||||
|
||||
- 1 platform admin in `admin_profiles`.
|
||||
- 1 school/institute.
|
||||
- 1 school admin, 2 teachers, 3 students.
|
||||
- institute memberships for the school admin, teachers, and students.
|
||||
- 2 classes with class-teacher and class-student rows.
|
||||
- 1 school timetable, 1 academic year, 1 term, 1 week, 4 academic days, and 16 academic periods.
|
||||
- 2 teacher timetables, 3 teacher timetable slots, and 3 taught lessons.
|
||||
- 2 planned lessons and 1 delivered lesson fixture.
|
||||
- Storage buckets `cc.users`, `cc.public.snapshots`, and `cc.examboards`.
|
||||
- TLDraw default snapshot paths on the teacher whiteboard rooms; object rows are not pre-created.
|
||||
|
||||
Fixture emails use the `classroomcopilot.dev` domain and are not real users. Do not replace this seed with live student/teacher data.
|
||||
|
||||
## Validation pattern used on Supabase dev
|
||||
|
||||
Do not run schema experiments on production. To validate this branch without mutating the live dev database, create a throwaway database on the Supabase dev Postgres container, clone only the `auth` and `storage` schema definitions from dev, apply the ordered chain, check row counts, then drop the throwaway database.
|
||||
|
||||
The 2026-05-28 validation used this shape on `ubuntu-ct-supabase-dev` (`192.168.0.94`):
|
||||
|
||||
```bash
|
||||
DB=cc_mig_validate_<timestamp>
|
||||
BASE=/tmp/cc-supabase-migration-validate
|
||||
|
||||
docker exec supabase-db psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS $DB WITH (FORCE);"
|
||||
docker exec supabase-db psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "CREATE DATABASE $DB;"
|
||||
|
||||
# The dev auth schema has a trigger that references public.handle_new_user();
|
||||
# create a no-op stub before restoring auth/storage schema-only into the throwaway DB.
|
||||
cat >/tmp/create_dummy.sql <<'SQL'
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
SQL
|
||||
docker exec -i supabase-db psql -U postgres -d "$DB" -v ON_ERROR_STOP=1 < /tmp/create_dummy.sql
|
||||
|
||||
docker exec supabase-db pg_dump -U postgres -d postgres --schema-only --no-owner --no-privileges -n auth -n storage > /tmp/${DB}_auth_storage_schema.sql
|
||||
docker exec -i supabase-db psql -U postgres -d "$DB" -v ON_ERROR_STOP=1 < /tmp/${DB}_auth_storage_schema.sql
|
||||
|
||||
for f in \
|
||||
"$BASE"/volumes/db/cc/61-gais-reference.sql \
|
||||
"$BASE"/volumes/db/cc/62-application-schema.sql \
|
||||
"$BASE"/volumes/db/cc/63-academic-calendar.sql \
|
||||
"$BASE"/volumes/db/cc/64-extended-schema.sql \
|
||||
"$BASE"/volumes/db/cc/65-phase-c.sql \
|
||||
"$BASE"/volumes/db/cc/66-taught-lessons-nullable.sql \
|
||||
"$BASE"/volumes/db/cc/67-dev-seed.sql; do
|
||||
docker exec -i supabase-db psql -U postgres -d "$DB" -v ON_ERROR_STOP=1 < "$f"
|
||||
done
|
||||
|
||||
# Smoke counts, then cleanup.
|
||||
docker exec supabase-db psql -U postgres -d "$DB" -Atc "select 'profiles='||count(*) from public.profiles union all select 'institutes='||count(*) from public.institutes union all select 'memberships='||count(*) from public.institute_memberships union all select 'classes='||count(*) from public.classes union all select 'academic_periods='||count(*) from public.academic_periods union all select 'teacher_timetable_slots='||count(*) from public.teacher_timetable_slots union all select 'taught_lessons='||count(*) from public.taught_lessons union all select 'planned_lessons='||count(*) from public.planned_lessons union all select 'lesson_deliveries='||count(*) from public.lesson_deliveries union all select 'buckets='||count(*) from storage.buckets where id like 'cc.%';"
|
||||
docker exec supabase-db psql -U postgres -d postgres -v ON_ERROR_STOP=1 -c "DROP DATABASE IF EXISTS $DB WITH (FORCE);"
|
||||
```
|
||||
|
||||
Expected smoke counts from the deterministic seed:
|
||||
|
||||
```text
|
||||
profiles=7
|
||||
institutes=1
|
||||
memberships=6
|
||||
classes=2
|
||||
academic_periods=16
|
||||
teacher_timetable_slots=3
|
||||
taught_lessons=3
|
||||
planned_lessons=2
|
||||
lesson_deliveries=1
|
||||
buckets=3
|
||||
```
|
||||
|
||||
## Production rule
|
||||
|
||||
This branch is not a production migration by itself. Before production use:
|
||||
|
||||
1. Take a production schema/data backup.
|
||||
2. Compare live production schema drift against this consolidated chain.
|
||||
3. Prepare explicit forward migrations for any live-only objects or data transforms.
|
||||
4. Validate those forward migrations on Supabase dev first.
|
||||
5. Only apply to production after human approval.
|
||||
@ -1,364 +0,0 @@
|
||||
--[ Database Schema Version ]--
|
||||
-- Version: 1.0.0
|
||||
-- Last Updated: 2024-02-24
|
||||
-- Description: Core schema setup for ClassConcepts with neoFS filesystem integration
|
||||
-- Dependencies: auth.users (Supabase Auth)
|
||||
|
||||
--[ Validation ]--
|
||||
do $$
|
||||
begin
|
||||
-- Verify required extensions
|
||||
if not exists (select 1 from pg_extension where extname = 'uuid-ossp') then
|
||||
raise exception 'Required extension uuid-ossp is not installed';
|
||||
end if;
|
||||
|
||||
-- Verify auth schema exists
|
||||
if not exists (select 1 from information_schema.schemata where schema_name = 'auth') then
|
||||
raise exception 'Required auth schema is not available';
|
||||
end if;
|
||||
|
||||
-- Verify storage schema exists
|
||||
if not exists (select 1 from information_schema.schemata where schema_name = 'storage') then
|
||||
raise exception 'Required storage schema is not available';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
--[ 1. Extensions ]--
|
||||
create extension if not exists "uuid-ossp";
|
||||
|
||||
-- Create rpc schema if it doesn't exist
|
||||
create schema if not exists rpc;
|
||||
grant usage on schema rpc to anon, authenticated;
|
||||
|
||||
-- Create exec_sql function for admin operations
|
||||
create or replace function exec_sql(query text)
|
||||
returns void as $$
|
||||
begin
|
||||
execute query;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
-- Create updated_at trigger function
|
||||
create or replace function public.handle_updated_at()
|
||||
returns trigger as $$
|
||||
begin
|
||||
new.updated_at = timezone('utc'::text, now());
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
-- Create completed_at trigger function for document artefacts
|
||||
create or replace function public.set_completed_at()
|
||||
returns trigger as $$
|
||||
begin
|
||||
if NEW.status = 'completed' and OLD.status != 'completed' then
|
||||
NEW.completed_at = now();
|
||||
end if;
|
||||
return NEW;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
--[ 5. Core Tables ]--
|
||||
-- Base user profiles
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key references auth.users(id) on delete cascade,
|
||||
email text not null unique,
|
||||
user_type text not null check (
|
||||
user_type in (
|
||||
'teacher',
|
||||
'student',
|
||||
'email_teacher',
|
||||
'email_student',
|
||||
'developer',
|
||||
'superadmin'
|
||||
)
|
||||
),
|
||||
username text not null unique,
|
||||
full_name text,
|
||||
display_name text,
|
||||
metadata jsonb default '{}'::jsonb,
|
||||
user_db_name text,
|
||||
school_db_name text,
|
||||
neo4j_sync_status text default 'pending' check (neo4j_sync_status in ('pending', 'ready', 'failed')),
|
||||
neo4j_synced_at timestamp with time zone,
|
||||
last_login timestamp with time zone,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
comment on table public.profiles is 'User profiles linked to Supabase auth.users';
|
||||
comment on column public.profiles.user_type is 'Type of user: teacher or student';
|
||||
|
||||
-- Active institutes
|
||||
create table if not exists public.institutes (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
name text not null,
|
||||
urn text unique,
|
||||
status text not null default 'active' check (status in ('active', 'inactive', 'pending')),
|
||||
address jsonb default '{}'::jsonb,
|
||||
website text,
|
||||
metadata jsonb default '{}'::jsonb,
|
||||
geo_coordinates jsonb default '{}'::jsonb,
|
||||
neo4j_uuid_string text,
|
||||
neo4j_public_sync_status text default 'pending' check (neo4j_public_sync_status in ('pending', 'synced', 'failed')),
|
||||
neo4j_public_sync_at timestamp with time zone,
|
||||
neo4j_private_sync_status text default 'not_started' check (neo4j_private_sync_status in ('not_started', 'pending', 'synced', 'failed')),
|
||||
neo4j_private_sync_at timestamp with time zone,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
comment on table public.institutes is 'Active institutes in the system';
|
||||
comment on column public.institutes.geo_coordinates is 'Geospatial coordinates from OSM search (latitude, longitude, boundingbox)';
|
||||
|
||||
--[ 6. neoFS Filesystem Tables ]--
|
||||
-- File cabinets for organizing files
|
||||
create table if not exists public.file_cabinets (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid not null references public.profiles(id) on delete cascade,
|
||||
name text not null,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
comment on table public.file_cabinets is 'User file cabinets for organizing documents and files';
|
||||
|
||||
-- Files stored in cabinets
|
||||
create table if not exists public.files (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
cabinet_id uuid not null references public.file_cabinets(id) on delete cascade,
|
||||
name text not null,
|
||||
path text not null,
|
||||
bucket text default 'file-cabinets' not null,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
mime_type text,
|
||||
metadata jsonb default '{}'::jsonb,
|
||||
size text,
|
||||
category text generated always as (
|
||||
case
|
||||
when mime_type like 'image/%' then 'image'
|
||||
when mime_type = 'application/pdf' then 'document'
|
||||
when mime_type in ('application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') then 'document'
|
||||
when mime_type in ('application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') then 'spreadsheet'
|
||||
when mime_type in ('application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation') then 'presentation'
|
||||
when mime_type like 'audio/%' then 'audio'
|
||||
when mime_type like 'video/%' then 'video'
|
||||
else 'other'
|
||||
end
|
||||
) stored
|
||||
);
|
||||
comment on table public.files is 'Files stored in user cabinets with automatic categorization';
|
||||
comment on column public.files.category is 'Automatically determined file category based on MIME type';
|
||||
|
||||
-- AI brains for processing files
|
||||
create table if not exists public.brains (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
user_id uuid not null references public.profiles(id) on delete cascade,
|
||||
name text not null,
|
||||
purpose text,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
comment on table public.brains is 'AI brains for processing and analyzing user files';
|
||||
|
||||
-- Brain-file associations
|
||||
create table if not exists public.brain_files (
|
||||
brain_id uuid not null references public.brains(id) on delete cascade,
|
||||
file_id uuid not null references public.files(id) on delete cascade,
|
||||
primary key (brain_id, file_id)
|
||||
);
|
||||
comment on table public.brain_files is 'Associations between AI brains and files for processing';
|
||||
|
||||
-- Document artefacts from file processing
|
||||
create table if not exists public.document_artefacts (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
file_id uuid references public.files(id) on delete cascade,
|
||||
page_number integer default 0 not null,
|
||||
type text not null,
|
||||
rel_path text not null,
|
||||
size_tag text,
|
||||
language text,
|
||||
chunk_index integer,
|
||||
extra jsonb,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
status text default 'completed' not null check (status in ('pending', 'processing', 'completed', 'failed')),
|
||||
started_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
completed_at timestamp with time zone,
|
||||
error_message text
|
||||
);
|
||||
comment on table public.document_artefacts is 'Extracted artefacts from document processing';
|
||||
comment on column public.document_artefacts.status is 'Extraction status: pending, processing, completed, or failed';
|
||||
comment on column public.document_artefacts.started_at is 'Timestamp when extraction process started';
|
||||
comment on column public.document_artefacts.completed_at is 'Timestamp when extraction process completed (success or failure)';
|
||||
comment on column public.document_artefacts.error_message is 'Error details if extraction failed';
|
||||
|
||||
-- Function execution logs
|
||||
create table if not exists public.function_logs (
|
||||
id serial primary key,
|
||||
file_id uuid references public.files(id) on delete cascade,
|
||||
timestamp timestamp with time zone default timezone('utc'::text, now()),
|
||||
step text,
|
||||
message text,
|
||||
data jsonb
|
||||
);
|
||||
comment on table public.function_logs is 'Logs of function executions and processing steps';
|
||||
|
||||
--[ 7. Relationship Tables ]--
|
||||
-- Institute memberships
|
||||
create table if not exists public.institute_memberships (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
profile_id uuid references public.profiles(id) on delete cascade,
|
||||
institute_id uuid references public.institutes(id) on delete cascade,
|
||||
role text not null check (role in ('teacher', 'student')),
|
||||
tldraw_preferences jsonb default '{}'::jsonb,
|
||||
metadata jsonb default '{}'::jsonb,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
unique(profile_id, institute_id)
|
||||
);
|
||||
comment on table public.institute_memberships is 'Manages user roles and relationships with institutes';
|
||||
|
||||
-- Membership requests
|
||||
create table if not exists public.institute_membership_requests (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
profile_id uuid references public.profiles(id) on delete cascade,
|
||||
institute_id uuid references public.institutes(id) on delete cascade,
|
||||
requested_role text check (requested_role in ('teacher', 'student')),
|
||||
status text default 'pending' check (status in ('pending', 'approved', 'rejected')),
|
||||
metadata jsonb default '{}'::jsonb,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
comment on table public.institute_membership_requests is 'Tracks requests to join institutes';
|
||||
|
||||
--[ 8. Audit Tables ]--
|
||||
-- System audit logs
|
||||
create table if not exists public.audit_logs (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
profile_id uuid references public.profiles(id) on delete set null,
|
||||
action_type text,
|
||||
table_name text,
|
||||
record_id uuid,
|
||||
changes jsonb,
|
||||
created_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
comment on table public.audit_logs is 'System-wide audit trail for important operations';
|
||||
|
||||
--[ 9. Exam Specifications ]--
|
||||
create table if not exists public.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,
|
||||
|
||||
-- Document storage details
|
||||
storage_loc text,
|
||||
doc_type text check (doc_type in ('pdf', 'json', 'md', 'html', 'txt', 'doctags')),
|
||||
doc_details jsonb default '{}'::jsonb, -- e.g. Tika extract
|
||||
docling_docs jsonb default '{}'::jsonb, -- e.g. Docling extracts settings and storage locations
|
||||
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
|
||||
comment on table public.eb_specifications is 'Exam board specifications and their primary document';
|
||||
comment on column public.eb_specifications.spec_code is 'Unique code for the specification, used for linking exams';
|
||||
comment on column public.eb_specifications.doc_details is 'Tika extract of the specification document';
|
||||
comment on column public.eb_specifications.docling_docs is 'Docling extracts settings and storage locations for the specification document';
|
||||
|
||||
--[ 10. Exam Papers / Entries ]--
|
||||
create table if not exists public.eb_exams (
|
||||
id uuid primary key default uuid_generate_v4(),
|
||||
exam_code text unique,
|
||||
spec_code text references public.eb_specifications(spec_code) on delete cascade,
|
||||
paper_code text,
|
||||
tier text,
|
||||
session text,
|
||||
type_code text,
|
||||
|
||||
-- Document storage details
|
||||
storage_loc text,
|
||||
doc_type text check (doc_type in ('pdf', 'json', 'md', 'html', 'txt', 'doctags')),
|
||||
doc_details jsonb default '{}'::jsonb, -- e.g. Tika extract
|
||||
docling_docs jsonb default '{}'::jsonb, -- e.g. Docling extracts settings and storage locations
|
||||
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now())
|
||||
);
|
||||
|
||||
comment on table public.eb_exams is 'Exam papers and related documents linked to specifications';
|
||||
comment on column public.eb_exams.exam_code is 'Unique code for the exam paper, used for linking questions';
|
||||
comment on column public.eb_exams.type_code is 'Type code for the exam document: Question Paper (QP), Mark Scheme (MS), Examiner Report (ER), Other (OT)';
|
||||
comment on column public.eb_exams.doc_details is 'Tika extract of the exam paper document';
|
||||
comment on column public.eb_exams.docling_docs is 'Docling extracts settings and storage locations for the exam paper document';
|
||||
|
||||
--[ 11. Indexes ]--
|
||||
-- Index for geospatial queries
|
||||
create index if not exists idx_institutes_geo_coordinates on public.institutes using gin(geo_coordinates);
|
||||
create index if not exists idx_institutes_urn on public.institutes(urn);
|
||||
|
||||
-- Document artefacts indexes
|
||||
create index if not exists idx_document_artefacts_file_status on public.document_artefacts(file_id, status);
|
||||
create index if not exists idx_document_artefacts_file_type on public.document_artefacts(file_id, type);
|
||||
create index if not exists idx_document_artefacts_status on public.document_artefacts(status);
|
||||
|
||||
-- File indexes
|
||||
create index if not exists idx_files_cabinet_id on public.files(cabinet_id);
|
||||
create index if not exists idx_files_mime_type on public.files(mime_type);
|
||||
create index if not exists idx_files_category on public.files(category);
|
||||
|
||||
-- Brain indexes
|
||||
create index if not exists idx_brains_user_id on public.brains(user_id);
|
||||
|
||||
-- Exam board indexes
|
||||
create index if not exists idx_eb_exams_exam_code on public.eb_exams(exam_code);
|
||||
create index if not exists idx_eb_exams_spec_code on public.eb_exams(spec_code);
|
||||
create index if not exists idx_eb_exams_paper_code on public.eb_exams(paper_code);
|
||||
create index if not exists idx_eb_exams_tier on public.eb_exams(tier);
|
||||
create index if not exists idx_eb_exams_session on public.eb_exams(session);
|
||||
create index if not exists idx_eb_exams_type_code on public.eb_exams(type_code);
|
||||
create index if not exists idx_eb_specifications_spec_code on public.eb_specifications(spec_code);
|
||||
create index if not exists idx_eb_specifications_exam_board_code on public.eb_specifications(exam_board_code);
|
||||
create index if not exists idx_eb_specifications_award_code on public.eb_specifications(award_code);
|
||||
create index if not exists idx_eb_specifications_subject_code on public.eb_specifications(subject_code);
|
||||
|
||||
--[ 12. Triggers ]--
|
||||
-- Set completed_at when document artefact status changes to completed
|
||||
create trigger trigger_set_completed_at
|
||||
before update on public.document_artefacts
|
||||
for each row
|
||||
execute function public.set_completed_at();
|
||||
|
||||
-- Set updated_at on profile updates
|
||||
create trigger trigger_profiles_updated_at
|
||||
before update on public.profiles
|
||||
for each row
|
||||
execute function public.handle_updated_at();
|
||||
|
||||
-- Set updated_at on institute updates
|
||||
create trigger trigger_institutes_updated_at
|
||||
before update on public.institutes
|
||||
for each row
|
||||
execute function public.handle_updated_at();
|
||||
|
||||
-- Set updated_at on institute_memberships updates
|
||||
create trigger trigger_institute_memberships_updated_at
|
||||
before update on public.institute_memberships
|
||||
for each row
|
||||
execute function public.handle_updated_at();
|
||||
|
||||
-- Set updated_at on institute_membership_requests updates
|
||||
create trigger trigger_institute_membership_requests_updated_at
|
||||
before update on public.institute_memberships
|
||||
for each row
|
||||
execute function public.handle_updated_at();
|
||||
|
||||
-- Set updated_at on eb_specifications updates
|
||||
create trigger trigger_eb_specifications_updated_at
|
||||
before update on public.eb_specifications
|
||||
for each row
|
||||
execute function public.handle_updated_at();
|
||||
|
||||
-- Set updated_at on eb_exams updates
|
||||
create trigger trigger_eb_exams_updated_at
|
||||
before update on public.eb_exams
|
||||
for each row
|
||||
execute function public.handle_updated_at();
|
||||
71
volumes/db/cc/61-gais-reference.sql
Normal file
71
volumes/db/cc/61-gais-reference.sql
Normal file
@ -0,0 +1,71 @@
|
||||
-- 001_gais_seed.sql
|
||||
-- GAIS (Get Information About Schools) reference tables
|
||||
-- Source: Edubase open data, https://www.get-information-schools.service.gov.uk/
|
||||
-- Apply once to the Supabase Postgres instance via the SQL editor.
|
||||
|
||||
-- ─── Local Authorities ───────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gais_local_authorities (
|
||||
code TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ─── Schools ─────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS gais_schools (
|
||||
urn TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
status TEXT, -- Open | Closed | Proposed to Open
|
||||
phase TEXT, -- Primary | Secondary | 16 plus | etc.
|
||||
type TEXT, -- Voluntary aided school | Academy | etc.
|
||||
type_group TEXT, -- Local authority maintained | Independent | etc.
|
||||
street TEXT,
|
||||
locality TEXT,
|
||||
town TEXT,
|
||||
county TEXT,
|
||||
postcode TEXT,
|
||||
website TEXT,
|
||||
telephone TEXT,
|
||||
head_title TEXT,
|
||||
head_first_name TEXT,
|
||||
head_last_name TEXT,
|
||||
la_code TEXT REFERENCES gais_local_authorities(code),
|
||||
la_name TEXT,
|
||||
number_of_pupils INTEGER,
|
||||
open_date DATE,
|
||||
close_date DATE,
|
||||
gender TEXT, -- Mixed | Girls | Boys
|
||||
religious_character TEXT,
|
||||
region TEXT, -- Government Office Region
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
);
|
||||
|
||||
-- ─── Indexes ─────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Full-text search on name + town + postcode
|
||||
CREATE INDEX IF NOT EXISTS gais_schools_name_fts
|
||||
ON gais_schools USING gin(to_tsvector('english', coalesce(name, '') || ' ' || coalesce(town, '') || ' ' || coalesce(postcode, '')));
|
||||
|
||||
-- Trigram index for ILIKE search (pg_trgm extension required)
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE INDEX IF NOT EXISTS gais_schools_name_trgm ON gais_schools USING gin(name gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS gais_schools_town_trgm ON gais_schools USING gin(town gin_trgm_ops);
|
||||
CREATE INDEX IF NOT EXISTS gais_schools_postcode_trgm ON gais_schools USING gin(postcode gin_trgm_ops);
|
||||
|
||||
-- Status and LA for filtered queries
|
||||
CREATE INDEX IF NOT EXISTS gais_schools_status ON gais_schools(status);
|
||||
CREATE INDEX IF NOT EXISTS gais_schools_la_code ON gais_schools(la_code);
|
||||
|
||||
-- ─── RLS ─────────────────────────────────────────────────────────────────────
|
||||
-- Public read (these are open-data reference tables).
|
||||
-- Writes are only via service-role (admin imports / seed scripts).
|
||||
|
||||
ALTER TABLE gais_local_authorities ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE gais_schools ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY "Public read gais_local_authorities"
|
||||
ON gais_local_authorities FOR SELECT USING (true);
|
||||
|
||||
CREATE POLICY "Public read gais_schools"
|
||||
ON gais_schools FOR SELECT USING (true);
|
||||
763
volumes/db/cc/62-application-schema.sql
Normal file
763
volumes/db/cc/62-application-schema.sql
Normal file
@ -0,0 +1,763 @@
|
||||
-- ============================================================
|
||||
-- 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()));
|
||||
@ -1,191 +0,0 @@
|
||||
--[ 8. Auth Functions ]--
|
||||
-- Create a secure function to check admin status
|
||||
create or replace function public.is_admin()
|
||||
returns boolean as $$
|
||||
select coalesce(
|
||||
(select true
|
||||
from public.profiles
|
||||
where id = auth.uid()
|
||||
and user_type = 'admin'),
|
||||
false
|
||||
);
|
||||
$$ language sql security definer;
|
||||
|
||||
-- Create a secure function to check super admin status
|
||||
create or replace function public.is_super_admin()
|
||||
returns boolean as $$
|
||||
select coalesce(
|
||||
(select true
|
||||
from public.profiles
|
||||
where id = auth.uid()
|
||||
and user_type = 'admin'),
|
||||
false
|
||||
);
|
||||
$$ language sql security definer;
|
||||
|
||||
-- Create public wrapper functions
|
||||
-- Note: These are now the main implementation functions, not wrappers
|
||||
-- The original auth schema functions have been moved to public schema
|
||||
|
||||
-- Grant execute permissions
|
||||
grant execute on function public.is_admin to authenticated;
|
||||
grant execute on function public.is_super_admin to authenticated;
|
||||
|
||||
-- Initial admin setup function
|
||||
create or replace function public.setup_initial_admin(admin_email text)
|
||||
returns json
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
declare
|
||||
result json;
|
||||
begin
|
||||
-- Only allow this to run as service role or superuser
|
||||
if not (
|
||||
current_user = 'service_role'
|
||||
or exists (
|
||||
select 1 from pg_roles
|
||||
where rolname = current_user
|
||||
and rolsuper
|
||||
)
|
||||
) then
|
||||
raise exception 'Must be run as service_role or superuser';
|
||||
end if;
|
||||
|
||||
-- Update user_type and username for admin
|
||||
update public.profiles
|
||||
set user_type = 'admin',
|
||||
username = coalesce(username, 'superadmin'),
|
||||
display_name = coalesce(display_name, 'Super Admin')
|
||||
where email = admin_email
|
||||
returning json_build_object(
|
||||
'id', id,
|
||||
'email', email,
|
||||
'user_type', user_type,
|
||||
'username', username,
|
||||
'display_name', display_name
|
||||
) into result;
|
||||
|
||||
if result is null then
|
||||
raise exception 'Admin user with email % not found', admin_email;
|
||||
end if;
|
||||
|
||||
return result;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Grant execute permissions
|
||||
revoke execute on function public.setup_initial_admin from public;
|
||||
grant execute on function public.setup_initial_admin to authenticated, service_role;
|
||||
|
||||
-- Create RPC wrapper for REST API access
|
||||
create or replace function rpc.setup_initial_admin(admin_email text)
|
||||
returns json
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
return public.setup_initial_admin(admin_email);
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Grant execute permissions for RPC wrapper
|
||||
grant execute on function rpc.setup_initial_admin to authenticated, service_role;
|
||||
|
||||
--[ 9. Utility Functions ]--
|
||||
-- Check if database is ready
|
||||
create or replace function check_db_ready()
|
||||
returns boolean
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
-- Check if essential schemas exist
|
||||
if not exists (
|
||||
select 1
|
||||
from information_schema.schemata
|
||||
where schema_name in ('auth', 'storage', 'public')
|
||||
) then
|
||||
return false;
|
||||
end if;
|
||||
|
||||
-- Check if essential tables exist
|
||||
if not exists (
|
||||
select 1
|
||||
from information_schema.tables
|
||||
where table_schema = 'auth'
|
||||
and table_name = 'users'
|
||||
) then
|
||||
return false;
|
||||
end if;
|
||||
|
||||
-- Check if RLS is enabled on public.profiles
|
||||
if not exists (
|
||||
select 1
|
||||
from pg_tables
|
||||
where schemaname = 'public'
|
||||
and tablename = 'profiles'
|
||||
and rowsecurity = true
|
||||
) then
|
||||
return false;
|
||||
end if;
|
||||
|
||||
return true;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Grant execute permission
|
||||
grant execute on function check_db_ready to anon, authenticated, service_role;
|
||||
|
||||
-- Function to handle new user registration
|
||||
create or replace function public.handle_new_user()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
security definer set search_path = public
|
||||
as $$
|
||||
declare
|
||||
default_user_type text := 'email_student';
|
||||
default_username text;
|
||||
begin
|
||||
-- Generate username from email
|
||||
default_username := split_part(new.email, '@', 1);
|
||||
|
||||
insert into public.profiles (
|
||||
id,
|
||||
email,
|
||||
user_type,
|
||||
username,
|
||||
display_name
|
||||
)
|
||||
values (
|
||||
new.id,
|
||||
new.email,
|
||||
coalesce(new.raw_user_meta_data->>'user_type', default_user_type),
|
||||
coalesce(new.raw_user_meta_data->>'username', default_username),
|
||||
coalesce(new.raw_user_meta_data->>'display_name', default_username)
|
||||
);
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
-- Trigger for new user creation
|
||||
drop trigger if exists on_auth_user_created on auth.users;
|
||||
create trigger on_auth_user_created
|
||||
after insert on auth.users
|
||||
for each row execute procedure public.handle_new_user();
|
||||
|
||||
--[ 11. Database Triggers ]--
|
||||
drop trigger if exists handle_profiles_updated_at on public.profiles;
|
||||
create trigger handle_profiles_updated_at
|
||||
before update on public.profiles
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
drop trigger if exists handle_institute_memberships_updated_at on public.institute_memberships;
|
||||
create trigger handle_institute_memberships_updated_at
|
||||
before update on public.institute_memberships
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
drop trigger if exists handle_membership_requests_updated_at on public.institute_membership_requests;
|
||||
create trigger handle_membership_requests_updated_at
|
||||
before update on public.institute_membership_requests
|
||||
for each row execute function public.handle_updated_at();
|
||||
225
volumes/db/cc/63-academic-calendar.sql
Normal file
225
volumes/db/cc/63-academic-calendar.sql
Normal file
@ -0,0 +1,225 @@
|
||||
-- ============================================================
|
||||
-- Classroom Copilot — Academic Calendar Source of Truth
|
||||
-- Migration 003: Supabase-backed academic calendar & timetable tables
|
||||
-- Run after: 002_schema.sql
|
||||
--
|
||||
-- Design: Supabase is the source of truth for all editable calendar
|
||||
-- and timetable data. Neo4j is a derived graph rebuilt from these tables.
|
||||
-- All tables include neo4j_node_id to track the corresponding Neo4j uuid_string.
|
||||
-- ============================================================
|
||||
|
||||
-- ─── 1. school_timetables ────────────────────────────────────────────────────
|
||||
-- One row per academic year configuration per school.
|
||||
-- periods_template JSONB stores the period definitions (code, name, times, type).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS school_timetables (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
year_label TEXT NOT NULL, -- e.g. '2025-2026'
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
periods_template JSONB NOT NULL DEFAULT '[]',
|
||||
neo4j_node_id TEXT, -- SchoolTimetable.uuid_string in Neo4j
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (institute_id, year_label)
|
||||
);
|
||||
|
||||
-- ─── 2. academic_years ───────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_years (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
school_timetable_id UUID NOT NULL REFERENCES school_timetables(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
year_label TEXT NOT NULL, -- '2025-2026'
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (school_timetable_id, year_label)
|
||||
);
|
||||
|
||||
-- ─── 3. academic_terms ───────────────────────────────────────────────────────
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_terms (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
academic_year_id UUID NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
term_name TEXT NOT NULL,
|
||||
term_number INTEGER NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (academic_year_id, term_number)
|
||||
);
|
||||
|
||||
-- ─── 4. academic_weeks ───────────────────────────────────────────────────────
|
||||
-- week_cycle 'A'|'B' for two-week timetable cycles.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_weeks (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
academic_term_id UUID NOT NULL REFERENCES academic_terms(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
week_number INTEGER NOT NULL, -- sequential within term
|
||||
start_date DATE NOT NULL, -- Monday of this week
|
||||
week_cycle TEXT NOT NULL DEFAULT 'A' CHECK (week_cycle IN ('A', 'B', '')),
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (academic_term_id, week_number)
|
||||
);
|
||||
|
||||
-- ─── 5. academic_days ────────────────────────────────────────────────────────
|
||||
-- One row per school day (Mon–Fri within term bounds).
|
||||
-- excluded_period_codes: period codes from the template that do NOT apply this day.
|
||||
-- academic_day_number: sequential count of Academic-type days across the year.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_days (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
academic_week_id UUID NOT NULL REFERENCES academic_weeks(id) ON DELETE CASCADE,
|
||||
academic_term_id UUID NOT NULL REFERENCES academic_terms(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
date DATE NOT NULL,
|
||||
day_of_week TEXT NOT NULL,
|
||||
day_type TEXT NOT NULL DEFAULT 'Academic'
|
||||
CHECK (day_type IN ('Academic', 'Holiday', 'Staff', 'OffTimetable')),
|
||||
academic_day_number INTEGER, -- null for non-Academic days
|
||||
excluded_period_codes TEXT[] NOT NULL DEFAULT '{}',
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (institute_id, date)
|
||||
);
|
||||
|
||||
-- ─── 6. teacher_timetables ───────────────────────────────────────────────────
|
||||
-- One per teacher per academic year.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teacher_timetables (
|
||||
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,
|
||||
school_timetable_id UUID NOT NULL REFERENCES school_timetables(id) ON DELETE CASCADE,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (profile_id, school_timetable_id)
|
||||
);
|
||||
|
||||
-- ─── 7. teacher_timetable_slots ──────────────────────────────────────────────
|
||||
-- Weekly recurring slot assignments (day + period → subject class).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teacher_timetable_slots (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
teacher_timetable_id UUID NOT NULL REFERENCES teacher_timetables(id) ON DELETE CASCADE,
|
||||
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
day_of_week TEXT NOT NULL,
|
||||
period_code TEXT NOT NULL,
|
||||
subject_class TEXT NOT NULL,
|
||||
start_time TEXT NOT NULL,
|
||||
end_time TEXT NOT NULL,
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (teacher_timetable_id, day_of_week, period_code)
|
||||
);
|
||||
|
||||
-- ============================================================
|
||||
-- Indexes
|
||||
-- ============================================================
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_school_tt_institute ON school_timetables(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_years_tt ON academic_years(school_timetable_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_years_inst ON academic_years(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_terms_year ON academic_terms(academic_year_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_terms_inst ON academic_terms(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_weeks_term ON academic_weeks(academic_term_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_weeks_inst ON academic_weeks(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_days_week ON academic_days(academic_week_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_days_term ON academic_days(academic_term_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_academic_days_inst_date ON academic_days(institute_id, date);
|
||||
CREATE INDEX IF NOT EXISTS idx_teacher_tt_profile ON teacher_timetables(profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_teacher_tt_inst ON teacher_timetables(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tt_slots_timetable ON teacher_timetable_slots(teacher_timetable_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tt_slots_profile ON teacher_timetable_slots(profile_id);
|
||||
|
||||
-- ============================================================
|
||||
-- updated_at triggers (tables that have updated_at)
|
||||
-- ============================================================
|
||||
|
||||
DO $$ DECLARE t TEXT; BEGIN
|
||||
FOREACH t IN ARRAY ARRAY[
|
||||
'school_timetables', 'teacher_timetables', 'teacher_timetable_slots'
|
||||
] 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 $$;
|
||||
|
||||
-- ============================================================
|
||||
-- Row Level Security
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE school_timetables ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE academic_years ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE academic_terms ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE academic_weeks ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE academic_days ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE teacher_timetables ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE teacher_timetable_slots ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- school_timetables: institute members can read
|
||||
CREATE POLICY "stt_inst_read" ON school_timetables FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "stt_service" ON school_timetables FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- academic_years
|
||||
CREATE POLICY "ay_inst_read" ON academic_years FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "ay_service" ON academic_years FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- academic_terms
|
||||
CREATE POLICY "at_inst_read" ON academic_terms FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "at_service" ON academic_terms FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- academic_weeks
|
||||
CREATE POLICY "aw_inst_read" ON academic_weeks FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "aw_service" ON academic_weeks FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- academic_days
|
||||
CREATE POLICY "ad_inst_read" ON academic_days FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "ad_service" ON academic_days FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- teacher_timetables: own row
|
||||
CREATE POLICY "tcht_own_read" ON teacher_timetables FOR SELECT
|
||||
USING (profile_id = auth.uid());
|
||||
CREATE POLICY "tcht_service" ON teacher_timetables FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- teacher_timetable_slots: own rows
|
||||
CREATE POLICY "tchts_own_read" ON teacher_timetable_slots FOR SELECT
|
||||
USING (profile_id = auth.uid());
|
||||
CREATE POLICY "tchts_service" ON teacher_timetable_slots FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
@ -1,20 +0,0 @@
|
||||
-- Storage policies configuration for Supabase
|
||||
-- Note: Storage bucket policies are managed by Supabase internally
|
||||
-- This file provides guidance on what should be configured
|
||||
|
||||
-- Storage bucket policies should be configured through:
|
||||
-- 1. Supabase Dashboard > Storage > Policies
|
||||
-- 2. Or via SQL with proper permissions (requires service_role or owner access)
|
||||
|
||||
-- Recommended policies for storage.buckets:
|
||||
-- - Super admin has full access to buckets
|
||||
-- - Users can create their own buckets
|
||||
-- - Users can view their own buckets or public buckets
|
||||
|
||||
-- Recommended policies for storage.objects:
|
||||
-- - Users can upload to buckets they own
|
||||
-- - Users can view objects in public buckets
|
||||
-- - Users can manage objects in buckets they own
|
||||
|
||||
-- Note: These policies require the service_role or appropriate permissions
|
||||
-- to be applied to the storage schema tables
|
||||
266
volumes/db/cc/64-extended-schema.sql
Normal file
266
volumes/db/cc/64-extended-schema.sql
Normal file
@ -0,0 +1,266 @@
|
||||
-- ============================================================
|
||||
-- Classroom Copilot — Extended Schema
|
||||
-- Migration 004: academic_term_breaks, academic_periods,
|
||||
-- taught_lessons, invitations + ALTER extensions
|
||||
-- Run after: 003_academic_calendar.sql
|
||||
-- ============================================================
|
||||
|
||||
-- ─── admin_profiles: add updated_at trigger (table already exists) ───────────
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_trigger
|
||||
WHERE tgname = 'trg_updated_at'
|
||||
AND tgrelid = 'public.admin_profiles'::regclass
|
||||
) THEN
|
||||
EXECUTE 'CREATE TRIGGER trg_updated_at
|
||||
BEFORE UPDATE ON admin_profiles
|
||||
FOR EACH ROW EXECUTE FUNCTION set_updated_at()';
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── 1. academic_term_breaks ─────────────────────────────────────────────────
|
||||
-- Explicit named holiday periods between terms.
|
||||
-- Admins name and date these; agents can look them up or even populate them.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_term_breaks (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
school_timetable_id UUID NOT NULL REFERENCES school_timetables(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
break_name TEXT NOT NULL, -- e.g. "Christmas Break", "Easter Break"
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
notes TEXT,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (school_timetable_id, break_name)
|
||||
);
|
||||
|
||||
-- ─── 2. academic_periods ─────────────────────────────────────────────────────
|
||||
-- One row per period per ACADEMIC day (not holiday/staff days).
|
||||
-- Instantiated at timetable setup time from school_timetables.periods_template.
|
||||
-- Enables per-period notes, room assignments, and substitutions.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_periods (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
academic_day_id UUID NOT NULL REFERENCES academic_days(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
period_code TEXT NOT NULL, -- e.g. "1", "2", "Reg", "Break1"
|
||||
period_name TEXT NOT NULL, -- e.g. "Period 1", "Registration"
|
||||
period_type TEXT NOT NULL CHECK (period_type IN ('lesson','break','registration','offtimetable')),
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
room_code TEXT, -- default room; overridden per taught_lesson
|
||||
notes TEXT,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (academic_day_id, period_code)
|
||||
);
|
||||
|
||||
-- ─── 3. invitations ──────────────────────────────────────────────────────────
|
||||
-- Tracks all staff and student invitations. Created by school admins.
|
||||
-- API calls Supabase magic link on creation; status updated on acceptance.
|
||||
-- metadata: year_group for students, subject/department for staff, etc.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS invitations (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
email TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('teacher','student','school_admin','department_head')),
|
||||
invited_by UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
token UUID NOT NULL DEFAULT uuid_generate_v4(),
|
||||
expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + interval '7 days'),
|
||||
status TEXT NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending','accepted','expired','cancelled')),
|
||||
metadata JSONB NOT NULL DEFAULT '{}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
-- Only one pending invitation per (institute, email) at a time.
|
||||
-- After acceptance/expiry/cancellation a new one may be issued.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_invitations_pending_unique
|
||||
ON invitations (institute_id, email)
|
||||
WHERE (status = 'pending');
|
||||
|
||||
-- ─── 4. taught_lessons ───────────────────────────────────────────────────────
|
||||
-- One row per actual lesson occurrence, materialized from the teacher's
|
||||
-- timetable slot template × matching academic_periods across the year.
|
||||
-- School admin controls the frame (periods, rooms, substitutions).
|
||||
-- Teachers control the content (lesson_plan, notes, tags, status).
|
||||
|
||||
CREATE TABLE IF NOT EXISTS taught_lessons (
|
||||
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
academic_period_id UUID NOT NULL REFERENCES academic_periods(id) ON DELETE CASCADE,
|
||||
teacher_timetable_slot_id UUID REFERENCES teacher_timetable_slots(id) ON DELETE SET NULL,
|
||||
class_id UUID NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
|
||||
teacher_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
||||
institute_id UUID NOT NULL REFERENCES institutes(id) ON DELETE CASCADE,
|
||||
-- Denormalized for fast timeline queries (avoids 4-table joins)
|
||||
date DATE NOT NULL,
|
||||
period_code TEXT NOT NULL,
|
||||
week_cycle TEXT NOT NULL DEFAULT '',
|
||||
day_of_week TEXT NOT NULL,
|
||||
-- Teacher-owned content
|
||||
lesson_plan JSONB NOT NULL DEFAULT '{}',
|
||||
whiteboard_room_id UUID REFERENCES whiteboard_rooms(id) ON DELETE SET NULL,
|
||||
status TEXT NOT NULL DEFAULT 'planned'
|
||||
CHECK (status IN ('planned','in_progress','completed','cancelled','substituted')),
|
||||
substitute_teacher_id UUID REFERENCES profiles(id) ON DELETE SET NULL,
|
||||
notes TEXT,
|
||||
tags TEXT[] NOT NULL DEFAULT '{}',
|
||||
neo4j_node_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
UNIQUE (academic_period_id, teacher_id)
|
||||
);
|
||||
|
||||
-- ─── 5. Extend existing tables with notes + tags ──────────────────────────────
|
||||
-- ADD COLUMN IF NOT EXISTS is idempotent — safe to re-run.
|
||||
|
||||
ALTER TABLE academic_terms ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||
ALTER TABLE academic_terms ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
ALTER TABLE academic_weeks ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||
ALTER TABLE academic_weeks ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
ALTER TABLE academic_days ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||
ALTER TABLE academic_days ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}';
|
||||
|
||||
-- week_cycle on teacher_timetable_slots: '' = applies both weeks, 'A'/'B' = specific cycle.
|
||||
ALTER TABLE teacher_timetable_slots ADD COLUMN IF NOT EXISTS week_cycle TEXT NOT NULL DEFAULT '';
|
||||
|
||||
-- Drop old UNIQUE and replace with cycle-aware version.
|
||||
-- The old constraint was (teacher_timetable_id, day_of_week, period_code).
|
||||
-- PostgreSQL's generated name may differ/truncate across bootstrap history, so detect
|
||||
-- the actual constraint by constrained column names instead of a stale hard-coded name.
|
||||
DO $$
|
||||
DECLARE
|
||||
old_constraint_name TEXT;
|
||||
BEGIN
|
||||
SELECT con.conname INTO old_constraint_name
|
||||
FROM pg_constraint con
|
||||
JOIN pg_class rel ON rel.oid = con.conrelid
|
||||
JOIN pg_namespace nsp ON nsp.oid = rel.relnamespace
|
||||
WHERE nsp.nspname = 'public'
|
||||
AND rel.relname = 'teacher_timetable_slots'
|
||||
AND con.contype = 'u'
|
||||
AND (
|
||||
SELECT array_agg(att.attname::text ORDER BY ord.ordinality)
|
||||
FROM unnest(con.conkey) WITH ORDINALITY AS ord(attnum, ordinality)
|
||||
JOIN pg_attribute att ON att.attrelid = con.conrelid AND att.attnum = ord.attnum
|
||||
) = ARRAY['teacher_timetable_id', 'day_of_week', 'period_code']::text[]
|
||||
LIMIT 1;
|
||||
|
||||
IF old_constraint_name IS NOT NULL THEN
|
||||
EXECUTE format('ALTER TABLE public.teacher_timetable_slots DROP CONSTRAINT %I', old_constraint_name);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'tts_unique_slot'
|
||||
AND conrelid = 'public.teacher_timetable_slots'::regclass
|
||||
) THEN
|
||||
ALTER TABLE public.teacher_timetable_slots
|
||||
ADD CONSTRAINT tts_unique_slot UNIQUE (teacher_timetable_id, week_cycle, day_of_week, period_code);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── 6. Indexes ───────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_term_breaks_tt ON academic_term_breaks(school_timetable_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_term_breaks_inst ON academic_term_breaks(institute_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ap_day ON academic_periods(academic_day_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ap_inst ON academic_periods(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ap_type ON academic_periods(period_type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_inst ON invitations(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_email ON invitations(email);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_token ON invitations(token);
|
||||
CREATE INDEX IF NOT EXISTS idx_inv_status ON invitations(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_period ON taught_lessons(academic_period_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_teacher ON taught_lessons(teacher_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_class ON taught_lessons(class_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_inst ON taught_lessons(institute_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_date ON taught_lessons(date);
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_inst_date ON taught_lessons(institute_id, date);
|
||||
|
||||
-- ─── 7. updated_at trigger ────────────────────────────────────────────────────
|
||||
|
||||
DO $$ DECLARE t TEXT; BEGIN
|
||||
FOREACH t IN ARRAY ARRAY['taught_lessons'] 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 $$;
|
||||
|
||||
-- ─── 8. Row Level Security ────────────────────────────────────────────────────
|
||||
|
||||
ALTER TABLE academic_term_breaks ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE academic_periods ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE invitations ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE taught_lessons ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
-- ── academic_term_breaks ──────────────────────────────────────────────────────
|
||||
-- Any institute member can read; all writes via service_role (API).
|
||||
|
||||
CREATE POLICY "atb_inst_read" ON academic_term_breaks FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "atb_service" ON academic_term_breaks FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- ── academic_periods ──────────────────────────────────────────────────────────
|
||||
-- Any institute member can read; all writes via service_role (API).
|
||||
|
||||
CREATE POLICY "ap_inst_read" ON academic_periods FOR SELECT
|
||||
USING (institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships WHERE profile_id = auth.uid()
|
||||
));
|
||||
CREATE POLICY "ap_service" ON academic_periods FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- ── invitations ───────────────────────────────────────────────────────────────
|
||||
-- School admins and the inviter can view their school's invitations.
|
||||
-- All mutations via service_role (invitations created server-side only).
|
||||
|
||||
CREATE POLICY "inv_admin_read" ON invitations FOR SELECT
|
||||
USING (
|
||||
invited_by = auth.uid()
|
||||
OR institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships
|
||||
WHERE profile_id = auth.uid()
|
||||
AND role IN ('school_admin', 'department_head')
|
||||
)
|
||||
);
|
||||
CREATE POLICY "inv_service" ON invitations FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
|
||||
-- ── taught_lessons ────────────────────────────────────────────────────────────
|
||||
-- Teachers read their own lessons; school admins read all in their school.
|
||||
-- Teachers can UPDATE their own lesson content (plan, notes, tags, status).
|
||||
-- Frame changes (room, substitute) and lesson creation: service_role only.
|
||||
|
||||
CREATE POLICY "tl_read" ON taught_lessons FOR SELECT
|
||||
USING (
|
||||
teacher_id = auth.uid()
|
||||
OR institute_id IN (
|
||||
SELECT institute_id FROM institute_memberships
|
||||
WHERE profile_id = auth.uid()
|
||||
AND role IN ('school_admin', 'department_head')
|
||||
)
|
||||
);
|
||||
CREATE POLICY "tl_teacher_update" ON taught_lessons FOR UPDATE
|
||||
USING (teacher_id = auth.uid())
|
||||
WITH CHECK (teacher_id = auth.uid());
|
||||
CREATE POLICY "tl_service" ON taught_lessons FOR ALL
|
||||
USING (auth.role() = 'service_role') WITH CHECK (auth.role() = 'service_role');
|
||||
@ -1,20 +0,0 @@
|
||||
-- Initial admin setup for ClassroomCopilot
|
||||
-- This file handles basic database setup and permissions
|
||||
|
||||
-- Ensure uuid-ossp extension is enabled
|
||||
create extension if not exists "uuid-ossp" schema extensions;
|
||||
|
||||
-- Grant basic permissions to authenticated users for public schema
|
||||
-- Note: These permissions are granted to allow users to work with the application
|
||||
grant usage on schema public to authenticated;
|
||||
grant all on all tables in schema public to authenticated;
|
||||
grant all on all sequences in schema public to authenticated;
|
||||
grant all on all functions in schema public to authenticated;
|
||||
|
||||
-- Set default privileges for future objects
|
||||
alter default privileges in schema public grant all on tables to authenticated;
|
||||
alter default privileges in schema public grant all on sequences to authenticated;
|
||||
alter default privileges in schema public grant all on functions to authenticated;
|
||||
|
||||
-- Note: The setup_initial_admin function is defined in 62-functions-triggers.sql
|
||||
-- and should be called with an admin email parameter when needed
|
||||
@ -1,95 +0,0 @@
|
||||
-- Files table augments and storage GC hooks
|
||||
|
||||
-- 1) Add columns to files if missing
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='files' and column_name='uploaded_by'
|
||||
) then
|
||||
alter table public.files add column uploaded_by uuid references public.profiles(id);
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='files' and column_name='size_bytes'
|
||||
) then
|
||||
alter table public.files add column size_bytes bigint;
|
||||
end if;
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_schema='public' and table_name='files' and column_name='source'
|
||||
) then
|
||||
alter table public.files add column source text default 'uploader-web';
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- 2) Unique index for cabinet/path combo
|
||||
create unique index if not exists uq_files_cabinet_path on public.files(cabinet_id, path);
|
||||
|
||||
-- 3) Storage GC helpers (ported from neoFS with storage schema)
|
||||
create or replace function public._delete_storage_objects(p_bucket text, p_path text)
|
||||
returns void
|
||||
language plpgsql security definer
|
||||
set search_path to 'public', 'storage'
|
||||
as $$
|
||||
begin
|
||||
if p_bucket is null or p_path is null then
|
||||
return;
|
||||
end if;
|
||||
delete from storage.objects where bucket_id = p_bucket and name = p_path;
|
||||
delete from storage.objects where bucket_id = p_bucket and name like p_path || '/%';
|
||||
end
|
||||
$$;
|
||||
|
||||
create or replace function public._storage_gc_sql()
|
||||
returns trigger
|
||||
language plpgsql security definer
|
||||
set search_path to 'public', 'storage'
|
||||
as $$
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
perform public._delete_storage_objects(old.bucket, old.path);
|
||||
elsif tg_op = 'UPDATE' then
|
||||
if (old.bucket is distinct from new.bucket) or (old.path is distinct from new.path) then
|
||||
perform public._delete_storage_objects(old.bucket, old.path);
|
||||
end if;
|
||||
end if;
|
||||
return null;
|
||||
end
|
||||
$$;
|
||||
|
||||
-- 4) Attach GC trigger to files bucket/path changes
|
||||
drop trigger if exists trg_files_gc on public.files;
|
||||
create trigger trg_files_gc
|
||||
after delete or update of bucket, path on public.files
|
||||
for each row execute function public._storage_gc_sql();
|
||||
|
||||
-- 5) Document artefacts GC: remove artefact objects from storage when rows change/delete
|
||||
create or replace function public._artefact_gc_sql()
|
||||
returns trigger
|
||||
language plpgsql security definer
|
||||
set search_path to 'public', 'storage'
|
||||
as $$
|
||||
declare
|
||||
v_bucket text;
|
||||
begin
|
||||
if tg_op = 'DELETE' then
|
||||
select f.bucket into v_bucket from public.files f where f.id = old.file_id;
|
||||
perform public._delete_storage_objects(v_bucket, old.rel_path);
|
||||
return old;
|
||||
elsif tg_op = 'UPDATE' then
|
||||
if (old.rel_path is distinct from new.rel_path) or (old.file_id is distinct from new.file_id) then
|
||||
select f.bucket into v_bucket from public.files f where f.id = old.file_id;
|
||||
perform public._delete_storage_objects(v_bucket, old.rel_path);
|
||||
end if;
|
||||
return new;
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists trg_document_artefacts_gc on public.document_artefacts;
|
||||
create trigger trg_document_artefacts_gc
|
||||
before delete or update of file_id, rel_path on public.document_artefacts
|
||||
for each row execute function public._artefact_gc_sql();
|
||||
|
||||
|
||||
53
volumes/db/cc/65-phase-c.sql
Normal file
53
volumes/db/cc/65-phase-c.sql
Normal file
@ -0,0 +1,53 @@
|
||||
-- ============================================================
|
||||
-- Classroom Copilot — Phase C Migration
|
||||
-- 003: Clean schema + lesson planning tables
|
||||
-- Run after: 002_schema.sql
|
||||
-- ============================================================
|
||||
|
||||
-- ============================================================
|
||||
-- 1. Drop legacy tables (Neo4j-era, replaced by Phase B/C design)
|
||||
-- ============================================================
|
||||
|
||||
DROP TABLE IF EXISTS lesson_whiteboards CASCADE;
|
||||
DROP TABLE IF EXISTS timetable_lessons CASCADE;
|
||||
DROP TABLE IF EXISTS timetable_classes CASCADE;
|
||||
DROP TABLE IF EXISTS timetables CASCADE;
|
||||
DROP TABLE IF EXISTS lessons CASCADE;
|
||||
DROP TABLE IF EXISTS audit_logs CASCADE;
|
||||
DROP TABLE IF EXISTS function_logs CASCADE;
|
||||
|
||||
-- ============================================================
|
||||
-- 2. planned_lessons — drop Neo4j-era field, add course support
|
||||
-- ============================================================
|
||||
|
||||
-- Drop stale Neo4j reference field and its index
|
||||
DROP INDEX IF EXISTS idx_pl_timetable_period;
|
||||
ALTER TABLE planned_lessons DROP COLUMN IF EXISTS timetable_period_id;
|
||||
|
||||
-- Course support (nullable — populated when courses feature ships)
|
||||
ALTER TABLE planned_lessons
|
||||
ADD COLUMN IF NOT EXISTS course_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS sequence_number INTEGER;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pl_course ON planned_lessons(course_id) WHERE course_id IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 3. lesson_deliveries — link to taught_lessons
|
||||
-- ============================================================
|
||||
|
||||
ALTER TABLE lesson_deliveries
|
||||
ADD COLUMN IF NOT EXISTS taught_lesson_id UUID
|
||||
REFERENCES taught_lessons(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ld_taught_lesson ON lesson_deliveries(taught_lesson_id)
|
||||
WHERE taught_lesson_id IS NOT NULL;
|
||||
|
||||
-- ============================================================
|
||||
-- 4. Verify Phase C tables exist (idempotent — safe to re-run)
|
||||
-- These are defined in 002_schema.sql; IF NOT EXISTS means
|
||||
-- running 002 first is sufficient, but listed here for clarity.
|
||||
-- ============================================================
|
||||
|
||||
-- planned_lessons, lesson_collaborators, lesson_deliveries
|
||||
-- curriculum_topics
|
||||
-- All present in 002_schema.sql — no action needed here.
|
||||
@ -1,84 +0,0 @@
|
||||
-- Enable RLS and define policies for filesystem tables
|
||||
|
||||
-- 1) Enable RLS
|
||||
alter table if exists public.file_cabinets enable row level security;
|
||||
alter table if exists public.files enable row level security;
|
||||
alter table if exists public.brain_files enable row level security;
|
||||
alter table if exists public.document_artefacts enable row level security;
|
||||
|
||||
drop policy if exists "User can access own cabinets" on public.file_cabinets;
|
||||
create policy "User can access own cabinets" on public.file_cabinets
|
||||
using (user_id = auth.uid())
|
||||
with check (user_id = auth.uid());
|
||||
|
||||
drop policy if exists "User can access files in own cabinet" on public.files;
|
||||
create policy "User can access files in own cabinet" on public.files
|
||||
using (exists (
|
||||
select 1 from public.file_cabinets c
|
||||
where c.id = files.cabinet_id and c.user_id = auth.uid()
|
||||
))
|
||||
with check (exists (
|
||||
select 1 from public.file_cabinets c
|
||||
where c.id = files.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
drop policy if exists "User can insert files into own cabinet" on public.files;
|
||||
create policy "User can insert files into own cabinet" on public.files for insert to authenticated
|
||||
with check (exists (
|
||||
select 1 from public.file_cabinets c
|
||||
where c.id = files.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
drop policy if exists "User can update files in own cabinet" on public.files;
|
||||
create policy "User can update files in own cabinet" on public.files for update to authenticated
|
||||
using (exists (
|
||||
select 1 from public.file_cabinets c
|
||||
where c.id = files.cabinet_id and c.user_id = auth.uid()
|
||||
))
|
||||
with check (exists (
|
||||
select 1 from public.file_cabinets c
|
||||
where c.id = files.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
drop policy if exists "User can delete files from own cabinet" on public.files;
|
||||
create policy "User can delete files from own cabinet" on public.files for delete
|
||||
using (exists (
|
||||
select 1 from public.file_cabinets c
|
||||
where c.id = files.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- 4) Brain-files: allow linking owned files to owned brains
|
||||
drop policy if exists "User can link files they own to their brains" on public.brain_files;
|
||||
create policy "User can link files they own to their brains" on public.brain_files
|
||||
using (
|
||||
exists (select 1 from public.brains b where b.id = brain_files.brain_id and b.user_id = auth.uid())
|
||||
and exists (
|
||||
select 1 from public.files f join public.file_cabinets c on f.cabinet_id = c.id
|
||||
where f.id = brain_files.file_id and c.user_id = auth.uid()
|
||||
)
|
||||
)
|
||||
with check (true);
|
||||
|
||||
-- 5) Document artefacts: allow reads to owners via file cabinet, writes via service_role
|
||||
drop policy if exists "artefacts_read_by_owner" on public.document_artefacts;
|
||||
create policy "artefacts_read_by_owner" on public.document_artefacts for select to authenticated
|
||||
using (exists (
|
||||
select 1 from public.files f join public.file_cabinets c on f.cabinet_id = c.id
|
||||
where f.id = document_artefacts.file_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
drop policy if exists "artefacts_rw_service" on public.document_artefacts;
|
||||
create policy "artefacts_rw_service" on public.document_artefacts to service_role
|
||||
using (true) with check (true);
|
||||
|
||||
-- Allow owners to delete their artefacts (needed for cascades under RLS)
|
||||
drop policy if exists "artefacts_delete_by_owner" on public.document_artefacts;
|
||||
create policy "artefacts_delete_by_owner" on public.document_artefacts for delete to authenticated
|
||||
using (exists (
|
||||
select 1 from public.files f join public.file_cabinets c on f.cabinet_id = c.id
|
||||
where f.id = document_artefacts.file_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- File vectors RLS and policies are defined in 67-vectors.sql after the table is created
|
||||
|
||||
|
||||
15
volumes/db/cc/66-taught-lessons-nullable.sql
Normal file
15
volumes/db/cc/66-taught-lessons-nullable.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- ============================================================
|
||||
-- Migration 005: taught_lessons nullable class_id
|
||||
-- + class_id FK on teacher_timetable_slots
|
||||
-- Run after: 004_extended_schema.sql
|
||||
-- ============================================================
|
||||
|
||||
-- taught_lessons.class_id: allow null so slots without a matched class can still materialize
|
||||
ALTER TABLE taught_lessons ALTER COLUMN class_id DROP NOT NULL;
|
||||
|
||||
-- teacher_timetable_slots: add proper class FK alongside existing subject_class text
|
||||
ALTER TABLE teacher_timetable_slots
|
||||
ADD COLUMN IF NOT EXISTS class_id UUID REFERENCES classes(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_tl_class_id ON taught_lessons(class_id) WHERE class_id IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tts_class_id ON teacher_timetable_slots(class_id) WHERE class_id IS NOT NULL;
|
||||
220
volumes/db/cc/67-dev-seed.sql
Normal file
220
volumes/db/cc/67-dev-seed.sql
Normal file
@ -0,0 +1,220 @@
|
||||
-- ============================================================
|
||||
-- Classroom Copilot — deterministic development seed
|
||||
-- Migration 067: small, repeatable fixtures for Supabase dev/staging
|
||||
-- Run after: 066-taught-lessons-nullable.sql
|
||||
--
|
||||
-- This intentionally excludes the full GAIS open-data import. It creates a
|
||||
-- compact school, users, classes, timetable, lessons, and storage bucket
|
||||
-- fixtures suitable for local/dev smoke tests without sensitive live data.
|
||||
-- ============================================================
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
||||
|
||||
-- Stable dev identities. Password for all fixture users is "devpassword".
|
||||
INSERT INTO auth.users (
|
||||
instance_id, id, aud, role, email, encrypted_password,
|
||||
email_confirmed_at, raw_app_meta_data, raw_user_meta_data,
|
||||
created_at, updated_at, confirmation_token, recovery_token, email_change_token_new, email_change
|
||||
)
|
||||
VALUES
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000001', 'authenticated', 'authenticated', 'platform.admin@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"Platform Admin","user_type":"admin"}', now(), now(), '', '', '', ''),
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000002', 'authenticated', 'authenticated', 'school.admin@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"School Admin","user_type":"admin"}', now(), now(), '', '', '', ''),
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000011', 'authenticated', 'authenticated', 'ada.teacher@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"Ada Teacher","user_type":"teacher"}', now(), now(), '', '', '', ''),
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000012', 'authenticated', 'authenticated', 'alan.teacher@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"Alan Teacher","user_type":"teacher"}', now(), now(), '', '', '', ''),
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000101', 'authenticated', 'authenticated', 's1.student@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"Student One","user_type":"student"}', now(), now(), '', '', '', ''),
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000102', 'authenticated', 'authenticated', 's2.student@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"Student Two","user_type":"student"}', now(), now(), '', '', '', ''),
|
||||
('00000000-0000-0000-0000-000000000000', '00000000-0000-4000-8000-000000000103', 'authenticated', 'authenticated', 's3.student@classroomcopilot.dev', crypt('devpassword', gen_salt('bf')), now(), '{"provider":"email","providers":["email"]}', '{"display_name":"Student Three","user_type":"student"}', now(), now(), '', '', '', '')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO public.profiles (id, email, user_type, username, full_name, display_name, school_id, metadata)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000001', 'platform.admin@classroomcopilot.dev', 'admin', 'platform-admin', 'Platform Admin', 'Platform Admin', NULL, '{"seed":"dev","role":"platform_admin"}'),
|
||||
('00000000-0000-4000-8000-000000000002', 'school.admin@classroomcopilot.dev', 'admin', 'school-admin', 'School Admin', 'School Admin', NULL, '{"seed":"dev","role":"school_admin"}'),
|
||||
('00000000-0000-4000-8000-000000000011', 'ada.teacher@classroomcopilot.dev', 'teacher', 'ada-teacher', 'Ada Teacher', 'Ada Teacher', NULL, '{"seed":"dev","department":"Science"}'),
|
||||
('00000000-0000-4000-8000-000000000012', 'alan.teacher@classroomcopilot.dev', 'teacher', 'alan-teacher', 'Alan Teacher', 'Alan Teacher', NULL, '{"seed":"dev","department":"Science"}'),
|
||||
('00000000-0000-4000-8000-000000000101', 's1.student@classroomcopilot.dev', 'student', 'student-one', 'Student One', 'Student One', NULL, '{"seed":"dev","year_group":"9"}'),
|
||||
('00000000-0000-4000-8000-000000000102', 's2.student@classroomcopilot.dev', 'student', 'student-two', 'Student Two', 'Student Two', NULL, '{"seed":"dev","year_group":"9"}'),
|
||||
('00000000-0000-4000-8000-000000000103', 's3.student@classroomcopilot.dev', 'student', 'student-three', 'Student Three', 'Student Three', NULL, '{"seed":"dev","year_group":"10"}')
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
user_type = EXCLUDED.user_type,
|
||||
username = EXCLUDED.username,
|
||||
full_name = EXCLUDED.full_name,
|
||||
display_name = EXCLUDED.display_name,
|
||||
school_id = EXCLUDED.school_id,
|
||||
metadata = EXCLUDED.metadata;
|
||||
|
||||
INSERT INTO public.admin_profiles (id, email, display_name, admin_role, is_super_admin, metadata)
|
||||
VALUES ('00000000-0000-4000-8000-000000000001', 'platform.admin@classroomcopilot.dev', 'Platform Admin', 'platform_admin', true, '{"seed":"dev"}')
|
||||
ON CONFLICT (id) DO UPDATE SET is_super_admin = true, metadata = EXCLUDED.metadata;
|
||||
|
||||
INSERT INTO public.institutes (id, name, urn, status, address, website, metadata, geo_coordinates)
|
||||
VALUES (
|
||||
'00000000-0000-4000-8000-000000000201',
|
||||
'Classroom Copilot Dev School',
|
||||
'DEV0001',
|
||||
'active',
|
||||
'{"line1":"1 Fixture Road","town":"Dev Town","postcode":"CC1 1DV","country":"GB"}',
|
||||
'https://classroomcopilot.dev',
|
||||
'{"seed":"dev","local_authority":"Fixture LA"}',
|
||||
'{"lat":51.5007,"lon":-0.1246}'
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, metadata = EXCLUDED.metadata;
|
||||
|
||||
UPDATE public.profiles
|
||||
SET school_id = '00000000-0000-4000-8000-000000000201'
|
||||
WHERE id <> '00000000-0000-4000-8000-000000000001';
|
||||
|
||||
INSERT INTO public.institute_memberships (profile_id, institute_id, role, metadata)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000002', '00000000-0000-4000-8000-000000000201', 'school_admin', '{"seed":"dev"}'),
|
||||
('00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', 'teacher', '{"seed":"dev"}'),
|
||||
('00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000201', 'teacher', '{"seed":"dev"}'),
|
||||
('00000000-0000-4000-8000-000000000101', '00000000-0000-4000-8000-000000000201', 'student', '{"seed":"dev"}'),
|
||||
('00000000-0000-4000-8000-000000000102', '00000000-0000-4000-8000-000000000201', 'student', '{"seed":"dev"}'),
|
||||
('00000000-0000-4000-8000-000000000103', '00000000-0000-4000-8000-000000000201', 'student', '{"seed":"dev"}')
|
||||
ON CONFLICT (profile_id, institute_id) DO UPDATE SET role = EXCLUDED.role, metadata = EXCLUDED.metadata;
|
||||
|
||||
INSERT INTO public.classes (id, institute_id, name, class_code, subject, key_stage, year_group, academic_year, description, created_by)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000201', '9 Science A', '9SCI-A', 'Science', '3', '9', '2026-2027', 'Deterministic dev Year 9 science class', '00000000-0000-4000-8000-000000000002'),
|
||||
('00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000201', '10 Physics B', '10PHY-B', 'Physics', '4', '10', '2026-2027', 'Deterministic dev Year 10 physics class', '00000000-0000-4000-8000-000000000002')
|
||||
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, class_code = EXCLUDED.class_code;
|
||||
|
||||
INSERT INTO public.class_teachers (class_id, teacher_id, is_primary, assigned_by)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000011', true, '00000000-0000-4000-8000-000000000002'),
|
||||
('00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000012', true, '00000000-0000-4000-8000-000000000002')
|
||||
ON CONFLICT (class_id, teacher_id) DO UPDATE SET is_primary = EXCLUDED.is_primary;
|
||||
|
||||
INSERT INTO public.class_students (class_id, student_id, enrolled_by)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000101', '00000000-0000-4000-8000-000000000002'),
|
||||
('00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000102', '00000000-0000-4000-8000-000000000002'),
|
||||
('00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000103', '00000000-0000-4000-8000-000000000002')
|
||||
ON CONFLICT (class_id, student_id) DO UPDATE SET status = 'active';
|
||||
|
||||
INSERT INTO public.whiteboard_rooms (id, user_id, institute_id, name, context_type, context_id, is_default, storage_path, node_type)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000401', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', 'Ada Teacher Workspace', 'profile', '00000000-0000-4000-8000-000000000011', true, 'cc.users/00000000-0000-4000-8000-000000000011/tldraw/default.json', 'profile_workspace'),
|
||||
('00000000-0000-4000-8000-000000000402', '00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000201', 'Alan Teacher Workspace', 'profile', '00000000-0000-4000-8000-000000000012', true, 'cc.users/00000000-0000-4000-8000-000000000012/tldraw/default.json', 'profile_workspace')
|
||||
ON CONFLICT (id) DO UPDATE SET storage_path = EXCLUDED.storage_path, is_default = EXCLUDED.is_default;
|
||||
|
||||
INSERT INTO public.school_timetables (id, institute_id, year_label, start_date, end_date, periods_template)
|
||||
VALUES (
|
||||
'00000000-0000-4000-8000-000000000501',
|
||||
'00000000-0000-4000-8000-000000000201',
|
||||
'2026-2027',
|
||||
'2026-09-01',
|
||||
'2026-09-07',
|
||||
'[{"code":"P1","name":"Period 1","type":"lesson","start_time":"09:00","end_time":"10:00"},{"code":"P2","name":"Period 2","type":"lesson","start_time":"10:05","end_time":"11:05"},{"code":"BR","name":"Break","type":"break","start_time":"11:05","end_time":"11:25"},{"code":"P3","name":"Period 3","type":"lesson","start_time":"11:25","end_time":"12:25"}]'
|
||||
)
|
||||
ON CONFLICT (institute_id, year_label) DO UPDATE SET periods_template = EXCLUDED.periods_template;
|
||||
|
||||
INSERT INTO public.academic_years (id, school_timetable_id, institute_id, year_label)
|
||||
VALUES ('00000000-0000-4000-8000-000000000511', '00000000-0000-4000-8000-000000000501', '00000000-0000-4000-8000-000000000201', '2026-2027')
|
||||
ON CONFLICT (school_timetable_id, year_label) DO NOTHING;
|
||||
|
||||
INSERT INTO public.academic_terms (id, academic_year_id, institute_id, term_name, term_number, start_date, end_date, notes, tags)
|
||||
VALUES ('00000000-0000-4000-8000-000000000521', '00000000-0000-4000-8000-000000000511', '00000000-0000-4000-8000-000000000201', 'Autumn fixture week', 1, '2026-09-01', '2026-09-07', 'Single deterministic week for dev smoke tests', '{dev}')
|
||||
ON CONFLICT (academic_year_id, term_number) DO UPDATE SET start_date = EXCLUDED.start_date, end_date = EXCLUDED.end_date;
|
||||
|
||||
INSERT INTO public.academic_weeks (id, academic_term_id, institute_id, week_number, start_date, week_cycle, notes, tags)
|
||||
VALUES ('00000000-0000-4000-8000-000000000531', '00000000-0000-4000-8000-000000000521', '00000000-0000-4000-8000-000000000201', 1, '2026-09-01', 'A', 'Deterministic week A', '{dev}')
|
||||
ON CONFLICT (academic_term_id, week_number) DO UPDATE SET week_cycle = EXCLUDED.week_cycle;
|
||||
|
||||
INSERT INTO public.academic_days (id, academic_week_id, academic_term_id, institute_id, date, day_of_week, academic_day_number, notes, tags)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000541', '00000000-0000-4000-8000-000000000531', '00000000-0000-4000-8000-000000000521', '00000000-0000-4000-8000-000000000201', '2026-09-01', 'Tuesday', 1, 'Fixture day 1', '{dev}'),
|
||||
('00000000-0000-4000-8000-000000000542', '00000000-0000-4000-8000-000000000531', '00000000-0000-4000-8000-000000000521', '00000000-0000-4000-8000-000000000201', '2026-09-02', 'Wednesday', 2, 'Fixture day 2', '{dev}'),
|
||||
('00000000-0000-4000-8000-000000000543', '00000000-0000-4000-8000-000000000531', '00000000-0000-4000-8000-000000000521', '00000000-0000-4000-8000-000000000201', '2026-09-03', 'Thursday', 3, 'Fixture day 3', '{dev}'),
|
||||
('00000000-0000-4000-8000-000000000544', '00000000-0000-4000-8000-000000000531', '00000000-0000-4000-8000-000000000521', '00000000-0000-4000-8000-000000000201', '2026-09-04', 'Friday', 4, 'Fixture day 4', '{dev}')
|
||||
ON CONFLICT (institute_id, date) DO UPDATE SET academic_day_number = EXCLUDED.academic_day_number;
|
||||
|
||||
INSERT INTO public.academic_periods (id, academic_day_id, institute_id, period_code, period_name, period_type, start_time, end_time)
|
||||
SELECT
|
||||
('00000000-0000-4000-8000-' || lpad((600 + d.day_no * 10 + p.period_no)::text, 12, '0'))::uuid,
|
||||
d.id,
|
||||
'00000000-0000-4000-8000-000000000201'::uuid,
|
||||
p.period_code,
|
||||
p.period_name,
|
||||
p.period_type,
|
||||
p.start_time::time,
|
||||
p.end_time::time
|
||||
FROM (VALUES
|
||||
('00000000-0000-4000-8000-000000000541'::uuid, 1),
|
||||
('00000000-0000-4000-8000-000000000542'::uuid, 2),
|
||||
('00000000-0000-4000-8000-000000000543'::uuid, 3),
|
||||
('00000000-0000-4000-8000-000000000544'::uuid, 4)
|
||||
) AS d(id, day_no)
|
||||
CROSS JOIN (VALUES
|
||||
(1, 'P1', 'Period 1', 'lesson', '09:00', '10:00'),
|
||||
(2, 'P2', 'Period 2', 'lesson', '10:05', '11:05'),
|
||||
(3, 'BR', 'Break', 'break', '11:05', '11:25'),
|
||||
(4, 'P3', 'Period 3', 'lesson', '11:25', '12:25')
|
||||
) AS p(period_no, period_code, period_name, period_type, start_time, end_time)
|
||||
ON CONFLICT (academic_day_id, period_code) DO UPDATE SET period_name = EXCLUDED.period_name;
|
||||
|
||||
INSERT INTO public.teacher_timetables (id, profile_id, institute_id, school_timetable_id, start_date, end_date)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000701', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000501', '2026-09-01', '2026-09-07'),
|
||||
('00000000-0000-4000-8000-000000000702', '00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000501', '2026-09-01', '2026-09-07')
|
||||
ON CONFLICT (profile_id, school_timetable_id) DO UPDATE SET start_date = EXCLUDED.start_date, end_date = EXCLUDED.end_date;
|
||||
|
||||
INSERT INTO public.teacher_timetable_slots (id, teacher_timetable_id, profile_id, institute_id, day_of_week, period_code, subject_class, start_time, end_time, week_cycle, class_id)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000711', '00000000-0000-4000-8000-000000000701', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', 'Tuesday', 'P1', '9 Science A', '09:00', '10:00', 'A', '00000000-0000-4000-8000-000000000301'),
|
||||
('00000000-0000-4000-8000-000000000712', '00000000-0000-4000-8000-000000000701', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', 'Wednesday', 'P2', '9 Science A', '10:05', '11:05', 'A', '00000000-0000-4000-8000-000000000301'),
|
||||
('00000000-0000-4000-8000-000000000713', '00000000-0000-4000-8000-000000000702', '00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000201', 'Thursday', 'P1', '10 Physics B', '09:00', '10:00', 'A', '00000000-0000-4000-8000-000000000302')
|
||||
ON CONFLICT (teacher_timetable_id, week_cycle, day_of_week, period_code) DO UPDATE SET class_id = EXCLUDED.class_id, subject_class = EXCLUDED.subject_class;
|
||||
|
||||
INSERT INTO public.taught_lessons (id, academic_period_id, teacher_timetable_slot_id, class_id, teacher_id, institute_id, date, period_code, week_cycle, day_of_week, lesson_plan, whiteboard_room_id, status, notes, tags)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000801', '00000000-0000-4000-8000-000000000611', '00000000-0000-4000-8000-000000000711', '00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', '2026-09-01', 'P1', 'A', 'Tuesday', '{"title":"Forces baseline","objectives":["Describe balanced and unbalanced forces"]}', '00000000-0000-4000-8000-000000000401', 'planned', 'Dev fixture taught lesson', '{dev,science}'),
|
||||
('00000000-0000-4000-8000-000000000802', '00000000-0000-4000-8000-000000000622', '00000000-0000-4000-8000-000000000712', '00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', '2026-09-02', 'P2', 'A', 'Wednesday', '{"title":"Particle model recap","objectives":["Compare solids liquids and gases"]}', '00000000-0000-4000-8000-000000000401', 'planned', 'Dev fixture taught lesson', '{dev,science}'),
|
||||
('00000000-0000-4000-8000-000000000803', '00000000-0000-4000-8000-000000000631', '00000000-0000-4000-8000-000000000713', '00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000201', '2026-09-03', 'P1', 'A', 'Thursday', '{"title":"Energy stores","objectives":["Identify energy stores in examples"]}', '00000000-0000-4000-8000-000000000402', 'planned', 'Dev fixture taught lesson', '{dev,physics}')
|
||||
ON CONFLICT (academic_period_id, teacher_id) DO UPDATE SET lesson_plan = EXCLUDED.lesson_plan, status = EXCLUDED.status;
|
||||
|
||||
INSERT INTO public.curriculum_topics (id, title, subject, key_stage, year_group, topic_type, total_lessons, department)
|
||||
VALUES
|
||||
('DEV-SCI-FORCES', 'Forces baseline', 'Science', '3', '9', 'Standard', 1, 'Science'),
|
||||
('DEV-PHY-ENERGY', 'Energy stores', 'Physics', '4', '10', 'Standard', 1, 'Science')
|
||||
ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title;
|
||||
|
||||
INSERT INTO public.planned_lessons (id, created_by, institute_id, class_id, whiteboard_room_id, topic_code, title, subject, year_group, estimated_duration_minutes, objectives, activities, status, tags)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000901', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000401', 'DEV-SCI-FORCES', 'Forces baseline planned lesson', 'Science', '9', 60, '["Describe balanced and unbalanced forces"]', '[{"type":"starter","title":"Force diagrams"}]', 'ready', '{dev,science}'),
|
||||
('00000000-0000-4000-8000-000000000902', '00000000-0000-4000-8000-000000000012', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000302', '00000000-0000-4000-8000-000000000402', 'DEV-PHY-ENERGY', 'Energy stores planned lesson', 'Physics', '10', 60, '["Identify energy stores"]', '[{"type":"main","title":"Energy transfer circus"}]', 'ready', '{dev,physics}')
|
||||
ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status, objectives = EXCLUDED.objectives;
|
||||
|
||||
INSERT INTO public.lesson_deliveries (id, planned_lesson_id, taught_lesson_id, delivered_by, class_id, institute_id, whiteboard_room_id, started_at, ended_at, notes)
|
||||
VALUES
|
||||
('00000000-0000-4000-8000-000000000911', '00000000-0000-4000-8000-000000000901', '00000000-0000-4000-8000-000000000801', '00000000-0000-4000-8000-000000000011', '00000000-0000-4000-8000-000000000301', '00000000-0000-4000-8000-000000000201', '00000000-0000-4000-8000-000000000401', '2026-09-01 09:00:00+00', '2026-09-01 10:00:00+00', 'Delivered lesson fixture')
|
||||
ON CONFLICT (id) DO UPDATE SET taught_lesson_id = EXCLUDED.taught_lesson_id;
|
||||
|
||||
-- Dev storage buckets expected by app/API. Object rows are intentionally omitted;
|
||||
-- TLDraw paths above point at where empty/default snapshots should be written.
|
||||
INSERT INTO storage.buckets (id, name, public)
|
||||
VALUES
|
||||
('cc.users', 'cc.users', false),
|
||||
('cc.public.snapshots', 'cc.public.snapshots', false),
|
||||
('cc.examboards', 'cc.examboards', true)
|
||||
ON CONFLICT (id) DO UPDATE SET public = EXCLUDED.public;
|
||||
|
||||
-- Lightweight verification breadcrumbs for SQL-level smoke checks.
|
||||
DO $$
|
||||
DECLARE
|
||||
profile_count integer;
|
||||
institute_count integer;
|
||||
class_count integer;
|
||||
taught_count integer;
|
||||
BEGIN
|
||||
SELECT count(*) INTO profile_count FROM public.profiles WHERE metadata->>'seed' = 'dev';
|
||||
SELECT count(*) INTO institute_count FROM public.institutes WHERE metadata->>'seed' = 'dev';
|
||||
SELECT count(*) INTO class_count FROM public.classes WHERE academic_year = '2026-2027';
|
||||
SELECT count(*) INTO taught_count FROM public.taught_lessons WHERE tags @> ARRAY['dev'];
|
||||
|
||||
IF profile_count <> 7 OR institute_count <> 1 OR class_count <> 2 OR taught_count <> 3 THEN
|
||||
RAISE EXCEPTION 'Dev seed verification failed: profiles=%, institutes=%, classes=%, taught_lessons=%', profile_count, institute_count, class_count, taught_count;
|
||||
END IF;
|
||||
END $$;
|
||||
@ -1,79 +0,0 @@
|
||||
-- Vectors: file_vectors table and similarity search function
|
||||
|
||||
-- 1) Ensure pgvector extension is available
|
||||
create extension if not exists vector;
|
||||
|
||||
-- 2) File vectors table
|
||||
create table if not exists public.file_vectors (
|
||||
id bigint generated by default as identity primary key,
|
||||
created_at timestamp with time zone default now() not null,
|
||||
embedding public.vector,
|
||||
metadata jsonb,
|
||||
content text
|
||||
);
|
||||
|
||||
-- 3) ANN index (skipped until embedding dimension is fixed)
|
||||
-- To enable: set column type to public.vector(<dim>) and uncomment:
|
||||
-- create index if not exists file_vectors_embedding_idx
|
||||
-- on public.file_vectors using ivfflat (embedding public.vector_cosine_ops)
|
||||
-- with (lists='100');
|
||||
|
||||
-- 3b) Enable RLS and set policies (moved here to avoid ordering issues)
|
||||
alter table if exists public.file_vectors enable row level security;
|
||||
|
||||
drop policy if exists "vectors_read_by_owner" on public.file_vectors;
|
||||
create policy "vectors_read_by_owner" on public.file_vectors for select to authenticated
|
||||
using (coalesce((metadata->>'file_id')::uuid, null) is null or exists (
|
||||
select 1 from public.files f join public.file_cabinets c on f.cabinet_id = c.id
|
||||
where f.id = (metadata->>'file_id')::uuid and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
drop policy if exists "vectors_rw_service" on public.file_vectors;
|
||||
create policy "vectors_rw_service" on public.file_vectors to service_role
|
||||
using (true) with check (true);
|
||||
|
||||
-- 4) Match function mirrored from neoFS (generic metadata mapping)
|
||||
create or replace function public.match_file_vectors(
|
||||
filter jsonb,
|
||||
match_count integer,
|
||||
query_embedding public.vector
|
||||
)
|
||||
returns table (
|
||||
id bigint,
|
||||
file_id uuid,
|
||||
cabinet_id uuid,
|
||||
artefact_type text,
|
||||
artefact_is text,
|
||||
original_path_prefix text,
|
||||
original_filename text,
|
||||
content text,
|
||||
metadata jsonb,
|
||||
similarity double precision
|
||||
)
|
||||
language sql stable as $$
|
||||
select
|
||||
fv.id,
|
||||
nullif(fv.metadata->>'file_id','')::uuid as file_id,
|
||||
nullif(fv.metadata->>'cabinet_id','')::uuid as cabinet_id,
|
||||
nullif(fv.metadata->>'artefact_type','') as artefact_type,
|
||||
nullif(fv.metadata->>'artefact_is','') as artefact_is,
|
||||
nullif(fv.metadata->>'original_path_prefix','') as original_path_prefix,
|
||||
nullif(fv.metadata->>'original_filename','') as original_filename,
|
||||
fv.content,
|
||||
fv.metadata,
|
||||
1 - (fv.embedding <=> query_embedding) as similarity
|
||||
from public.file_vectors fv
|
||||
where
|
||||
(coalesce(filter ? 'file_id', false) = false or (fv.metadata->>'file_id')::uuid = (filter->>'file_id')::uuid)
|
||||
and (coalesce(filter ? 'cabinet_id', false) = false or (fv.metadata->>'cabinet_id')::uuid = (filter->>'cabinet_id')::uuid)
|
||||
and (coalesce(filter ? 'artefact_type', false) = false or (fv.metadata->>'artefact_type') = (filter->>'artefact_type'))
|
||||
and (coalesce(filter ? 'artefact_id', false) = false or (fv.metadata->>'artefact_id') = (filter->>'artefact_id'))
|
||||
and (coalesce(filter ? 'original_path_prefix', false) = false or (fv.metadata->>'original_path_prefix') like (filter->>'original_path_prefix') || '%')
|
||||
and (coalesce(filter ? 'original_path_prefix_ilike', false)= false or (fv.metadata->>'original_path_prefix') ilike (filter->>'original_path_prefix_ilike') || '%')
|
||||
and (coalesce(filter ? 'original_filename', false) = false or (fv.metadata->>'original_filename') = (filter->>'original_filename'))
|
||||
and (coalesce(filter ? 'original_filename_ilike', false)= false or (fv.metadata->>'original_filename') ilike (filter->>'original_filename_ilike'))
|
||||
order by fv.embedding <=> query_embedding
|
||||
limit greatest(coalesce(match_count, 10), 1)
|
||||
$$;
|
||||
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
-- Cabinet memberships for sharing access
|
||||
|
||||
create table if not exists public.cabinet_memberships (
|
||||
id uuid default uuid_generate_v4() primary key,
|
||||
cabinet_id uuid not null references public.file_cabinets(id) on delete cascade,
|
||||
profile_id uuid not null references public.profiles(id) on delete cascade,
|
||||
role text not null check (role in ('owner','editor','viewer')),
|
||||
created_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
updated_at timestamp with time zone default timezone('utc'::text, now()),
|
||||
unique(cabinet_id, profile_id)
|
||||
);
|
||||
|
||||
create index if not exists idx_cabinet_memberships_cabinet on public.cabinet_memberships(cabinet_id);
|
||||
create index if not exists idx_cabinet_memberships_profile on public.cabinet_memberships(profile_id);
|
||||
|
||||
-- Updated at trigger
|
||||
drop trigger if exists trg_cabinet_memberships_updated_at on public.cabinet_memberships;
|
||||
create trigger trg_cabinet_memberships_updated_at
|
||||
before update on public.cabinet_memberships
|
||||
for each row execute function public.handle_updated_at();
|
||||
|
||||
-- RLS and policies
|
||||
alter table if exists public.cabinet_memberships enable row level security;
|
||||
|
||||
-- Members can select their own memberships; cabinet owners can also see memberships
|
||||
drop policy if exists cm_read_self_or_owner on public.cabinet_memberships;
|
||||
create policy cm_read_self_or_owner on public.cabinet_memberships for select to authenticated
|
||||
using (
|
||||
profile_id = auth.uid() or exists (
|
||||
select 1 from public.file_cabinets c where c.id = cabinet_memberships.cabinet_id and c.user_id = auth.uid()
|
||||
)
|
||||
);
|
||||
|
||||
-- Cabinet owners can insert memberships
|
||||
drop policy if exists cm_insert_by_owner on public.cabinet_memberships;
|
||||
create policy cm_insert_by_owner on public.cabinet_memberships for insert to authenticated
|
||||
with check (exists (
|
||||
select 1 from public.file_cabinets c where c.id = cabinet_memberships.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Cabinet owners can update memberships (e.g., role)
|
||||
drop policy if exists cm_update_by_owner on public.cabinet_memberships;
|
||||
create policy cm_update_by_owner on public.cabinet_memberships for update to authenticated
|
||||
using (exists (
|
||||
select 1 from public.file_cabinets c where c.id = cabinet_memberships.cabinet_id and c.user_id = auth.uid()
|
||||
))
|
||||
with check (exists (
|
||||
select 1 from public.file_cabinets c where c.id = cabinet_memberships.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Cabinet owners can delete memberships
|
||||
drop policy if exists cm_delete_by_owner on public.cabinet_memberships;
|
||||
create policy cm_delete_by_owner on public.cabinet_memberships for delete to authenticated
|
||||
using (exists (
|
||||
select 1 from public.file_cabinets c where c.id = cabinet_memberships.cabinet_id and c.user_id = auth.uid()
|
||||
));
|
||||
|
||||
-- Extend access to cabinets/files for members (after table exists)
|
||||
drop policy if exists "User can access cabinets via membership" on public.file_cabinets;
|
||||
create policy "User can access cabinets via membership" on public.file_cabinets for select to authenticated
|
||||
using (exists (
|
||||
select 1 from public.cabinet_memberships m
|
||||
where m.cabinet_id = file_cabinets.id and m.profile_id = auth.uid()
|
||||
));
|
||||
|
||||
drop policy if exists "User can access files via membership" on public.files;
|
||||
create policy "User can access files via membership" on public.files for select to authenticated
|
||||
using (exists (
|
||||
select 1 from public.cabinet_memberships m
|
||||
where m.cabinet_id = files.cabinet_id and m.profile_id = auth.uid()
|
||||
));
|
||||
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
-- Ensure storage objects for all artefacts are removed when a file is deleted
|
||||
-- by deleting the entire "cabinet_id/file_id" directory prefix in Storage.
|
||||
|
||||
-- Helper to delete all objects under a prefix
|
||||
create or replace function public._delete_storage_prefix(p_bucket text, p_prefix text)
|
||||
returns void
|
||||
language plpgsql security definer
|
||||
set search_path to 'public', 'storage'
|
||||
as $$
|
||||
begin
|
||||
if p_bucket is null or p_prefix is null then
|
||||
return;
|
||||
end if;
|
||||
-- Delete any objects whose name starts with the prefix + '/'
|
||||
delete from storage.objects where bucket_id = p_bucket and name like p_prefix || '/%';
|
||||
-- In case an object exists exactly at the prefix (rare but safe)
|
||||
delete from storage.objects where bucket_id = p_bucket and name = p_prefix;
|
||||
end
|
||||
$$;
|
||||
|
||||
-- Update file-level GC to also delete the parent directory prefix (cabinet_id/file_id)
|
||||
create or replace function public._storage_gc_sql()
|
||||
returns trigger
|
||||
language plpgsql security definer
|
||||
set search_path to 'public', 'storage'
|
||||
as $$
|
||||
declare
|
||||
v_prefix text;
|
||||
begin
|
||||
-- Derive directory prefix from the file path by removing the last segment (filename)
|
||||
-- Example: 'cabinet_id/file_id/filename.ext' -> 'cabinet_id/file_id'
|
||||
v_prefix := regexp_replace(old.path, '/[^/]+$', '');
|
||||
|
||||
if tg_op = 'DELETE' then
|
||||
-- Delete the original object and any artefacts under the file's directory
|
||||
perform public._delete_storage_objects(old.bucket, old.path);
|
||||
perform public._delete_storage_prefix(old.bucket, v_prefix);
|
||||
elsif tg_op = 'UPDATE' then
|
||||
if (old.bucket is distinct from new.bucket) or (old.path is distinct from new.path) then
|
||||
perform public._delete_storage_objects(old.bucket, old.path);
|
||||
perform public._delete_storage_prefix(old.bucket, v_prefix);
|
||||
end if;
|
||||
end if;
|
||||
return null;
|
||||
end
|
||||
$$;
|
||||
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
-- Add directory support to files table
|
||||
-- Migration: Add directory support for folder uploads
|
||||
|
||||
-- Add new columns to files table
|
||||
ALTER TABLE files
|
||||
ADD COLUMN IF NOT EXISTS is_directory BOOLEAN DEFAULT FALSE,
|
||||
ADD COLUMN IF NOT EXISTS parent_directory_id UUID REFERENCES files(id) ON DELETE CASCADE,
|
||||
ADD COLUMN IF NOT EXISTS relative_path TEXT,
|
||||
ADD COLUMN IF NOT EXISTS directory_manifest JSONB,
|
||||
ADD COLUMN IF NOT EXISTS upload_session_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS processing_status TEXT DEFAULT 'uploaded' CHECK (processing_status IN ('uploaded', 'processing', 'completed', 'failed', 'queued'));
|
||||
|
||||
-- Create index for efficient directory queries
|
||||
CREATE INDEX IF NOT EXISTS idx_files_parent_directory ON files(parent_directory_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_upload_session ON files(upload_session_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_processing_status ON files(processing_status);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_is_directory ON files(is_directory);
|
||||
|
||||
-- Create directory manifest structure
|
||||
COMMENT ON COLUMN files.is_directory IS 'True if this record represents a directory/folder';
|
||||
COMMENT ON COLUMN files.parent_directory_id IS 'ID of parent directory if this file is inside an uploaded folder';
|
||||
COMMENT ON COLUMN files.relative_path IS 'Relative path within the uploaded directory structure';
|
||||
COMMENT ON COLUMN files.directory_manifest IS 'JSON manifest of directory contents including file count, total size, structure';
|
||||
COMMENT ON COLUMN files.upload_session_id IS 'Groups files uploaded together in a single directory upload session';
|
||||
COMMENT ON COLUMN files.processing_status IS 'Simple status tracking without auto-processing';
|
||||
|
||||
-- Example directory_manifest structure:
|
||||
-- {
|
||||
-- "total_files": 15,
|
||||
-- "total_size_bytes": 12345678,
|
||||
-- "directory_structure": {
|
||||
-- "documents/": {
|
||||
-- "file1.pdf": {"size": 123456, "mime_type": "application/pdf"},
|
||||
-- "subdirectory/": {
|
||||
-- "file2.docx": {"size": 234567, "mime_type": "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}
|
||||
-- }
|
||||
-- }
|
||||
-- },
|
||||
-- "upload_timestamp": "2024-09-23T12:00:00Z",
|
||||
-- "upload_method": "directory_picker"
|
||||
-- }
|
||||
Loading…
x
Reference in New Issue
Block a user