From d0f28782e24a18be510f0805c9df3bb1487d6088 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:24:30 +0530 Subject: [PATCH 1/2] Unique constraint on project name and org id and reduce allowed project name limit to 1 --- ..._unique_constraint_on_project_name_and_.py | 29 +++++++++++++++++++ backend/app/crud/project.py | 11 +++++++ backend/app/models/onboarding.py | 2 +- backend/app/models/project.py | 6 +++- backend/app/tests/crud/test_project.py | 16 ++++++++++ 5 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py diff --git a/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py b/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py new file mode 100644 index 00000000..cb1b1838 --- /dev/null +++ b/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py @@ -0,0 +1,29 @@ +"""unique constraint on project_name and org id + +Revision ID: 8725df286943 +Revises: 38f0e8c8dc92 +Create Date: 2025-08-27 12:22:36.633904 + +""" +from alembic import op +import sqlalchemy as sa +import sqlmodel.sql.sqltypes + + +# revision identifiers, used by Alembic. +revision = '8725df286943' +down_revision = '38f0e8c8dc92' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_unique_constraint('uq_project_name_org_id', 'project', ['name', 'organization_id']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uq_project_name_org_id', 'project', type_='unique') + # ### end Alembic commands ### diff --git a/backend/app/crud/project.py b/backend/app/crud/project.py index 9c7e7153..a876ae03 100644 --- a/backend/app/crud/project.py +++ b/backend/app/crud/project.py @@ -11,6 +11,17 @@ def create_project(*, session: Session, project_create: ProjectCreate) -> Project: + project = get_project_by_name( + session=session, + organization_id=project_create.organization_id, + project_name=project_create.name, + ) + if project: + logger.error( + f"[create_project] Project already exists | 'project_id': {project.id}, 'name': {project.name}" + ) + raise HTTPException(409, "Project already exists") + db_project = Project.model_validate(project_create) db_project.inserted_at = now() db_project.updated_at = now() diff --git a/backend/app/models/onboarding.py b/backend/app/models/onboarding.py index 3b35896a..49214067 100644 --- a/backend/app/models/onboarding.py +++ b/backend/app/models/onboarding.py @@ -29,7 +29,7 @@ class OnboardingRequest(SQLModel): ) project_name: str = Field( description="Name of the project under the organization", - min_length=3, + min_length=1, max_length=100, ) email: EmailStr | None = Field( diff --git a/backend/app/models/project.py b/backend/app/models/project.py index 442b740a..43c0012e 100644 --- a/backend/app/models/project.py +++ b/backend/app/models/project.py @@ -1,6 +1,6 @@ from datetime import datetime from typing import Optional, List -from sqlmodel import Field, Relationship, SQLModel +from sqlmodel import Field, Relationship, SQLModel, UniqueConstraint from app.core.util import now @@ -26,6 +26,10 @@ class ProjectUpdate(SQLModel): # Database model for Project class Project(ProjectBase, table=True): + __table_args__ = ( + UniqueConstraint("name", "organization_id", name="uq_project_name_org_id"), + ) + id: int = Field(default=None, primary_key=True) organization_id: int = Field( foreign_key="organization.id", index=True, nullable=False, ondelete="CASCADE" diff --git a/backend/app/tests/crud/test_project.py b/backend/app/tests/crud/test_project.py index 06aa38ac..9698b466 100644 --- a/backend/app/tests/crud/test_project.py +++ b/backend/app/tests/crud/test_project.py @@ -34,6 +34,22 @@ def test_create_project(db: Session) -> None: assert project.organization_id == organization.id +def test_create_project_duplicate_name(db: Session) -> None: + """Test creating a project with a duplicate name.""" + organization = create_test_organization(db) + + project_name = random_lower_string() + project_data = ProjectCreate( + name=project_name, + description="Test description", + is_active=True, + organization_id=organization.id, + ) + project = create_project(session=db, project_create=project_data) + with pytest.raises(HTTPException, match="Project already exists"): + create_project(session=db, project_create=project_data) + + def test_get_project_by_id(db: Session) -> None: """Test retrieving a project by ID.""" project = create_test_project(db) From 2b01b0857980acf304305e14afc780804096cbc1 Mon Sep 17 00:00:00 2001 From: Aviraj <100823015+avirajsingh7@users.noreply.github.com> Date: Wed, 27 Aug 2025 12:25:43 +0530 Subject: [PATCH 2/2] pre commit --- ...5df286943_unique_constraint_on_project_name_and_.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py b/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py index cb1b1838..342751fb 100644 --- a/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py +++ b/backend/app/alembic/versions/8725df286943_unique_constraint_on_project_name_and_.py @@ -11,19 +11,21 @@ # revision identifiers, used by Alembic. -revision = '8725df286943' -down_revision = '38f0e8c8dc92' +revision = "8725df286943" +down_revision = "38f0e8c8dc92" branch_labels = None depends_on = None def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.create_unique_constraint('uq_project_name_org_id', 'project', ['name', 'organization_id']) + op.create_unique_constraint( + "uq_project_name_org_id", "project", ["name", "organization_id"] + ) # ### end Alembic commands ### def downgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.drop_constraint('uq_project_name_org_id', 'project', type_='unique') + op.drop_constraint("uq_project_name_org_id", "project", type_="unique") # ### end Alembic commands ###