162 lines
5.8 KiB
Python
162 lines
5.8 KiB
Python
"""API routes for CustomFieldDefinition CRUD — generic across entities.
|
|
|
|
Paket 4 (#357): applies the W4b pattern — entity validation (422) and
|
|
owner-module read check (403) via ENTITY_PLUGIN_OWNERS, central helpers
|
|
from entity_permission_service, and the {items, total} list shape all
|
|
frontend consumers read.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.db import get_db
|
|
from app.deps import get_current_user, require_permission
|
|
from app.schemas.custom_field_definition import (
|
|
CustomFieldDefinitionCreate,
|
|
CustomFieldDefinitionListResponse,
|
|
CustomFieldDefinitionResponse,
|
|
CustomFieldDefinitionUpdate,
|
|
)
|
|
from app.services import custom_field_service
|
|
from app.services.entity_permission_service import (
|
|
check_entity_read_permission,
|
|
validate_entity_type,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/v1/custom-fields", tags=["custom-fields-definitions"])
|
|
|
|
|
|
@router.get(
|
|
"/definitions",
|
|
response_model=CustomFieldDefinitionListResponse,
|
|
dependencies=[Depends(require_permission("custom_fields:read"))],
|
|
)
|
|
async def list_definitions(
|
|
entity: str | None = Query(None, description="Filter by entity type (e.g. 'contact', 'company')"),
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""List all active custom field definitions for the current tenant.
|
|
|
|
Returns ``{items, total}`` — the shape the frontend API client and
|
|
all six consumers (CustomFields page, ContactsList, SortPanel,
|
|
GroupPanel, FilterPanel) read.
|
|
"""
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
if entity:
|
|
validate_entity_type(entity)
|
|
check_entity_read_permission(current_user, entity)
|
|
|
|
definitions = await custom_field_service.list_definitions(
|
|
db,
|
|
tenant_id,
|
|
entity=entity,
|
|
user_id=uuid.UUID(current_user["user_id"]),
|
|
is_system_admin=current_user.get("is_system_admin", False),
|
|
)
|
|
return CustomFieldDefinitionListResponse(
|
|
items=[CustomFieldDefinitionResponse.model_validate(d) for d in definitions],
|
|
total=len(definitions),
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/definitions",
|
|
response_model=CustomFieldDefinitionResponse,
|
|
status_code=201,
|
|
dependencies=[Depends(require_permission("custom_fields:write"))],
|
|
)
|
|
async def create_definition(
|
|
body: CustomFieldDefinitionCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new custom field definition.
|
|
|
|
W4b: the entity must be a registered entity type (422) and the user
|
|
needs the owning module's read permission (e.g. contacts:read) on top
|
|
of custom_fields:write — writing definitions for modules you cannot
|
|
even read would bypass module isolation.
|
|
"""
|
|
validate_entity_type(body.entity)
|
|
check_entity_read_permission(current_user, body.entity)
|
|
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
definition = await custom_field_service.create_definition(
|
|
db, tenant_id, user_id, body.model_dump()
|
|
)
|
|
return definition
|
|
|
|
|
|
@router.patch(
|
|
"/definitions/{definition_id}",
|
|
response_model=CustomFieldDefinitionResponse,
|
|
dependencies=[Depends(require_permission("custom_fields:write"))],
|
|
)
|
|
async def update_definition(
|
|
definition_id: str,
|
|
body: CustomFieldDefinitionUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update an existing custom field definition."""
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
try:
|
|
def_id = uuid.UUID(definition_id)
|
|
except (ValueError, TypeError):
|
|
raise HTTPException(400, detail={"detail": "Invalid definition_id", "code": "invalid_id"}) from None
|
|
|
|
# Filter out None values from the update body
|
|
update_data = {k: v for k, v in body.model_dump().items() if v is not None}
|
|
if not update_data:
|
|
raise HTTPException(400, detail={"detail": "No fields to update", "code": "no_updates"})
|
|
|
|
try:
|
|
definition = await custom_field_service.update_definition(
|
|
db, tenant_id, def_id, update_data, user_id=user_id, is_system_admin=is_admin
|
|
)
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail={"detail": str(e), "code": "forbidden"}) from e
|
|
if definition is None:
|
|
raise HTTPException(404, detail={"detail": "Definition not found", "code": "not_found"})
|
|
# W4b: changing a definition still requires read access to its entity module
|
|
check_entity_read_permission(current_user, definition.entity)
|
|
return definition
|
|
|
|
|
|
@router.delete(
|
|
"/definitions/{definition_id}",
|
|
status_code=204,
|
|
dependencies=[Depends(require_permission("custom_fields:write"))],
|
|
)
|
|
async def delete_definition(
|
|
definition_id: str,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete a custom field definition."""
|
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
|
user_id = uuid.UUID(current_user["user_id"])
|
|
is_admin = current_user.get("is_system_admin", False)
|
|
try:
|
|
def_id = uuid.UUID(definition_id)
|
|
except (ValueError, TypeError):
|
|
raise HTTPException(400, detail={"detail": "Invalid definition_id", "code": "invalid_id"}) from None
|
|
|
|
try:
|
|
deleted = await custom_field_service.delete_definition(
|
|
db, tenant_id, def_id, user_id=user_id, is_system_admin=is_admin
|
|
)
|
|
except PermissionError as e:
|
|
raise HTTPException(status_code=403, detail={"detail": str(e), "code": "forbidden"}) from e
|
|
if not deleted:
|
|
raise HTTPException(404, detail={"detail": "Definition not found", "code": "not_found"})
|
|
return None
|