from typing import Any, Dict, List, Optional from fastapi import APIRouter, Request from core.sap import forward_to_sapb1 def _build_sap_params( skip: int, top: int, fields: Optional[str], filter: Optional[str], orderby: Optional[str] = None, ) -> Dict[str, Any]: sap_params: Dict[str, Any] = { "$skip": skip, "$top": top, "$inlinecount": "allpages", } if fields: sap_params["$select"] = fields.replace(" ", "") if filter: sap_params["$filter"] = filter if orderby: sap_params["$orderby"] = orderby return sap_params def _build_list_headers(top: int, filter: Optional[str]) -> Dict[str, str]: headers = {"B1S-PageSize": str(top)} if filter: headers["B1S-CaseInsensitive"] = "true" return headers def make_crud_router( resource: str, *, tags: List[str], key_field: Optional[str] = None, key_quoted: bool = False, supports_orderby: bool = False, supports_get_by_key: bool = True, supports_create: bool = True, supports_patch: bool = True, patch_headers: Optional[Dict[str, str]] = None, ) -> APIRouter: """ Genera el router list/get/create/update repetido antes en cada módulo de routers/ (mismo bloque de $skip/$top/$filter y de forward_to_sapb1, solo cambiaba el nombre del recurso OData). Centralizarlo evita que un copy-paste entre recursos deje, por ejemplo, un PATCH apuntando al endpoint_path de otro recurso. """ router = APIRouter(tags=tags) def key_expr(key: str) -> str: return f"{resource}('{key}')" if key_quoted else f"{resource}({key})" if supports_orderby: @router.get(f"/api/{resource}", name=f"list_{resource}") async def list_resource( skip: int = 0, top: int = 50, fields: Optional[str] = None, filter: Optional[str] = None, orderby: Optional[str] = None, ): sap_params = _build_sap_params(skip, top, fields, filter, orderby) return forward_to_sapb1( method="GET", endpoint_path=resource, params=sap_params, extra_headers=_build_list_headers(top, filter), ).json() else: @router.get(f"/api/{resource}", name=f"list_{resource}") async def list_resource( skip: int = 0, top: int = 50, fields: Optional[str] = None, filter: Optional[str] = None, ): sap_params = _build_sap_params(skip, top, fields, filter) return forward_to_sapb1( method="GET", endpoint_path=resource, params=sap_params, extra_headers=_build_list_headers(top, filter), ).json() if supports_get_by_key: @router.get(f"/api/{resource}/{{key}}", name=f"get_{resource}_by_key") async def get_by_key(key: str): return forward_to_sapb1( method="GET", endpoint_path=key_expr(key), ).json() if supports_create: @router.post(f"/api/{resource}", status_code=201, name=f"create_{resource}") async def create_resource(request: Request): data = await request.json() resp = forward_to_sapb1( method="POST", endpoint_path=resource, json_data=data, ) created = resp.json() return {key_field: created[key_field]} if supports_patch: @router.patch(f"/api/{resource}/{{key}}", status_code=204, name=f"update_{resource}") async def update_resource(key: str, request: Request): data = await request.json() # Las cabeceras de Service Layer (B1S-*) las decide quien llama # (el frontend), no el backend: cualquier header "B1S-*" que # venga en el request se reenvía tal cual a SAP. Ej.: # B1S-ReplaceCollectionsOnPatch para reemplazar una colección # hija (StockTransferLines) en vez de mergearla por LineNum, # necesario solo cuando ese PATCH agrega o quita líneas. headers = dict(patch_headers or {}) headers.update( {k: v for k, v in request.headers.items() if k.lower().startswith("b1s-")} ) forward_to_sapb1( method="PATCH", endpoint_path=key_expr(key), json_data=data, extra_headers=headers or None, ) return router