Repository: keephq/keep
Pull request: feat: topology applications api
<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #1968 

## 📑 Description

Add CRUD for Topology Applications


Problem statement:
[➕ Feature] Allow to group services into applications in topology 
**Reasoning**
If you happen to have huge system with a lot of services (100+ services), it would be beneficial to group them into applications and to enrich alerts with information about affected application. 

**Describe the solution you'd like**
As a user, I can select services on topology map and create an application with name, description and selected services. I could later alter all these or delete the application. As a user, I can see the affected application in alert or incident page.

Hints from issue discussion:

Commit message: fix: remove unused imports

Patch to review:
diff --git a/keep/api/core/db.py b/keep/api/core/db.py
index 89c550c6dc..7cea195357 100644
--- a/keep/api/core/db.py
+++ b/keep/api/core/db.py
@@ -2782,55 +2782,6 @@ def update_incident_name(tenant_id: str, incident_id: UUID, name: str) -> Incide
         return incident
 
 
-# Fetch all topology data
-def get_all_topology_data(
-    tenant_id: str,
-    provider_id: Optional[str] = None,
-    service: Optional[str] = None,
-    environment: Optional[str] = None,
-) -> List[TopologyServiceDtoOut]:
-    with Session(engine) as session:
-        query = select(TopologyService).where(TopologyService.tenant_id == tenant_id)
-
-        # @tb: let's filter by service only for now and take care of it when we handle multilpe
-        # services and environments and cmdbs
-        # the idea is that we show the service topology regardless of the underlying provider/env
-        # if provider_id is not None and service is not None and environment is not None:
-        if service is not None:
-            query = query.where(
-                TopologyService.service == service,
-                # TopologyService.source_provider_id == provider_id,
-                # TopologyService.environment == environment,
-            )
-
-            service_instance = session.exec(query).first()
-            if not service_instance:
-                return []
-
-            services = session.exec(
-                select(TopologyServiceDependency)
-                .where(
-                    TopologyServiceDependency.depends_on_service_id
-                    == service_instance.id
-                )
-                .options(joinedload(TopologyServiceDependency.service))
-            ).all()
-            services = [service_instance, *[service.service for service in services]]
-        else:
-            # Fetch services for the tenant
-            services = session.exec(
-                query.options(
-                    selectinload(TopologyService.dependencies).selectinload(
-                        TopologyServiceDependency.dependent_service
-                    )
-                )
-            ).all()
-
-        service_dtos = [TopologyServiceDtoOut.from_orm(service) for service in services]
-
-        return service_dtos
-
-
 def get_topology_data_by_dynamic_matcher(
     tenant_id: str, matchers_value: dict[str, str]
 ) -> TopologyService | None:
