# Guía de desarrollo — Nube Backend DaaS

Convenciones y patrones específicos de este proyecto. Para la base genérica del core (routers, servicios, modelos, CRUD, excepciones) ver [CORE_STRUCTURE.md](CORE_STRUCTURE.md).

---

## Autenticación en rutas

Este proyecto no gestiona usuarios propios. El token Bearer lo emite el sistema padre (vCloud) y solo se valida aquí.

### Ruta accesible por cualquier usuario autenticado

```python
from fastapi import APIRouter, Depends
from app.dependencies.auth import get_current_user

router = APIRouter(prefix="/recursos", tags=["Recursos"])

@router.get("/")
def listar(user=Depends(get_current_user)):
    # user contiene: sub (username), iss (org@tenant), exp, jti
    return {}
```

### Ruta restringida a operaciones internas

Requiere el header `X-Internal-Key` además del Bearer token. Usar para inserts, updates, deletes y cualquier operación de escritura.

```python
from app.dependencies.auth import get_current_user, require_internal

@router.post("/")
def crear(
    body: MiSchema,
    user=Depends(get_current_user),
    _=Depends(require_internal),
):
    ...
```

El cliente debe enviar:
```http
Authorization: Bearer <token_vcloud>
X-Internal-Key: <INTERNAL_API_KEY del .env>
```

---

## Capa gRPC — Cliente DaaS

Este proyecto consume un servicio gRPC externo (C#/.NET) a través de `CloudServicesCrudClient`. El cliente **no se instancia en el router** — vive en el servicio correspondiente.

### Patrón de uso

```python
# app/services/MiRecursoService.py
from app.grpc.clients.CloudServicesCrudClient import CloudServicesCrudClient

class MiRecursoService:

    @staticmethod
    def listar():
        client = CloudServicesCrudClient()
        return client.get_all("NombreDelModelo", columns=["id", "nombre"])

    @staticmethod
    def obtener(id: str):
        client = CloudServicesCrudClient()
        return client.get_by_id("NombreDelModelo", id=id)
```

> El nombre del modelo (`"NombreDelModelo"`) corresponde al nombre de la entidad en el servidor gRPC externo — consultar con el equipo C# qué nombre usar para cada tabla.

### Métodos disponibles

| Método | Descripción |
|---|---|
| `get_all(model, columns, limit, order_by)` | Todos los registros, con columnas y orden opcionales |
| `get_by_id(model, id, columns)` | Un registro por ID |
| `create(model, data)` | Inserta un registro |
| `update(model, id, data)` | Actualiza un registro por ID |
| `delete(model, id)` | Elimina un registro por ID |
| `search(model, and_filters, or_filters, columns, search_term, page, page_size, order_by)` | Búsqueda con filtros, paginación y texto libre |
| `count(model, and_filters, or_filters)` | Total de registros que cumplen los filtros |
| `bulk_create(model, records)` | Inserta múltiples registros |
| `upsert(model, data, conflict_fields)` | Insert o update según campos de conflicto |
| `bulk_update(model, data, and_filters, or_filters)` | Actualiza múltiples registros por filtro |
| `bulk_delete(model, and_filters, or_filters)` | Elimina múltiples registros por filtro |
| `execute_batch(operations)` | Ejecuta múltiples operaciones mixtas en una sola llamada |
| `get_fields(model)` | Retorna los campos y tipos de un modelo |

### Filtros en `search()`, `count()`, `bulk_update()` y `bulk_delete()`

Los filtros se pasan como lista de dicts con `field`, `op` y `value`:

```python
client.search(
    model="Usuario",
    and_filters=[
        {"field": "Activo",   "op": "eq",  "value": "true"},
        {"field": "TenantId", "op": "eq",  "value": "abc-123"},
    ],
    or_filters=[
        {"field": "Rol", "op": "eq", "value": "admin"},
        {"field": "Rol", "op": "eq", "value": "staff"},
    ],
    columns=["Id", "Nombre", "Email"],
    page=1,
    page_size=20,
    order_by=[{"field": "Nombre", "direction": "asc"}],
)
```

Operadores disponibles: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `like`, `in`, `between`, `isnull`, `isnotnull`.

Para `between` se usa `value` y `value2`:
```python
{"field": "FechaCreacion", "op": "between", "value": "2024-01-01", "value2": "2024-12-31"}
```

### Operaciones batch

`execute_batch` permite mezclar creates, updates, deletes y upserts en una sola llamada gRPC:

```python
client.execute_batch([
    {"type": "create", "model": "Log",    "data": {"Mensaje": "inicio"}},
    {"type": "update", "model": "Usuario","id": "42", "data": {"Activo": False}},
    {"type": "delete", "model": "Sesion", "id": "99"},
])
```

Tipos válidos: `create`, `update`, `delete`, `upsert`.

---

## Agregar un segundo cliente gRPC

Cuando se integre otro servidor gRPC, el patrón es:

**1. Agregar el `.proto`** en `proto/` y compilar:
```bash
bash compile_proto.sh
```

**2. Crear el cliente** en `app/grpc/clients/`:
```python
# app/grpc/clients/OtroServicioClient.py
import grpc
from app.grpc.generated import OtroServicio_pb2_grpc as pb2_grpc
from app.core.config import Settings

settings = Settings()

class OtroServicioClient:
    def __init__(self):
        with open(settings.GRPC_OTRO_SERVICIO_DAO_CERT, "rb") as f:
            credentials = grpc.ssl_channel_credentials(root_certificates=f.read())
        self.channel = grpc.secure_channel(settings.GRPC_OTRO_SERVICIO_DAO_HOST, credentials)
        self.stub = pb2_grpc.OtroServicioStub(self.channel)
```

**3. Agregar las variables** en `app/core/config.py` bajo `# gRPC DAOs`:
```python
GRPC_OTRO_SERVICIO_DAO_HOST: str = os.getenv("GRPC_OTRO_SERVICIO_DAO_HOST")
GRPC_OTRO_SERVICIO_DAO_CERT: str = os.getenv("GRPC_OTRO_SERVICIO_DAO_CERT", "certs/otro_servicio.crt")
```

**4. Agregar al `.env`** y al `.env.example`:
```env
GRPC_OTRO_SERVICIO_DAO_HOST=servidor:50051
GRPC_OTRO_SERVICIO_DAO_CERT=certs/otro_servicio.crt
```
