major major

This commit is contained in:
2026-03-07 17:32:08 +00:00
parent ea95bf965f
commit 5e61f40911
60 changed files with 57972 additions and 575 deletions
@@ -0,0 +1,119 @@
-- Migration: Schools, Agencies, and Timetables
-- 1. Create SchoolType Enum
CREATE TYPE "SchoolType" AS ENUM ('MANAGED', 'INDEPENDENT', 'AGENCY');
-- 2. Create School Table
CREATE TABLE "School" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "SchoolType" NOT NULL DEFAULT 'MANAGED',
"adminId" TEXT NOT NULL, -- The user who created this school
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "School_pkey" PRIMARY KEY ("id")
);
-- 3. Create TeacherSchool (Mapping Table)
CREATE TABLE "TeacherSchool" (
"id" SERIAL NOT NULL,
"teacherId" TEXT NOT NULL,
"schoolId" TEXT NOT NULL,
"isManaged" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "TeacherSchool_pkey" PRIMARY KEY ("id"),
CONSTRAINT "TeacherSchool_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "Teacher"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TeacherSchool_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX "TeacherSchool_teacherId_schoolId_key" ON "TeacherSchool"("teacherId", "schoolId");
-- 4. Create Term Table
CREATE TABLE "Term" (
"id" SERIAL NOT NULL,
"schoolId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Term_pkey" PRIMARY KEY ("id"),
CONSTRAINT "Term_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- 5. Create Holiday Table
CREATE TABLE "Holiday" (
"id" SERIAL NOT NULL,
"schoolId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Holiday_pkey" PRIMARY KEY ("id"),
CONSTRAINT "Holiday_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- 6. Create SchoolTimetableSlot Table
CREATE TABLE "SchoolTimetableSlot" (
"id" SERIAL NOT NULL,
"schoolId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"startTime" TEXT NOT NULL, -- e.g. "09:00"
"endTime" TEXT NOT NULL, -- e.g. "10:00"
"isTeachingSlot" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "SchoolTimetableSlot_pkey" PRIMARY KEY ("id"),
CONSTRAINT "SchoolTimetableSlot_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- 7. Create TimetableTemplate and TimetableEntry
CREATE TABLE "TimetableTemplate" (
"id" SERIAL NOT NULL,
"schoolId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"teacherId" TEXT NOT NULL,
CONSTRAINT "TimetableTemplate_pkey" PRIMARY KEY ("id"),
CONSTRAINT "TimetableTemplate_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TimetableTemplate_teacherId_fkey" FOREIGN KEY ("teacherId") REFERENCES "Teacher"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE "TimetableEntry" (
"id" SERIAL NOT NULL,
"timetableTemplateId" INTEGER NOT NULL,
"schoolTimetableSlotId" INTEGER NOT NULL,
"classId" INTEGER NOT NULL,
"subjectId" INTEGER NOT NULL,
"dayOfWeek" INTEGER NOT NULL, -- 1=Monday, 7=Sunday
CONSTRAINT "TimetableEntry_pkey" PRIMARY KEY ("id"),
CONSTRAINT "TimetableEntry_timetableTemplateId_fkey" FOREIGN KEY ("timetableTemplateId") REFERENCES "TimetableTemplate"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TimetableEntry_schoolTimetableSlotId_fkey" FOREIGN KEY ("schoolTimetableSlotId") REFERENCES "SchoolTimetableSlot"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TimetableEntry_classId_fkey" FOREIGN KEY ("classId") REFERENCES "Class"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "TimetableEntry_subjectId_fkey" FOREIGN KEY ("subjectId") REFERENCES "Subject"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- 8. Alter Existing Tables to add schoolId (Since DB is reset, we assume tables are empty and can add NOT NULL safely)
-- For schemas heavily dependent on multi-tenancy:
ALTER TABLE "Admin" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Student" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Parent" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Class" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Subject" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Lesson" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Exam" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Assignment" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Result" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Attendance" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Event" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
ALTER TABLE "Announcement" ADD COLUMN "schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE;
-- 9. Cleanup Lesson table constraints and deprecated fields
ALTER TABLE "Lesson" DROP COLUMN "day";
-- Enable RLS on new tables
ALTER TABLE "School" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "TeacherSchool" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Term" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "Holiday" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "SchoolTimetableSlot" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "TimetableTemplate" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "TimetableEntry" ENABLE ROW LEVEL SECURITY;
@@ -0,0 +1,87 @@
-- TeacherSchoolSchedule table to model when teachers work at specific schools
CREATE TABLE "TeacherSchoolSchedule" (
"id" SERIAL PRIMARY KEY,
"teacherId" TEXT NOT NULL REFERENCES "Teacher"("id") ON DELETE CASCADE ON UPDATE CASCADE,
"schoolId" TEXT NOT NULL REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
-- 1=Monday ... 7=Sunday, aligned with TimetableEntry.dayOfWeek
"daysOfWeek" INTEGER[] NOT NULL
);
ALTER TABLE "TeacherSchoolSchedule" ENABLE ROW LEVEL SECURITY;
-- RLS policies for School, TeacherSchool, and TeacherSchoolSchedule
-- Allow admins full control over School
CREATE POLICY "Admins have full access to School"
ON "School"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
-- Teachers can read schools they are mapped to via TeacherSchool
CREATE POLICY "Teachers can view their schools"
ON "School"
FOR SELECT
TO authenticated
USING (
is_admin()
OR id IN (
SELECT "schoolId" FROM "TeacherSchool" WHERE "teacherId" = requesting_user_id()
)
);
-- Optionally allow any authenticated user to browse the school directory
CREATE POLICY "Anyone can view schools"
ON "School"
FOR SELECT
TO authenticated
USING (true);
-- Teachers can manage schools where they are the adminId (independent/agency owners)
CREATE POLICY "Teachers manage their own schools"
ON "School"
FOR ALL
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND "adminId" = requesting_user_id()
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND "adminId" = requesting_user_id()
);
-- TeacherSchool policies
CREATE POLICY "Admins have full access to TeacherSchool"
ON "TeacherSchool"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers can view their TeacherSchool mappings"
ON "TeacherSchool"
FOR SELECT
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
);
-- TeacherSchoolSchedule policies
CREATE POLICY "Admins have full access to TeacherSchoolSchedule"
ON "TeacherSchoolSchedule"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers can view their schedules"
ON "TeacherSchoolSchedule"
FOR SELECT
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
);
@@ -0,0 +1,126 @@
-- RLS policies for timetable-related tables (Term, Holiday, SchoolTimetableSlot, TimetableTemplate, TimetableEntry)
-- Helper condition: teacher can manage schools where they are linked and not managed (independent/agency)
CREATE OR REPLACE FUNCTION teacher_can_manage_school(_school_id text)
RETURNS boolean
LANGUAGE sql STABLE
AS $$
SELECT EXISTS (
SELECT 1
FROM "TeacherSchool"
WHERE "teacherId" = requesting_user_id()
AND "schoolId" = _school_id
AND "isManaged" = false
);
$$;
-- TERM
CREATE POLICY "Admins have full access on Term"
ON "Term"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers manage terms for their schools"
ON "Term"
FOR ALL
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
);
-- HOLIDAY
CREATE POLICY "Admins have full access on Holiday"
ON "Holiday"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers manage holidays for their schools"
ON "Holiday"
FOR ALL
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
);
-- SCHOOL TIMETABLE SLOT
CREATE POLICY "Admins have full access on SchoolTimetableSlot"
ON "SchoolTimetableSlot"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers manage slots for their schools"
ON "SchoolTimetableSlot"
FOR ALL
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
);
-- TIMETABLE TEMPLATE
CREATE POLICY "Admins have full access on TimetableTemplate"
ON "TimetableTemplate"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers manage templates for their schools"
ON "TimetableTemplate"
FOR ALL
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND teacher_can_manage_school("schoolId")
);
-- TIMETABLE ENTRY
CREATE POLICY "Admins have full access on TimetableEntry"
ON "TimetableEntry"
FOR ALL
USING (is_admin())
WITH CHECK (is_admin());
CREATE POLICY "Teachers manage entries for their schools"
ON "TimetableEntry"
FOR ALL
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND EXISTS (
SELECT 1
FROM "TimetableTemplate" tt
WHERE tt.id = "timetableTemplateId"
AND teacher_can_manage_school(tt."schoolId")
)
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND EXISTS (
SELECT 1
FROM "TimetableTemplate" tt
WHERE tt.id = "timetableTemplateId"
AND teacher_can_manage_school(tt."schoolId")
)
);
@@ -0,0 +1,40 @@
-- Allow teachers to create and manage their own lessons for schools they manage
-- Teachers can insert lessons for their managed schools
CREATE POLICY "Teachers can insert lessons for their schools"
ON "Lesson"
FOR INSERT
TO authenticated
WITH CHECK (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
AND teacher_can_manage_school("schoolId")
);
-- Teachers can update their own lessons
CREATE POLICY "Teachers can update their lessons"
ON "Lesson"
FOR UPDATE
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
AND teacher_can_manage_school("schoolId")
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
AND teacher_can_manage_school("schoolId")
);
-- Teachers can delete their own lessons
CREATE POLICY "Teachers can delete their lessons"
ON "Lesson"
FOR DELETE
TO authenticated
USING (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
AND teacher_can_manage_school("schoolId")
);
@@ -0,0 +1,172 @@
-- Academic years and School Timetable (slot layout) with slot ordering
-- Adds AcademicYear, SchoolTimetable; links Term/Holiday/Slots to them; renames Template/Entry and adds schoolTimetableId + audit fields.
-- 1. AcademicYear
CREATE TABLE "AcademicYear" (
"id" SERIAL NOT NULL,
"schoolId" TEXT NOT NULL,
"startYear" INTEGER NOT NULL,
"endYear" INTEGER NOT NULL,
"name" TEXT NOT NULL,
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AcademicYear_pkey" PRIMARY KEY ("id"),
CONSTRAINT "AcademicYear_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "AcademicYear_endYear_check" CHECK ("endYear" = "startYear" + 1)
);
CREATE INDEX "AcademicYear_schoolId_idx" ON "AcademicYear"("schoolId");
-- 2. SchoolTimetable (slot layout for an academic year)
CREATE TABLE "SchoolTimetable" (
"id" SERIAL NOT NULL,
"academicYearId" INTEGER NOT NULL,
"schoolId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SchoolTimetable_pkey" PRIMARY KEY ("id"),
CONSTRAINT "SchoolTimetable_academicYearId_fkey" FOREIGN KEY ("academicYearId") REFERENCES "AcademicYear"("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "SchoolTimetable_schoolId_fkey" FOREIGN KEY ("schoolId") REFERENCES "School"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX "SchoolTimetable_academicYearId_idx" ON "SchoolTimetable"("academicYearId");
CREATE INDEX "SchoolTimetable_schoolId_idx" ON "SchoolTimetable"("schoolId");
-- 3. Term: add academicYearId, createdById, createdAt
ALTER TABLE "Term"
ADD COLUMN "academicYearId" INTEGER,
ADD COLUMN "createdById" TEXT,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- 4. Holiday: add academicYearId, createdById, createdAt
ALTER TABLE "Holiday"
ADD COLUMN "academicYearId" INTEGER,
ADD COLUMN "createdById" TEXT,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- 5. SchoolTimetableSlot: add schoolTimetableId, position, createdById (keep schoolId for denormalization)
ALTER TABLE "SchoolTimetableSlot"
ADD COLUMN "schoolTimetableId" INTEGER,
ADD COLUMN "position" INTEGER,
ADD COLUMN "createdById" TEXT,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- 6. Backfill: one AcademicYear (2026-2027) and one SchoolTimetable per school
INSERT INTO "AcademicYear" ("schoolId", "startYear", "endYear", "name")
SELECT "id", 2026, 2027, '2026-2027' FROM "School";
INSERT INTO "SchoolTimetable" ("academicYearId", "schoolId", "name")
SELECT ay."id", ay."schoolId", 'Standard Week'
FROM "AcademicYear" ay;
UPDATE "Term" t
SET "academicYearId" = ay."id"
FROM "AcademicYear" ay
WHERE ay."schoolId" = t."schoolId";
UPDATE "Holiday" h
SET "academicYearId" = ay."id"
FROM "AcademicYear" ay
WHERE ay."schoolId" = h."schoolId";
UPDATE "SchoolTimetableSlot" s
SET "schoolTimetableId" = st."id", "position" = sub.rn
FROM "SchoolTimetable" st,
(SELECT "id", ROW_NUMBER() OVER (PARTITION BY "schoolId" ORDER BY "id") AS rn FROM "SchoolTimetableSlot") sub
WHERE st."schoolId" = s."schoolId" AND sub."id" = s."id";
-- 7. Enforce NOT NULL and FKs for Term and SchoolTimetableSlot; optional FK for Holiday
ALTER TABLE "Holiday"
ADD CONSTRAINT "Holiday_academicYearId_fkey" FOREIGN KEY ("academicYearId") REFERENCES "AcademicYear"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- Term and SchoolTimetableSlot
ALTER TABLE "Term"
ADD CONSTRAINT "Term_academicYearId_fkey" FOREIGN KEY ("academicYearId") REFERENCES "AcademicYear"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Term" ALTER COLUMN "academicYearId" SET NOT NULL;
ALTER TABLE "SchoolTimetableSlot"
ADD CONSTRAINT "SchoolTimetableSlot_schoolTimetableId_fkey" FOREIGN KEY ("schoolTimetableId") REFERENCES "SchoolTimetable"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "SchoolTimetableSlot" ALTER COLUMN "schoolTimetableId" SET NOT NULL;
ALTER TABLE "SchoolTimetableSlot" ALTER COLUMN "position" SET NOT NULL;
CREATE INDEX "Term_academicYearId_idx" ON "Term"("academicYearId");
CREATE INDEX "SchoolTimetableSlot_schoolTimetableId_idx" ON "SchoolTimetableSlot"("schoolTimetableId");
-- 8. Rename TimetableTemplate -> TeacherTimetableTemplate; add schoolTimetableId, createdById, createdAt
ALTER TABLE "TimetableTemplate"
ADD COLUMN "schoolTimetableId" INTEGER,
ADD COLUMN "createdById" TEXT,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
UPDATE "TimetableTemplate" tt
SET "schoolTimetableId" = (
SELECT st."id" FROM "SchoolTimetable" st WHERE st."schoolId" = tt."schoolId" ORDER BY st."id" LIMIT 1
);
ALTER TABLE "TimetableTemplate"
ADD CONSTRAINT "TimetableTemplate_schoolTimetableId_fkey" FOREIGN KEY ("schoolTimetableId") REFERENCES "SchoolTimetable"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "TimetableTemplate" ALTER COLUMN "schoolTimetableId" SET NOT NULL;
ALTER TABLE "TimetableTemplate" RENAME TO "TeacherTimetableTemplate";
CREATE INDEX "TeacherTimetableTemplate_schoolTimetableId_idx" ON "TeacherTimetableTemplate"("schoolTimetableId");
-- 9. Rename TimetableEntry -> TeacherTimetableEntry; column timetableTemplateId -> teacherTimetableTemplateId
ALTER TABLE "TimetableEntry" RENAME TO "TeacherTimetableEntry";
ALTER TABLE "TeacherTimetableEntry" RENAME COLUMN "timetableTemplateId" TO "teacherTimetableTemplateId";
-- FK constraint name still points to old table name; drop and re-add
ALTER TABLE "TeacherTimetableEntry" DROP CONSTRAINT "TimetableEntry_timetableTemplateId_fkey";
ALTER TABLE "TeacherTimetableEntry"
ADD CONSTRAINT "TeacherTimetableEntry_teacherTimetableTemplateId_fkey"
FOREIGN KEY ("teacherTimetableTemplateId") REFERENCES "TeacherTimetableTemplate"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE INDEX "TeacherTimetableEntry_teacherTimetableTemplateId_idx" ON "TeacherTimetableEntry"("teacherTimetableTemplateId");
-- 10. Enable RLS on new tables
ALTER TABLE "AcademicYear" ENABLE ROW LEVEL SECURITY;
ALTER TABLE "SchoolTimetable" ENABLE ROW LEVEL SECURITY;
-- 11. RLS policies for AcademicYear and SchoolTimetable (teacher_can_manage_school from 20260305020000)
CREATE POLICY "Admins have full access on AcademicYear"
ON "AcademicYear" FOR ALL USING (is_admin()) WITH CHECK (is_admin());
CREATE POLICY "Teachers manage academic years for their schools"
ON "AcademicYear" FOR ALL TO authenticated
USING (requesting_user_role() = 'teacher' AND teacher_can_manage_school("schoolId"))
WITH CHECK (requesting_user_role() = 'teacher' AND teacher_can_manage_school("schoolId"));
CREATE POLICY "Admins have full access on SchoolTimetable"
ON "SchoolTimetable" FOR ALL USING (is_admin()) WITH CHECK (is_admin());
CREATE POLICY "Teachers manage school timetables for their schools"
ON "SchoolTimetable" FOR ALL TO authenticated
USING (requesting_user_role() = 'teacher' AND teacher_can_manage_school("schoolId"))
WITH CHECK (requesting_user_role() = 'teacher' AND teacher_can_manage_school("schoolId"));
-- 12. Update TeacherTimetableEntry policies to use renamed table/column (drop old, create new)
DROP POLICY IF EXISTS "Admins have full access on TimetableEntry" ON "TeacherTimetableEntry";
DROP POLICY IF EXISTS "Teachers manage entries for their schools" ON "TeacherTimetableEntry";
CREATE POLICY "Admins have full access on TeacherTimetableEntry"
ON "TeacherTimetableEntry" FOR ALL USING (is_admin()) WITH CHECK (is_admin());
CREATE POLICY "Teachers manage entries for their schools"
ON "TeacherTimetableEntry" FOR ALL TO authenticated
USING (
requesting_user_role() = 'teacher'
AND EXISTS (
SELECT 1 FROM "TeacherTimetableTemplate" tt
WHERE tt."id" = "teacherTimetableTemplateId" AND teacher_can_manage_school(tt."schoolId")
)
)
WITH CHECK (
requesting_user_role() = 'teacher'
AND EXISTS (
SELECT 1 FROM "TeacherTimetableTemplate" tt
WHERE tt."id" = "teacherTimetableTemplateId" AND teacher_can_manage_school(tt."schoolId")
)
);
@@ -0,0 +1,22 @@
-- Fix Lesson id sequence when it gets out of sync (e.g. after seed_schedule inserts with explicit ids).
-- Call this before bulk-inserting lessons so new rows get unique ids.
CREATE OR REPLACE FUNCTION sync_lesson_id_sequence()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
BEGIN
PERFORM setval(
pg_get_serial_sequence('"Lesson"', 'id'),
COALESCE((SELECT MAX(id) FROM "Lesson"), 1)
);
END;
$$;
COMMENT ON FUNCTION sync_lesson_id_sequence() IS 'Sets Lesson id sequence to max(id)+1 so next inserts do not violate Lesson_pkey.';
-- Allow authenticated users to call (needed for generate-lessons actions)
GRANT EXECUTE ON FUNCTION sync_lesson_id_sequence() TO authenticated;
GRANT EXECUTE ON FUNCTION sync_lesson_id_sequence() TO service_role;
@@ -0,0 +1,9 @@
-- Allow teachers to add themselves to a school (link from directory on My Schools page)
CREATE POLICY "Teachers can insert their own TeacherSchool mapping"
ON "TeacherSchool"
FOR INSERT
TO authenticated
WITH CHECK (
requesting_user_role() = 'teacher'
AND "teacherId" = requesting_user_id()
);
@@ -0,0 +1,117 @@
-- Students can belong to multiple classes: add StudentClass junction table and remove Student.classId
-- 1. Create junction table
CREATE TABLE "StudentClass" (
"studentId" TEXT NOT NULL REFERENCES "Student"("id") ON DELETE CASCADE,
"classId" INTEGER NOT NULL REFERENCES "Class"("id") ON DELETE CASCADE,
CONSTRAINT "StudentClass_pkey" PRIMARY KEY ("studentId", "classId")
);
CREATE INDEX "StudentClass_classId_idx" ON "StudentClass"("classId");
CREATE INDEX "StudentClass_studentId_idx" ON "StudentClass"("studentId");
-- 2. Backfill from existing Student.classId
INSERT INTO "StudentClass" ("studentId", "classId")
SELECT id, "classId" FROM "Student" WHERE "classId" IS NOT NULL;
-- 3. Drop FK and column on Student
ALTER TABLE "Student" DROP CONSTRAINT IF EXISTS "Student_classId_fkey";
ALTER TABLE "Student" DROP COLUMN IF EXISTS "classId";
-- 4. RLS for StudentClass
ALTER TABLE "StudentClass" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Admins have full access" ON "StudentClass" FOR ALL USING (
EXISTS (SELECT 1 FROM "Admin" WHERE id = requesting_user_id())
);
CREATE POLICY "Users can view permitted student-class links" ON "StudentClass" FOR SELECT USING (
is_admin()
OR ("studentId" = requesting_user_id() AND requesting_user_role() = 'student')
OR ("studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent')
OR ("classId" IN (SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id()) AND requesting_user_role() = 'teacher')
OR ("classId" IN (SELECT id FROM "Class" WHERE "supervisorId" = requesting_user_id()) AND requesting_user_role() = 'teacher')
);
CREATE POLICY "Admins can insert student-class" ON "StudentClass" FOR INSERT WITH CHECK (is_admin());
CREATE POLICY "Admins can update student-class" ON "StudentClass" FOR UPDATE USING (is_admin());
CREATE POLICY "Admins can delete student-class" ON "StudentClass" FOR DELETE USING (is_admin());
-- 5. Update auth helper: classes the user can see (student/parent via StudentClass, teacher via Lesson/Class)
CREATE OR REPLACE FUNCTION auth_user_classes() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$
BEGIN
RETURN QUERY
SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student'
UNION
SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent'
UNION
SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher'
UNION
SELECT id FROM "Class" WHERE "supervisorId" = requesting_user_id() AND requesting_user_role() = 'teacher';
END;
$$;
-- 6. Update auth_user_students: teacher sees students in their classes via StudentClass
CREATE OR REPLACE FUNCTION auth_user_students() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$
BEGIN
RETURN QUERY
SELECT id FROM "Student" WHERE id = requesting_user_id() AND requesting_user_role() = 'student'
UNION
SELECT id FROM "Student" WHERE "parentId" = requesting_user_id() AND requesting_user_role() = 'parent'
UNION
SELECT "studentId" FROM "StudentClass" WHERE "classId" IN (
SELECT "classId" FROM "Lesson" WHERE "teacherId" = requesting_user_id()
) AND requesting_user_role() = 'teacher';
END;
$$;
-- 7. Update auth_user_lessons: student/parent see lessons for any of their classes (via StudentClass)
CREATE OR REPLACE FUNCTION auth_user_lessons() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$
BEGIN
RETURN QUERY
SELECT id FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher'
UNION
SELECT id FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student'
)
UNION
SELECT id FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent'
);
END;
$$;
-- 8. Update auth_user_subjects
CREATE OR REPLACE FUNCTION auth_user_subjects() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$
BEGIN
RETURN QUERY
SELECT "A" FROM "_SubjectToTeacher" WHERE "B" = requesting_user_id() AND requesting_user_role() = 'teacher'
UNION
SELECT "subjectId" FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student'
)
UNION
SELECT "subjectId" FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent'
);
END;
$$;
-- 9. Update auth_user_teachers
CREATE OR REPLACE FUNCTION auth_user_teachers() RETURNS SETOF text LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$
BEGIN
RETURN QUERY
SELECT id FROM "Teacher" WHERE id = requesting_user_id() AND requesting_user_role() = 'teacher'
UNION
SELECT "teacherId" FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student'
)
UNION
SELECT "teacherId" FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent'
);
END;
$$;
-- 10. Class policy referenced classId on Student; Student no longer has classId. Policy uses auth_user_classes() which we updated.
-- Event/Announcement policies use auth_user_classes() - no change needed.
@@ -0,0 +1,17 @@
-- Fix auth_user_subjects(): use TeacherSubject (not _SubjectToTeacher) and include teacher's lesson subjects
CREATE OR REPLACE FUNCTION auth_user_subjects() RETURNS SETOF integer LANGUAGE plpgsql SECURITY DEFINER SET search_path = public STABLE AS $$
BEGIN
RETURN QUERY
SELECT "subjectId" FROM "TeacherSubject" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher'
UNION
SELECT "subjectId" FROM "Lesson" WHERE "teacherId" = requesting_user_id() AND requesting_user_role() = 'teacher'
UNION
SELECT "subjectId" FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" = requesting_user_id() AND requesting_user_role() = 'student'
)
UNION
SELECT "subjectId" FROM "Lesson" WHERE "classId" IN (
SELECT "classId" FROM "StudentClass" WHERE "studentId" IN (SELECT id FROM "Student" WHERE "parentId" = requesting_user_id()) AND requesting_user_role() = 'parent'
);
END;
$$;