diff --git a/keep/api/models/db/migrations/versions/2024-09-22-14-16_01ebe17218c0.py b/keep/api/models/db/migrations/versions/2024-09-22-14-16_01ebe17218c0.py
new file mode 100644
index 0000000000..ebba401a7c
--- /dev/null
+++ b/keep/api/models/db/migrations/versions/2024-09-22-14-16_01ebe17218c0.py
@@ -0,0 +1,62 @@
+"""Topology applications
+
+Revision ID: 01ebe17218c0
+Revises: 5d7ae55efc6a
+Create Date: 2024-09-22 14:16:17.078591
+
+"""
+
+import sqlalchemy as sa
+import sqlmodel
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = "01ebe17218c0"
+down_revision = "5d7ae55efc6a"
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.create_table(
+        "topologyapplication",
+        sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
+        sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
+        sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
+        sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
+        sa.ForeignKeyConstraint(
+            ["tenant_id"],
+            ["tenant.id"],
+        ),
+        sa.PrimaryKeyConstraint("id"),
+    )
+    op.create_table(
+        "topologyserviceapplication",
+        sa.Column("service_id", sa.Integer(), nullable=False),
+        sa.Column("application_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
+        sa.ForeignKeyConstraint(
+            ["application_id"],
+            ["topologyapplication.id"],
+        ),
+        sa.ForeignKeyConstraint(
+            ["service_id"],
+            ["topologyservice.id"],
+        ),
+        sa.PrimaryKeyConstraint("service_id", "application_id"),
+    )
+
+    with op.batch_alter_table("topologyservice", schema=None) as batch_op:
+        batch_op.drop_column("application")
+
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    # ### commands auto generated by Alembic - please adjust! ###
+    with op.batch_alter_table("topologyservice", schema=None) as batch_op:
+        batch_op.add_column(sa.Column("application", sa.VARCHAR(), nullable=True))
+
+    op.drop_table("topologyserviceapplication")
+    op.drop_table("topologyapplication")
+    # ### end Alembic commands ###
diff --git a/keep/api/models/db/topology.py b/keep/api/models/db/topology.py
index 4537902765..94e036091e 100644
--- a/keep/api/models/db/topology.py
+++ b/keep/api/models/db/topology.py
@@ -1,10 +1,24 @@
 from datetime import datetime
 from typing import List, Optional
+from uuid import UUID, uuid4
 
 from pydantic import BaseModel
 from sqlalchemy import DateTime, ForeignKey
 from sqlmodel import JSON, Column, Field, Relationship, SQLModel, func
 
+class TopologyServiceApplication(SQLModel, table=True):
+    service_id: int = Field(foreign_key="topologyservice.id", primary_key=True)
+    application_id: UUID = Field(foreign_key="topologyapplication.id", primary_key=True)
+
+class TopologyApplication(SQLModel, table=True):
+    id: UUID = Field(default_factory=uuid4, primary_key=True)
+    tenant_id: str = Field(sa_column=Column(ForeignKey("tenant.id")))
+    name: str
+    description: Optional[str] = None
+    services: List["TopologyService"] = Relationship(
+        back_populates="applications",
+        link_model=TopologyServiceApplication
+    )
 
 class TopologyService(SQLModel, table=True):
     id: Optional[int] = Field(primary_key=True, default=None)
@@ -17,7 +31,6 @@ class TopologyService(SQLModel, table=True):
     display_name: str
     description: Optional[str]
     team: Optional[str]
-    application: Optional[str]
     email: Optional[str]
     slack: Optional[str]
     ip_address: Optional[str] = None
@@ -41,11 +54,15 @@ class TopologyService(SQLModel, table=True):
         },
     )
 
+    applications: List[TopologyApplication] = Relationship(
+        back_populates="services",
+        link_model=TopologyServiceApplication
+    )
+
     class Config:
         orm_mode = True
         unique_together = ["tenant_id", "service", "environment", "source_provider_id"]
 
-
 class TopologyServiceDependency(SQLModel, table=True):
     id: Optional[int] = Field(primary_key=True, default=None)
     service_id: int = Field(
@@ -86,7 +103,6 @@ class TopologyServiceDtoBase(BaseModel, extra="ignore"):
     environment: str = "unknown"
     description: Optional[str] = None
     team: Optional[str] = None
-    application: Optional[str] = None
     email: Optional[str] = None
     slack: Optional[str] = None
     ip_address: Optional[str] = None
@@ -105,13 +121,55 @@ class TopologyServiceDependencyDto(BaseModel, extra="ignore"):
     protocol: Optional[str] = "unknown"
 
 
+class TopologyApplicationDto(BaseModel, extra="ignore"):
+    id: UUID
+    name: str
+    description: Optional[str] = None
+    services: List[TopologyService] = Relationship(
+        back_populates="applications",
+        link_model="TopologyServiceApplication"
+    )
+
+
+class TopologyServiceDtoIn(BaseModel, extra="ignore"):
+    id: int
+
+
+class TopologyApplicationDtoIn(BaseModel, extra="ignore"):
+    id: Optional[UUID] = None
+    name: str
+    description: Optional[str] = None
+    services: List[TopologyServiceDtoIn] = []
+
+
+class TopologyApplicationServiceDto(BaseModel, extra="ignore"):
+    id: int
+    name: str
+    service: str
+
+class TopologyApplicationDtoOut(TopologyApplicationDto):
+    services: List[TopologyApplicationServiceDto] = []
+
+    @classmethod
+    def from_orm(cls, application: "TopologyApplication") -> "TopologyApplicationDtoOut":
+        return cls(
+            id=application.id,
+            name=application.name,
+            description=application.description,
+            services=[
+                TopologyApplicationServiceDto(id=service.id, name=service.display_name, service=service.service) for service in application.services if service.id is not None
+            ]
+        )
+
+
 class TopologyServiceDtoOut(TopologyServiceDtoBase):
     id: int
     dependencies: List[TopologyServiceDependencyDto]
+    application_ids: List[UUID]
     updated_at: Optional[datetime]
 
     @classmethod
-    def from_orm(cls, service: "TopologyService") -> "TopologyServiceDtoOut":
+    def from_orm(cls, service: "TopologyService", application_ids: List[UUID]) -> "TopologyServiceDtoOut":
         return cls(
             id=service.id,
             source_provider_id=service.source_provider_id,
@@ -122,7 +180,6 @@ def from_orm(cls, service: "TopologyService") -> "TopologyServiceDtoOut":
             environment=service.environment,
             description=service.description,
             team=service.team,
-            application=service.application,
             email=service.email,
             slack=service.slack,
             ip_address=service.ip_address,
@@ -137,5 +194,6 @@ def from_orm(cls, service: "TopologyService") -> "TopologyServiceDtoOut":
                 )
                 for dep in service.dependencies
             ],
