mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 21:43:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
Migrates the `core` schema business logic from inline `db.prepare()` calls in the API to PostgreSQL RPC functions (`core.fn_*`) with a unified JSON envelope for errors and HTTP status mapping.
### Database (Liquibase changesets 006–017)
- **006** — RPC infra: `rpc_ok`, `rpc_err`, `rpc_created`, `rpc_from_exception`
- **007** — Catalogs and tenant settings
- **008** — Companies CRUD
- **009** — Projects, checklists, document lists
- **010** — Workers CRUD, pipeline, checklist, assign
- **011** — Budget CRUD + `fn_budget_replace`
- **012** — Badge jobs
- **013** — Payroll (settings, attendance, loans, weeks, destajo)
- **014** — Worker import batch + document store
- **015** — Project/company/worker document metadata RPCs
- **016–017** — Fixes: `needs_badge` default on worker create; Liquibase `splitStatements:false` on function changesets
### API
- `api/rpc.ts` — `callCoreFn()`, `RpcCallError` (jsonb payload fix: pass JS object, not `JSON.stringify`)
- `api/http_errors.ts` — `mapRpcToStatus()`, `respondRpc()`, `respondApiError()`, `onAppError()`
- Refactored: `main.ts`, `companies.ts`, `db.ts`, `budget.ts`, `excel.ts`, `payroll.ts`, `payroll_http.ts`
- Front helpers: `web-panel/composables/api-response.ts`, `web-saas/composables/api-response.ts`
### Envelope contract
DB functions return `{ ok, code, layer: "db", message, context, data, errors }`. The API adds `status` (HTTP code) via `respondRpc()` / `respondApiError()`.
### Out of scope
`iam`, `platform`, `saas.ts`, auth/sessions, S3, PDF generation, Excel parsing, and bootstrap scripts still use direct SQL where appropriate.
## Test plan
- [x] `deno check main.ts` — compila sin errores de tipos
- [x] `npm run build` — web-panel y web-saas compilan
- [x] `deno test` — 25 tests unitarios (http_errors, companies, budget, mx, document_validity)
- [x] Liquibase migrations `006`–`017` aplicadas en Postgres local (`--context-filter=dev`)
- [x] API levantada localmente; `/v1/health` OK
- [x] Smoke CRUD vía `scripts/crud-smoke-test.sh`: empresas, proyectos, trabajadores, catálogos (create/get/list/patch)
- [ ] Import Excel de trabajadores (flujo multipart + S3/local storage)
- [ ] Import presupuesto desde Excel
- [ ] Flujo nómina: asistencia → cerrar semana
- [ ] CI en el remoto (sin checks reportados aún)
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-06667c14-38e8-42a8-9ed9-6b1322f12ae7?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-06667c14-38e8-42a8-9ed9-6b1322f12ae7&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
134 lines
5.8 KiB
Markdown
134 lines
5.8 KiB
Markdown
# Base de datos (Postgres + Liquibase)
|
|
|
|
PANELS usa **Postgres** organizado como monolito modular: dos bases de datos
|
|
físicamente separadas en el mismo servidor/instancia por ambiente.
|
|
|
|
| Base de datos | Esquema(s) | Changelog | Qué vive ahí |
|
|
|---|---|---|---|
|
|
| `panels_platform` | `public` | `db/platform/` | Control plane SaaS: `tenants`, `platform_users` (identidad de PANELS como operador), `smtp_settings`. Solo lo tocan las rutas `/v1/saas/*`. |
|
|
| `panels_product` | `iam` | `db/iam/` | Identidad/roles/permisos **de cada tenant** (ej. usuarios de ARCTEC). Aislado de `core` -- sin JOIN/FK cruzado. |
|
|
| `panels_product` | `core` | `db/core/` | Negocio: empresas, personal, obras, expedientes, presupuesto, nómina, gafetes. |
|
|
|
|
No hay FK real entre `panels_platform` y `panels_product` (bases distintas),
|
|
ni entre los esquemas `iam` y `core` (aislamiento a propósito). `tenant_id`
|
|
es una referencia lógica validada en la capa de aplicación. Ver el plan de
|
|
migración para el detalle de la arquitectura y las reglas del monolito
|
|
modular.
|
|
|
|
## Aprovisionamiento (una vez por ambiente, antes de Liquibase)
|
|
|
|
Ver [`db/provision/README.md`](provision/README.md): crea los 6 roles de
|
|
Postgres (`panels_{platform,iam,core}_{owner,app}`), las 2 bases de datos,
|
|
los esquemas `iam`/`core`, la extensión `citext`, y los usuarios ACL de
|
|
Redis. Incluye `dev-local.sh` para reproducir todo esto en una máquina de
|
|
desarrollo sin depender de Coolify, y `verify-isolation.sh` para confirmar
|
|
que el aislamiento realmente se cumple.
|
|
|
|
## Aplicar migraciones
|
|
|
|
En **Coolify** no se corre a mano: el servicio `migrate` de `docker-compose.yml`
|
|
aplica `all --context-filter=!dev` en cada deploy, antes de levantar `api`.
|
|
|
|
A mano (Java 17+ y credenciales `_owner`, nunca `_app`):
|
|
|
|
```bash
|
|
# Local, después de correr db/provision/dev-local.sh:
|
|
set -a && source .env.dev-local && set +a
|
|
./db/update.sh all --context-filter=dev # dev: incluye datos de demo
|
|
./db/update.sh all --context-filter='!dev' # staging/producción: solo esquema + catálogos
|
|
|
|
# Un solo módulo
|
|
./db/update.sh core --context-filter='!dev'
|
|
./db/update.sh iam --context-filter='!dev'
|
|
./db/update.sh platform --context-filter='!dev'
|
|
```
|
|
|
|
**Importante**: Liquibase, si NO se le pasa `--context-filter`, corre TODOS
|
|
los changesets sin importar su `context` -- incluidos los de `context="dev"`.
|
|
No pasar el filtro en staging/producción NO es "modo seguro por defecto",
|
|
es lo contrario: cargaría el tenant/empresas/proyecto de demostración. Por
|
|
eso `--context-filter` es obligatorio siempre, nunca opcional.
|
|
|
|
`npm run db:migrate` corre `./db/update.sh all`. **Las migraciones ya NO
|
|
corren automáticamente al arrancar la API** -- son un paso explícito de
|
|
deploy (a diferencia del `runLiquibase()` que existía con SQLite).
|
|
|
|
Contexts de Liquibase:
|
|
- Sin `context`: changesets de esquema y catálogos requeridos (`risk_levels`,
|
|
`document_types`, etc.) -- corren en **todos** los ambientes.
|
|
- `context="dev"`: datos de demostración (tenant/empresas/proyecto ARCTEC) --
|
|
**nunca** en staging/producción. Siempre pasar `--context-filter`
|
|
explícito; no confiar en el default de Liquibase.
|
|
|
|
## Nuevos cambios de schema
|
|
|
|
Un changeset nuevo en `db/{platform,iam,core}/changesets/`, referenciado en
|
|
el `changelog-master.xml` correspondiente. Reglas:
|
|
|
|
- Nunca editar un changeset ya aplicado en un ambiente compartido -- crear
|
|
uno nuevo.
|
|
- Usar `dbms:postgresql` en vez de `context` para lógica específica de motor.
|
|
- No añadir migraciones directamente en `api/*.ts`.
|
|
- Las herramientas (Liquibase + driver JDBC de Postgres) viven en
|
|
`db/tools/` (gitignored); `bootstrap-tools.sh` las descarga.
|
|
|
|
## Bootstrap del primer usuario administrador
|
|
|
|
El `password_hash` (PBKDF2) no lo puede generar un changeset SQL. El primer
|
|
`platform_admin` y el primer `tenant_admin` de un tenant nuevo se crean con
|
|
`scripts/bootstrap-admin.ts` (ver Fase 4 del plan de migración), no con
|
|
lógica de seed en el arranque de la API ni con datos hardcodeados en
|
|
Liquibase.
|
|
|
|
## Funciones RPC (`core.fn_*`)
|
|
|
|
A partir del changeset `006-rpc-infra.sql`, el esquema `core` expone la
|
|
lógica de negocio como funciones PostgreSQL invocadas desde la API con
|
|
`SELECT core.fn_nombre($1::jsonb)`. La capa TypeScript **no** debe usar
|
|
`db.prepare()` contra tablas de `core` en rutas de negocio; solo
|
|
[`api/rpc.ts`](../api/rpc.ts) ejecuta el `SELECT` del RPC.
|
|
|
|
### Envelope de respuesta (BD)
|
|
|
|
Toda función devuelve un `jsonb` con esta forma:
|
|
|
|
```json
|
|
{
|
|
"ok": true,
|
|
"code": "OK",
|
|
"layer": "db",
|
|
"message": "Mensaje detallado en español",
|
|
"context": { "fn": "fn_empresa_get", "id": 5 },
|
|
"data": { },
|
|
"errors": null
|
|
}
|
|
```
|
|
|
|
- `code`: código de negocio (`OK`, `CREATED`, `VALIDATION`, `NOT_FOUND`,
|
|
`CONFLICT`, `INTERNAL`, …).
|
|
- `layer`: siempre `"db"` desde Postgres.
|
|
- `message`: texto legible y específico (nunca genérico).
|
|
- `context`: metadatos seguros para depuración (nombre de función, ids).
|
|
- `data`: payload de éxito; `errors`: mapa de campos en validación.
|
|
|
|
Helpers en `006-rpc-infra.sql`: `rpc_ok`, `rpc_err`, `rpc_created`,
|
|
`rpc_from_exception`.
|
|
|
|
### Contrato API
|
|
|
|
[`api/http_errors.ts`](../api/http_errors.ts) mapea `code` → HTTP y añade
|
|
`status` al body. `respondRpc()` preserva `layer` y `message` de la BD;
|
|
`respondApiError()` construye envelopes de capa `"api"`.
|
|
|
|
### Convenciones al añadir funciones
|
|
|
|
1. Un solo parámetro `payload jsonb`.
|
|
2. Prefijo `fn_` para funciones que devuelven envelope.
|
|
3. `SECURITY INVOKER` y `SET search_path = core`.
|
|
4. Errores de negocio con `RETURN core.rpc_err(...)`; constraints de
|
|
Postgres capturados en `EXCEPTION` y traducidos a mensajes claros.
|
|
5. `GRANT EXECUTE ... TO panels_core_app` en el mismo changeset.
|
|
6. Comentario de ejemplo `SELECT core.fn_*(...)` en el SQL.
|
|
|
|
Changesets RPC: `006` (infra) … `015` (documentos). Ver
|
|
[`changelog-master.xml`](core/changelog-master.xml).
|