fix: add check on primary key columns to avoid select exception (#507)

This commit is contained in:
xMase 2023-03-01 12:23:17 +01:00 committed by GitHub
parent 6193ea28df
commit c2c6eaf44e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 6 deletions

View File

@ -194,6 +194,7 @@ const paginateConfig: PaginateConfig<CatEntity> {
* Type: TypeORM partial selection * Type: TypeORM partial selection
* Default: None * Default: None
* https://typeorm.io/select-query-builder#partial-selection * https://typeorm.io/select-query-builder#partial-selection
* Note: You must include the primary key in the selection.
*/ */
select: ['name', 'color'], select: ['name', 'color'],
@ -266,7 +267,7 @@ const paginateConfig: PaginateConfig<CatEntity> {
Eager loading should work with typeorm's eager property out the box. Like so Eager loading should work with typeorm's eager property out the box. Like so
```typescript ```typescript
import { Entity, OneToMany } from 'typeorm'; import { Entity, OneToMany } from 'typeorm'
@Entity() @Entity()
export class CatEntity { export class CatEntity {
@ -295,7 +296,7 @@ class CatService {
public findAll(query: PaginateQuery): Promise<Paginated<CatEntity>> { public findAll(query: PaginateQuery): Promise<Paginated<CatEntity>> {
return paginate(query, this.catsRepository, { return paginate(query, this.catsRepository, {
sortableColumns: ['id', 'name', 'color', 'age'], sortableColumns: ['id', 'name', 'color', 'age'],
loadEagerRelations: true // set this property as true to enable the eager loading loadEagerRelations: true, // set this property as true to enable the eager loading
}) })
} }
} }

View File

@ -61,6 +61,25 @@ export function extractVirtualProperty(
) )
} }
export function includesAllPrimaryKeyColumns(qb: SelectQueryBuilder<unknown>, propertyPath: string[]): boolean {
if (!qb || !propertyPath) {
return false
}
return qb.expressionMap.mainAlias?.metadata?.primaryColumns
.map((column) => column.propertyPath)
.every((column) => propertyPath.includes(column))
}
export function hasColumnWithPropertyPath(
qb: SelectQueryBuilder<unknown>,
columnProperties: ColumnProperties
): boolean {
if (!qb || !columnProperties) {
return false
}
return !!qb.expressionMap.mainAlias?.metadata?.hasColumnWithPropertyPath(columnProperties.propertyName)
}
export function checkIsRelation(qb: SelectQueryBuilder<unknown>, propertyPath: string): boolean { export function checkIsRelation(qb: SelectQueryBuilder<unknown>, propertyPath: string): boolean {
if (!qb || !propertyPath) { if (!qb || !propertyPath) {
return false return false

View File

@ -1871,6 +1871,20 @@ describe('paginate', () => {
expect(result.links.current).toBe('?page=1&limit=20&sortBy=countCat:ASC&filter.countCat=$gt:0') expect(result.links.current).toBe('?page=1&limit=20&sortBy=countCat:ASC&filter.countCat=$gt:0')
}) })
it("should return all columns if select doesn't contain all primary columns", async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id', 'name'],
select: ['name'],
}
const query: PaginateQuery = {
path: '',
}
const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.data).toStrictEqual(cats)
})
it('should return all items even if deleted', async () => { it('should return all items even if deleted', async () => {
const config: PaginateConfig<CatEntity> = { const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'], sortableColumns: ['id'],

View File

@ -23,6 +23,8 @@ import {
positiveNumberOrDefault, positiveNumberOrDefault,
RelationColumn, RelationColumn,
SortBy, SortBy,
hasColumnWithPropertyPath,
includesAllPrimaryKeyColumns,
} from './helper' } from './helper'
import { FilterOperator, FilterSuffix } from './operator' import { FilterOperator, FilterSuffix } from './operator'
import { addFilter } from './filter' import { addFilter } from './filter'
@ -208,16 +210,19 @@ export async function paginate<T extends ObjectLiteral>(
} }
// When we partial select the columns (main or relation) we must add the primary key column otherwise // When we partial select the columns (main or relation) we must add the primary key column otherwise
// typeorm will not be able to map the result TODO: write it in the docs // typeorm will not be able to map the result.
const selectParams = config.select || query.select const selectParams = config.select || query.select
if (selectParams?.length > 0) { if (selectParams?.length > 0 && includesAllPrimaryKeyColumns(queryBuilder, selectParams)) {
const cols: string[] = selectParams.reduce((cols, currentCol) => { const cols: string[] = selectParams.reduce((cols, currentCol) => {
if (query.select?.includes(currentCol) ?? true) { if (query.select?.includes(currentCol) ?? true) {
const columnProperties = getPropertiesByColumnName(currentCol) const columnProperties = getPropertiesByColumnName(currentCol)
const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath) const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
const { isVirtualProperty } = extractVirtualProperty(queryBuilder, columnProperties)
if (hasColumnWithPropertyPath(queryBuilder, columnProperties) || isVirtualProperty) {
// here we can avoid to manually fix and add the query of virtual columns // here we can avoid to manually fix and add the query of virtual columns
cols.push(fixColumnAlias(columnProperties, queryBuilder.alias, isRelation)) cols.push(fixColumnAlias(columnProperties, queryBuilder.alias, isRelation))
} }
}
return cols return cols
}, []) }, [])
queryBuilder.select(cols) queryBuilder.select(cols)