From a0f8e02a701b6179914a2213fc0a4e56def58eba Mon Sep 17 00:00:00 2001 From: "jiho.park" Date: Sun, 12 May 2024 10:03:28 +0900 Subject: [PATCH 1/3] chore: v0.12.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 12dae05..cb20c08 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nestjs-library/crud", - "version": "0.12.0", + "version": "0.12.1", "description": "Automatically generate CRUD Rest API based on NestJS and TypeOrm", "homepage": "https://github.com/woowabros/nestjs-library-crud", "repository": { From 23e805871530456644200d7ae43777c8243f3070 Mon Sep 17 00:00:00 2001 From: "jiho.park" Date: Thu, 29 Aug 2024 12:51:43 +0900 Subject: [PATCH 2/3] fix: search using params when body is empty --- spec/search/search-with-params.spec.ts | 84 +++++++++++++++++++ .../interceptor/search-request.interceptor.ts | 12 ++- 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 spec/search/search-with-params.spec.ts diff --git a/spec/search/search-with-params.spec.ts b/spec/search/search-with-params.spec.ts new file mode 100644 index 0000000..c19e7b6 --- /dev/null +++ b/spec/search/search-with-params.spec.ts @@ -0,0 +1,84 @@ +import { Controller, Injectable, Module, HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { IsOptional } from 'class-validator'; +import request from 'supertest'; +import { Entity, BaseEntity, Repository, PrimaryColumn, Column, ObjectLiteral } from 'typeorm'; + +import { Crud } from '../../src/lib/crud.decorator'; +import { CrudService } from '../../src/lib/crud.service'; +import { CrudController } from '../../src/lib/interface'; +import { TestHelper } from '../test.helper'; + +@Entity('test') +class TestEntity extends BaseEntity { + @PrimaryColumn() + @IsOptional({ always: true }) + col1: number; + + @Column({ type: 'jsonb', nullable: true }) + @IsOptional({ always: true }) + col2: ObjectLiteral; + + @Column({ type: 'jsonb', nullable: true }) + @IsOptional({ always: true }) + col3: ObjectLiteral; + + @Column() + @IsOptional({ always: true }) + key: string; +} + +@Injectable() +class TestService extends CrudService { + constructor(@InjectRepository(TestEntity) repository: Repository) { + super(repository); + } +} + +@Crud({ entity: TestEntity }) +@Controller('base/:key') +class TestController implements CrudController { + constructor(public readonly crudService: TestService) {} +} + +@Module({ + imports: [TypeOrmModule.forFeature([TestEntity])], + controllers: [TestController], + providers: [TestService], +}) +class TestModule {} + +describe('Search with params', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([TestEntity])], + }).compile(); + app = moduleFixture.createNestApplication(); + await app.init(); + + for (let i = 0; i < 10; i++) { + await request(app.getHttpServer()) + .post(`/base/key-${i}`) + .send({ + col1: i, + col2: [{ multiple2: i % 2 === 0, multiple4: i % 4 === 0 }], + col3: [{ multiple3: i % 3 === 0, multiple5: i % 5 === 0 }], + }) + .expect(HttpStatus.CREATED); + } + }); + + afterAll(async () => { + await TestHelper.dropTypeOrmEntityTables(); + await app?.close(); + }); + + it('should be search using params', async () => { + const { body } = await request(app.getHttpServer()).post('/base/key-1/search').send({}).expect(HttpStatus.OK); + expect(body.data).toHaveLength(1); + expect(body.data[0].key).toEqual('key-1'); + }); +}); diff --git a/src/lib/interceptor/search-request.interceptor.ts b/src/lib/interceptor/search-request.interceptor.ts index 762705b..4fa86b1 100644 --- a/src/lib/interceptor/search-request.interceptor.ts +++ b/src/lib/interceptor/search-request.interceptor.ts @@ -36,15 +36,21 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption const searchOptions = crudOptions.routes?.[method] ?? {}; const customSearchRequestOptions: CustomSearchRequestOptions = req[CUSTOM_REQUEST_OPTIONS]; - if (req.params && req.body?.where && Array.isArray(req.body.where)) { + if (req.params) { const paramsCondition = Object.entries(req.params).reduce( (queryFilter, [key, operand]) => ({ ...queryFilter, [key]: { operator: '=', operand } }), {}, ); - for (const queryFilter of req.body.where) { - _.merge(queryFilter, paramsCondition); + if (req.body?.where && Array.isArray(req.body.where)) { + for (const queryFilter of req.body.where) { + _.merge(queryFilter, paramsCondition); + } + } else { + req.body ??= {}; + req.body.where = [paramsCondition]; } } + const paginationType = (searchOptions.paginationType ?? CRUD_POLICY[method].default.paginationType) as PaginationType; const pagination = PaginationHelper.getPaginationRequest(paginationType, req.body); const isNextPage = PaginationHelper.isNextPage(pagination); From 7c096dcdf6a3203b3d4cb75ee9e958d4d8ea5e68 Mon Sep 17 00:00:00 2001 From: "jiho.park" Date: Thu, 29 Aug 2024 13:21:17 +0900 Subject: [PATCH 3/3] fix: check empty params --- src/lib/interceptor/create-request.interceptor.ts | 2 +- src/lib/interceptor/read-many-request.interceptor.ts | 2 +- src/lib/interceptor/search-request.interceptor.ts | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/lib/interceptor/create-request.interceptor.ts b/src/lib/interceptor/create-request.interceptor.ts index 67f4d21..9c6b922 100644 --- a/src/lib/interceptor/create-request.interceptor.ts +++ b/src/lib/interceptor/create-request.interceptor.ts @@ -27,7 +27,7 @@ export function CreateRequestInterceptor(crudOptions: CrudOptions, factoryOption const req = context.switchToHttp().getRequest(); const createOptions = crudOptions.routes?.[method] ?? {}; - if (req.params) { + if (Object.keys(req.params ?? {}).length > 0) { Object.assign(req.body, req.params); } const body = await this.validateBody(req.body); diff --git a/src/lib/interceptor/read-many-request.interceptor.ts b/src/lib/interceptor/read-many-request.interceptor.ts index 9d1c03c..6cae79f 100644 --- a/src/lib/interceptor/read-many-request.interceptor.ts +++ b/src/lib/interceptor/read-many-request.interceptor.ts @@ -34,7 +34,7 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti const customReadManyRequestOptions: CustomReadManyRequestOptions = req[CUSTOM_REQUEST_OPTIONS]; const paginationType = (readManyOptions.paginationType ?? CRUD_POLICY[method].default.paginationType) as PaginationType; - if (req.params) { + if (Object.keys(req.params ?? {}).length > 0) { Object.assign(req.query, req.params); } diff --git a/src/lib/interceptor/search-request.interceptor.ts b/src/lib/interceptor/search-request.interceptor.ts index 4fa86b1..b229a7f 100644 --- a/src/lib/interceptor/search-request.interceptor.ts +++ b/src/lib/interceptor/search-request.interceptor.ts @@ -35,8 +35,11 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption const req: Record = context.switchToHttp().getRequest(); const searchOptions = crudOptions.routes?.[method] ?? {}; const customSearchRequestOptions: CustomSearchRequestOptions = req[CUSTOM_REQUEST_OPTIONS]; + const paginationType = (searchOptions.paginationType ?? CRUD_POLICY[method].default.paginationType) as PaginationType; + const pagination = PaginationHelper.getPaginationRequest(paginationType, req.body); + const isNextPage = PaginationHelper.isNextPage(pagination); - if (req.params) { + if (Object.keys(req.params ?? {}).length > 0 && !isNextPage) { const paramsCondition = Object.entries(req.params).reduce( (queryFilter, [key, operand]) => ({ ...queryFilter, [key]: { operator: '=', operand } }), {}, @@ -51,10 +54,6 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption } } - const paginationType = (searchOptions.paginationType ?? CRUD_POLICY[method].default.paginationType) as PaginationType; - const pagination = PaginationHelper.getPaginationRequest(paginationType, req.body); - const isNextPage = PaginationHelper.isNextPage(pagination); - const requestSearchDto = await (async () => { if (isNextPage) { const isQueryValid = pagination.setQuery(pagination.query);