Files
mini-erp/mini-backend/core/crud_router.py
T

133 lines
4.5 KiB
Python
Raw Normal View History

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