+            application_ids=application_ids,
             updated_at=service.updated_at,
         )
diff --git a/keep/api/routes/topology.py b/keep/api/routes/topology.py
index 5a9b88541f..191396a0d7 100644
--- a/keep/api/routes/topology.py
+++ b/keep/api/routes/topology.py
@@ -2,13 +2,14 @@
 from typing import List, Optional
 
 from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import JSONResponse
+from sqlmodel import Session
 
-from keep.api.core.db import (  # Assuming this function exists to fetch topology data
-    get_all_topology_data,
-)
-from keep.api.models.db.topology import TopologyServiceDtoOut
+from keep.api.core.db import get_session
+from keep.api.models.db.topology import TopologyApplicationDtoIn, TopologyApplicationDtoOut, TopologyServiceDtoOut
 from keep.identitymanager.authenticatedentity import AuthenticatedEntity
 from keep.identitymanager.identitymanagerfactory import IdentityManagerFactory
+from keep.topologies.topologies_service import TopologiesService
 
 logger = logging.getLogger(__name__)
 router = APIRouter()
@@ -26,6 +27,7 @@ def get_topology_data(
     authenticated_entity: AuthenticatedEntity = Depends(
         IdentityManagerFactory.get_auth_verifier(["read:topology"])
     ),
+    session: Session = Depends(get_session),
 ) -> List[TopologyServiceDtoOut]:
     tenant_id = authenticated_entity.tenant_id
     logger.info("Getting topology data", extra={tenant_id: tenant_id})
