637 lines
24 KiB
Python
637 lines
24 KiB
Python
"""
|
|
Real Estate routes for the backend API.
|
|
Implements stateless endpoints for real estate database operations with AI-powered natural language processing.
|
|
"""
|
|
|
|
import logging
|
|
import json
|
|
from typing import Optional, Dict, Any, List, Union
|
|
from fastapi import APIRouter, HTTPException, Depends, Body, Request, Query, Path, status
|
|
|
|
# Import auth modules
|
|
from modules.security.auth import limiter, getCurrentUser
|
|
|
|
# Import models
|
|
from modules.datamodels.datamodelUam import User
|
|
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata
|
|
from modules.datamodels.datamodelRealEstate import (
|
|
Projekt,
|
|
Parzelle,
|
|
Dokument,
|
|
Gemeinde,
|
|
Kanton,
|
|
Land,
|
|
)
|
|
|
|
# Import interfaces
|
|
from modules.interfaces.interfaceDbRealEstateObjects import getInterface as getRealEstateInterface
|
|
|
|
# Import feature logic
|
|
from modules.features.realEstate.mainRealEstate import (
|
|
processNaturalLanguageCommand,
|
|
executeDirectQuery,
|
|
)
|
|
|
|
# Import attribute utilities for model schema
|
|
from modules.shared.attributeUtils import getModelAttributeDefinitions
|
|
|
|
# Configure logger
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Create router for real estate endpoints
|
|
router = APIRouter(
|
|
prefix="/api/realestate",
|
|
tags=["Real Estate"],
|
|
responses={
|
|
404: {"description": "Not found"},
|
|
400: {"description": "Bad request"},
|
|
401: {"description": "Unauthorized"},
|
|
403: {"description": "Forbidden"},
|
|
500: {"description": "Internal server error"}
|
|
}
|
|
)
|
|
|
|
|
|
@router.post("/command", response_model=Dict[str, Any])
|
|
@limiter.limit("120/minute")
|
|
async def process_command(
|
|
request: Request,
|
|
userInput: str = Body(..., embed=True, description="Natural language command"),
|
|
currentUser: User = Depends(getCurrentUser)
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Process natural language command and execute corresponding CRUD operation.
|
|
|
|
Uses AI to analyze user intent and extract parameters, then executes the appropriate
|
|
CRUD operation. Works stateless without session management.
|
|
|
|
Example user inputs:
|
|
- "Erstelle ein neues Projekt namens 'Hauptstrasse 42'"
|
|
- "Zeige mir alle Projekte in Zürich"
|
|
- "Aktualisiere Projekt XYZ mit Status 'Planung'"
|
|
- "Lösche Parzelle ABC"
|
|
- "SELECT * FROM Projekt WHERE plz = '8000'"
|
|
|
|
Headers:
|
|
- X-CSRF-Token: CSRF token (required for security)
|
|
|
|
Returns:
|
|
{
|
|
"success": true,
|
|
"intent": "CREATE|READ|UPDATE|DELETE|QUERY",
|
|
"entity": "Projekt|Parzelle|...|null",
|
|
"result": {...}
|
|
}
|
|
"""
|
|
try:
|
|
# Validate CSRF token (middleware also checks, but explicit validation for better error messages)
|
|
csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
|
|
if not csrf_token:
|
|
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="CSRF token missing. Please include X-CSRF-Token header."
|
|
)
|
|
|
|
# Basic CSRF token format validation
|
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
# Validate token is hex string
|
|
try:
|
|
int(csrf_token, 16)
|
|
except ValueError:
|
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
logger.info(f"Processing command request from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
|
logger.debug(f"User input: {userInput}")
|
|
|
|
# Process natural language command with AI
|
|
result = await processNaturalLanguageCommand(
|
|
currentUser=currentUser,
|
|
userInput=userInput
|
|
)
|
|
|
|
return result
|
|
|
|
except ValueError as e:
|
|
logger.error(f"Validation error in process_command: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Validation error: {str(e)}"
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error processing command: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Error processing command: {str(e)}"
|
|
)
|
|
|
|
@router.post("/query", response_model=Dict[str, Any])
|
|
@limiter.limit("120/minute")
|
|
async def execute_query(
|
|
request: Request,
|
|
body: Dict[str, Any] = Body(...),
|
|
currentUser: User = Depends(getCurrentUser)
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Execute a direct SQL query without session management.
|
|
|
|
Executes the query directly and returns the result. No query history is saved.
|
|
|
|
Request body:
|
|
{
|
|
"queryText": "SELECT * FROM Projekt WHERE plz = '8000'",
|
|
"parameters": { // Optional
|
|
"$1": "8000"
|
|
}
|
|
}
|
|
|
|
Headers:
|
|
- X-CSRF-Token: CSRF token (required for security)
|
|
|
|
WARNING: This endpoint executes raw SQL queries. Ensure proper validation
|
|
and sanitization on the frontend. Consider implementing query whitelisting
|
|
or only allowing SELECT statements for production use.
|
|
|
|
Returns:
|
|
{
|
|
"status": "success",
|
|
"rows": [...],
|
|
"columns": [...],
|
|
"rowCount": 15,
|
|
"executionTime": 0.123
|
|
}
|
|
"""
|
|
try:
|
|
# Validate CSRF token (middleware also checks, but explicit validation for better error messages)
|
|
csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
|
|
if not csrf_token:
|
|
logger.warning(f"CSRF token missing for POST /api/realestate/query from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="CSRF token missing. Please include X-CSRF-Token header."
|
|
)
|
|
|
|
# Basic CSRF token format validation
|
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/query from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
# Validate token is hex string
|
|
try:
|
|
int(csrf_token, 16)
|
|
except ValueError:
|
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/query from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
# Extract fields from body
|
|
queryText = body.get("queryText")
|
|
if not queryText:
|
|
raise ValueError("queryText is required")
|
|
|
|
parameters = body.get("parameters")
|
|
|
|
logger.info(f"Processing query request from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
|
logger.debug(f"Query text: {queryText}")
|
|
if parameters:
|
|
logger.debug(f"Query parameters: {parameters}")
|
|
|
|
# Execute direct query
|
|
result = await executeDirectQuery(
|
|
currentUser=currentUser,
|
|
queryText=queryText,
|
|
parameters=parameters,
|
|
)
|
|
|
|
return result
|
|
|
|
except ValueError as e:
|
|
logger.error(f"Validation error in execute_query: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Validation error: {str(e)}"
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error executing query: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Error executing query: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/tables", response_model=Dict[str, Any])
|
|
@limiter.limit("120/minute")
|
|
async def get_available_tables(
|
|
request: Request,
|
|
currentUser: User = Depends(getCurrentUser)
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Get all available real estate tables.
|
|
|
|
Returns a list of available table names with their descriptions.
|
|
|
|
Headers:
|
|
- X-CSRF-Token: CSRF token (required for security)
|
|
|
|
Example:
|
|
- GET /api/realestate/tables
|
|
"""
|
|
try:
|
|
# Validate CSRF token if provided
|
|
csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
|
|
if not csrf_token:
|
|
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="CSRF token missing. Please include X-CSRF-Token header."
|
|
)
|
|
|
|
# Basic CSRF token format validation
|
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
# Validate token is hex string
|
|
try:
|
|
int(csrf_token, 16)
|
|
except ValueError:
|
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
logger.info(f"Getting available tables for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
|
|
|
# Define available tables with descriptions
|
|
tables = [
|
|
{
|
|
"name": "Projekt",
|
|
"description": "Real estate projects",
|
|
"model": "Projekt"
|
|
},
|
|
{
|
|
"name": "Parzelle",
|
|
"description": "Plots/parcels",
|
|
"model": "Parzelle"
|
|
},
|
|
{
|
|
"name": "Dokument",
|
|
"description": "Documents",
|
|
"model": "Dokument"
|
|
},
|
|
{
|
|
"name": "Gemeinde",
|
|
"description": "Municipalities",
|
|
"model": "Gemeinde"
|
|
},
|
|
{
|
|
"name": "Kanton",
|
|
"description": "Cantons",
|
|
"model": "Kanton"
|
|
},
|
|
{
|
|
"name": "Land",
|
|
"description": "Countries",
|
|
"model": "Land"
|
|
},
|
|
]
|
|
|
|
return {
|
|
"tables": tables,
|
|
"count": len(tables)
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting available tables: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Error getting available tables: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/table/{table}", response_model=PaginatedResponse[Any])
|
|
@limiter.limit("120/minute")
|
|
async def get_table_data(
|
|
request: Request,
|
|
table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"),
|
|
pagination: Optional[str] = Query(None, description="JSON-encoded PaginationParams object"),
|
|
currentUser: User = Depends(getCurrentUser)
|
|
) -> PaginatedResponse[Dict[str, Any]]:
|
|
"""
|
|
Get all data from a specific real estate table with optional pagination.
|
|
|
|
Available tables:
|
|
- Projekt: Real estate projects
|
|
- Parzelle: Plots/parcels
|
|
- Dokument: Documents
|
|
- Gemeinde: Municipalities
|
|
- Kanton: Cantons
|
|
- Land: Countries
|
|
|
|
Query Parameters:
|
|
- pagination: JSON-encoded PaginationParams object, or None for no pagination
|
|
|
|
Headers:
|
|
- X-CSRF-Token: CSRF token (required for security)
|
|
|
|
Examples:
|
|
- GET /api/realestate/table/Projekt (no pagination - returns all items)
|
|
- GET /api/realestate/table/Parzelle?pagination={"page":1,"pageSize":10,"sort":[]}
|
|
- GET /api/realestate/table/Gemeinde?pagination={"page":2,"pageSize":20,"sort":[{"field":"label","direction":"asc"}]}
|
|
"""
|
|
try:
|
|
# Validate CSRF token if provided
|
|
csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
|
|
if not csrf_token:
|
|
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="CSRF token missing. Please include X-CSRF-Token header."
|
|
)
|
|
|
|
# Basic CSRF token format validation
|
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
|
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
# Validate token is hex string
|
|
try:
|
|
int(csrf_token, 16)
|
|
except ValueError:
|
|
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
logger.info(f"Getting table data for '{table}' from user {currentUser.id} (mandate: {currentUser.mandateId})")
|
|
|
|
# Map table names to model classes and getter methods
|
|
table_mapping = {
|
|
"Projekt": (Projekt, "getProjekte"),
|
|
"Parzelle": (Parzelle, "getParzellen"),
|
|
"Dokument": (Dokument, "getDokumente"),
|
|
"Gemeinde": (Gemeinde, "getGemeinden"),
|
|
"Kanton": (Kanton, "getKantone"),
|
|
"Land": (Land, "getLaender"),
|
|
}
|
|
|
|
# Validate table name
|
|
if table not in table_mapping:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid table name '{table}'. Available tables: {', '.join(table_mapping.keys())}"
|
|
)
|
|
|
|
# Get interface and fetch data
|
|
realEstateInterface = getRealEstateInterface(currentUser)
|
|
model_class, method_name = table_mapping[table]
|
|
getter_method = getattr(realEstateInterface, method_name)
|
|
|
|
# Fetch all records (no filter for now)
|
|
records = getter_method(recordFilter=None)
|
|
|
|
# Keep records as model instances (like routeDataFiles does with FileItem)
|
|
# FastAPI will automatically serialize Pydantic models to JSON
|
|
items = records
|
|
|
|
# If table is empty, create an empty instance with all fields set to None/empty
|
|
# This allows the frontend to extract column structure from the response
|
|
# All fields will be None/empty - no IDs or other values generated
|
|
if not items:
|
|
try:
|
|
# Get all model fields
|
|
model_fields = model_class.model_fields
|
|
empty_values = {}
|
|
|
|
# Set all fields to None - explicitly set every field to None
|
|
# This ensures no default_factory is called and no IDs are generated
|
|
for field_name in model_fields.keys():
|
|
empty_values[field_name] = None
|
|
|
|
# Create instance with all None values
|
|
# Use model_validate with allow_none=True or construct directly
|
|
empty_instance = model_class.model_construct(**empty_values)
|
|
items = [empty_instance]
|
|
logger.debug(f"Created empty instance for {table} with all fields set to None")
|
|
except Exception as e:
|
|
logger.warning(f"Could not create empty instance for {table}: {str(e)}. Returning empty list.")
|
|
items = []
|
|
|
|
# Parse pagination parameter
|
|
paginationParams = None
|
|
if pagination:
|
|
try:
|
|
paginationDict = json.loads(pagination)
|
|
paginationParams = PaginationParams(**paginationDict) if paginationDict else None
|
|
except (json.JSONDecodeError, ValueError) as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid pagination parameter: {str(e)}"
|
|
)
|
|
|
|
# Apply pagination if requested
|
|
if paginationParams:
|
|
# Apply sorting if specified
|
|
if paginationParams.sort:
|
|
for sort_field in reversed(paginationParams.sort): # Reverse to apply in priority order
|
|
field_name = sort_field.field
|
|
direction = sort_field.direction.lower()
|
|
|
|
def sort_key(item):
|
|
# Access attribute from model instance
|
|
value = getattr(item, field_name, None)
|
|
# Handle None values - put them at the end for asc, at the start for desc
|
|
if value is None:
|
|
return (1, None) # Use tuple to ensure None values sort consistently
|
|
return (0, value)
|
|
|
|
items.sort(key=sort_key, reverse=(direction == "desc"))
|
|
|
|
# Apply pagination
|
|
total_items = len(items)
|
|
total_pages = (total_items + paginationParams.pageSize - 1) // paginationParams.pageSize # Ceiling division
|
|
start_idx = (paginationParams.page - 1) * paginationParams.pageSize
|
|
end_idx = start_idx + paginationParams.pageSize
|
|
paginated_items = items[start_idx:end_idx]
|
|
|
|
return PaginatedResponse(
|
|
items=paginated_items,
|
|
pagination=PaginationMetadata(
|
|
currentPage=paginationParams.page,
|
|
pageSize=paginationParams.pageSize,
|
|
totalItems=total_items,
|
|
totalPages=total_pages,
|
|
sort=paginationParams.sort,
|
|
filters=paginationParams.filters
|
|
)
|
|
)
|
|
else:
|
|
# No pagination - return all items (as model instances, like routeDataFiles)
|
|
return PaginatedResponse(
|
|
items=items,
|
|
pagination=None
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error getting table data for '{table}': {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Error getting table data: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/table/{table}", response_model=Dict[str, Any])
|
|
@limiter.limit("120/minute")
|
|
async def create_table_record(
|
|
request: Request,
|
|
table: str = Path(..., description="Table name (Projekt, Parzelle, Dokument, Gemeinde, Kanton, Land)"),
|
|
data: Dict[str, Any] = Body(..., description="Record data to create"),
|
|
currentUser: User = Depends(getCurrentUser)
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Create a new record in a specific real estate table.
|
|
|
|
Available tables:
|
|
- Projekt: Real estate projects
|
|
- Parzelle: Plots/parcels
|
|
- Dokument: Documents
|
|
- Gemeinde: Municipalities
|
|
- Kanton: Cantons
|
|
- Land: Countries
|
|
|
|
Request Body:
|
|
- JSON object with fields matching the table's data model
|
|
|
|
Headers:
|
|
- X-CSRF-Token: CSRF token (required for security)
|
|
|
|
Examples:
|
|
- POST /api/realestate/table/Projekt
|
|
Body: {"label": "Hauptstrasse 42", "statusProzess": "Eingang"}
|
|
- POST /api/realestate/table/Parzelle
|
|
Body: {"label": "Parzelle 1", "strasseNr": "Hauptstrasse 42", "plz": "8000", "bauzone": "W3"}
|
|
"""
|
|
try:
|
|
# Validate CSRF token
|
|
csrf_token = request.headers.get("X-CSRF-Token") or request.headers.get("x-csrf-token")
|
|
if not csrf_token:
|
|
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="CSRF token missing. Please include X-CSRF-Token header."
|
|
)
|
|
|
|
# Basic CSRF token format validation
|
|
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
|
|
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
# Validate token is hex string
|
|
try:
|
|
int(csrf_token, 16)
|
|
except ValueError:
|
|
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {currentUser.id}")
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Invalid CSRF token format"
|
|
)
|
|
|
|
logger.info(f"Creating record in table '{table}' for user {currentUser.id} (mandate: {currentUser.mandateId})")
|
|
logger.debug(f"Record data: {data}")
|
|
|
|
# Map table names to model classes and create methods
|
|
table_mapping = {
|
|
"Projekt": (Projekt, "createProjekt"),
|
|
"Parzelle": (Parzelle, "createParzelle"),
|
|
"Dokument": (Dokument, "createDokument"),
|
|
"Gemeinde": (Gemeinde, "createGemeinde"),
|
|
"Kanton": (Kanton, "createKanton"),
|
|
"Land": (Land, "createLand"),
|
|
}
|
|
|
|
# Validate table name
|
|
if table not in table_mapping:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid table name '{table}'. Available tables: {', '.join(table_mapping.keys())}"
|
|
)
|
|
|
|
# Get interface
|
|
realEstateInterface = getRealEstateInterface(currentUser)
|
|
model_class, method_name = table_mapping[table]
|
|
create_method = getattr(realEstateInterface, method_name)
|
|
|
|
# Ensure mandateId is set (will be set by interface if missing)
|
|
if "mandateId" not in data:
|
|
data["mandateId"] = currentUser.mandateId
|
|
|
|
# Create model instance from data
|
|
try:
|
|
model_instance = model_class(**data)
|
|
except Exception as e:
|
|
logger.error(f"Error creating {table} model instance: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail=f"Invalid data for {table}: {str(e)}"
|
|
)
|
|
|
|
# Create record
|
|
try:
|
|
created_record = create_method(model_instance)
|
|
|
|
# Convert to dictionary for response
|
|
if hasattr(created_record, 'model_dump'):
|
|
return created_record.model_dump()
|
|
else:
|
|
return created_record
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating {table} record: {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Error creating {table} record: {str(e)}"
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error creating record in table '{table}': {str(e)}", exc_info=True)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Error creating record: {str(e)}"
|
|
)
|
|
|