Skip to content

Add admin delete endpoints for all routers #281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 201 additions & 0 deletions alembic/versions/20250715_132810_b26912f54fb6_ondelete_cascades.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
"""ondelete_cascades

Revision ID: b26912f54fb6
Revises: c41a40d022fb
Create Date: 2025-07-15 13:28:10.964046

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


from sqlalchemy import Text
import app.db.types

# revision identifiers, used by Alembic.
revision: str = "b26912f54fb6"
down_revision: Union[str, None] = "c41a40d022fb"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f("fk_asset_entity_id_entity"), "asset", type_="foreignkey")
op.create_foreign_key(
op.f("fk_asset_entity_id_entity"),
"asset",
"entity",
["entity_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(op.f("fk_contribution_agent_id_agent"), "contribution", type_="foreignkey")
op.drop_constraint(op.f("fk_contribution_entity_id_entity"), "contribution", type_="foreignkey")
op.drop_constraint(op.f("fk_contribution_role_id_role"), "contribution", type_="foreignkey")
op.create_foreign_key(
op.f("fk_contribution_entity_id_entity"),
"contribution",
"entity",
["entity_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
op.f("fk_contribution_role_id_role"),
"contribution",
"role",
["role_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
op.f("fk_contribution_agent_id_agent"),
"contribution",
"agent",
["agent_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
op.f("fk_electrical_recording_stimulus_recording_id_electrica_985a"),
"electrical_recording_stimulus",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_electrical_recording_stimulus_recording_id_electrical_cell_recording"),
"electrical_recording_stimulus",
"electrical_cell_recording",
["recording_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
op.f("fk_etype_classification_etype_class_id_etype_class"),
"etype_classification",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_etype_classification_entity_id_entity"), "etype_classification", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_etype_classification_entity_id_entity"),
"etype_classification",
"entity",
["entity_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
op.f("fk_etype_classification_etype_class_id_etype_class"),
"etype_classification",
"etype_class",
["etype_class_id"],
["id"],
ondelete="CASCADE",
)
op.drop_constraint(
op.f("fk_mtype_classification_entity_id_entity"), "mtype_classification", type_="foreignkey"
)
op.drop_constraint(
op.f("fk_mtype_classification_mtype_class_id_mtype_class"),
"mtype_classification",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_mtype_classification_entity_id_entity"),
"mtype_classification",
"entity",
["entity_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
op.f("fk_mtype_classification_mtype_class_id_mtype_class"),
"mtype_classification",
"mtype_class",
["mtype_class_id"],
["id"],
ondelete="CASCADE",
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(
op.f("fk_mtype_classification_mtype_class_id_mtype_class"),
"mtype_classification",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_mtype_classification_entity_id_entity"), "mtype_classification", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_mtype_classification_mtype_class_id_mtype_class"),
"mtype_classification",
"mtype_class",
["mtype_class_id"],
["id"],
)
op.create_foreign_key(
op.f("fk_mtype_classification_entity_id_entity"),
"mtype_classification",
"entity",
["entity_id"],
["id"],
)
op.drop_constraint(
op.f("fk_etype_classification_etype_class_id_etype_class"),
"etype_classification",
type_="foreignkey",
)
op.drop_constraint(
op.f("fk_etype_classification_entity_id_entity"), "etype_classification", type_="foreignkey"
)
op.create_foreign_key(
op.f("fk_etype_classification_entity_id_entity"),
"etype_classification",
"entity",
["entity_id"],
["id"],
)
op.create_foreign_key(
op.f("fk_etype_classification_etype_class_id_etype_class"),
"etype_classification",
"etype_class",
["etype_class_id"],
["id"],
)
op.drop_constraint(
op.f("fk_electrical_recording_stimulus_recording_id_electrical_cell_recording"),
"electrical_recording_stimulus",
type_="foreignkey",
)
op.create_foreign_key(
op.f("fk_electrical_recording_stimulus_recording_id_electrica_985a"),
"electrical_recording_stimulus",
"electrical_cell_recording",
["recording_id"],
["id"],
)
op.drop_constraint(op.f("fk_contribution_agent_id_agent"), "contribution", type_="foreignkey")
op.drop_constraint(op.f("fk_contribution_role_id_role"), "contribution", type_="foreignkey")
op.drop_constraint(op.f("fk_contribution_entity_id_entity"), "contribution", type_="foreignkey")
op.create_foreign_key(
op.f("fk_contribution_role_id_role"), "contribution", "role", ["role_id"], ["id"]
)
op.create_foreign_key(
op.f("fk_contribution_entity_id_entity"), "contribution", "entity", ["entity_id"], ["id"]
)
op.create_foreign_key(
op.f("fk_contribution_agent_id_agent"), "contribution", "agent", ["agent_id"], ["id"]
)
op.drop_constraint(op.f("fk_asset_entity_id_entity"), "asset", type_="foreignkey")
op.create_foreign_key(
op.f("fk_asset_entity_id_entity"), "asset", "entity", ["entity_id"], ["id"]
)
# ### end Alembic commands ###
47 changes: 33 additions & 14 deletions app/db/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,12 @@ class MTypeClassification(Identifiable):
authorized_project_id: Mapped[uuid.UUID]
authorized_public: Mapped[bool] = mapped_column(default=False)

mtype_class_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("mtype_class.id"), index=True)
entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True)
mtype_class_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("mtype_class.id", ondelete="CASCADE"), index=True
)
entity_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("entity.id", ondelete="CASCADE"), index=True
)


class ETypeClassification(Identifiable):
Expand All @@ -392,8 +396,12 @@ class ETypeClassification(Identifiable):
authorized_project_id: Mapped[uuid.UUID]
authorized_public: Mapped[bool] = mapped_column(default=False)

etype_class_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("etype_class.id"), index=True)
entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True)
etype_class_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("etype_class.id", ondelete="CASCADE"), index=True
)
entity_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("entity.id", ondelete="CASCADE"), index=True
)


class MTypesMixin:
Expand All @@ -408,8 +416,8 @@ def mtypes(cls) -> Mapped[list["MTypeClass"]]:
primaryjoin=f"{cls.__name__}.id == MTypeClassification.entity_id",
secondary="mtype_classification",
uselist=True,
viewonly=True,
order_by="MTypeClass.pref_label",
passive_deletes=True,
)


Expand All @@ -425,8 +433,8 @@ def etypes(cls) -> Mapped[list["ETypeClass"]]:
primaryjoin=f"{cls.__name__}.id == ETypeClassification.entity_id",
secondary="etype_classification",
uselist=True,
viewonly=True,
order_by="ETypeClass.pref_label",
passive_deletes=True,
)


Expand Down Expand Up @@ -459,14 +467,16 @@ class Entity(LegacyMixin, Identifiable):
authorized_project_id: Mapped[uuid.UUID]
authorized_public: Mapped[bool] = mapped_column(default=False)

contributions: Mapped[list["Contribution"]] = relationship(uselist=True, viewonly=True)
contributions: Mapped[list["Contribution"]] = relationship(
"Contribution", uselist=True, passive_deletes=True, back_populates="entity"
)
assets: Mapped[list["Asset"]] = relationship(
"Asset",
uselist=True,
viewonly=True,
primaryjoin=lambda: sa.and_(
Entity.id == Asset.entity_id, Asset.status != AssetStatus.DELETED
),
passive_deletes=True,
)

__mapper_args__ = { # noqa: RUF012
Expand Down Expand Up @@ -573,12 +583,18 @@ class AnalysisSoftwareSourceCode(NameDescriptionVectorMixin, Entity):

class Contribution(Identifiable):
__tablename__ = "contribution"
agent_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("agent.id"), index=True)
agent_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("agent.id", ondelete="CASCADE"), index=True
)
agent = relationship("Agent", uselist=False, foreign_keys=agent_id)
role_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("role.id"), index=True)
role_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("role.id", ondelete="CASCADE"), index=True
)
role = relationship("Role", uselist=False)
entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True)
entity = relationship("Entity", uselist=False)
entity_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("entity.id", ondelete="CASCADE"), index=True
)
entity = relationship("Entity", uselist=False, back_populates="contributions")

__table_args__ = (
UniqueConstraint("entity_id", "role_id", "agent_id", name="unique_contribution_1"),
Expand Down Expand Up @@ -784,7 +800,7 @@ class ElectricalRecordingStimulus(Entity, NameDescriptionVectorMixin):
end_time: Mapped[float | None]

recording_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("electrical_cell_recording.id"),
ForeignKey("electrical_cell_recording.id", ondelete="CASCADE"),
index=True,
)

Expand All @@ -810,6 +826,7 @@ class ElectricalCellRecording(
stimuli: Mapped[list[ElectricalRecordingStimulus]] = relationship(
uselist=True,
foreign_keys="ElectricalRecordingStimulus.recording_id",
passive_deletes=True,
)

__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
Expand Down Expand Up @@ -1025,7 +1042,9 @@ class Asset(Identifiable):
sha256_digest: Mapped[bytes | None] = mapped_column(LargeBinary(32))
meta: Mapped[JSON_DICT] # not used yet. can be useful?
label: Mapped[AssetLabel]
entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True)
entity_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("entity.id", ondelete="CASCADE"), index=True
)

# partial unique index
__table_args__ = (
Expand Down
22 changes: 12 additions & 10 deletions app/queries/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ def router_read_one[T: BaseModel, I: Identifiable](
the model data as a Pydantic model.
"""
query = sa.select(db_model_class).where(db_model_class.id == id_)
if id_model_class := get_declaring_class(db_model_class, "authorized_project_id"):
if authorized_project_id and (
id_model_class := get_declaring_class(db_model_class, "authorized_project_id")
):
query = constrain_to_accessible_entities(
query, authorized_project_id, db_model_class=id_model_class
)
Expand Down Expand Up @@ -343,20 +345,20 @@ def router_delete_one[T: BaseModel, I: Identifiable](
query = constrain_to_accessible_entities(
query, authorized_project_id, db_model_class=id_model_class
)
with (
ensure_result(error_message=f"{db_model_class.__name__} not found"),
ensure_foreign_keys_integrity(
error_message=(
f"{db_model_class.__name__} cannot be deleted "
f"because of foreign keys integrity violation"
)
),
):

with ensure_result(error_message=f"{db_model_class.__name__} not found"):
obj = db.execute(query).scalars().one()

with ensure_foreign_keys_integrity(
error_message=(
f"{db_model_class.__name__} cannot be deleted "
f"because of foreign keys integrity violation"
)
):
# Use ORM delete in order to ensure that ondelete cascades are triggered in parents when
# subclasses are deleted as it is the case with Activity/SimulationGeneration.
db.delete(obj)
db.flush()


def router_update_activity_one[T: BaseModel, I: Activity](
Expand Down
1 change: 1 addition & 0 deletions app/routers/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
read_many = router.get("")(app.service.circuit.read_many)
read_one = router.get("/{id_}")(app.service.circuit.read_one)
create_one = router.post("")(app.service.circuit.create_one)
delete_one = router.delete("/{id_}")(app.service.circuit.delete_one)
1 change: 1 addition & 0 deletions app/routers/contribution.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
read_many = router.get("")(app.service.contribution.read_many)
read_one = router.get("/{id_}")(app.service.contribution.read_one)
create_one = router.post("")(app.service.contribution.create_one)
delete_one = router.delete("/{id_}")(app.service.contribution.delete_one)
1 change: 1 addition & 0 deletions app/routers/electrical_cell_recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
read_many = router.get("")(app.service.electrical_cell_recording.read_many)
read_one = router.get("/{id_}")(app.service.electrical_cell_recording.read_one)
create_one = router.post("")(app.service.electrical_cell_recording.create_one)
delete_one = router.delete("/{id_}")(app.service.electrical_cell_recording.delete_one)
1 change: 1 addition & 0 deletions app/routers/emodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@
read_many = router.get("")(app.service.emodel.read_many)
read_one = router.get("/{id_}")(app.service.emodel.read_one)
create_one = router.post("")(app.service.emodel.create_one)
delete_one = router.delete("/{id_}")(app.service.emodel.delete_one)
1 change: 1 addition & 0 deletions app/routers/etype.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@

read_many = router.get("")(app.service.etype.read_many)
read_one = router.get("/{id_}")(app.service.etype.read_one)
delete_one = router.delete("/{id_}")(app.service.etype.delete_one)
1 change: 1 addition & 0 deletions app/routers/etype_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
)

create_one = router.post("")(app.service.etype_classification.create_one)
delete_one = router.delete("/{id_}")(app.service.etype_classification.delete_one)
1 change: 1 addition & 0 deletions app/routers/experimental_bouton_density.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
read_many = router.get("")(app.service.experimental_bouton_density.read_many)
read_onw = router.get("/{id_}")(app.service.experimental_bouton_density.read_one)
create_one = router.post("")(app.service.experimental_bouton_density.create_one)
delete_one = router.delete("/{id_}")(app.service.experimental_bouton_density.delete_one)
Loading