289 lines
9.8 KiB
SQL
289 lines
9.8 KiB
SQL
-- Persist session-level evaluator output for learner reviews and teacher/admin views.
|
|
|
|
CREATE TABLE IF NOT EXISTS app.session_evaluation (
|
|
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL CHECK (status IN ('ready','degraded','error')),
|
|
source TEXT NOT NULL,
|
|
scope TEXT NOT NULL,
|
|
stage TEXT NOT NULL,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
error TEXT,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE app.session_evaluation ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS p_session_evaluation_select ON app.session_evaluation;
|
|
DROP POLICY IF EXISTS p_session_evaluation_insert ON app.session_evaluation;
|
|
DROP POLICY IF EXISTS p_session_evaluation_update ON app.session_evaluation;
|
|
DROP POLICY IF EXISTS p_session_evaluation_delete ON app.session_evaluation;
|
|
|
|
CREATE POLICY p_session_evaluation_select
|
|
ON app.session_evaluation FOR SELECT USING (
|
|
app.is_ai_context()
|
|
OR app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_evaluation.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_session_evaluation_insert
|
|
ON app.session_evaluation FOR INSERT WITH CHECK (
|
|
app.is_ai_context()
|
|
OR app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_evaluation.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_session_evaluation_update
|
|
ON app.session_evaluation FOR UPDATE USING (
|
|
app.is_ai_context()
|
|
OR app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_evaluation.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
) WITH CHECK (
|
|
app.is_ai_context()
|
|
OR app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_evaluation.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_session_evaluation_delete
|
|
ON app.session_evaluation FOR DELETE USING (
|
|
app.is_ai_context()
|
|
OR app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_evaluation.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
-- Learner-edited case formulation worksheet for session review.
|
|
CREATE TABLE IF NOT EXISTS app.case_worksheet (
|
|
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
ALTER TABLE app.case_worksheet ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS p_case_worksheet_select ON app.case_worksheet;
|
|
DROP POLICY IF EXISTS p_case_worksheet_insert ON app.case_worksheet;
|
|
DROP POLICY IF EXISTS p_case_worksheet_update ON app.case_worksheet;
|
|
DROP POLICY IF EXISTS p_case_worksheet_delete ON app.case_worksheet;
|
|
|
|
CREATE POLICY p_case_worksheet_select
|
|
ON app.case_worksheet FOR SELECT USING (
|
|
app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.case_worksheet.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_case_worksheet_insert
|
|
ON app.case_worksheet FOR INSERT WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.case_worksheet.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_case_worksheet_update
|
|
ON app.case_worksheet FOR UPDATE USING (
|
|
EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.case_worksheet.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
) WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.case_worksheet.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_case_worksheet_delete
|
|
ON app.case_worksheet FOR DELETE USING (
|
|
EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.case_worksheet.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
-- Live coaching interventions delivered during a session.
|
|
CREATE TABLE IF NOT EXISTS app.live_coach_events (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
session_id UUID NOT NULL REFERENCES app.sessions(id) ON DELETE CASCADE,
|
|
turn_seq INT NOT NULL CHECK (turn_seq >= 1),
|
|
stage TEXT NOT NULL,
|
|
learner_text_excerpt TEXT,
|
|
client_reply_excerpt TEXT,
|
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_live_coach_events_session_turn
|
|
ON app.live_coach_events(session_id, turn_seq, created_at);
|
|
|
|
ALTER TABLE app.live_coach_events ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS p_live_coach_events_select ON app.live_coach_events;
|
|
DROP POLICY IF EXISTS p_live_coach_events_insert ON app.live_coach_events;
|
|
|
|
CREATE POLICY p_live_coach_events_select
|
|
ON app.live_coach_events FOR SELECT USING (
|
|
app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.live_coach_events.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
CREATE POLICY p_live_coach_events_insert
|
|
ON app.live_coach_events FOR INSERT WITH CHECK (
|
|
EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.live_coach_events.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
-- Public share cards for learner-owned session reviews. Payload is intentionally
|
|
-- a sanitized preview, not the transcript or worksheet raw source.
|
|
CREATE TABLE IF NOT EXISTS app.session_share_link (
|
|
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
|
|
created_by UUID NOT NULL REFERENCES app.app_user(user_id),
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
payload JSONB NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
revoked_at TIMESTAMPTZ
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_session_share_token_active
|
|
ON app.session_share_link(token_hash)
|
|
WHERE revoked_at IS NULL;
|
|
|
|
ALTER TABLE app.session_share_link ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS p_session_share_select ON app.session_share_link;
|
|
DROP POLICY IF EXISTS p_session_share_insert ON app.session_share_link;
|
|
DROP POLICY IF EXISTS p_session_share_update ON app.session_share_link;
|
|
DROP POLICY IF EXISTS p_session_share_delete ON app.session_share_link;
|
|
|
|
CREATE POLICY p_session_share_select
|
|
ON app.session_share_link FOR SELECT USING (
|
|
app.is_ai_context()
|
|
OR app.current_role_name() IN ('admin','instructor')
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_share_link.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
CREATE POLICY p_session_share_insert
|
|
ON app.session_share_link FOR INSERT WITH CHECK (
|
|
app.is_ai_context()
|
|
OR (
|
|
created_by = app.current_uid()
|
|
AND EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_share_link.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
)
|
|
);
|
|
CREATE POLICY p_session_share_update
|
|
ON app.session_share_link FOR UPDATE USING (
|
|
app.is_ai_context()
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_share_link.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
) WITH CHECK (
|
|
app.is_ai_context()
|
|
OR (
|
|
created_by = app.current_uid()
|
|
AND EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_share_link.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
)
|
|
);
|
|
CREATE POLICY p_session_share_delete
|
|
ON app.session_share_link FOR DELETE USING (
|
|
app.is_ai_context()
|
|
OR EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_share_link.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
|
|
-- Learner-owned history archive state. This is display state only; session,
|
|
-- turn, review, share, research, and audit evidence remain intact.
|
|
CREATE TABLE IF NOT EXISTS app.session_archive_state (
|
|
session_id UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
|
|
learner_id UUID NOT NULL REFERENCES app.app_user(user_id),
|
|
archived_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_session_archive_learner
|
|
ON app.session_archive_state(learner_id, archived_at DESC);
|
|
|
|
ALTER TABLE app.session_archive_state ENABLE ROW LEVEL SECURITY;
|
|
|
|
DROP POLICY IF EXISTS p_session_archive_select ON app.session_archive_state;
|
|
DROP POLICY IF EXISTS p_session_archive_insert ON app.session_archive_state;
|
|
DROP POLICY IF EXISTS p_session_archive_update ON app.session_archive_state;
|
|
DROP POLICY IF EXISTS p_session_archive_delete ON app.session_archive_state;
|
|
|
|
CREATE POLICY p_session_archive_select
|
|
ON app.session_archive_state FOR SELECT USING (
|
|
app.current_role_name() IN ('admin','instructor')
|
|
OR learner_id = app.current_uid()
|
|
);
|
|
CREATE POLICY p_session_archive_insert
|
|
ON app.session_archive_state FOR INSERT WITH CHECK (
|
|
learner_id = app.current_uid()
|
|
AND EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_archive_state.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
CREATE POLICY p_session_archive_update
|
|
ON app.session_archive_state FOR UPDATE USING (
|
|
learner_id = app.current_uid()
|
|
) WITH CHECK (
|
|
learner_id = app.current_uid()
|
|
AND EXISTS (
|
|
SELECT 1 FROM app.sessions s
|
|
WHERE s.id = app.session_archive_state.session_id
|
|
AND s.learner_id = app.current_uid()
|
|
)
|
|
);
|
|
CREATE POLICY p_session_archive_delete
|
|
ON app.session_archive_state FOR DELETE USING (
|
|
learner_id = app.current_uid()
|
|
);
|