feat: add device_types table with system seed data
Creates DeviceType SQLAlchemy model and migration 073 that provisions the device_types table with 28 system-seeded device types across 7 categories (network, compute, storage, cloud, endpoint, infrastructure, security). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
95
backend/alembic/versions/073_add_device_types_table.py
Normal file
95
backend/alembic/versions/073_add_device_types_table.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"""Add device_types table with system seed data.
|
||||||
|
|
||||||
|
Revision ID: 073
|
||||||
|
Revises: 072
|
||||||
|
Create Date: 2026-04-04
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
|
revision = "073"
|
||||||
|
down_revision = "072"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
SYSTEM_DEVICE_TYPES = [
|
||||||
|
("router", "Router", "network", 0),
|
||||||
|
("switch", "Switch", "network", 1),
|
||||||
|
("firewall", "Firewall", "network", 2),
|
||||||
|
("access-point", "Access Point", "network", 3),
|
||||||
|
("load-balancer", "Load Balancer", "network", 4),
|
||||||
|
("server", "Server", "compute", 0),
|
||||||
|
("workstation", "Workstation", "compute", 1),
|
||||||
|
("vm", "Virtual Machine", "compute", 2),
|
||||||
|
("container", "Container", "compute", 3),
|
||||||
|
("nas", "NAS", "storage", 0),
|
||||||
|
("san", "SAN", "storage", 1),
|
||||||
|
("cloud-storage", "Cloud Storage", "storage", 2),
|
||||||
|
("cloud", "Cloud", "cloud", 0),
|
||||||
|
("aws", "AWS", "cloud", 1),
|
||||||
|
("azure", "Azure", "cloud", 2),
|
||||||
|
("gcp", "Google Cloud", "cloud", 3),
|
||||||
|
("printer", "Printer", "endpoint", 0),
|
||||||
|
("phone", "Phone", "endpoint", 1),
|
||||||
|
("iot", "IoT Device", "endpoint", 2),
|
||||||
|
("camera", "Camera", "endpoint", 3),
|
||||||
|
("tablet", "Tablet", "endpoint", 4),
|
||||||
|
("laptop", "Laptop", "endpoint", 5),
|
||||||
|
("ups", "UPS", "infrastructure", 0),
|
||||||
|
("pdu", "PDU", "infrastructure", 1),
|
||||||
|
("rack", "Rack", "infrastructure", 2),
|
||||||
|
("patch-panel", "Patch Panel", "infrastructure", 3),
|
||||||
|
("nvr", "NVR", "security", 0),
|
||||||
|
("badge-reader", "Badge Reader", "security", 1),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"device_types",
|
||||||
|
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("slug", sa.String(50), nullable=False),
|
||||||
|
sa.Column("label", sa.String(100), nullable=False),
|
||||||
|
sa.Column("category", sa.String(50), nullable=False),
|
||||||
|
sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("false")),
|
||||||
|
sa.Column("team_id", UUID(as_uuid=True), sa.ForeignKey("teams.id", ondelete="CASCADE"), nullable=True),
|
||||||
|
sa.Column("sort_order", sa.Integer(), nullable=False, server_default=sa.text("0")),
|
||||||
|
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.text("now()")),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE device_types ADD CONSTRAINT uq_device_types_slug_team "
|
||||||
|
"UNIQUE NULLS NOT DISTINCT (slug, team_id)"
|
||||||
|
)
|
||||||
|
op.create_index("idx_device_types_team", "device_types", ["team_id"])
|
||||||
|
|
||||||
|
device_types_table = sa.table(
|
||||||
|
"device_types",
|
||||||
|
sa.column("id", UUID(as_uuid=True)),
|
||||||
|
sa.column("slug", sa.String),
|
||||||
|
sa.column("label", sa.String),
|
||||||
|
sa.column("category", sa.String),
|
||||||
|
sa.column("is_system", sa.Boolean),
|
||||||
|
sa.column("team_id", UUID(as_uuid=True)),
|
||||||
|
sa.column("sort_order", sa.Integer),
|
||||||
|
)
|
||||||
|
|
||||||
|
op.bulk_insert(device_types_table, [
|
||||||
|
{
|
||||||
|
"id": uuid.uuid4(),
|
||||||
|
"slug": slug,
|
||||||
|
"label": label,
|
||||||
|
"category": category,
|
||||||
|
"is_system": True,
|
||||||
|
"team_id": None,
|
||||||
|
"sort_order": sort_order,
|
||||||
|
}
|
||||||
|
for slug, label, category, sort_order in SYSTEM_DEVICE_TYPES
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("device_types")
|
||||||
@@ -54,6 +54,7 @@ from .session_branch import SessionBranch
|
|||||||
from .fork_point import ForkPoint
|
from .fork_point import ForkPoint
|
||||||
from .session_handoff import SessionHandoff
|
from .session_handoff import SessionHandoff
|
||||||
from .session_resolution_output import SessionResolutionOutput
|
from .session_resolution_output import SessionResolutionOutput
|
||||||
|
from .device_type import DeviceType
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"User",
|
"User",
|
||||||
@@ -122,4 +123,5 @@ __all__ = [
|
|||||||
"ForkPoint",
|
"ForkPoint",
|
||||||
"SessionHandoff",
|
"SessionHandoff",
|
||||||
"SessionResolutionOutput",
|
"SessionResolutionOutput",
|
||||||
|
"DeviceType",
|
||||||
]
|
]
|
||||||
|
|||||||
51
backend/app/models/device_type.py
Normal file
51
backend/app/models/device_type.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""Device type model for network diagrams."""
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import String, Boolean, Integer, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
|
||||||
|
from app.core.database import Base
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceType(Base):
|
||||||
|
"""A device type for network diagram nodes (system or team-custom)."""
|
||||||
|
__tablename__ = "device_types"
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
slug: Mapped[str] = mapped_column(
|
||||||
|
String(50), nullable=False,
|
||||||
|
comment="Unique identifier used in diagram node data",
|
||||||
|
)
|
||||||
|
label: Mapped[str] = mapped_column(
|
||||||
|
String(100), nullable=False,
|
||||||
|
comment="Display name",
|
||||||
|
)
|
||||||
|
category: Mapped[str] = mapped_column(
|
||||||
|
String(50), nullable=False,
|
||||||
|
comment="network, compute, storage, cloud, endpoint, infrastructure, security",
|
||||||
|
)
|
||||||
|
is_system: Mapped[bool] = mapped_column(
|
||||||
|
Boolean, nullable=False, default=False,
|
||||||
|
comment="True for built-in types that cannot be deleted",
|
||||||
|
)
|
||||||
|
team_id: Mapped[Optional[uuid.UUID]] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
ForeignKey("teams.id", ondelete="CASCADE"),
|
||||||
|
nullable=True,
|
||||||
|
comment="NULL for system types, set for team-custom types",
|
||||||
|
)
|
||||||
|
sort_order: Mapped[int] = mapped_column(
|
||||||
|
Integer, nullable=False, default=0,
|
||||||
|
comment="Display order within category",
|
||||||
|
)
|
||||||
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user