---
url: /contracts/API_AND_SCHEMA_DISCIPLINE.md
---
# Disciplina de Diseño de APIs y Esquemas SQL (`API_AND_SCHEMA_DISCIPLINE.md`)

Estado: **Reglas de diseño normativas y obligatorias para ninaku-app-api**.

Este documento complementa a [ARCHITECTURE.md](../reference/ARCHITECTURE.md). Su propósito es prevenir los dos fallos de diseño más comunes en el crecimiento de la plataforma: crear APIs que reflejen pasivamente tablas de la base de datos sin expresar intención de negocio, y expandir el esquema PostgreSQL con tablas especulativas que carecen de un módulo propietario o de un propósito operacional real.

***

## 1. APIs por Intención de Negocio (Prohibición del CRUD Genérico por Tabla)

Ninaku **no** expone endpoints HTTP por el simple hecho de que exista una tabla SQL en la base de datos. Cada endpoint publicado debe representar de forma explícita:

* Una acción de negocio concreta.
* Una consulta requerida por un operador, pantalla o integración autorizada.
* Una transición explícita dentro del ciclo de vida de un agregado.
* Un contrato público estable entre Ninaku y un consumidor externo.

La pregunta por defecto **nunca** es *"¿qué endpoints CRUD necesita esta tabla?"*. Las preguntas obligatorias son:

1. ¿Qué tarea intenta completar el usuario o sistema?
2. ¿Qué módulo de negocio es el propietario inmutable de esa operación?
3. ¿Qué invariantes de dominio se deben proteger?
4. ¿Cuál es el contrato público mínimo y estable que expresa ese caso de uso?

### Ejemplos Concretos

```text
❌ DISEÑO RECHAZADO (CRUD orientado a tablas):
POST   /sale-lines
PATCH  /sale-lines/{id}
POST   /payment-attempts
POST   /inventory-movements
POST   /journal-entries

✅ DISEÑO OBLIGATORIO (Orientado a intención y flujos de negocio):
POST   /api/v1/sales/orders
POST   /api/v1/sales/orders/{id}/confirm
POST   /api/v1/sales/orders/{id}/cancel

POST   /api/v1/inventory/transfers
POST   /api/v1/inventory/transfers/{id}/dispatch
POST   /api/v1/inventory/transfers/{id}/receive

POST   /api/v1/payments/intents
POST   /api/v1/payments/{id}/capture
POST   /api/v1/payments/{id}/refund
```

El backend coordina internamente las mutaciones en las tablas y módulos correspondientes dentro de una transacción. El cliente jamás necesita conocer el diseño físico de la base de datos para ejecutar una acción comercial.

***

## 2. APIs Diseñadas Alrededor de Flujos y Pantallas (Presupuesto de Llamadas)

Las APIs operacionales deben optimizar los flujos de pantalla del usuario sin convertirse en "mega-APIs" descontroladas.

### A. Presupuesto de Llamadas HTTP

**Regla de Oro**: Para la carga inicial normal de cualquier pantalla con sesión válida, la aplicación cliente tiene un **presupuesto de como máximo dos (2) solicitudes de datos**:

1. **Contexto de Sesión**: `GET /api/v1/session/context` (identidad, selección de tenant/outlet, permisos y capacidades).
2. **Workspace de Pantalla**: `GET /api/v1/<modulo>/workspace` (resumen, configuración visible y primera página limitada).

El contenido pesado, historiales o búsquedas avanzadas se cargan síncronamente bajo demanda.

### B. Criterios de Optimización en Pantalla

No se optimiza únicamente para reducir peticiones HTTP; se deben equilibrar todos los factores del sistema:

* **Cantidad de consultas SQL**: Evitar patrón N+1 mediante proyecciones de lectura unificadas o agrupamiento explícito.
* **Tamaño del payload**: Enviar únicamente las propiedades requeridas para la tarea.
* **Paginación estable**: Keyset por cursores opacos.
* **Uso eficiente de índices**: Consultas alineadas con los patrones de acceso reales.

***

## 3. Toda Tabla Debe Justificar su Existencia (Las 7 Preguntas)

Cada tabla canónica en el esquema PostgreSQL debe responder obligatoriamente a las siguientes **siete preguntas de justificación** antes de ser creada o modificada:

1. **Owner (Propietario)**: ¿Qué módulo de negocio es el dueño exclusivo de esta tabla?
2. **Business Fact (Hecho de negocio)**: ¿Qué hecho inmutable o estado del ciclo de vida representa?
3. **Writer (Escritor)**: ¿Qué caso de uso o función específica tiene permitido insertar o mutar filas aquí?
4. **Reader (Lector)**: ¿Qué caso de uso, reporte, proyección o integración la consume?
5. **Invariant (Invariante)**: ¿Qué regla de negocio, unicidad, historial o auditoría exige que los datos residan en esta estructura?
6. **Lifecycle (Ciclo de vida)**: ¿Cómo se crea, cambia, expira, archiva o conserva la fila?
7. **Why Separate (Por qué separada)**: ¿Por qué este concepto no puede residir de forma segura dentro de un agregado o tabla existente?

Si una propuesta de tabla no puede responder a estas siete preguntas de manera contundente, debe ser rediseñada, consolidada con otra estructura o rechazada.

***

## 4. Optimización RLS en PostgreSQL: Subconsultas Escalares

Para garantizar el cumplimiento de los SLA de rendimiento bajo aislamiento multi-tenant en PostgreSQL 18, todas las políticas de Row Level Security (RLS) deben aplicar el patrón de **subconsulta escalar**.

### A. La Regla de la Subconsulta Escalar

Dentro de cualquier cláusula `CREATE POLICY` (`USING` o `WITH CHECK`), toda llamada a funciones de contexto tenant de la extensión de identidad (`identity.has_active_tenant_context()`, `identity.current_organization_id()`, `identity.current_identity_id()`) **debe escribirse obligatoriamente envuelta entre paréntesis como una subconsulta escalar `(SELECT ...)`**:

```sql
-- ❌ FORMA LENTA RECHAZADA (Evaluación por fila):
CREATE POLICY organization_isolation_policy ON sales.orders
    FOR ALL TO ninaku_runtime
    USING (
        identity.has_active_tenant_context()
        AND organization_id = identity.current_organization_id()
    );

-- ✅ FORMA OPTIMIZADA OBLIGATORIA (Subconsulta escalar / InitPlan):
CREATE POLICY organization_isolation_policy ON sales.orders
    FOR ALL TO ninaku_runtime
    USING (
        (SELECT identity.has_active_tenant_context())
        AND organization_id = (SELECT identity.current_organization_id())
    );
```

### B. Fundamento Técnico y Evidencia Medida

1. **Por qué la función desnuda es lenta**: Las funciones de contexto están definidas como `LANGUAGE sql STABLE SECURITY DEFINER` con cláusulas `SET`. Debido a las cláusulas `SET` de seguridad, el planificador de PostgreSQL **no puede inlinear la función**. Si se escribe desnuda, PostgreSQL evalúa la función completa (incluyendo sus joins internos contra tablas de identidad y membresías) **una vez por cada fila escaneada en la consulta**.
2. **Por qué la subconsulta escalar es rápida**: Al envolver la llamada entre paréntesis como `(SELECT identity.current_organization_id())`, el planificador de PostgreSQL la trata como un **`InitPlan`**. La función se ejecuta una **sola vez por sentencia HTTP/SQL**, y su resultado escalar constante se reutiliza para filtrar todas las filas.
3. **Evidencia Medida en Benchmarks (`PERFORMANCE_BASELINE.md`)**:
   * En tablas de prueba con 1,000 filas por organización:
     * **Forma desnuda (S3)**: Latencia P95 = **18.91 ms** | Buffers accedidos = **4,022**.
     * **Subconsulta escalar (S4)**: Latencia P95 = **1.29 ms** | Buffers accedidos = **20**.
   * **Factor de Aceleración**: **14.7x más rápido**, reduciendo el acceso al buffer en un 99.5%.

### C. Alcance Exacto de la Regla

* **Aplica ÚNICAMENTE**: Dentro de las cláusulas `USING` y `WITH CHECK` de las sentencias `CREATE POLICY`.
* **NO Aplica**: Dentro de cuerpos de funciones SQL/PLpgSQL, triggers ni restricciones `CHECK` de tabla. En esas estructuras, el código ya ejecuta por fila por naturaleza, y envolverlo agregaría ruido sin alterar el plan de ejecución.

### D. Verificación Automatizada en CI

El script de auditoría de base de datos **`npm run db:sweep`** (que ejecuta `check-policy-context-subqueries.ts`) analiza estáticamente todos los archivos `.sql` del repositorio. Si detecta una llamada desnuda a las funciones de contexto dentro de una política RLS, el comando falla de inmediato. Imprime `RLS_POLICY_CONTEXT_SUBQUERY_OK` cuando el baseline está limpio.

***

## 5. Aislamiento Multi-Tenant mediante GUCs de PostgreSQL

