Skip to content

Bug de TypeORM: ORDER BY con expresiones/columnas + JOINs + paginación

Descripción

Cuando se combina leftJoinAndSelect con skip()/take() y un addOrderBy() que no se corresponde con una propiedad seleccionada (una expresión como LOWER(), CASE, COLLATE, o un nombre de columna distinto del nombre de la propiedad), TypeORM genera SQL incorrecto: la expresión de ordenación no está en la lista de columnas del SELECT y su lógica interna de paginación con JOINs la requiere.

Causa raíz

TypeORM, al paginar con JOINs, construye un subquery distinctAlias y para ordenar llama a createOrderByCombinedWithSelectExpression(). En la versión instalada (0.3.31) ese método resuelve la columna así:

js
const column = alias.metadata.findColumnWithPropertyPath(propertyPath);
... DriverUtils.buildAlias(driver, undefined, aliasName, column.databaseName)

Si orderCriteria contiene un punto (p. ej. user.search_normalized o CASE miembro.tipo ...), se toma alias, se busca la propiedad y se accede a column.databaseName sin comprobar que column exista. Cuando la propiedad no se encuentra, column es undefined y salta TypeError: Cannot read properties of undefined (reading 'databaseName').

La rama problemática solo se ejecuta cuando se cumple:

(this.expressionMap.skip || this.expressionMap.take) && this.expressionMap.joinAttributes.length > 0

Es decir, paginación + al menos un JOIN.

Issues relacionados

  • TypeORM #8014cerrado (etiquetado question, sin PR de fix). Paginación rota al ordenar por columna no incluida en el select.
  • TypeORM #6294abierto (bug, postgres). Order by en campos anidados rompe skip()/take().
  • TypeORM #8213abierto (bug). databaseName undefined con leftJoinAndSelect + orderBy + take/skip.
  • TypeORM #11808cerrado con el PR #11904, milestone 1.0. Mismo error al ordenar por campos de una entidad joineada.

Estado de la verificación (2026-09)

  • TypeORM instalado: 0.3.31 (package.json fija ^0.3.25), que es la última 0.3.x.
  • En npm la versión actual es 1.1.1. El fix de #11808 (PR #11904) va al milestone 1.0, es decir, no está backportado a 0.3.x.
  • Comprobado en el código instalado (node_modules/typeorm/query-builder/SelectQueryBuilder.js, createOrderByCombinedWithSelectExpression): el bug sigue presente en 0.3.31.
  • Conclusión: el bug upstream NO está resuelto para la versión que usamos, pero no nos afecta tal como está escrito el código (ver siguiente sección).

Por qué no nos afecta hoy

  1. Convención de nombres: las entidades usan el mismo nombre en la propiedad y en la columna (snake_case), de modo que findColumnWithPropertyPath encuentra la columna y column.databaseName nunca es undefined. Ejemplos: src/entities/asignacion.entity.ts (created_at), src/entities/user.entity.ts.
  2. Workaround de alias en todas las ordenaciones configurables por el usuario (ver más abajo), que evita meter expresiones en el orderBy paginado.
  3. La única consulta que ordena con expresión CASE (tribunal-miembro.findAll) no tiene JOINs, por lo que no entra en la rama problemática. Ver src/services/tribunal-miembro.service.ts (orderBy(CASE ...) + skip/take, sin joins).

Workaround aplicado

Usar addSelect para incluir la expresión de ordenación en el SELECT con un alias, y luego ordenar por el alias en lugar de la expresión directamente:

ts
// Antes (rompe con JOINs + skip/take):
qb.addOrderBy("LOWER(entidad.nombre)", "ASC");

// Después (funciona en todos los casos):
qb.addSelect("LOWER(entidad.nombre)", "sort_0");
qb.addOrderBy("sort_0", "ASC");

Las columnas extra del SELECT se descartan al mapear a la entidad (no contaminan los DTOs), pero la lógica interna de paginación de TypeORM las ve y no rompe.

Advertencias para no reintroducir el bug

  • No ordenar por expresiones (LOWER(), CASE, COLLATE, etc.) directamente en consultas paginadas con JOINs: usar siempre el workaround del alias.
  • Mantener la convención de que propiedad y columna comparten nombre; si se mapea una propiedad camelCase a una columna snake_case, no usar el nombre de columna en orderBy (usar el de la propiedad).
  • No añadir JOINs a tribunal-miembro.findAll sin adaptar antes su orderBy con CASE (hoy funciona precisamente porque no tiene JOINs).
  • Al ordenar por campos de un alias joineado, usar el nombre de propiedad, nunca el de la columna de base de datos.

Test de regresión

src/__tests__/typeorm-orderby-pagination.test.ts levanta un DataSource sqljs en memoria con las entidades reales User/UserProfile y cubre:

  • El patrón del workaround (addSelect(LOWER(...), "sort_0") + addOrderBy("sort_0")) con JOIN + skip/take.
  • La ordenación por propiedad (nombre = columna) con JOIN + skip/take.
  • Un canario con it.fails que documenta que ordenar por un nombre de columna distinto del de la propiedad sigue reventando en 0.3.x. Cuando el canario empiece a fallar (es decir, deje de lanzar), significará que el bug se ha corregido en la versión instalada y se podrá retirar el workaround.

¿Cuándo se puede eliminar?

  1. Migrar a TypeORM 1.x (major; incluye el fix de #11808 y presumiblemente los demás). Revisar los issues enlazados y el changelog antes de subir.
  2. Tras la migración, el canario del test dejará de fallar; señal de que el workaround ya no es necesario.
  3. Probar a eliminar el workaround en una entidad piloto (sin JOINs primero, con JOINs después) y ejecutar la suite completa.
  4. Si todo pasa, migrar los servicios al addOrderBy directo.

Fecha de aplicación

Julio 2026 — TypeORM v0.3.x. Verificado en septiembre de 2026 sobre TypeORM 0.3.31.

Archivos afectados (workaround)

  • src/services/asignacion.service.ts
  • src/services/aula.service.ts
  • src/services/convocatoria.service.ts
  • src/services/edificio.service.ts
  • src/services/ejercicio.service.ts
  • src/services/email-template.service.ts
  • src/services/sede.service.ts
  • src/services/tribunal-miembro.service.ts
  • src/services/user.service.ts
  • src/services/vigilante.service.ts

SIVA — Sistema Integral de Vigilantes de Aulas