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:
parent
a6b336806e
commit
9b6aaad032
19
README.md
19
README.md
@ -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
|
||||||
|
@ -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 () => {
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user