El aislamiento de tenants no se confía al código de la aplicación NestJS ni a filtros `WHERE` manuales. Se exige a nivel de base de datos mediante variables de configuración de sesión de PostgreSQL (GUCs - Grand Unified Configuration) y políticas RLS.

### A. Establecimiento de Contexto con `set_config`

La capa de infraestructura (`foundation/tenancy/tenant-context-runner.service.ts`) establece las variables de sesión dentro de la transacción de PostgreSQL usando la función parametrizada `set_config`:

```ts
// Se aplica usando set_config con is_local = true (transacción local)
await client.query(`
  SELECT 
    set_config('app.actor_identity_id', $1, true),
    set_config('app.identity_id', $2, true),
    set_config('app.membership_id', $3, true),
    set_config('app.organization_id', $4, true),
    set_config('app.impersonation_session_id', $5, true);
`, [actorId, identityId, membershipId, organizationId, impersonationId]);
```

*Se utiliza `set_config($1, $2, true)` en lugar de concatenar cadenas `SET LOCAL` para prevenir inyecciones SQL y garantizar que el contexto se limpie automáticamente al hacer `COMMIT` o `ROLLBACK`.*

### B. Afirmación del Contexto con `identity.assert_tenant_context()`

Inmediatamente después de aplicar los GUCs, la transacción ejecuta:

```sql
SELECT identity.assert_tenant_context();
```

Si el contexto de organización no es válido, la membresía está suspendida/terminada o el tenant no existe, PostgreSQL lanza un error con SQLSTATE `42501`. La aplicación captura esta excepción y la remapea a un error `403 Forbidden` (`tenant.context_rejected`).

***

## 6. Invariantes de Esquema SQL y Prohibición de Diseño Especulativo

1. **Esquema Propietario Explícito**: Todo archivo `.sql` debe declarar explícitamente el esquema al que pertenecen sus tablas (ej. `sales.orders`, `inventory.stock_ledger`). Prohibido crear tablas en el esquema `public`.
2. **Auditoría Inmutable**: Las tablas de eventos, libros contables, transacciones y balances incluyen marcas de tiempo inmutables `created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()`. Queda prohibido actualizar o eliminar registros de auditoría o historial.
3. **Claves Foráneas y Reglas de Borrado**:
   * Las relaciones entre entidades principales del dominio exigen restricciones `FOREIGN KEY`.
   * Para datos maestros o de configuración, se utiliza `ON DELETE RESTRICT`. Prohibido usar `ON DELETE CASCADE` en tablas transaccionales de negocio para evitar eliminaciones accidentales en cascada.
4. **Prohibición Estricta de Esquema Especulativo**: Prohibido agregar tablas o columnas "por si acaso las necesitamos en el futuro". Cada tabla y columna debe estar conectada a un caso de uso real en desarrollo.

***

## 7. Declaración del Contrato de Respuesta en OpenAPI

Dado que el interceptor global `src/foundation/http/envelope/http-envelope.interceptor.ts` envuelve automáticamente todas las respuestas JSON en la estructura `{ data }` o `{ data, meta }`, los controladores deben documentar sus respuestas mediante los decoradores de sobre de Foundation.

### Decoradores Obligatorios (`src/foundation/openapi/api-envelope-response.decorator.ts`)

```ts
import { ApiDataResponse, ApiCollectionResponse } from '@/foundation/openapi/api-envelope-response.decorator';

@Controller('products')
export class ProductsController {

  @Get(':id')
  @ApiDataResponse({ name: 'Product', schema: ProductSchema })
  async findOne(): Promise<ProductResponse> { ... }

  @Get()
  @ApiCollectionResponse({ name: 'Product', schema: ProductSchema })
  async list(): Promise<HttpResponseWithMeta<ProductResponse[], { pageInfo: PageInfo }>> { ... }

  @Post()
  @ApiDataResponse({ name: 'Product', schema: ProductSchema, status: HttpStatus.CREATED })
  async create(): Promise<ProductResponse> { ... }
}
```

* **`ApiDataResponse`**: Declara un recurso único o resultado de comando envuelto en `{ data: T }`.
* **`ApiCollectionResponse`**: Declara una colección paginada envuelta en `{ data: T[], meta: { pageInfo: PageInfo } }`.

### Verificación de OpenAPI en CI/CD

El documento `docs/generated/openapi.json` se deriva automáticamente de los esquemas Zod a través del conversor `convertZodSchemaToOpenApiSchema`.

* **`npm run openapi:generate`**: Compila el proyecto y regenera la especificación OpenAPI.
* **`npm run openapi:check`**: Compara byte a byte la especificación regenerada contra `docs/generated/openapi.json` en CI. Si existe cualquier discrepancia, la build falla de inmediato.