@@ -41,13 +43,9 @@ def get_topology_data(
     #     )
 
     try:
-        topology_data = get_all_topology_data(
-            tenant_id, provider_id, service_id, environment
+        topology_data = TopologiesService.get_all_topology_data(
+            tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
         )
-        if not includeEmptyDeps:
-            topology_data = [
-                topology for topology in topology_data if topology.dependencies
-            ]
         return topology_data
     except Exception:
         logger.exception("Failed to get topology data")
@@ -55,3 +53,81 @@ def get_topology_data(
             status_code=400,
             detail="Unknown exception when getting topology data, please contact us",
         )
+
+
+@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
+def get_applications(
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> List[TopologyApplicationDtoOut]:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Getting applications", extra={"tenant_id": tenant_id})
+    try:
+        return TopologiesService.get_applications_by_tenant_id(tenant_id, session)
+    except Exception as e:
+        logger.exception(f"Failed to get applications: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail="Unknown exception when getting applications, please contact us",
+        )
+    
+@router.post("/applications", description="Create a new application", response_model=TopologyApplicationDtoOut)
+def create_application(
+    application: TopologyApplicationDtoIn,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyApplicationDtoOut:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Creating application", extra={tenant_id: tenant_id})
+    try:
+        return TopologiesService.create_application_by_tenant_id(tenant_id, application, session)
+    except Exception:
+        logger.exception("Failed to create application")
+        raise HTTPException(
+            status_code=400,
+            detail="Unknown exception when creating application, please contact us",
+        )
+    
+@router.put("/applications/{application_id}", description="Update an application", response_model=TopologyApplicationDtoOut)
+def update_application(
+    application_id: str,
+    application: TopologyApplicationDtoIn,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyApplicationDtoOut:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Updating application", extra={tenant_id: tenant_id})
+    try:
+        return TopologiesService.update_application_by_id(tenant_id, application_id, application, session)
+    except Exception:
+        logger.exception("Failed to update application")
+        raise HTTPException(
+            status_code=400,
+            detail="Unknown exception when updating application, please contact us",
+        )
+
+@router.delete("/applications/{application_id}", description="Delete an application")
+def delete_application(
+    application_id: str,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+):
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Deleting application", extra={tenant_id: tenant_id})
+    try:
+        TopologiesService.delete_application_by_id(tenant_id, application_id, session)
+        return JSONResponse(status_code=200, content={"message": "Application deleted successfully"})
+    except Exception:
+        logger.exception("Failed to delete application")
+        raise HTTPException(
+            status_code=400,
+            detail="Unknown exception when deleting application, please contact us",
+        )
diff --git a/keep/topologies/topologies_service.py b/keep/topologies/topologies_service.py
new file mode 100644
index 0000000000..7590c1d632
--- /dev/null
+++ b/keep/topologies/topologies_service.py
@@ -0,0 +1,180 @@
+import logging
+from typing import List, Optional
+from fastapi import HTTPException
+from sqlalchemy.orm import joinedload, selectinload
+
+from sqlmodel import Session, select
+
+from keep.api.models.db.topology import TopologyApplication, TopologyApplicationDtoIn, TopologyApplicationDtoOut, TopologyService, TopologyServiceApplication, TopologyServiceDependency, TopologyServiceDtoOut
+
+logger = logging.getLogger(__name__)
+
+class TopologiesService:
+    @staticmethod
+    def get_all_topology_data(
+        tenant_id: str,
+        session: Session,
+        provider_id: Optional[str] = None,
+        service: Optional[str] = None,
+        environment: Optional[str] = None,
+        includeEmptyDeps: Optional[bool] = False,
+    ) -> List[TopologyServiceDtoOut]:
+        query = select(TopologyService).where(TopologyService.tenant_id == tenant_id)
+
+        # @tb: let's filter by service only for now and take care of it when we handle multilpe
+        # services and environments and cmdbs
+        # the idea is that we show the service topology regardless of the underlying provider/env
+        # if provider_id is not None and service is not None and environment is not None:
+        if service is not None:
+            query = query.where(
+                TopologyService.service == service,
+                # TopologyService.source_provider_id == provider_id,
+                # TopologyService.environment == environment,
+            )
+
+            service_instance = session.exec(query).first()
+            if not service_instance:
+                return []
+
+            services = session.exec(
+                select(TopologyServiceDependency)
+                .where(
+                    TopologyServiceDependency.depends_on_service_id
+                    == service_instance.id
+                )
+                .options(joinedload(TopologyServiceDependency.service))
+            ).all()
+            services = [service_instance, *[service.service for service in services]]
+        else:
+            # Fetch services for the tenant
+            services = session.exec(
+                query.options(
+                    selectinload(TopologyService.dependencies).selectinload(
+                        TopologyServiceDependency.dependent_service
+                    )
+                )
+            ).all()
+
+        # Fetch application IDs for all services in a single query
+        service_ids = [service.id for service in services]
+        application_service_query = select(TopologyServiceApplication).where(
+            TopologyServiceApplication.service_id.in_(service_ids)
+        ).group_by(
+            TopologyServiceApplication.service_id
+        )
+        application_service_results = session.exec(application_service_query).all()
+
+        # Create a dictionary mapping service IDs to application IDs
+        service_to_app_ids = {}
+        for result in application_service_results:
+            if result.service_id not in service_to_app_ids:
+                service_to_app_ids[result.service_id] = []
+            service_to_app_ids[result.service_id].append(result.application_id)
+
+        logger.info(f"Service to app ids: {service_to_app_ids}")
+
+        service_dtos = [
+            TopologyServiceDtoOut.from_orm(service, application_ids=service_to_app_ids.get(service.id, []))
+            for service in services if service.dependencies or includeEmptyDeps
+        ]
+
+        return service_dtos
+
+
+    @staticmethod
+    def get_applications_by_tenant_id(tenant_id: str, session: Session) -> List[TopologyApplicationDtoOut]:
+        applications = session.exec(select(TopologyApplication).where(TopologyApplication.tenant_id == tenant_id)).all()
+        result = []
+        for application in applications:
+            try:
+                app_dto = TopologyApplicationDtoOut.from_orm(application)
+                result.append(app_dto)
+            except Exception as e:
+                logger.error(f"Error converting application to DTO: {str(e)}", 
+                            extra={"application_id": application.id, "error": str(e)})
+        
+        return result
+        
+    @staticmethod
+    def create_application_by_tenant_id(tenant_id: str, application: TopologyApplicationDtoIn, session: Session) -> TopologyApplicationDtoOut:
+        new_application = TopologyApplication(
+            tenant_id=tenant_id,
+            name=application.name,
+            description=application.description,
+        )
+        session.add(new_application)
+        session.flush()  # This assigns an ID to new_application
+
+        service_ids = [service.id for service in application.services]
+        # Fetch existing services
+        existing_services = session.exec(
+            select(TopologyService)
+            .where(TopologyService.tenant_id == tenant_id)
+            .where(TopologyService.id.in_(service_ids))
+        ).all()
+
+        # Create TopologyServiceApplication links
+        for service in existing_services:
+            link = TopologyServiceApplication(
+                service_id=service.id,
+                application_id=new_application.id
+            )
+            session.add(link)
+
+        session.commit()
+        
+        session.expire(new_application, ['services'])
+        
+        return TopologyApplicationDtoOut.from_orm(new_application)
+
+    @staticmethod
+    def update_application_by_id(tenant_id: str, application_id: str, application: TopologyApplicationDtoIn, session: Session) -> TopologyApplicationDtoOut:
+        application_db = session.exec(
+            select(TopologyApplication)
+            .where(TopologyApplication.tenant_id == tenant_id)
+            .where(TopologyApplication.id == application_id)
+        ).first()
+        if not application_db:
+            raise HTTPException(status_code=404, detail="Application not found")
+        
+        application_db.name = application.name
+        application_db.description = application.description
+
+        new_service_ids = set(service.id for service in application.services)
+
+        # Remove existing links not in the update request
+        session.query(TopologyServiceApplication) \
+            .where(TopologyServiceApplication.application_id == application_id) \
+            .where(TopologyServiceApplication.service_id.not_in(new_service_ids)) \
+            .delete()
+
+        # Add new links
+        existing_links = session.exec(
+            select(TopologyServiceApplication.service_id)
+            .where(TopologyServiceApplication.application_id == application_id)
+        ).all()
+        existing_service_ids = set(existing_links)
+        
+        new_links = [
+            TopologyServiceApplication(service_id=service_id, application_id=application_id)
+            for service_id in new_service_ids - existing_service_ids
+        ]
+        session.add_all(new_links)
+
+        session.commit()
+        session.refresh(application_db)
+        return TopologyApplicationDtoOut.from_orm(application_db)
+
+    @staticmethod
+    def delete_application_by_id(tenant_id: str, application_id: str, session: Session):
+        application = session.exec(
+            select(TopologyApplication)
+            .where(TopologyApplication.tenant_id == tenant_id)
+            .where(TopologyApplication.id == application_id)
+        ).first()
+        if not application:
+            raise 
+        session.delete(application)
+        session.commit()
+        return None
+


Code review comments:
diff_hunk: @@ -41,17 +43,91 @@ def get_topology_data(
     #     )
 
     try:
-        topology_data = get_all_topology_data(
-            tenant_id, provider_id, service_id, environment
+        topology_data = TopologiesService.get_all_topology_data(
+            tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
         )
-        if not includeEmptyDeps:
-            topology_data = [
-                topology for topology in topology_data if topology.dependencies
-            ]
         return topology_data
     except Exception:
         logger.exception("Failed to get topology data")
         raise HTTPException(
             status_code=400,
             detail="Unknown exception when getting topology data, please contact us",
         )
+
+
+@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
+def get_applications(
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> List[TopologyApplicationDtoOut]:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Getting applications", extra={"tenant_id": tenant_id})
+    try:
+        return TopologiesService.get_applications_by_tenant_id(tenant_id, session)
+    except Exception as e:
+        logger.exception(f"Failed to get applications: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail="Unknown exception when getting applications, please contact us",
+        )
+    
+@router.post("/applications", description="Create a new application", response_model=TopologyApplicationDtoOut)
+def create_application(
+    application: TopologyApplicationDtoIn,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyApplicationDtoOut:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Creating application", extra={tenant_id: tenant_id})
+    try:
+        return TopologiesService.create_application_by_tenant_id(tenant_id, application, session)
+    except Exception:
+        logger.exception("Failed to create application")
+        raise HTTPException(
+            status_code=400,
+            detail="Unknown exception when creating application, please contact us",
+        )
+    
+@router.put("/applications/{application_id}", description="Update an application", response_model=TopologyApplicationDtoOut)
path: keep/api/routes/topology.py
text: @user2:
```suggestion
@user1.put(
    "/applications/{application_id}",
    description="Update an application",
    response_model=TopologyApplicationDtoOut
)
```

diff_hunk: @@ -0,0 +1,180 @@
+import logging
+from typing import List, Optional
+from fastapi import HTTPException
+from sqlalchemy.orm import joinedload, selectinload
+
+from sqlmodel import Session, select
+
+from keep.api.models.db.topology import TopologyApplication, TopologyApplicationDtoIn, TopologyApplicationDtoOut, TopologyService, TopologyServiceApplication, TopologyServiceDependency, TopologyServiceDtoOut
path: keep/topologies/topologies_service.py
text: @user1:
```suggestion
from keep.api.models.db.topology import (
    TopologyApplication,
    TopologyApplicationDtoIn,
    TopologyApplicationDtoOut,
    TopologyService,
    TopologyServiceApplication,
    TopologyServiceDependency,
    TopologyServiceDtoOut,
)
```

diff_hunk: @@ -41,17 +43,91 @@ def get_topology_data(
     #     )
 
     try:
-        topology_data = get_all_topology_data(
-            tenant_id, provider_id, service_id, environment
+        topology_data = TopologiesService.get_all_topology_data(
+            tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
         )
-        if not includeEmptyDeps:
-            topology_data = [
-                topology for topology in topology_data if topology.dependencies
-            ]
         return topology_data
     except Exception:
         logger.exception("Failed to get topology data")
         raise HTTPException(
             status_code=400,
             detail="Unknown exception when getting topology data, please contact us",
         )
+
+
+@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
+def get_applications(
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> List[TopologyApplicationDtoOut]:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Getting applications", extra={"tenant_id": tenant_id})
+    try:
+        return TopologiesService.get_applications_by_tenant_id(tenant_id, session)
+    except Exception as e:
path: keep/api/routes/topology.py
text: @user1:
Can we use more specified exceptions here?

diff_hunk: @@ -2,13 +2,14 @@
 from typing import List, Optional
 
 from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import JSONResponse
+from sqlmodel import Session
 
-from keep.api.core.db import (  # Assuming this function exists to fetch topology data
-    get_all_topology_data,
-)
-from keep.api.models.db.topology import TopologyServiceDtoOut
+from keep.api.core.db import get_session
+from keep.api.models.db.topology import TopologyApplicationDtoIn, TopologyApplicationDtoOut, TopologyServiceDtoOut
path: keep/api/routes/topology.py
text: @user1:
```suggestion
from keep.api.models.db.topology import (
    TopologyApplicationDtoIn,
    TopologyApplicationDtoOut,
    TopologyServiceDtoOut,
)
```

path: keep/topologies/topologies_service.py
text: @user2:
Very likely, we need to move db-related code to db.py until we finally decide how to refactor it globally. @user1 wdyt?

@author:
We have providers_service.py with db stuff, I thought it would be nice to have it separated. WDYT?

@user1:
I'm commuting from London today, hopefully tomorrow I'll deep dive @author @user2

diff_hunk: @@ -0,0 +1,180 @@
+import logging
+from typing import List, Optional
+from fastapi import HTTPException
+from sqlalchemy.orm import joinedload, selectinload
+
+from sqlmodel import Session, select
+
+from keep.api.models.db.topology import TopologyApplication, TopologyApplicationDtoIn, TopologyApplicationDtoOut, TopologyService, TopologyServiceApplication, TopologyServiceDependency, TopologyServiceDtoOut
+
+logger = logging.getLogger(__name__)
+
+class TopologiesService:
+    @staticmethod
+    def get_all_topology_data(
+        tenant_id: str,
+        session: Session,
+        provider_id: Optional[str] = None,
+        service: Optional[str] = None,
+        environment: Optional[str] = None,
+        includeEmptyDeps: Optional[bool] = False,
+    ) -> List[TopologyServiceDtoOut]:
+        query = select(TopologyService).where(TopologyService.tenant_id == tenant_id)
+
+        # @tb: let's filter by service only for now and take care of it when we handle multilpe
+        # services and environments and cmdbs
+        # the idea is that we show the service topology regardless of the underlying provider/env
+        # if provider_id is not None and service is not None and environment is not None:
+        if service is not None:
+            query = query.where(
+                TopologyService.service == service,
+                # TopologyService.source_provider_id == provider_id,
+                # TopologyService.environment == environment,
path: keep/topologies/topologies_service.py
text: @user1:
Leftovers? :)

diff_hunk: @@ -41,17 +43,91 @@ def get_topology_data(
     #     )
 
     try:
-        topology_data = get_all_topology_data(
-            tenant_id, provider_id, service_id, environment
+        topology_data = TopologiesService.get_all_topology_data(
+            tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
         )
-        if not includeEmptyDeps:
-            topology_data = [
-                topology for topology in topology_data if topology.dependencies
-            ]
         return topology_data
     except Exception:
         logger.exception("Failed to get topology data")
         raise HTTPException(
             status_code=400,
             detail="Unknown exception when getting topology data, please contact us",
         )
+
+
+@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
path: keep/api/routes/topology.py
text: @user2:
Just to align style with other routes
```suggestion
@user1.get(
    "/applications", 
    description="Get all applications", 
    response_model=List[TopologyApplicationDtoOut]
)
```

diff_hunk: @@ -41,17 +43,91 @@ def get_topology_data(
     #     )
 
     try:
-        topology_data = get_all_topology_data(
-            tenant_id, provider_id, service_id, environment
+        topology_data = TopologiesService.get_all_topology_data(
+            tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
         )
-        if not includeEmptyDeps:
-            topology_data = [
-                topology for topology in topology_data if topology.dependencies
-            ]
         return topology_data
     except Exception:
         logger.exception("Failed to get topology data")
         raise HTTPException(
             status_code=400,
             detail="Unknown exception when getting topology data, please contact us",
         )
+
+
+@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
+def get_applications(
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> List[TopologyApplicationDtoOut]:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Getting applications", extra={"tenant_id": tenant_id})
+    try:
+        return TopologiesService.get_applications_by_tenant_id(tenant_id, session)
+    except Exception as e:
+        logger.exception(f"Failed to get applications: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail="Unknown exception when getting applications, please contact us",
+        )
+    
+@router.post("/applications", description="Create a new application", response_model=TopologyApplicationDtoOut)
+def create_application(
+    application: TopologyApplicationDtoIn,
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["write:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> TopologyApplicationDtoOut:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Creating application", extra={tenant_id: tenant_id})
+    try:
+        return TopologiesService.create_application_by_tenant_id(tenant_id, application, session)
+    except Exception:
path: keep/api/routes/topology.py
text: @user1:
And again, do we know what exceptions are expected here?

diff_hunk: @@ -41,17 +43,91 @@ def get_topology_data(
     #     )
 
     try:
-        topology_data = get_all_topology_data(
-            tenant_id, provider_id, service_id, environment
+        topology_data = TopologiesService.get_all_topology_data(
+            tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
         )
-        if not includeEmptyDeps:
-            topology_data = [
-                topology for topology in topology_data if topology.dependencies
-            ]
         return topology_data
     except Exception:
         logger.exception("Failed to get topology data")
         raise HTTPException(
             status_code=400,
             detail="Unknown exception when getting topology data, please contact us",
         )
+
+
+@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
+def get_applications(
+    authenticated_entity: AuthenticatedEntity = Depends(
+        IdentityManagerFactory.get_auth_verifier(["read:topology"])
+    ),
+    session: Session = Depends(get_session),
+) -> List[TopologyApplicationDtoOut]:
+    tenant_id = authenticated_entity.tenant_id
+    logger.info("Getting applications", extra={"tenant_id": tenant_id})
+    try:
+        return TopologiesService.get_applications_by_tenant_id(tenant_id, session)
+    except Exception as e:
+        logger.exception(f"Failed to get applications: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail="Unknown exception when getting applications, please contact us",
+        )
+    
+@router.post("/applications", description="Create a new application", response_model=TopologyApplicationDtoOut)
path: keep/api/routes/topology.py
text: @user2:
```suggestion
@user1.post(
    "/applications", 
    description="Create a new application",
    response_model=TopologyApplicationDtoOut
)
```