diff --git a/backend/app/alembic/versions/4aa1f48c6321_add_inconistency_fixes.py b/backend/app/alembic/versions/4aa1f48c6321_add_inconistency_fixes.py new file mode 100644 index 00000000..f8163202 --- /dev/null +++ b/backend/app/alembic/versions/4aa1f48c6321_add_inconistency_fixes.py @@ -0,0 +1,84 @@ +"""Fixing inconsistencies + +Revision ID: 4aa1f48c6321 +Revises: 3389c67fdcb4 +Create Date: 2025-07-03 16:46:13.642386 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "4aa1f48c6321" +down_revision = "f2589428c1d0" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column( + "collection", "project_id", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "credential", + "inserted_at", + existing_type=postgresql.TIMESTAMP(), + nullable=False, + ) + op.alter_column( + "credential", "updated_at", existing_type=postgresql.TIMESTAMP(), nullable=False + ) + op.alter_column( + "credential", "project_id", existing_type=sa.INTEGER(), nullable=False + ) + op.create_index( + op.f("ix_openai_assistant_assistant_id"), + "openai_assistant", + ["assistant_id"], + unique=True, + ) + op.drop_constraint("project_organization_id_fkey", "project", type_="foreignkey") + op.create_foreign_key( + None, "project", "organization", ["organization_id"], ["id"], ondelete="CASCADE" + ) + op.drop_constraint("credential_project_id_fkey", "credential", type_="foreignkey") + op.create_foreign_key( + None, "credential", "project", ["project_id"], ["id"], ondelete="CASCADE" + ) + + +def downgrade(): + op.drop_constraint(None, "project", type_="foreignkey") + op.create_foreign_key( + "project_organization_id_fkey", + "project", + "organization", + ["organization_id"], + ["id"], + ) + op.drop_index( + op.f("ix_openai_assistant_assistant_id"), table_name="openai_assistant" + ) + op.drop_constraint(None, "credential", type_="foreignkey") + op.create_foreign_key( + "credential_project_id_fkey", + "credential", + "project", + ["project_id"], + ["id"], + ondelete="SET NULL", + ) + op.alter_column( + "credential", "updated_at", existing_type=postgresql.TIMESTAMP(), nullable=True + ) + op.alter_column( + "credential", "inserted_at", existing_type=postgresql.TIMESTAMP(), nullable=True + ) + op.alter_column( + "collection", "project_id", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "credential", "project_id", existing_type=sa.INTEGER(), nullable=True + ) diff --git a/backend/app/models/assistants.py b/backend/app/models/assistants.py index 5647455d..8546a074 100644 --- a/backend/app/models/assistants.py +++ b/backend/app/models/assistants.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Optional, List from sqlmodel import Field, Relationship, SQLModel -from sqlalchemy import Column, String +from sqlalchemy import Column, String, Text from sqlalchemy.dialects.postgresql import ARRAY from app.core.util import now @@ -10,15 +10,19 @@ class AssistantBase(SQLModel): assistant_id: str = Field(index=True, unique=True) name: str - instructions: str + instructions: str = Field(sa_column=Column(Text, nullable=False)) model: str vector_store_ids: List[str] = Field( default_factory=list, sa_column=Column(ARRAY(String)) ) temperature: float = 0.1 max_num_results: int = 20 - project_id: int = Field(foreign_key="project.id") - organization_id: int = Field(foreign_key="organization.id") + project_id: int = Field( + foreign_key="project.id", nullable=False, ondelete="CASCADE" + ) + organization_id: int = Field( + foreign_key="organization.id", nullable=False, ondelete="CASCADE" + ) class Assistant(AssistantBase, table=True): diff --git a/backend/app/models/collection.py b/backend/app/models/collection.py index 27bc66e4..965771a3 100644 --- a/backend/app/models/collection.py +++ b/backend/app/models/collection.py @@ -35,7 +35,7 @@ class Collection(SQLModel, table=True): project_id: int = Field( foreign_key="project.id", - nullable=True, + nullable=False, ondelete="CASCADE", ) diff --git a/backend/app/models/credentials.py b/backend/app/models/credentials.py index 0a05cff2..7096ef29 100644 --- a/backend/app/models/credentials.py +++ b/backend/app/models/credentials.py @@ -7,8 +7,12 @@ class CredsBase(SQLModel): - organization_id: int = Field(foreign_key="organization.id") - project_id: Optional[int] = Field(default=None, foreign_key="project.id") + organization_id: int = Field( + foreign_key="organization.id", nullable=False, ondelete="CASCADE" + ) + project_id: int = Field( + default=None, foreign_key="project.id", nullable=False, ondelete="CASCADE" + ) is_active: bool = True @@ -53,16 +57,16 @@ class Credential(CredsBase, table=True): index=True, description="Provider name like 'openai', 'gemini'" ) credential: str = Field( - sa_column=sa.Column(sa.String), + sa_column=sa.Column(sa.String, nullable=False), description="Encrypted provider-specific credentials", ) inserted_at: datetime = Field( default_factory=now, - sa_column=sa.Column(sa.DateTime, default=datetime.utcnow), + sa_column=sa.Column(sa.DateTime, default=datetime.utcnow, nullable=False), ) updated_at: datetime = Field( default_factory=now, - sa_column=sa.Column(sa.DateTime, onupdate=datetime.utcnow), + sa_column=sa.Column(sa.DateTime, onupdate=datetime.utcnow, nullable=False), ) deleted_at: Optional[datetime] = Field( default=None, sa_column=sa.Column(sa.DateTime, nullable=True) diff --git a/backend/app/models/organization.py b/backend/app/models/organization.py index fc52bf83..90eed18b 100644 --- a/backend/app/models/organization.py +++ b/backend/app/models/organization.py @@ -38,19 +38,19 @@ class Organization(OrganizationBase, table=True): # Relationship back to Creds api_keys: list["APIKey"] = Relationship( - back_populates="organization", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="organization", cascade_delete=True ) creds: list["Credential"] = Relationship( - back_populates="organization", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="organization", cascade_delete=True ) project: list["Project"] = Relationship( - back_populates="organization", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="organization", cascade_delete=True ) assistants: list["Assistant"] = Relationship( - back_populates="organization", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="organization", cascade_delete=True ) collections: list["Collection"] = Relationship( - back_populates="organization", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="organization", cascade_delete=True ) diff --git a/backend/app/models/project.py b/backend/app/models/project.py index df63f0d4..de2ceb3c 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -27,7 +27,9 @@ class ProjectUpdate(SQLModel): # Database model for Project class Project(ProjectBase, table=True): id: int = Field(default=None, primary_key=True) - organization_id: int = Field(foreign_key="organization.id", index=True) + organization_id: int = Field( + foreign_key="organization.id", index=True, nullable=False, ondelete="CASCADE" + ) inserted_at: datetime = Field(default_factory=now, nullable=False) updated_at: datetime = Field(default_factory=now, nullable=False) @@ -35,17 +37,17 @@ class Project(ProjectBase, table=True): back_populates="project", cascade_delete=True ) creds: list["Credential"] = Relationship( - back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="project", cascade_delete=True ) assistants: list["Assistant"] = Relationship( - back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="project", cascade_delete=True ) api_keys: list["APIKey"] = Relationship( - back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="project", cascade_delete=True ) organization: Optional["Organization"] = Relationship(back_populates="project") collections: list["Collection"] = Relationship( - back_populates="project", sa_relationship_kwargs={"cascade": "all, delete"} + back_populates="project", cascade_delete=True ) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 66a7e932..fa4e3967 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -57,7 +57,7 @@ class User(UserBase, table=True): projects: list["ProjectUser"] = Relationship( back_populates="user", cascade_delete=True ) - api_keys: list["APIKey"] = Relationship(back_populates="user") + api_keys: list["APIKey"] = Relationship(back_populates="user", cascade_delete=True) class UserOrganization(UserBase): diff --git a/backend/app/tests/api/routes/test_creds.py b/backend/app/tests/api/routes/test_creds.py index 2b440eee..52bf8429 100644 --- a/backend/app/tests/api/routes/test_creds.py +++ b/backend/app/tests/api/routes/test_creds.py @@ -56,6 +56,7 @@ def test_set_credential(db: Session, superuser_token_headers: dict[str, str]): data = response.json()["data"] assert isinstance(data, list) assert len(data) == 1 + assert data[0]["organization_id"] == project.organization_id assert data[0]["provider"] == Provider.OPENAI.value assert data[0]["credential"]["model"] == "gpt-4" @@ -70,7 +71,7 @@ def test_set_credentials_for_invalid_project_org_relationship( credential_data_invalid = { "organization_id": org1.id, "is_active": True, - "project_id": project2.id, # Invalid project for org1 + "project_id": project2.id, "credential": {Provider.OPENAI.value: {"api_key": "sk-123", "model": "gpt-4"}}, } @@ -389,11 +390,12 @@ def test_duplicate_credential_creation( def test_multiple_provider_credentials( db: Session, superuser_token_headers: dict[str, str] ): - org = create_test_organization(db) + project = create_test_project(db) # Create OpenAI credentials openai_credential = { - "organization_id": org.id, + "organization_id": project.organization_id, + "project_id": project.id, "is_active": True, "credential": { Provider.OPENAI.value: { @@ -406,7 +408,8 @@ def test_multiple_provider_credentials( # Create Langfuse credentials langfuse_credential = { - "organization_id": org.id, + "organization_id": project.organization_id, + "project_id": project.id, "is_active": True, "credential": { Provider.LANGFUSE.value: { @@ -434,7 +437,7 @@ def test_multiple_provider_credentials( # Fetch all credentials response = client.get( - f"{settings.API_V1_STR}/credentials/{org.id}", + f"{settings.API_V1_STR}/credentials/{project.organization_id}", headers=superuser_token_headers, ) assert response.status_code == 200 diff --git a/backend/app/tests/crud/test_credentials.py b/backend/app/tests/crud/test_credentials.py index 3a87ce10..827c7e01 100644 --- a/backend/app/tests/crud/test_credentials.py +++ b/backend/app/tests/crud/test_credentials.py @@ -1,5 +1,5 @@ -from sqlmodel import Session import pytest +from sqlmodel import Session from app.crud import ( set_creds_for_org, @@ -31,7 +31,6 @@ def test_set_credentials_for_org(db: Session) -> None: "host": "https://cloud.langfuse.com", }, } - credentials_create = CredsCreate( organization_id=project.organization_id, project_id=project.id, @@ -109,7 +108,9 @@ def test_update_creds_for_org(db: Session) -> None: ) # Update credentials updated_creds = {"api_key": "updated-key"} - creds_update = CredsUpdate(provider="openai", credential=updated_creds) + creds_update = CredsUpdate( + project_id=project.id, provider="openai", credential=updated_creds + ) updated = update_creds_for_org( session=db, org_id=credential.organization_id, creds_in=creds_update @@ -189,7 +190,6 @@ def test_invalid_provider(db: Session) -> None: # Test with unsupported provider credentials_data = {"gemini": {"api_key": "test-key"}} - credentials_create = CredsCreate( organization_id=project.organization_id, project_id=project.id, @@ -235,7 +235,6 @@ def test_langfuse_credential_validation(db: Session) -> None: # Missing host } } - credentials_create = CredsCreate( organization_id=project.organization_id, project_id=project.id,