Appearance
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 > 0Es decir, paginación + al menos un JOIN.
Issues relacionados
- TypeORM #8014 — cerrado (etiquetado
question, sin PR de fix). Paginación rota al ordenar por columna no incluida en el select. - TypeORM #6294 — abierto (bug, postgres). Order by en campos anidados rompe
skip()/take(). - TypeORM #8213 — abierto (bug).
databaseNameundefined conleftJoinAndSelect+orderBy+take/skip. - TypeORM #11808 — cerrado 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.jsonfija^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
- Convención de nombres: las entidades usan el mismo nombre en la propiedad y en la columna (snake_case), de modo que
findColumnWithPropertyPathencuentra la columna ycolumn.databaseNamenunca esundefined. Ejemplos:src/entities/asignacion.entity.ts(created_at),src/entities/user.entity.ts. - Workaround de alias en todas las ordenaciones configurables por el usuario (ver más abajo), que evita meter expresiones en el
orderBypaginado. - 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. Versrc/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.findAllsin adaptar antes suorderByconCASE(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.failsque 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?
- 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.
- Tras la migración, el canario del test dejará de fallar; señal de que el workaround ya no es necesario.
- Probar a eliminar el workaround en una entidad piloto (sin JOINs primero, con JOINs después) y ejecutar la suite completa.
- Si todo pasa, migrar los servicios al
addOrderBydirecto.
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.tssrc/services/aula.service.tssrc/services/convocatoria.service.tssrc/services/edificio.service.tssrc/services/ejercicio.service.tssrc/services/email-template.service.tssrc/services/sede.service.tssrc/services/tribunal-miembro.service.tssrc/services/user.service.tssrc/services/vigilante.service.ts