Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e553f94
Add configuration management models and migration scripts
avirajsingh7 Nov 5, 2025
b69fb83
implement create endpoint for config management
avirajsingh7 Nov 5, 2025
3e01b59
Implement CRUD operations for configuration and version management, i…
avirajsingh7 Nov 5, 2025
c8c3665
Add a list versions endpoint
avirajsingh7 Nov 5, 2025
f13ab72
Implement GET and PATCH endpoints for configuration management, inclu…
avirajsingh7 Nov 6, 2025
33961b0
Refactor config management to replace 'config_json' with 'config_blob…
avirajsingh7 Nov 6, 2025
52a6c86
Enhance configuration and version listing by adding ordering:
avirajsingh7 Nov 7, 2025
1b510fd
test for config routes
avirajsingh7 Nov 12, 2025
87a3150
test for routes version
avirajsingh7 Nov 12, 2025
409576a
Add tests for configuration and version management, including creatio…
avirajsingh7 Nov 12, 2025
6349af1
precommit
avirajsingh7 Nov 12, 2025
6288630
update migration
avirajsingh7 Nov 13, 2025
d8f2733
precomit
avirajsingh7 Nov 13, 2025
5903751
add init
avirajsingh7 Nov 13, 2025
4316763
Review queries and index
avirajsingh7 Nov 13, 2025
6b460e2
precommit
avirajsingh7 Nov 13, 2025
07f2440
fix migration
avirajsingh7 Nov 13, 2025
4a159ae
Merge remote-tracking branch 'origin/main' into feature/config_manage…
avirajsingh7 Nov 19, 2025
0483759
Update migration and resolve comments
avirajsingh7 Nov 19, 2025
c248f05
Refactor config and version CRUD methods to use 'or_raise' for better…
avirajsingh7 Nov 21, 2025
de674bb
Refactor unique name checks to use 'or_raise' for improved error hand…
avirajsingh7 Nov 21, 2025
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
101 changes: 101 additions & 0 deletions backend/app/alembic/versions/ecda6b144627_config_management_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Config management tables

Revision ID: ecda6b144627
Revises: 633e69806207
Create Date: 2025-11-19 13:16:50.954576

