Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02f68387e8 | ||
|
|
eab7c01f46 | ||
|
|
b65e3f2d38 | ||
|
|
84d8303cdb | ||
|
|
bc674ea696 | ||
|
|
b758b40d85 | ||
|
|
e0d2c5c619 | ||
|
|
5573b8fede |
@@ -0,0 +1,21 @@
|
||||
name: supabase-ci
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Validate Docker Compose syntax
|
||||
run: docker compose -f docker-compose.yml config >/tmp/supabase-compose.rendered.yml
|
||||
|
||||
- name: Build Supabase MCP image
|
||||
working-directory: selfhosted-supabase-mcp
|
||||
run: docker build -t supabase-mcp-ci:${{ github.sha }} .
|
||||
+42
-16
@@ -1,22 +1,48 @@
|
||||
# Environment files
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.so
|
||||
build/
|
||||
dist/
|
||||
eggs/
|
||||
*.egg-info/
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
/dist
|
||||
/build
|
||||
|
||||
# Environment files (never commit secrets)
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
.archive/
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Docker volume RUNTIME data (large binary/runtime files - not schema SQL)
|
||||
volumes/db-data/
|
||||
volumes/storage/
|
||||
volumes/pooler/
|
||||
volumes/logs/
|
||||
|
||||
|
||||
# Backup files
|
||||
*.bak
|
||||
*.bak.*
|
||||
backups/
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
logs/
|
||||
queue_workers.log
|
||||
|
||||
# Large files
|
||||
*.csv
|
||||
*.xlsx
|
||||
*.sqlite
|
||||
*.db
|
||||
|
||||
# Docker
|
||||
docker-compose.override.yml
|
||||
|
||||
# Supabase local development
|
||||
.gitignore
|
||||
.DS_Store
|
||||
*.log
|
||||
*.tmp
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
1. Data Model Questions
|
||||
Q1: Class vs Group Relationship
|
||||
You say, "Current groups are generic." but there is no class implementation in supabase yet.
|
||||
|
||||
|
||||
Q2: Student-Teacher Relationships
|
||||
Should students be explicitly enrolled in classes by teachers, or:
|
||||
|
||||
Can any student from the same institution join any class?
|
||||
Only teachers can add or remove students in classes that belong to them. Classes can belong to multiple teachers from the same institute. We will add partnering instution access later (e.g. for teacher supply agencies to teach classes). Only teachers of the class (and admins) can add or remove students to classes, and manage enrollment requests.
|
||||
|
||||
|
||||
Are there enrollment requests/approvals? Yes. Students from any institute can enrol to ad-hoc lessons/timetables. We forgot to add that ad-hoc timetables are created by normal institute teachers and partnering supply teacher institutes (institutes that are designated as supply haven't been implemented yet - see below for details).
|
||||
|
||||
Can students be in multiple classes with the same teacher? Yes. Some classes may be designated as clubs, extra-curricular or other 'extra' type classes depending on the timetable.
|
||||
|
||||
Q3: Timetable Scope
|
||||
A single teacher may teach multiple institutes over time. Should timetables be:
|
||||
|
||||
A) Per teacher (spanning all their institutes)?
|
||||
B) Per teacher-per institute combination?
|
||||
C) Institute-wide with teacher assignments?
|
||||
|
||||
We are focussin two types of timetabling: Ad-hoc and Recurring. We will implement the following features in Q1:
|
||||
A) Ad-hoc Timetable Creation: Supply teachers and school institute teachers crete ad-hoc lessons/timetables. Ad-hoc timetables are simple and should be entered manually.
|
||||
B) Recurring Timetable Creation: Recurring timetables are created by normal institute teachers. These are created by supplying details about the nature of the timetable e.g. single/bi weekly, applicable days, number of lessons/blocks in the day, times of the lessons, classes for those lessons, possibly other details such as rooms etc. For a supply teacher they may indicate the insitute that they are teaching at or not (we will need to link partnering supply teacher institutes later).
|
||||
|
||||
|
||||
2. Lesson & Whiteboard Association
|
||||
Q4: Whiteboard Lifecycle
|
||||
|
||||
Should each lesson instance have exactly one whiteboard (created on first open)?
|
||||
If the timetable is ad-hoc then yes.
|
||||
|
||||
Can a lesson have multiple whiteboards (e.g., prep notes + lesson board)? Yes this sounds like a good idea but only for recurring timetables belonging to an institute teachers.
|
||||
|
||||
Should whiteboards persist when lessons are rescheduled/cancelled?
|
||||
Canelling/rescheduling lessons is only available to institute teachers with recurring timetables. If a lesson is rescheduled/modified then we should keep the whiteboard associated with the lesson. If it is cancelled then we should delete the whiteboard.
|
||||
|
||||
Q5: Student Access to Whiteboards
|
||||
|
||||
Real-time collaborative during lesson only? Yes, real-time collaboration is only available during the live lessons only.
|
||||
|
||||
Read-only access after lesson completion? Yes, after completion students should be able to get a read-only version of the same whiteboard (they will be able to save it to their own area when modified).
|
||||
|
||||
Full editing permissions for students? For now we allow full editing permissions for the student. There is a presentation mode that we are working on and eventually we will add the ability for teachers to manage student interaction with more granularity.
|
||||
|
||||
3. Scheduling Flexibility
|
||||
|
||||
Q6: Ad-hoc vs Recurring
|
||||
The requirement mentions both minimal ad-hoc and complex recurring timetables. Should we:
|
||||
|
||||
C) Build recurrence gradually (start with ad-hoc, add recurrence later). We will implement RRULES later.
|
||||
|
||||
|
||||
Q4: Timetable Draft States
|
||||
Should timetables have lifecycle states?
|
||||
|
||||
simple active/inactive for now, more complex states when adding rrecurrence.
|
||||
|
||||
|
||||
4. Technical Implementation
|
||||
|
||||
Q8: Supabase Schema Management
|
||||
Current migration files are numbered (001_cc_schema.sql, etc.). For new features:
|
||||
Switch to timestamp-based naming (20250225120000_...)
|
||||
|
||||
Q9: Frontend Architecture
|
||||
Should I create new route structures like:
|
||||
|
||||
/timetable - main timetabling interface
|
||||
/classes - class management
|
||||
/lessons/:id - individual lesson views
|
||||
Create NEW ROUTE STRUCTURES.
|
||||
|
||||
5. Feature Scope & Phasing
|
||||
Q10: MVP Scope Priority
|
||||
From the full feature set, what's the absolute minimum for first release?
|
||||
|
||||
My understanding:
|
||||
|
||||
✅ Teacher creates classes with students
|
||||
✅ Teacher creates simple timetable (single repeating pattern)
|
||||
✅ Lessons linked to whiteboards
|
||||
✅ Students access whiteboards in real-time
|
||||
Out of MVP?
|
||||
|
||||
Multi-week rotating schedules? Later
|
||||
Full academic year with holidays? Later
|
||||
Exam scheduling? Later
|
||||
Report generation? Later
|
||||
Duty blocks? Later
|
||||
Break notes whiteboards? Later
|
||||
|
||||
|
||||
Q11: Competitive Features
|
||||
Are there any existing products we should study for UX patterns?
|
||||
|
||||
Google Classroom?
|
||||
Microsoft Teams/Classroom?
|
||||
|
||||
Yes, use the research tool to research these.
|
||||
Specialized tools like Firefly, iSAMS, SIMS? Not yet. We will integrate with these later.
|
||||
|
||||
6. Business Logic Questions
|
||||
Q12: Lesson Modifications
|
||||
When a recurring lesson is modified:
|
||||
|
||||
|
||||
D) All three options? Yes, do all three options below.
|
||||
Edit this instance only (create exception)?
|
||||
Edit this and future instances?
|
||||
Edit all instances in series?
|
||||
|
||||
Q13: Class Transfers
|
||||
Can students move between classes mid-term? How should their whiteboard history be handled? No, we will handle this later.
|
||||
|
||||
Q14: Notifications
|
||||
We will not implement a notification system yet.
|
||||
|
||||
Q15: Resource Attachments
|
||||
Lesson resources mentioned as future consideration. Should we:
|
||||
|
||||
A) Not implement now, leave schema extensible. We have started a file management system already that we will build in.
|
||||
+10
-11
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,242 @@
|
||||
-- Migration: Add application-specific functions
|
||||
-- This migration adds 9 functions that exist on the live database but were
|
||||
-- not included in earlier schema migrations.
|
||||
|
||||
-- =============================================================================
|
||||
-- 1. setup_initial_admin
|
||||
-- Sets up an admin user by updating their user_type and username.
|
||||
-- SECURITY DEFINER: Must be run as service_role or superuser.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.setup_initial_admin(admin_email text)
|
||||
RETURNS json
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
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;
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 2. is_admin
|
||||
-- Returns true if the current user has admin role.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.is_admin()
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
select coalesce(
|
||||
(select true
|
||||
from public.profiles
|
||||
where id = auth.uid()
|
||||
and user_type = 'admin'),
|
||||
false
|
||||
);
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 3. is_super_admin
|
||||
-- Alias for is_admin (same logic).
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.is_super_admin()
|
||||
RETURNS boolean
|
||||
LANGUAGE sql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
select coalesce(
|
||||
(select true
|
||||
from public.profiles
|
||||
where id = auth.uid()
|
||||
and user_type = 'admin'),
|
||||
false
|
||||
);
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 4. check_db_ready
|
||||
-- Health check: verifies essential schemas, tables, and RLS are in place.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.check_db_ready()
|
||||
RETURNS boolean
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
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;
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 5. match_file_vectors
|
||||
-- Vector similarity search over file artefacts.
|
||||
-- NOTE: Requires the `file_vectors` table to exist (vector extension needed).
|
||||
-- This function was created for a table that may not have been migrated yet.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.match_file_vectors(
|
||||
filter jsonb,
|
||||
match_count integer,
|
||||
query_embedding 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 $function$
|
||||
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)
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 6. set_completed_at
|
||||
-- Trigger function: sets completed_at when status changes to 'completed'.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.set_completed_at()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
begin
|
||||
if NEW.status = 'completed' and OLD.status != 'completed' then
|
||||
NEW.completed_at = now();
|
||||
end if;
|
||||
return NEW;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 7. handle_updated_at
|
||||
-- Trigger function: sets updated_at to current UTC time on UPDATE.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.handle_updated_at()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
begin
|
||||
new.updated_at = timezone('utc'::text, now());
|
||||
return new;
|
||||
end;
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 8. update_updated_at_column
|
||||
-- Alternative trigger function: sets updated_at to NOW() on UPDATE.
|
||||
-- (Duplicate of handle_updated_at with slightly different syntax)
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.update_updated_at_column()
|
||||
RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
-- =============================================================================
|
||||
-- 9. exec_sql
|
||||
-- Executes arbitrary SQL. SECURITY DEFINER — use with extreme caution.
|
||||
-- =============================================================================
|
||||
CREATE OR REPLACE FUNCTION public.exec_sql(query text)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
AS $function$
|
||||
begin
|
||||
execute query;
|
||||
end;
|
||||
$function$;
|
||||
+371
-7
@@ -44,6 +44,41 @@ services:
|
||||
- /auth/v1/verify
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: auth-v1-open-callback
|
||||
url: http://auth:9999/callback
|
||||
routes:
|
||||
@@ -53,6 +88,41 @@ services:
|
||||
- /auth/v1/callback
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: auth-v1-open-authorize
|
||||
url: http://auth:9999/authorize
|
||||
routes:
|
||||
@@ -62,7 +132,41 @@ services:
|
||||
- /auth/v1/authorize
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
## Secure Auth routes
|
||||
- name: auth-v1
|
||||
_comment: 'GoTrue: /auth/v1/* -> http://auth:9999/*'
|
||||
@@ -74,6 +178,41 @@ services:
|
||||
- /auth/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
@@ -95,6 +234,41 @@ services:
|
||||
- /rest/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
@@ -116,6 +290,41 @@ services:
|
||||
- /graphql/v1
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: true
|
||||
@@ -143,6 +352,41 @@ services:
|
||||
- /realtime/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
@@ -163,6 +407,41 @@ services:
|
||||
- /realtime/v1/api
|
||||
plugins:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
- name: key-auth
|
||||
config:
|
||||
hide_credentials: false
|
||||
@@ -183,7 +462,42 @@ services:
|
||||
- /storage/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
- x-upsert
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
## Edge Functions routes
|
||||
- name: functions-v1
|
||||
_comment: 'Edge Functions: /functions/v1/* -> http://functions:9000/*'
|
||||
@@ -195,7 +509,41 @@ services:
|
||||
- /functions/v1/
|
||||
plugins:
|
||||
- name: cors
|
||||
|
||||
config:
|
||||
origins:
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
## Analytics routes
|
||||
- name: analytics-v1
|
||||
_comment: 'Analytics: /analytics/v1/* -> http://logflare:4000/*'
|
||||
@@ -243,25 +591,41 @@ services:
|
||||
- name: cors
|
||||
config:
|
||||
origins:
|
||||
- "http://localhost:3000"
|
||||
- "http://127.0.0.1:3000"
|
||||
- "https://app.classroomcopilot.ai"
|
||||
- "https://api.classroomcopilot.ai"
|
||||
- "http://192.168.0.74"
|
||||
- "http://localhost:3000" # keep for local dev if needed
|
||||
- "http://localhost:5173" # vite default
|
||||
- "http://192.168.0.94:50001"
|
||||
- "http://192.168.0.74"
|
||||
methods:
|
||||
- GET
|
||||
- POST
|
||||
- PUT
|
||||
- PATCH
|
||||
- DELETE
|
||||
- OPTIONS
|
||||
- HEAD
|
||||
headers:
|
||||
- Accept
|
||||
- Accept-Profile
|
||||
- Authorization
|
||||
- Content-Type
|
||||
- Content-Profile
|
||||
- X-Client-Info
|
||||
- X-Supabase-Api-Version
|
||||
- apikey
|
||||
- Mcp-Session-Id
|
||||
- Prefer
|
||||
- Range
|
||||
- Range-Unit
|
||||
- X-Requested-With
|
||||
exposed_headers:
|
||||
- Mcp-Session-Id
|
||||
- Content-Range
|
||||
- Content-Profile
|
||||
- X-Total-Count
|
||||
credentials: true
|
||||
max_age: 3600
|
||||
preflight_continue: false
|
||||
|
||||
## Protected Dashboard - catch all remaining routes
|
||||
#- name: dashboard
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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', '[email protected]', 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', '[email protected]', 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', '[email protected]', 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', '[email protected]', 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', '[email protected]', 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', '[email protected]', 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', '[email protected]', 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', '[email protected]', 'admin', 'platform-admin', 'Platform Admin', 'Platform Admin', NULL, '{"seed":"dev","role":"platform_admin"}'),
|
||||
('00000000-0000-4000-8000-000000000002', '[email protected]', 'admin', 'school-admin', 'School Admin', 'School Admin', NULL, '{"seed":"dev","role":"school_admin"}'),
|
||||
('00000000-0000-4000-8000-000000000011', '[email protected]', 'teacher', 'ada-teacher', 'Ada Teacher', 'Ada Teacher', NULL, '{"seed":"dev","department":"Science"}'),
|
||||
('00000000-0000-4000-8000-000000000012', '[email protected]', 'teacher', 'alan-teacher', 'Alan Teacher', 'Alan Teacher', NULL, '{"seed":"dev","department":"Science"}'),
|
||||
('00000000-0000-4000-8000-000000000101', '[email protected]', 'student', 'student-one', 'Student One', 'Student One', NULL, '{"seed":"dev","year_group":"9"}'),
|
||||
('00000000-0000-4000-8000-000000000102', '[email protected]', 'student', 'student-two', 'Student Two', 'Student Two', NULL, '{"seed":"dev","year_group":"9"}'),
|
||||
('00000000-0000-4000-8000-000000000103', '[email protected]', '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', '[email protected]', '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"
|
||||
-- }
|
||||
Reference in New Issue
Block a user