From 068baec17980b67eef2ed27ea0c4928b0b7069a2 Mon Sep 17 00:00:00 2001 From: chihlasm Date: Sat, 4 Apr 2026 07:41:13 +0000 Subject: [PATCH] feat: add network_diagrams table Create NetworkDiagram SQLAlchemy model with JSONB nodes/edges, team-scoped with client/asset metadata, and Alembic migration 074. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../074_add_network_diagrams_table.py | 41 ++++++++++++++ backend/app/models/__init__.py | 2 + backend/app/models/network_diagram.py | 53 +++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 backend/alembic/versions/074_add_network_diagrams_table.py create mode 100644 backend/app/models/network_diagram.py diff --git a/backend/alembic/versions/074_add_network_diagrams_table.py b/backend/alembic/versions/074_add_network_diagrams_table.py new file mode 100644 index 00000000..a95bd89b --- /dev/null +++ b/backend/alembic/versions/074_add_network_diagrams_table.py @@ -0,0 +1,41 @@ +"""Add network_diagrams table. + +Revision ID: 074 +Revises: 073 +Create Date: 2026-04-04 +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID, JSONB + + +revision = "074" +down_revision = "073" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "network_diagrams", + sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("team_id", UUID(as_uuid=True), sa.ForeignKey("teams.id", ondelete="CASCADE"), nullable=False), + sa.Column("name", sa.String(255), nullable=False), + sa.Column("client_name", sa.String(255), nullable=True), + sa.Column("asset_name", sa.String(255), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + sa.Column("nodes", JSONB(), nullable=False, server_default=sa.text("'[]'::jsonb")), + sa.Column("edges", JSONB(), nullable=False, server_default=sa.text("'[]'::jsonb")), + sa.Column("thumbnail_url", sa.Text(), nullable=True), + sa.Column("is_archived", sa.Boolean(), nullable=False, server_default=sa.text("false")), + sa.Column("created_by", UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()")), + sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()")), + ) + + op.create_index("idx_network_diagrams_team", "network_diagrams", ["team_id"]) + op.create_index("idx_network_diagrams_client", "network_diagrams", ["team_id", "client_name"]) + + +def downgrade() -> None: + op.drop_table("network_diagrams") diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index d7520f91..6469af30 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -55,6 +55,7 @@ from .fork_point import ForkPoint from .session_handoff import SessionHandoff from .session_resolution_output import SessionResolutionOutput from .device_type import DeviceType +from .network_diagram import NetworkDiagram __all__ = [ "User", @@ -124,4 +125,5 @@ __all__ = [ "SessionHandoff", "SessionResolutionOutput", "DeviceType", + "NetworkDiagram", ] diff --git a/backend/app/models/network_diagram.py b/backend/app/models/network_diagram.py new file mode 100644 index 00000000..a8ba7092 --- /dev/null +++ b/backend/app/models/network_diagram.py @@ -0,0 +1,53 @@ +"""Network diagram model.""" +import uuid +from datetime import datetime, timezone +from typing import Optional, TYPE_CHECKING + +from sqlalchemy import String, Text, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.dialects.postgresql import UUID, JSONB + +from app.core.database import Base + +if TYPE_CHECKING: + from app.models.user import User + + +class NetworkDiagram(Base): + """A network topology diagram, team-scoped.""" + __tablename__ = "network_diagrams" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + team_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("teams.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + name: Mapped[str] = mapped_column(String(255), nullable=False) + client_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + asset_name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + description: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + nodes: Mapped[list] = mapped_column(JSONB, nullable=False, server_default="'[]'") + edges: Mapped[list] = mapped_column(JSONB, nullable=False, server_default="'[]'") + thumbnail_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + is_archived: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=False, + ) + created_by: Mapped[Optional[uuid.UUID]] = mapped_column( + UUID(as_uuid=True), + ForeignKey("users.id"), + nullable=True, + ) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), default=lambda: datetime.now(timezone.utc) + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + ) + + creator: Mapped[Optional["User"]] = relationship("User", foreign_keys=[created_by])