Skip to content

0045. Paginación server-side opcional con meta global en el envelope

Estado

Aceptada.

Contexto

Casi todos los GET de listado del backend devolvían el array completo sin skip/take ni total (/cycles, /sectors, /pools, /roles, /users, /feeding-curves, /cycles/:id/feedings, /cycles/:id/samplings, /cycles/:id/populations, /cycles/:id/harvests, /pools/:id/events, /pools/:id/water-params, /platform/organizations, /platform/users). Funcionaba porque el volumen por ciclo/piscina es acotado (~120-150 filas), pero listados sin ese techo natural (/users, /pools, /platform/organizations a medida que crecen los tenants) no escalan indefinidamente. camaroneras_mobile (offline-first) consume varios de estos mismos endpoints esperando el array completo (data as List sin enviar query params) — cualquier diseño tenía que ser retrocompatible sin tocar el repo mobile.

Decisión

  • page/pageSize son opcionales (PaginationQueryDto, src/common/dto/). Si ninguno viene, el endpoint devuelve el listado completo — comportamiento idéntico al actual, sin meta. Así camaroneras_mobile sigue funcionando sin cambios de código.
  • Cuando se envían, la respuesta agrega meta: { total, page, pageSize } como campo hermano de data (no reemplaza data por un objeto {items, total}) — evita romper a los consumidores que hacen data as List en el resto de los casos.
  • paginationArgs/paginationMeta (src/common/utils/pagination.ts) centralizan el cálculo de skip/take y del objeto meta; cada service hace Promise.all([findMany({...args}), args ? count() : undefined]).
  • Excepción: alimentacion (feedings), muestreos (samplings) y raleos (harvests) tienen campos derivados que dependen del historial completo ordenado del ciclo (acumuladoKg, incremento/promIncremento con ventana de 28 días, totalRaleoLb). Paginar la query de Prisma ahí corrompería esos cálculos, así que estos tres paginan en memoria: calculan los derivados sobre el dataset completo y recién después slicean el array de items para la página pedida. El summary de cada uno sigue agregando todas las filas del ciclo, no solo la página actual.
  • wrapResponsesInEnvelope (src/common/swagger/build-document.ts, ver ADR-0036) agrega meta como propiedad opcional (nullable: true) al envelope de toda respuesta 2xx, no solo las de listado — es fiel al contrato real (StandardResponse<T> en response.interceptor.ts ya declara meta?: PaginationMeta para cualquier respuesta), y evita mantener dos formas de envelope en el spec.

Consecuencias

  • camaroneras_mobile no requirió ningún cambio: no envía page/pageSize, así que sigue recibiendo el array completo en los endpoints que ya consumía.
  • La API Reference (/api/) documenta page/pageSize como query params opcionales y meta como parte del envelope automáticamente (spec regenerado vía npm run openapi:export + sync:openapi) — los *-flow.md de los módulos tocados no necesitaron edición manual (siguen el patrón de ADR-0036: bodies y responses 2xx viven en el spec, no a mano).
  • El admin y el panel de plataforma (camaroneras_platform) quedan pendientes de migrar sus tablas a paginación server-side consumiendo este meta (ver _planning/paginacion-server-side-plan.md); mientras tanto pueden seguir usando los endpoints sin params (comportamiento sin cambios).