ixiclinicDocs
DesarrolladoresBase de datos

Migraciones

Flujo generate → migrate de Drizzle, la migración automática en el arranque de producción, la función NCF de numeración fiscal, el riesgo de drift y la regla de aplicar todo cambio a ambas bases.

Las migraciones de ixiclinic son archivos SQL versionados generados por Drizzle Kit y guardados en apps/api/src/db/migrations/. Cada migración queda registrada en migrations/meta/_journal.json (tag, índice y timestamp).

Flujo generate → migrate

Desde apps/api:

  1. Editar el esquema en src/db/schema/<dominio>.ts.
  2. Generar la migración:
    pnpm db:generate
    Drizzle compara el esquema con el estado anterior y escribe un nuevo NNNN_<nombre>.sql en src/db/migrations/, añadiendo la entrada correspondiente a meta/_journal.json.
  3. Revisar el SQL generado antes de aplicarlo (ver el aviso de drift más abajo).
  4. Aplicar la migración a la base de desarrollo (Neon):
    pnpm db:migrate

Migración automática en el arranque de producción

En producción no se corre db:migrate a mano. El contenedor del API aplica las migraciones pendientes al arrancar, mediante apps/api/src/migrate.ts:

import { migrate } from 'drizzle-orm/postgres-js/migrator';
// ...
const client = postgres(databaseUrl, { max: 1 });
const db = drizzle(client);
await migrate(db, { migrationsFolder });

El CMD del Dockerfile lo invoca antes de levantar el servidor:

CMD ["sh", "-c", "node dist/migrate.js && exec node dist/index.js"]

Si migrate.js sale con código distinto de cero, el API no arranca — se prefiere fallar rápido a correr sobre un esquema roto. Las migraciones SQL se copian junto al migrate.js compilado dentro de la imagen (COPY ... src/db/migrations -> dist/db/migrations).

La función de numeración fiscal (NCF)

No todo el esquema vive en migraciones. apps/api/src/db/ensure-ncf-function.ts aplica DDL idempotente en el arranque para piezas de cumplimiento fiscal dominicano que conviene mantener convergentes entre desarrollo y producción sin una migración manual. Crea, entre otras cosas:

  • La función PostgreSQL next_fiscal_number(tenant_id, ncf_type), que incrementa de forma atómica (con FOR UPDATE) el contador de la secuencia fiscal activa y devuelve el NCF/eNCF formateado. Autoprovisiona la secuencia B02 (Consumidor Final) si el tenant no tiene una; el resto de tipos (B01, B15, B16…) requieren autorización DGII y deben configurarse en Ajustes.
  • Un índice único parcial uq_fiscal_seq_active sobre (tenant_id, ncf_type) para secuencias activas.
  • La función next_generic_number(tenant_id) y la tabla generic_sequences para documentos no fiscales (p. ej. FAC-000001).
  • Las tablas e-CF (ecf_configurations, ecf_submissions) y el soporte de facturación agrupada de empresas (columnas invoices.company_id, orders.grouped_invoice_id, etc.).

Todas estas sentencias usan CREATE ... IF NOT EXISTS / ADD COLUMN IF NOT EXISTS / CREATE OR REPLACE, por lo que se pueden ejecutar repetidamente sin efectos secundarios.

Riesgo de drift al correr db:generate

pnpm db:generate compara el esquema TypeScript con el estado del journal, pero el DDL idempotente de ensure-ncf-function.ts y otros ajustes aplicados fuera de las migraciones no están reflejados en ese estado. El resultado es que db:generate a veces arrastra drift: incluye en el .sql cambios que en realidad ya existen en la base.

Importante: revisa siempre el .sql recién generado y recórtalo para que contenga únicamente lo nuevo. Si dejas DDL redundante o conflictivo, el migrate.js del arranque de producción puede fallar y, como el API falla rápido, el contenedor no levantará.

Regla: aplicar cambios a ambas bases

Desarrollo (Neon) y producción (Docker) son bases distintas. Cualquier cambio de esquema debe llegar a las dos:

  • Desarrollo (Neon): pnpm db:migrate (o pnpm db:push para iterar rápido).
  • Producción (Docker): se aplica sola al desplegar, vía node dist/migrate.js en el arranque.

Ver Neon vs Docker para el detalle de los entornos.

Baseline de migraciones en una base existente

Si una base ya tiene el esquema (creado fuera del migrador, p. ej. con db:push), hay que marcar las migraciones existentes como ya aplicadas para que el migrador solo aplique las nuevas de ahí en adelante. Para eso está apps/api/scripts/baseline-prod-migrations.mjs:

DATABASE_URL="postgresql://<usuario>:<clave>@<host>:<puerto>/<base>" \
  node apps/api/scripts/baseline-prod-migrations.mjs

El script es genérico: lee DATABASE_URL del entorno (no trae credenciales), recorre meta/_journal.json, calcula el hash SHA-256 de cada .sql —tal como hace Drizzle— e inserta las entradas faltantes en drizzle.__drizzle_migrations. Es idempotente: las migraciones ya registradas se omiten.

On this page