"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = "ecda6b144627"
down_revision = "633e69806207"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"config",
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(length=128), nullable=False),
sa.Column(
"description", sqlmodel.sql.sqltypes.AutoString(length=512), nullable=True
),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("project_id", sa.Integer(), nullable=False),
sa.Column("inserted_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_index(
"idx_config_project_id_updated_at_active",
"config",
["project_id", "updated_at"],
unique=False,
postgresql_where=sa.text("deleted_at IS NULL"),
)
op.create_index(
"uq_config_project_id_name_active",
"config",
["project_id", "name"],
unique=True,
postgresql_where=sa.text("deleted_at IS NULL"),
)
op.create_table(
"config_version",
sa.Column(
"config_blob", postgresql.JSONB(astext_type=sa.Text()), nullable=False
),
sa.Column(
"commit_message",
sqlmodel.sql.sqltypes.AutoString(length=512),
nullable=True,
),
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("config_id", sa.Uuid(), nullable=False),
sa.Column("version", sa.Integer(), nullable=False),
sa.Column("inserted_at", sa.DateTime(), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=False),
sa.Column("deleted_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["config_id"], ["config.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint(
"config_id", "version", name="uq_config_version_config_id_version"
),
)
op.create_index(
"idx_config_version_config_id_version_active",
"config_version",
["config_id", "version"],
unique=False,
postgresql_where=sa.text("deleted_at IS NULL"),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
"idx_config_version_config_id_version_active",
table_name="config_version",
postgresql_where=sa.text("deleted_at IS NULL"),
)
op.drop_table("config_version")
op.drop_index(
"uq_config_project_id_name_active",
table_name="config",
postgresql_where=sa.text("deleted_at IS NULL"),
)
op.drop_index(
"idx_config_project_id_updated_at_active",
table_name="config",
postgresql_where=sa.text("deleted_at IS NULL"),
)
op.drop_table("config")
# ### end Alembic commands ###
2 changes: 2 additions & 0 deletions backend/app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
api_keys,
assistants,
collections,
config,
documents,
doc_transformation_job,
login,
Expand Down Expand Up @@ -31,6 +32,7 @@
api_router.include_router(assistants.router)
api_router.include_router(collections.router)
api_router.include_router(collection_job.router)
api_router.include_router(config.router)
api_router.include_router(credentials.router)
api_router.include_router(cron.router)
api_router.include_router(documents.router)
Expand Down
10 changes: 10 additions & 0 deletions backend/app/api/routes/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from fastapi import APIRouter

from app.api.routes.config import config, version

router = APIRouter(prefix="/configs", tags=["Config Management"])

router.include_router(config.router)
router.include_router(version.router)

__all__ = ["router"]
133 changes: 133 additions & 0 deletions backend/app/api/routes/config/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from uuid import UUID
from fastapi import APIRouter, Depends, Query, HTTPException

from app.api.deps import SessionDep, AuthContextDep
from app.crud.config import ConfigCrud
from app.models import (
Config,
ConfigCreate,
ConfigUpdate,
ConfigPublic,
ConfigWithVersion,
ConfigVersion,
Message,
)
from app.utils import APIResponse
from app.api.permissions import Permission, require_permission

router = APIRouter()


@router.post(
"/",
response_model=APIResponse[ConfigWithVersion],
status_code=201,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def create_config(
config_create: ConfigCreate,
current_user: AuthContextDep,
session: SessionDep,
):
"""
create new config along with initial version
"""
config_crud = ConfigCrud(session=session, project_id=current_user.project.id)
config, version = config_crud.create_or_raise(config_create)

response = ConfigWithVersion(**config.model_dump(), version=version)

return APIResponse.success_response(
data=response,
)


@router.get(
"/",
response_model=APIResponse[list[ConfigPublic]],
status_code=200,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def list_configs(
current_user: AuthContextDep,
session: SessionDep,
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=100, description="Maximum records to return"),
):
"""
List all configurations for the current project.
Ordered by updated_at in descending order.
"""
config_crud = ConfigCrud(session=session, project_id=current_user.project.id)
configs = config_crud.read_all(skip=skip, limit=limit)
return APIResponse.success_response(
data=configs,
)


@router.get(
"/{config_id}",
response_model=APIResponse[ConfigPublic],
status_code=200,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def get_config(
config_id: UUID,
current_user: AuthContextDep,
session: SessionDep,
):
"""
Get a specific configuration by its ID.
"""
config_crud = ConfigCrud(session=session, project_id=current_user.project.id)
config = config_crud.exists_or_raise(config_id=config_id)
return APIResponse.success_response(
data=config,
)


@router.patch(
"/{config_id}",
response_model=APIResponse[ConfigPublic],
status_code=200,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def update_config(
config_id: UUID,
config_update: ConfigUpdate,
current_user: AuthContextDep,
session: SessionDep,
):
"""
Update a specific configuration.
"""
config_crud = ConfigCrud(session=session, project_id=current_user.project.id)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming this a DB call, may be could be done in a service instead of exposing to the router.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this route only calls the CRUD directly and has no extra logic, adding a service layer would be unnecessary overhead. We can introduce one later if the logic grows.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

config = config_crud.update_or_raise(
config_id=config_id, config_update=config_update
)

return APIResponse.success_response(
data=config,
)


@router.delete(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could explore soft deleting instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Prajna1999 Did not understand this one

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in not hard deleting from the database but toggling the flag that makes it invisible for GET-ing APIs but the entry remains in the db

"/{config_id}",
response_model=APIResponse[Message],
status_code=200,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: status code would be 204 I guess.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

204 is used when we return no content.
Here we return a message so 200 is acceptable.

dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def delete_config(
config_id: UUID,
current_user: AuthContextDep,
session: SessionDep,
):
"""
Delete a specific configuration.
"""
config_crud = ConfigCrud(session=session, project_id=current_user.project.id)
config_crud.delete_or_raise(config_id=config_id)

return APIResponse.success_response(
data=Message(message="Config deleted successfully"),
)
123 changes: 123 additions & 0 deletions backend/app/api/routes/config/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from uuid import UUID
from fastapi import APIRouter, Depends, Query, HTTPException, Path

from app.api.deps import SessionDep, AuthContextDep
from app.crud.config import ConfigCrud, ConfigVersionCrud
from app.models import (
ConfigVersionCreate,
ConfigVersionPublic,
Message,
ConfigVersionItems,
)
from app.utils import APIResponse
from app.api.permissions import Permission, require_permission

router = APIRouter()


@router.post(
"/{config_id}/versions",
response_model=APIResponse[ConfigVersionPublic],
status_code=201,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def create_version(
config_id: UUID,
version_create: ConfigVersionCreate,
current_user: AuthContextDep,
session: SessionDep,
):
"""
Create a new version for an existing configuration.
The version number is automatically incremented.
"""
version_crud = ConfigVersionCrud(
session=session, project_id=current_user.project.id, config_id=config_id
)
version = version_crud.create_or_raise(version_create=version_create)

return APIResponse.success_response(
data=ConfigVersionPublic(**version.model_dump()),
Copy link
Copy Markdown
Collaborator

@Prajna1999 Prajna1999 Nov 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: expand the keys of the dict object for better readability. Its better not to spread dicts without santizing the keys first.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the point, but in this case the model is already validated and sanitized by Pydantic, so spreading the dict is safe. Expanding every field manually doesn’t add value and would just increase noise. I'd prefer to keep it as is unless we hit a specific issue.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool!

)


@router.get(
"/{config_id}/versions",
response_model=APIResponse[list[ConfigVersionItems]],
status_code=200,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def list_versions(
config_id: UUID,
current_user: AuthContextDep,
session: SessionDep,
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=100, description="Maximum records to return"),
):
"""
List all versions for a specific configuration.
Ordered by version number in descending order.
"""
version_crud = ConfigVersionCrud(
session=session, project_id=current_user.project.id, config_id=config_id
)
versions = version_crud.read_all(
skip=skip,
limit=limit,
)
return APIResponse.success_response(
data=versions,
)


@router.get(
"/{config_id}/versions/{version_number}",
response_model=APIResponse[ConfigVersionPublic],
status_code=200,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def get_version(
config_id: UUID,
current_user: AuthContextDep,
session: SessionDep,
version_number: int = Path(
..., ge=1, description="The version number of the config"
),
):
"""
Get a specific version of a config.
"""
version_crud = ConfigVersionCrud(
session=session, project_id=current_user.project.id, config_id=config_id
)
version = version_crud.exists_or_raise(version_number=version_number)
return APIResponse.success_response(
data=version,
)


@router.delete(
"/{config_id}/versions/{version_number}",
response_model=APIResponse[Message],
status_code=200,
dependencies=[Depends(require_permission(Permission.REQUIRE_PROJECT))],
)
def delete_version(
config_id: UUID,
current_user: AuthContextDep,
session: SessionDep,
version_number: int = Path(
..., ge=1, description="The version number of the config"
),
):
"""
Delete a specific version of a config.
"""
version_crud = ConfigVersionCrud(
session=session, project_id=current_user.project.id, config_id=config_id
)
version_crud.delete_or_raise(version_number=version_number)

return APIResponse.success_response(
data=Message(message="Config Version deleted successfully"),
)
4 changes: 4 additions & 0 deletions backend/app/crud/config/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from app.crud.config.config import ConfigCrud
from app.crud.config.version import ConfigVersionCrud

__all__ = ["ConfigCrud", "ConfigVersionCrud"]
Loading