Datos y persistencia
Motor: PostgreSQL gestionado (DigitalOcean), DBENGINE=postgres. Venía de MySQL / SQL Server y la
migración es parcial.
Entidades
Viven en proteus-backend/src/schemas/** (más src/modules/**/entities), agrupadas por schema de
Postgres.
Acá las entidades son los schemas. Los archivos *.entity.ts solo le importan al ormconfig.json, que
no se usa.
| Schema | Qué guarda | Ejemplos |
|---|---|---|
customer | El paciente y su vínculo con financiadores | Patient, PatientAgreement, PatientAgreementElementCategory (cartilla), PatientInformation, ContactNumber |
finances | Episodios, autorizaciones, insumos, entregas | AtentionProcess, Authorization, AuthorizationDetails, AuthorizationRequest, AmbulatoryAuthorization, Billing, HospitalBedOccupancy, PatientDiagnostics, Document, Supply*, DeliveryNote*, Warehouse* |
configuration | Catálogos y configuración — el schema más grande (~50 entidades) | Insurance, Agreement, Element, ElementCategory, Price, Product, User, Role, Permission, RolePermission, Company, EntityStatus, Diagnostic, Hospital*, DocumentType, SiiCredential |
pami_billings | El modelo de pago capitado y la facturación a PAMI | Capita* (Modulo, Categoria, Adenda, TasaUsoMinima, SnapshotDiario, Alerta), internacion, ambulatorio, practicasRealizadas*, liquidaciones, transmisiones, transmisionArchivo*, debitos, debitosSII, bocasAtencion |
logs | OMEs y logs | OmePami, OmeDetalle |
callcenter | Contactos (legacy) | — |
Bases comunes: BasicEntity, LoggeableEntity, ContactableEntity, EntityCode, ReportEntity.
Las entidades del motor de reglas están aparte, en su módulo.
Conexiones (src/config/bbdd/)
| Conexión | Estado |
|---|---|
PostgreSQLConnection | La conexión real y la única que se inicializa. synchronize está comentado (OFF) |
MSSQLConnection | ⚠️ Su initialize() no se llama en ningún lado |
RedisConnection | Locks distribuidos, caches y metadata de usuario |
QueueConnection | RabbitMQ — ver Workers y colas |
MSSQLConnection.dataSource es undefined en runtime, siempreTanto en modo API como en workers. Cualquier service que lea de ahí muere con
TypeError: Cannot read properties of undefined (reading 'query').
Si tocás un service que la usa, apuntalo a PostgreSQLConnection.dataSource (es la misma base, las
mismas variables DB_SQL_DEFAULT_*).
Consumidores que todavía arrastran el bug: DashboardKpisService, PamiBillingSIIService,
ControlAccountService, PamiSIIServiceV4, bulkInsertIgnore.
Schema y "migraciones"
src/migrations/ no son migraciones de TypeORM: son scripts de seed/bootstrap idempotentes que
corren al arrancar la app (RoleMigrations, RolesPermissionMigrations, EntityStatusMigrations,
MovementTypeMigrations, InsuranceMigrations, AppConfigurationMCache, RulesEngineMigrations…).
El DDL real se aplica a mano: ver DDL y ops.
Los nueve gotchas de Postgres
Los que más tiempo cuestan, en orden de frecuencia:
1. synchronize está OFF
Agregar una columna a la entidad no la crea en la base. Sin el ALTER TABLE previo, toda query que
toque esa entidad revienta — no solo la que usa la columna nueva.
2. operator does not exist: bigint = character varying
Falta un cast. Postgres no castea implícito. Verificá los tipos reales en
information_schema.columns.
3. LIKE es case-sensitive
Usá ILIKE / ILike.
4. Columnas boolean en la entidad son smallint en la base
Comparar con COALESCE(col,0)=1 y no mandar booleanos (tira 22P02). Casos conocidos: ACTIVE,
EXTERNALGENERATED, CHRONIC, AtentionProcess.BILLED.
5. bigint y numeric vuelven como string
Es el driver pg. No hagas aritmética directa sobre esos valores.
6. NULLS FIRST / NULLS LAST hay que ponerlo explícito
El default de Postgres difiere del de MySQL.
7. Baja lógica, no DELETE
ACTIVE = false + ENDDATE = now(). No borres filas.
8. El SQL crudo se arregla incrementalmente
Endpoint por endpoint, a medida que aparece roto. Mucho de lo que queda es código muerto y no vale portarlo. La receta está en Porteo de SQL.
9. Un módulo entero "que no anda y no dice por qué" puede ser la conexión
MSSQLConnection.dataSource es undefined, así que el TypeError termina en CustomResponse.error(), que
lo guarda en err — y un Error serializa a {} en JSON. El front recibe
{"success":false,"err":{}} sin ninguna pista.
El mensaje real está en la consola del servidor. Miralo antes de sospechar de la query.
Índices únicos y NULL
En Postgres, dos NULL se consideran distintos en un índice único. O sea: UNIQUE (COMPANYID, CODE)
no impide dos filas con COMPANYID IS NULL y el mismo CODE.
Si la columna del scope es nullable, indexá COALESCE(COMPANYID, 0). Y para que la unicidad sea
case-insensitive y a prueba de espacios, indexá UPPER(TRIM(CODE)) — upper() y btrim() son IMMUTABLE,
así que se pueden indexar.
Si el índice normaliza con UPPER(TRIM(...)) y el service compara el valor crudo, el usuario ve un error
23505 de Postgres en vez del mensaje de negocio. Y validá también en el update, no solo en el
create.