From 9b6aaad0320db28c12e14d01d259666939c0e380 Mon Sep 17 00:00:00 2001 From: Philipp Date: Tue, 21 Mar 2023 20:27:52 +0100 Subject: [PATCH] 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. --- README.md | 19 +++++++++++---- src/paginate.spec.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++ src/paginate.ts | 19 +++++++++------ 3 files changed, 82 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 80edfe9..d005203 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ http://localhost:3000/cats?limit=5&page=2&sortBy=color:DESC&search=i&filter.age= ```ts import { Controller, Injectable, Get } from '@nestjs/common' 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' @Entity() @@ -114,6 +114,12 @@ export class CatEntity { @Column('int') age: number + + @Column({ nullable: true }) + lastVetVisit: Date | null + + @CreateDateColumn() + createdAt: string } @Injectable() @@ -127,10 +133,12 @@ export class CatsService { return paginate(query, this.catsRepository, { sortableColumns: ['id', 'name', 'color', 'age'], nullSort: 'last', - searchableColumns: ['name', 'color', 'age'], defaultSortBy: [['id', 'DESC']], + searchableColumns: ['name', 'color', 'age'], + select: ['id', 'name', 'color', 'age', 'lastVetVisit'], filterableColumns: { - age: [FilterOperator.GTE, FilterOperator.LTE], + name: [FilterOperator.EQ, FilterSuffix.NOT], + age: true, }, }) } @@ -184,12 +192,13 @@ const paginateConfig: PaginateConfig { /** * Required: false - * Type: TypeORM partial selection + * Type: (keyof CatEntity)[] * Default: None + * Description: TypeORM partial selection. Limit selection further by using `select` query param. * https://typeorm.io/select-query-builder#partial-selection * Note: You must include the primary key in the selection. */ - select: ['name', 'color'], + select: ['id', 'name', 'color'], /** * Required: false diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index 76bc532..cfac1b5 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -1968,6 +1968,8 @@ describe('paginate', () => { const result = await paginate(query, catRepo, config) 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 () => { @@ -2010,8 +2012,54 @@ describe('paginate', () => { const result = await paginate(query, catRepo, config) result.data.forEach((cat) => { + expect(cat.id).toBeDefined() + expect(cat.name).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 = { + sortableColumns: ['id'], + } + const query: PaginateQuery = { + path: '', + select: ['id', 'name'], + } + + const result = await paginate(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 = { + sortableColumns: ['id'], + select: ['id', 'name', 'color'], + } + const query: PaginateQuery = { + path: '', + select: ['id', 'color', 'age'], + } + + const result = await paginate(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 () => { @@ -2036,6 +2084,8 @@ describe('paginate', () => { 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 () => { @@ -2066,6 +2116,8 @@ describe('paginate', () => { 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 () => { @@ -2091,6 +2143,8 @@ describe('paginate', () => { 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 () => { @@ -2122,6 +2176,8 @@ describe('paginate', () => { 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 () => { diff --git a/src/paginate.ts b/src/paginate.ts index d2bf682..ebe71bb 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -43,6 +43,7 @@ export class Paginated { sortBy: SortBy searchBy: Column[] search: string + select: string[] filter?: { [column: string]: string | string[] } } links: { @@ -176,14 +177,13 @@ export async function paginate( // 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. - 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)) { const cols: string[] = selectParams.reduce((cols, currentCol) => { - if (query.select?.includes(currentCol) ?? true) { - const columnProperties = getPropertiesByColumnName(currentCol) - const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath) - cols.push(fixColumnAlias(columnProperties, queryBuilder.alias, isRelation)) - } + const columnProperties = getPropertiesByColumnName(currentCol) + const isRelation = checkIsRelation(queryBuilder, columnProperties.propertyPath) + cols.push(fixColumnAlias(columnProperties, queryBuilder.alias, isRelation)) return cols }, []) queryBuilder.select(cols) @@ -269,6 +269,10 @@ export async function paginate( const searchByQuery = 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 ? '&' + stringify( @@ -279,7 +283,7 @@ export async function paginate( ) : '' - 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 @@ -295,6 +299,7 @@ export async function paginate( sortBy, search: query.search, searchBy: query.search ? searchBy : undefined, + select: isQuerySelected ? selectParams : undefined, filter: query.filter, }, links: {