major major
This commit is contained in:
@@ -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;
|
||||
$$;
|
||||
Reference in New Issue
Block a user