fix: select handling (#556)

BREAKING CHANGE: We handle query `select` the same way as all other parameters meters. You can only select columns in the query which have been selected in the config.
This commit is contained in:
Philipp 2023-03-21 20:27:52 +01:00 committed by GitHub
parent a6b336806e
commit 9b6aaad032
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 82 additions and 12 deletions

View File

@ -98,7 +98,7 @@ http://localhost:3000/cats?limit=5&page=2&sortBy=color:DESC&search=i&filter.age=
```ts ```ts
import { Controller, Injectable, Get } from '@nestjs/common' import { Controller, Injectable, Get } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm' import { InjectRepository } from '@nestjs/typeorm'
import { FilterOperator, Paginate, PaginateQuery, paginate, Paginated } from 'nestjs-paginate' import { FilterOperator, FilterSuffix, Paginate, PaginateQuery, paginate, Paginated } from 'nestjs-paginate'
import { Repository, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' import { Repository, Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity() @Entity()
@ -114,6 +114,12 @@ export class CatEntity {
@Column('int') @Column('int')
age: number age: number
@Column({ nullable: true })
lastVetVisit: Date | null
@CreateDateColumn()
createdAt: string
} }
@Injectable() @Injectable()
@ -127,10 +133,12 @@ export class CatsService {
return paginate(query, this.catsRepository, { return paginate(query, this.catsRepository, {
sortableColumns: ['id', 'name', 'color', 'age'], sortableColumns: ['id', 'name', 'color', 'age'],
nullSort: 'last', nullSort: 'last',
searchableColumns: ['name', 'color', 'age'],
defaultSortBy: [['id', 'DESC']], defaultSortBy: [['id', 'DESC']],
searchableColumns: ['name', 'color', 'age'],
select: ['id', 'name', 'color', 'age', 'lastVetVisit'],
filterableColumns: { filterableColumns: {
age: [FilterOperator.GTE, FilterOperator.LTE], name: [FilterOperator.EQ, FilterSuffix.NOT],
age: true,
}, },
}) })
} }
@ -184,12 +192,13 @@ const paginateConfig: PaginateConfig<CatEntity> {
/** /**
* Required: false * Required: false
* Type: TypeORM partial selection * Type: (keyof CatEntity)[]
* Default: None * Default: None
* Description: TypeORM partial selection. Limit selection further by using `select` query param.
* 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. * Note: You must include the primary key in the selection.
*/ */
select: ['name', 'color'], select: ['id', 'name', 'color'],
/** /**
* Required: false * Required: false

View File

@ -1968,6 +1968,8 @@ describe('paginate', () => {
const result = await paginate<CatEntity>(query, catRepo, config) const result = await paginate<CatEntity>(query, catRepo, config)
expect(result.data).toStrictEqual(cats) expect(result.data).toStrictEqual(cats)
expect(result.meta.select).toStrictEqual(undefined)
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC')
}) })
it('should return all items even if deleted', async () => { it('should return all items even if deleted', async () => {
@ -2010,8 +2012,54 @@ describe('paginate', () => {
const result = await paginate<CatEntity>(query, catRepo, config) const result = await paginate<CatEntity>(query, catRepo, config)
result.data.forEach((cat) => { result.data.forEach((cat) => {
expect(cat.id).toBeDefined()
expect(cat.name).toBeDefined()
expect(cat.color).not.toBeDefined() expect(cat.color).not.toBeDefined()
}) })
expect(result.meta.select).toBe(undefined)
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC')
})
it('should ignore query select', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
}
const query: PaginateQuery = {
path: '',
select: ['id', 'name'],
}
const result = await paginate<CatEntity>(query, catRepo, config)
result.data.forEach((cat) => {
expect(cat.id).toBeDefined()
expect(cat.name).toBeDefined()
expect(cat.color).toBeDefined()
})
expect(result.meta.select).toEqual(undefined)
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC')
})
it('should only query select columns which have been config selected', async () => {
const config: PaginateConfig<CatEntity> = {
sortableColumns: ['id'],
select: ['id', 'name', 'color'],
}
const query: PaginateQuery = {
path: '',
select: ['id', 'color', 'age'],
}
const result = await paginate<CatEntity>(query, catRepo, config)
result.data.forEach((cat) => {
expect(cat.id).toBeDefined()
expect(cat.name).not.toBeDefined()
expect(cat.color).toBeDefined()
expect(cat.age).not.toBeDefined()
})
expect(result.meta.select).toEqual(['id', 'color'])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,color')
}) })
it('should return the specified relationship columns only', async () => { it('should return the specified relationship columns only', async () => {
@ -2036,6 +2084,8 @@ describe('paginate', () => {
expect(toy.id).not.toBeDefined() expect(toy.id).not.toBeDefined()
}) })
}) })
expect(result.meta.select).toBe(undefined)
expect(result.links.current).toBe('?page=1&limit=20&sortBy=name:ASC')
}) })
it('should return selected columns', async () => { it('should return selected columns', async () => {
@ -2066,6 +2116,8 @@ describe('paginate', () => {
expect(cat.toys).toHaveLength(0) expect(cat.toys).toHaveLength(0)
} }
}) })
expect(result.meta.select).toStrictEqual(['id', 'toys.(size.height)'])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,toys.(size.height)')
}) })
it('should only select columns via query which are selected in config', async () => { it('should only select columns via query which are selected in config', async () => {
@ -2091,6 +2143,8 @@ describe('paginate', () => {
expect(cat.home).toBeNull() expect(cat.home).toBeNull()
} }
}) })
expect(result.meta.select).toStrictEqual(['id', 'home.id'])
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC&select=id,home.id')
}) })
it('should return the specified nested relationship columns only', async () => { it('should return the specified nested relationship columns only', async () => {
@ -2122,6 +2176,8 @@ describe('paginate', () => {
expect(cat.home).toBeNull() expect(cat.home).toBeNull()
} }
}) })
expect(result.meta.select).toBe(undefined)
expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC')
}) })
it('should return the right amount of results if a many to many relation is involved', async () => { it('should return the right amount of results if a many to many relation is involved', async () => {

View File

@ -43,6 +43,7 @@ export class Paginated<T> {
sortBy: SortBy<T> sortBy: SortBy<T>
searchBy: Column<T>[] searchBy: Column<T>[]
search: string search: string
select: string[]
filter?: { [column: string]: string | string[] } filter?: { [column: string]: string | string[] }
} }
links: { links: {
@ -176,14 +177,13 @@ 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. // typeorm will not be able to map the result.
const selectParams = config.select || query.select const selectParams =
config.select && query.select ? config.select.filter((column) => query.select.includes(column)) : config.select
if (selectParams?.length > 0 && includesAllPrimaryKeyColumns(queryBuilder, selectParams)) { 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) { const columnProperties = getPropertiesByColumnName(currentCol)
const columnProperties = getPropertiesByColumnName(currentCol) const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath)
const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath) cols.push(fixColumnAlias(columnProperties, queryBuilder.alias, isRelation))
cols.push(fixColumnAlias(columnProperties, queryBuilder.alias, isRelation))
}
return cols return cols
}, []) }, [])
queryBuilder.select(cols) queryBuilder.select(cols)
@ -269,6 +269,10 @@ export async function paginate<T extends ObjectLiteral>(
const searchByQuery = const searchByQuery =
query.searchBy && searchBy.length ? searchBy.map((column) => `&searchBy=${column}`).join('') : '' query.searchBy && searchBy.length ? searchBy.map((column) => `&searchBy=${column}`).join('') : ''
// Only expose select in meta data if query select differs from config select
const isQuerySelected = selectParams?.length !== config.select?.length
const selectQuery = isQuerySelected ? `&select=${selectParams.join(',')}` : ''
const filterQuery = query.filter const filterQuery = query.filter
? '&' + ? '&' +
stringify( stringify(
@ -279,7 +283,7 @@ export async function paginate<T extends ObjectLiteral>(
) )
: '' : ''
const options = `&limit=${limit}${sortByQuery}${searchQuery}${searchByQuery}${filterQuery}` const options = `&limit=${limit}${sortByQuery}${searchQuery}${searchByQuery}${selectQuery}${filterQuery}`
const buildLink = (p: number): string => path + '?page=' + p + options const buildLink = (p: number): string => path + '?page=' + p + options
@ -295,6 +299,7 @@ export async function paginate<T extends ObjectLiteral>(
sortBy, sortBy,
search: query.search, search: query.search,
searchBy: query.search ? searchBy : undefined, searchBy: query.search ? searchBy : undefined,
select: isQuerySelected ? selectParams : undefined,
filter: query.filter, filter: query.filter,
}, },
links: { links: {