Migration 036 adds ondelete rules to 5 transitive FKs that would
otherwise block user deletion (calendar_events via calendars,
project_tasks via projects, todos via projects, etc.).
DELETE /api/admin/users/{user_id} with self-action guard, last-admin
guard, session revocation, and audit logging. Frontend gets a red
two-click confirm button in the IAM actions menu.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
105 lines
3.2 KiB
Python
105 lines
3.2 KiB
Python
"""Add ondelete to transitive FK constraints.
|
|
|
|
Without these, deleting a user would fail because the DB-level CASCADE
|
|
only reaches first-level children (calendars, projects, people, locations).
|
|
Second-level children (calendar_events via calendar_id, project_tasks via
|
|
project_id, etc.) need their own ondelete rules to allow the full cascade.
|
|
|
|
FK changes:
|
|
calendar_events.calendar_id → CASCADE (events die with calendar)
|
|
calendar_events.location_id → SET NULL (optional ref, just unlink)
|
|
project_tasks.project_id → CASCADE (tasks die with project)
|
|
project_tasks.person_id → SET NULL (optional assignee, just unlink)
|
|
todos.project_id → SET NULL (optional ref, just unlink)
|
|
|
|
Revision ID: 036
|
|
Revises: 035
|
|
Create Date: 2026-02-27
|
|
"""
|
|
from alembic import op
|
|
|
|
revision = "036"
|
|
down_revision = "035"
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# calendar_events.calendar_id → CASCADE
|
|
op.drop_constraint(
|
|
"fk_calendar_events_calendar_id", "calendar_events", type_="foreignkey"
|
|
)
|
|
op.create_foreign_key(
|
|
"fk_calendar_events_calendar_id",
|
|
"calendar_events",
|
|
"calendars",
|
|
["calendar_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
|
|
# calendar_events.location_id → SET NULL
|
|
op.drop_constraint(
|
|
"calendar_events_location_id_fkey", "calendar_events", type_="foreignkey"
|
|
)
|
|
op.create_foreign_key(
|
|
"calendar_events_location_id_fkey",
|
|
"calendar_events",
|
|
"locations",
|
|
["location_id"],
|
|
["id"],
|
|
ondelete="SET NULL",
|
|
)
|
|
|
|
# project_tasks.project_id → CASCADE
|
|
op.drop_constraint(
|
|
"project_tasks_project_id_fkey", "project_tasks", type_="foreignkey"
|
|
)
|
|
op.create_foreign_key(
|
|
"project_tasks_project_id_fkey",
|
|
"project_tasks",
|
|
"projects",
|
|
["project_id"],
|
|
["id"],
|
|
ondelete="CASCADE",
|
|
)
|
|
|
|
# project_tasks.person_id → SET NULL
|
|
op.drop_constraint(
|
|
"project_tasks_person_id_fkey", "project_tasks", type_="foreignkey"
|
|
)
|
|
op.create_foreign_key(
|
|
"project_tasks_person_id_fkey",
|
|
"project_tasks",
|
|
"people",
|
|
["person_id"],
|
|
["id"],
|
|
ondelete="SET NULL",
|
|
)
|
|
|
|
# todos.project_id → SET NULL
|
|
op.drop_constraint(
|
|
"todos_project_id_fkey", "todos", type_="foreignkey"
|
|
)
|
|
op.create_foreign_key(
|
|
"todos_project_id_fkey",
|
|
"todos",
|
|
"projects",
|
|
["project_id"],
|
|
["id"],
|
|
ondelete="SET NULL",
|
|
)
|
|
|
|
|
|
def downgrade() -> None:
|
|
# Reverse: remove ondelete by re-creating without it
|
|
for table, col, ref_table, constraint in [
|
|
("todos", "project_id", "projects", "todos_project_id_fkey"),
|
|
("project_tasks", "person_id", "people", "project_tasks_person_id_fkey"),
|
|
("project_tasks", "project_id", "projects", "project_tasks_project_id_fkey"),
|
|
("calendar_events", "location_id", "locations", "calendar_events_location_id_fkey"),
|
|
("calendar_events", "calendar_id", "calendars", "fk_calendar_events_calendar_id"),
|
|
]:
|
|
op.drop_constraint(constraint, table, type_="foreignkey")
|
|
op.create_foreign_key(constraint, table, ref_table, [col], ["id"])
|