diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f70af7..c46c89d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,7 +56,6 @@ jobs: - name: Coveralls uses: coverallsapp/github-action@v2 - # https://github.com/actions/typescript-action/blob/main/.github/workflows/codeql-analysis.yml # https://github.com/actions/typescript-action/blob/main/.github/workflows/test.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 679d373..4b53547 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,61 +1,61 @@ name: 'CodeQL' on: - push: - branches: [main] - pull_request: - # The branches below must be a subset of the branches above - branches: [main] - schedule: - - cron: "0 17 * * 4" + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: '0 17 * * 4' permissions: - contents: read + contents: read jobs: - analyse: - permissions: - security-events: write - name: Analyse - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - with: - # We must fetch at least the immediate parents so that if this is - # a pull request then we can checkout the head. - fetch-depth: 2 - - # If this run was triggered by a pull request event, then checkout - # the head of the pull request instead of the merge commit. - - run: git checkout HEAD^2 - if: ${{ github.event_name == 'pull_request' }} - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - queries: +security-extended - # Override language selection by uncommenting this and choosing your languages - # with: - # languages: go, javascript, csharp, python, cpp, java - - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - # ℹ️ Command-line programs to run using the OS shell. - # πŸ“š https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + analyse: + permissions: + security-events: write + name: Analyse + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + queries: +security-extended + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # ℹ️ Command-line programs to run using the OS shell. + # πŸ“š https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.vscode/settings.json b/.vscode/settings.json index 47584e5..8467bfa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { - "cSpell.words": ["Denormalized", "ILIKE", "nestjs", "Pgsql", "typeorm"], + "cSpell.words": ["Denormalized", "ILIKE", "metatype", "nestjs", "Pgsql", "typeorm"], "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/docker-compose.test.yml b/docker-compose.test.yml index f91e5b2..c08f068 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -12,8 +12,6 @@ services: - --collation-server=utf8mb4_unicode_ci ports: - '3306:3306' - volumes: - - 'mysql_data:/var/lib/mysql' postgresql: image: postgres:latest @@ -24,10 +22,3 @@ services: POSTGRES_PASSWORD: $POSTGRESQL_DATABASE_PASSWORD ports: - '5432:5432' - volumes: - - postgresql_data:/var/lib/postgresql/data -volumes: - mysql_data: - driver: local - postgresql_data: - driver: local diff --git a/spec/auth-guard/auth-guard.spec.ts b/spec/auth-guard/auth-guard.spec.ts index 344d57e..cb4bf06 100644 --- a/spec/auth-guard/auth-guard.spec.ts +++ b/spec/auth-guard/auth-guard.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('AuthGuard', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AuthGuardModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -19,7 +19,7 @@ describe('AuthGuard', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/author/author-object.spec.ts b/spec/author/author-object.spec.ts index 78a3c8a..62aa5c6 100644 --- a/spec/author/author-object.spec.ts +++ b/spec/author/author-object.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('Author - object', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([TestEntity])], }).compile(); @@ -16,7 +16,7 @@ describe('Author - object', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/author/author-value.spec.ts b/spec/author/author-value.spec.ts index 25df377..1aa3fac 100644 --- a/spec/author/author-value.spec.ts +++ b/spec/author/author-value.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('Author', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmMysqlModule([TestEntity])], }).compile(); @@ -16,7 +16,7 @@ describe('Author', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/author/author.interceptor.ts b/spec/author/author.interceptor.ts index e0f005a..9137b85 100644 --- a/spec/author/author.interceptor.ts +++ b/spec/author/author.interceptor.ts @@ -1,6 +1,5 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common'; import { Request } from 'express'; -import _ from 'lodash'; @Injectable() export class AuthorInterceptor implements NestInterceptor { diff --git a/spec/author/author.spec.ts b/spec/author/author.spec.ts index 520a563..3303c18 100644 --- a/spec/author/author.spec.ts +++ b/spec/author/author.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('Author', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmMysqlModule([TestEntity])], }).compile(); @@ -16,7 +16,7 @@ describe('Author', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.controller.create.spec.ts b/spec/base/base.controller.create.spec.ts index 5150e68..3beebbe 100644 --- a/spec/base/base.controller.create.spec.ts +++ b/spec/base/base.controller.create.spec.ts @@ -4,26 +4,25 @@ import request from 'supertest'; import { BaseEntity } from './base.entity'; import { BaseModule } from './base.module'; -import { BaseService } from './base.service'; import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - let service: BaseService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(BaseService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + beforeEach(async () => { + await BaseEntity.delete({}); + }); + + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.controller.delete.spec.ts b/spec/base/base.controller.delete.spec.ts index a15449d..0fd3663 100644 --- a/spec/base/base.controller.delete.spec.ts +++ b/spec/base/base.controller.delete.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -18,7 +18,11 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + beforeEach(async () => { + await BaseEntity.delete({}); + }); + + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.controller.read-many.spec.ts b/spec/base/base.controller.read-many.spec.ts index 273b570..56fb1b5 100644 --- a/spec/base/base.controller.read-many.spec.ts +++ b/spec/base/base.controller.read-many.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -17,7 +17,7 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.controller.read-one.spec.ts b/spec/base/base.controller.read-one.spec.ts index 8fe7e92..21f7ec6 100644 --- a/spec/base/base.controller.read-one.spec.ts +++ b/spec/base/base.controller.read-one.spec.ts @@ -11,7 +11,7 @@ describe('BaseController', () => { let app: INestApplication; let service: BaseService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -23,7 +23,7 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -36,14 +36,14 @@ describe('BaseController', () => { }); it('should be returned only one entity', async () => { - const response = await request(app.getHttpServer()) + const { body } = await request(app.getHttpServer()) .get(`/base/${id}`) - .query({ fields: ['id', 'name', 'createdAt'] }); + .query({ fields: ['id', 'name', 'createdAt'] }) + .expect(HttpStatus.OK); - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.body.id).toEqual(id); - expect(response.body.name).toEqual(expect.any(String)); - expect(response.body.lastModifiedAt).toBeUndefined(); + expect(body.id).toEqual(id); + expect(body.name).toEqual(expect.any(String)); + expect(body.lastModifiedAt).toBeUndefined(); }); it('should be fields feature with multiple options', async () => { diff --git a/spec/base/base.controller.recover.spec.ts b/spec/base/base.controller.recover.spec.ts index c36fe63..e65b249 100644 --- a/spec/base/base.controller.recover.spec.ts +++ b/spec/base/base.controller.recover.spec.ts @@ -4,29 +4,21 @@ import request from 'supertest'; import { BaseEntity } from './base.entity'; import { BaseModule } from './base.module'; -import { BaseService } from './base.service'; import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - let service: BaseService; - let entities: BaseEntity[]; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(BaseService); - entities = await Promise.all( - ['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name }))), - ); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -38,24 +30,22 @@ describe('BaseController', () => { }); it('recover the entity after delete', async () => { - const id = entities[0].id; + const name = 'name1'; + const created = await request(app.getHttpServer()).post('/base').send({ name }).expect(HttpStatus.CREATED); + const id = created.body.id; + await request(app.getHttpServer()).get(`/base/${id}`).expect(HttpStatus.OK); - // Delete await request(app.getHttpServer()).delete(`/base/${id}`).expect(HttpStatus.OK); - // getOne -> NotFOUND await request(app.getHttpServer()).get(`/base/${id}`).expect(HttpStatus.NOT_FOUND); - // getMany -> idκ°€ μ—†λ‹€. const { body } = await request(app.getHttpServer()).get('/base').expect(HttpStatus.OK); expect(body.data.some((entity: any) => entity.id === id)).toBeFalsy(); - // Recover const recoverResponse = await request(app.getHttpServer()).post(`/base/${id}/recover`).expect(HttpStatus.CREATED); expect(recoverResponse.body.deletedAt).toBeNull(); - // GetOne -> OK const getResponse = await request(app.getHttpServer()).get(`/base/${id}`).expect(HttpStatus.OK); expect(getResponse.body.deletedAt).toBeNull(); }); diff --git a/spec/base/base.controller.search.spec.ts b/spec/base/base.controller.search.spec.ts index 28c6023..07243ed 100644 --- a/spec/base/base.controller.search.spec.ts +++ b/spec/base/base.controller.search.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -17,7 +17,7 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.controller.spec.ts b/spec/base/base.controller.spec.ts index e389bed..5f80fb8 100644 --- a/spec/base/base.controller.spec.ts +++ b/spec/base/base.controller.spec.ts @@ -12,7 +12,7 @@ describe('BaseController', () => { let controller: BaseController; let service: BaseService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -25,7 +25,7 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.controller.swagger.spec.ts b/spec/base/base.controller.swagger.spec.ts index a8c5f4d..b9d591c 100644 --- a/spec/base/base.controller.swagger.spec.ts +++ b/spec/base/base.controller.swagger.spec.ts @@ -14,7 +14,7 @@ describe('BaseController Swagger Decorator', () => { let controller: BaseController; let routeSet: Record; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -29,13 +29,13 @@ describe('BaseController Swagger Decorator', () => { } as InstanceWrapper); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); it('should generate api operation and response to ReadOne method', () => { - const readOne = 'BaseController_reservedReadOne'; + const readOne = 'get /base/{id}'; expect(routeSet[readOne].responses).toEqual({ '200': { content: expect.any(Object), @@ -54,7 +54,8 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to ReadMany method', () => { - const readMany = 'BaseController_reservedReadMany'; + const readMany = 'get /base'; + expect(routeSet[readMany].responses).toEqual({ '200': { content: expect.any(Object), @@ -114,7 +115,7 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to Create method', () => { - const create = 'BaseController_reservedCreate'; + const create = 'post /base'; expect(routeSet[create].responses).toEqual({ '201': { description: 'Created ok', @@ -137,7 +138,8 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to Delete method', () => { - const deleteKey = 'BaseController_reservedDelete'; + const deleteKey = 'delete /base/{id}'; + expect(routeSet[deleteKey].responses).toEqual({ '200': { description: 'Deleted ok', @@ -160,7 +162,7 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to Update method', () => { - const updateKey = 'BaseController_reservedUpdate'; + const updateKey = 'patch /base/{id}'; expect(routeSet[updateKey].responses).toEqual({ '200': { description: 'Updated ok', @@ -183,7 +185,7 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to Upsert method', () => { - const updateKey = 'BaseController_reservedUpsert'; + const updateKey = 'put /base/{id}'; expect(routeSet[updateKey].responses).toEqual({ '200': { description: 'Upsert ok', @@ -208,7 +210,7 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to Recover method', () => { - const recover = 'BaseController_reservedRecover'; + const recover = 'post /base/{id}/recover'; expect(routeSet[recover].responses).toEqual({ '201': { description: 'Recovered ok', @@ -230,7 +232,7 @@ describe('BaseController Swagger Decorator', () => { }); it('should generate api operation and response to Search method', () => { - const search = 'BaseController_reservedSearch'; + const search = 'post /base/search'; expect(routeSet[search].responses).toEqual({ '200': { content: expect.any(Object), diff --git a/spec/base/base.controller.update.spec.ts b/spec/base/base.controller.update.spec.ts index 58a745b..b0cffb4 100644 --- a/spec/base/base.controller.update.spec.ts +++ b/spec/base/base.controller.update.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -18,7 +18,11 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + beforeEach(async () => { + await BaseEntity.delete({}); + }); + + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -52,7 +56,7 @@ describe('BaseController', () => { expect(created.statusCode).toEqual(HttpStatus.CREATED); const id = created.body.id; - await new Promise((res) => setTimeout(res, 1000)); + await new Promise((res) => setTimeout(res, 100)); await request(app.getHttpServer()).patch(`/base/${id}`).send({ name: 'name2' }).expect(HttpStatus.OK); const readOne = await request(app.getHttpServer()).get(`/base/${id}`).expect(HttpStatus.OK); diff --git a/spec/base/base.controller.upsert.spec.ts b/spec/base/base.controller.upsert.spec.ts index ba29a61..fd09f51 100644 --- a/spec/base/base.controller.upsert.spec.ts +++ b/spec/base/base.controller.upsert.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('BaseController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -18,7 +18,11 @@ describe('BaseController', () => { await app.init(); }); - afterEach(async () => { + beforeEach(async () => { + await BaseEntity.delete({}); + }); + + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/base/base.entity.ts b/spec/base/base.entity.ts index 13c8e38..2086a11 100644 --- a/spec/base/base.entity.ts +++ b/spec/base/base.entity.ts @@ -8,8 +8,8 @@ import { CrudAbstractEntity } from '../crud.abstract.entity'; @Entity('base') export class BaseEntity extends CrudAbstractEntity { @Column({ nullable: true }) - @IsString({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS] }) - @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT] }) + @IsString({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.PARAMS, GROUP.SEARCH] }) + @IsOptional({ groups: [GROUP.CREATE, GROUP.UPDATE, GROUP.READ_MANY, GROUP.UPSERT, GROUP.SEARCH] }) name: string; @Column({ nullable: true }) diff --git a/spec/custom-entity/custom-entity.controller.create.spec.ts b/spec/custom-entity/custom-entity.controller.create.spec.ts index 727c95a..6db74ca 100644 --- a/spec/custom-entity/custom-entity.controller.create.spec.ts +++ b/spec/custom-entity/custom-entity.controller.create.spec.ts @@ -4,26 +4,21 @@ import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; import { CustomEntityModule } from './custom-entity.module'; -import { CustomEntityService } from './custom-entity.service'; import { TestHelper } from '../test.helper'; describe('CustomEntity - Create', () => { let app: INestApplication; - let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(CustomEntityService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/custom-entity/custom-entity.controller.delete.spec.ts b/spec/custom-entity/custom-entity.controller.delete.spec.ts index 241623f..069b63f 100644 --- a/spec/custom-entity/custom-entity.controller.delete.spec.ts +++ b/spec/custom-entity/custom-entity.controller.delete.spec.ts @@ -4,26 +4,21 @@ import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; import { CustomEntityModule } from './custom-entity.module'; -import { CustomEntityService } from './custom-entity.service'; import { TestHelper } from '../test.helper'; describe('CustomEntity - Delete', () => { let app: INestApplication; - let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(CustomEntityService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -36,9 +31,9 @@ describe('CustomEntity - Delete', () => { it('removes one entity', async () => { const name = 'name1'; - const created = await request(app.getHttpServer()).post('/base').send({ name }); - expect(created.statusCode).toEqual(HttpStatus.CREATED); - const uuid = created.body.uuid; + const { + body: { uuid }, + } = await request(app.getHttpServer()).post('/base').send({ name }).expect(HttpStatus.CREATED); await request(app.getHttpServer()).delete(`/base/${uuid}`).expect(HttpStatus.OK); diff --git a/spec/custom-entity/custom-entity.controller.read-many.spec.ts b/spec/custom-entity/custom-entity.controller.read-many.spec.ts index 013886c..7698dc3 100644 --- a/spec/custom-entity/custom-entity.controller.read-many.spec.ts +++ b/spec/custom-entity/custom-entity.controller.read-many.spec.ts @@ -1,6 +1,5 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; @@ -12,7 +11,7 @@ describe('CustomEntity - ReadMany', () => { let app: INestApplication; let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); @@ -20,13 +19,15 @@ describe('CustomEntity - ReadMany', () => { service = moduleFixture.get(CustomEntityService); await Promise.all( - _.range(100).map((number) => service.repository.save(service.repository.create({ uuid: `${number}`, name: `name-${number}` }))), + Array.from({ length: 100 }, (_, index) => index).map((number) => + service.repository.save(service.repository.create({ uuid: `${number}`, name: `name-${number}` })), + ), ); await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -45,7 +46,6 @@ describe('CustomEntity - ReadMany', () => { expect(response.statusCode).toEqual(HttpStatus.OK); expect(response.body.data).toHaveLength(defaultLimit); - expect(response.body.metadata.nextCursor).toBeDefined(); expect(response.body.metadata.limit).toEqual(defaultLimit); expect(response.body.data[0].uuid).toBeDefined(); @@ -61,7 +61,6 @@ describe('CustomEntity - ReadMany', () => { expect(nextResponse.statusCode).toEqual(HttpStatus.OK); expect(nextResponse.body.data).toHaveLength(defaultLimit); - expect(nextResponse.body.metadata.nextCursor).toBeDefined(); expect(nextResponse.body.metadata.limit).toEqual(defaultLimit); expect(firstResponse.body.metadata.nextCursor).not.toEqual(nextResponse.body.metadata.nextCursor); diff --git a/spec/custom-entity/custom-entity.controller.read-one.spec.ts b/spec/custom-entity/custom-entity.controller.read-one.spec.ts index e712534..2255776 100644 --- a/spec/custom-entity/custom-entity.controller.read-one.spec.ts +++ b/spec/custom-entity/custom-entity.controller.read-one.spec.ts @@ -11,7 +11,7 @@ describe('CustomEntity - ReadOne', () => { let app: INestApplication; let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); @@ -23,14 +23,14 @@ describe('CustomEntity - ReadOne', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); describe('READ_ONE', () => { let id: string; - beforeEach(async () => { + beforeAll(async () => { id = (await service.getAll())?.[0]?.uuid; }); diff --git a/spec/custom-entity/custom-entity.controller.recover.spec.ts b/spec/custom-entity/custom-entity.controller.recover.spec.ts index f99d2c3..c6a94d0 100644 --- a/spec/custom-entity/custom-entity.controller.recover.spec.ts +++ b/spec/custom-entity/custom-entity.controller.recover.spec.ts @@ -4,29 +4,21 @@ import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; import { CustomEntityModule } from './custom-entity.module'; -import { CustomEntityService } from './custom-entity.service'; import { TestHelper } from '../test.helper'; describe('CustomEntity - Delete', () => { let app: INestApplication; - let service: CustomEntityService; - let entities: CustomEntity[]; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(CustomEntityService); - entities = await Promise.all( - ['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name }))), - ); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -38,23 +30,22 @@ describe('CustomEntity - Delete', () => { }); it('recover the entity after delete', async () => { - const uuid = entities[0].uuid; + const name = 'name1'; + const { + body: { uuid }, + } = await request(app.getHttpServer()).post('/base').send({ name }).expect(HttpStatus.CREATED); + await request(app.getHttpServer()).get(`/base/${uuid}`).expect(HttpStatus.OK); - // Delete await request(app.getHttpServer()).delete(`/base/${uuid}`).expect(HttpStatus.OK); - // getOne -> NotFOUND await request(app.getHttpServer()).get(`/base/${uuid}`).expect(HttpStatus.NOT_FOUND); - // getMany -> idκ°€ μ—†λ‹€. const { body } = await request(app.getHttpServer()).get('/base').expect(HttpStatus.OK); expect(body.data.some((entity: any) => entity.uuid === uuid)).toBeFalsy(); - // Recover await request(app.getHttpServer()).post(`/base/${uuid}/recover`).expect(HttpStatus.CREATED); - // GetOne -> OK await request(app.getHttpServer()).get(`/base/${uuid}`).expect(HttpStatus.OK); }); }); diff --git a/spec/custom-entity/custom-entity.controller.search.spec.ts b/spec/custom-entity/custom-entity.controller.search.spec.ts index 04bcfb0..64ba92d 100644 --- a/spec/custom-entity/custom-entity.controller.search.spec.ts +++ b/spec/custom-entity/custom-entity.controller.search.spec.ts @@ -1,6 +1,5 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; @@ -12,7 +11,7 @@ describe('CustomEntity - Search', () => { let app: INestApplication; let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); @@ -20,14 +19,16 @@ describe('CustomEntity - Search', () => { service = moduleFixture.get(CustomEntityService); await Promise.all( - _.range(100).map((number) => service.repository.save(service.repository.create({ uuid: `${number}`, name: `name-${number}` }))), + Array.from({ length: 100 }, (_, index) => index).map((number) => + service.repository.save(service.repository.create({ uuid: `${number}`, name: `name-${number}` })), + ), ); await service.repository.save(service.repository.create({ uuid: 'test' })); await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/custom-entity/custom-entity.controller.update.spec.ts b/spec/custom-entity/custom-entity.controller.update.spec.ts index fb4b431..89a1fd4 100644 --- a/spec/custom-entity/custom-entity.controller.update.spec.ts +++ b/spec/custom-entity/custom-entity.controller.update.spec.ts @@ -4,26 +4,21 @@ import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; import { CustomEntityModule } from './custom-entity.module'; -import { CustomEntityService } from './custom-entity.service'; import { TestHelper } from '../test.helper'; describe('CustomEntity - Update', () => { let app: INestApplication; - let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(CustomEntityService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/custom-entity/custom-entity.controller.upsert.spec.ts b/spec/custom-entity/custom-entity.controller.upsert.spec.ts index 14e1c71..e7ba9ac 100644 --- a/spec/custom-entity/custom-entity.controller.upsert.spec.ts +++ b/spec/custom-entity/custom-entity.controller.upsert.spec.ts @@ -4,26 +4,21 @@ import request from 'supertest'; import { CustomEntity } from './custom-entity.entity'; import { CustomEntityModule } from './custom-entity.module'; -import { CustomEntityService } from './custom-entity.service'; import { TestHelper } from '../test.helper'; describe('CustomEntity - Upsert', () => { let app: INestApplication; - let service: CustomEntityService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [CustomEntityModule, TestHelper.getTypeOrmMysqlModule([CustomEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(CustomEntityService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/custom-swagger-decorator/apply-api-extra-model.spec.ts b/spec/custom-swagger-decorator/apply-api-extra-model.spec.ts index 702db9c..92dbb4b 100644 --- a/spec/custom-swagger-decorator/apply-api-extra-model.spec.ts +++ b/spec/custom-swagger-decorator/apply-api-extra-model.spec.ts @@ -10,7 +10,7 @@ import { TestHelper } from '../test.helper'; describe('Apply ApiExtraModels Decorator', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [DynamicCrudModule({ readMany: { decorators: [ApiExtraModels(ExtraModel)] } })], }).compile(); @@ -20,7 +20,7 @@ describe('Apply ApiExtraModels Decorator', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/custom-swagger-decorator/without-custom-decorator.spec.ts b/spec/custom-swagger-decorator/without-custom-decorator.spec.ts index d68d524..cd4c2dd 100644 --- a/spec/custom-swagger-decorator/without-custom-decorator.spec.ts +++ b/spec/custom-swagger-decorator/without-custom-decorator.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('No custom Swagger Decorator', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [DynamicCrudModule({ readMany: { decorators: [] } })], }).compile(); @@ -19,7 +19,7 @@ describe('No custom Swagger Decorator', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/exclude-swagger/exclude-swagger.spec.ts b/spec/exclude-swagger/exclude-swagger.spec.ts index b6b8d97..62c0f6b 100644 --- a/spec/exclude-swagger/exclude-swagger.spec.ts +++ b/spec/exclude-swagger/exclude-swagger.spec.ts @@ -14,7 +14,7 @@ describe('exclude swagger by route', () => { let controller: ExcludeSwaggerController; let routeSet: Record; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ExcludeSwaggerModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -30,18 +30,18 @@ describe('exclude swagger by route', () => { } as InstanceWrapper); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); it('should not generate recover route in swagger', async () => { - const recover = 'HideSwaggerController_reservedRecover'; + const recover = 'post /exclude-swagger/{id}/recover'; expect(routeSet[recover]).toBeUndefined(); }); it('Should be changed swagger readOne response interface', () => { - const readOne = 'ExcludeSwaggerController_reservedReadOne'; + const readOne = 'get /exclude-swagger/{id}'; expect(routeSet[readOne].responses).toEqual({ '200': { content: { @@ -64,7 +64,7 @@ describe('exclude swagger by route', () => { }); it('Should be changed swagger update response interface', () => { - const update = 'ExcludeSwaggerController_reservedUpdate'; + const update = 'patch /exclude-swagger/{id}'; expect(routeSet[update].responses).toEqual({ '200': { content: { @@ -87,7 +87,7 @@ describe('exclude swagger by route', () => { }); it('Should be changed swagger Create request body interface', () => { - const create = 'ExcludeSwaggerController_reservedCreate'; + const create = 'post /exclude-swagger'; expect(routeSet[create].root).toEqual({ method: 'post', path: '/exclude-swagger', diff --git a/spec/logging/logging.spec.ts b/spec/logging/logging.spec.ts index 5975580..2cb8c5a 100644 --- a/spec/logging/logging.spec.ts +++ b/spec/logging/logging.spec.ts @@ -4,7 +4,6 @@ import { Controller, Injectable, Module } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; import { IsOptional } from 'class-validator'; -import _ from 'lodash'; import request from 'supertest'; import { Entity, BaseEntity, Repository, PrimaryColumn, Column, DeleteDateColumn } from 'typeorm'; @@ -53,7 +52,7 @@ describe('Logging', () => { let app: INestApplication; let loggerSpy: jest.SpyInstance; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([TestEntity])], }) @@ -65,7 +64,7 @@ describe('Logging', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -84,11 +83,11 @@ describe('Logging', () => { _findOptions: { where: {}, take: 20, - withDeleted: false, order: { col1: 'DESC' }, + withDeleted: false, relations: [], }, - _pagination: { type: 'cursor' }, + _pagination: { _isNext: false, type: 'cursor', _where: btoa('{}') }, _sort: 'DESC', }), 'CRUD GET /base', diff --git a/spec/mongodb/mongodb.spec.ts b/spec/mongodb/mongodb.spec.ts index 255a464..69750d2 100644 --- a/spec/mongodb/mongodb.spec.ts +++ b/spec/mongodb/mongodb.spec.ts @@ -66,7 +66,7 @@ describe('mongodb', () => { let mongod: MongoMemoryServer; let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { mongod = await MongoMemoryServer.create(); const mongoUri = mongod.getUri(); @@ -77,7 +77,7 @@ describe('mongodb', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await mongod?.stop(); await app?.close(); }); diff --git a/spec/multiple-primary-key/multiple-primary-key.controller.create.spec.ts b/spec/multiple-primary-key/multiple-primary-key.controller.create.spec.ts index f8beffe..d3e3717 100644 --- a/spec/multiple-primary-key/multiple-primary-key.controller.create.spec.ts +++ b/spec/multiple-primary-key/multiple-primary-key.controller.create.spec.ts @@ -4,26 +4,21 @@ import request from 'supertest'; import { MultiplePrimaryKeyEntity } from './multiple-primary-key.entity'; import { MultiplePrimaryKeyModule } from './multiple-primary-key.module'; -import { MultiplePrimaryKeyService } from './multiple-primary-key.service'; import { TestHelper } from '../test.helper'; describe('MultiplePrimaryKey - Create', () => { let app: INestApplication; - let service: MultiplePrimaryKeyService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [MultiplePrimaryKeyModule, TestHelper.getTypeOrmMysqlModule([MultiplePrimaryKeyEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(MultiplePrimaryKeyService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/multiple-primary-key/multiple-primary-key.controller.delete.spec.ts b/spec/multiple-primary-key/multiple-primary-key.controller.delete.spec.ts index 69aee8e..028826a 100644 --- a/spec/multiple-primary-key/multiple-primary-key.controller.delete.spec.ts +++ b/spec/multiple-primary-key/multiple-primary-key.controller.delete.spec.ts @@ -4,26 +4,21 @@ import request from 'supertest'; import { MultiplePrimaryKeyEntity } from './multiple-primary-key.entity'; import { MultiplePrimaryKeyModule } from './multiple-primary-key.module'; -import { MultiplePrimaryKeyService } from './multiple-primary-key.service'; import { TestHelper } from '../test.helper'; describe('MultiplePrimaryKey - Delete', () => { let app: INestApplication; - let service: MultiplePrimaryKeyService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [MultiplePrimaryKeyModule, TestHelper.getTypeOrmMysqlModule([MultiplePrimaryKeyEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(MultiplePrimaryKeyService); - await Promise.all(['name1', 'name2'].map((name: string) => service.repository.save(service.repository.create({ name })))); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/multiple-primary-key/multiple-primary-key.controller.read-one.spec.ts b/spec/multiple-primary-key/multiple-primary-key.controller.read-one.spec.ts index 8029504..0b9233b 100644 --- a/spec/multiple-primary-key/multiple-primary-key.controller.read-one.spec.ts +++ b/spec/multiple-primary-key/multiple-primary-key.controller.read-one.spec.ts @@ -11,7 +11,7 @@ describe('MultiplePrimaryKey - ReadOne', () => { let app: INestApplication; let service: MultiplePrimaryKeyService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [MultiplePrimaryKeyModule, TestHelper.getTypeOrmMysqlModule([MultiplePrimaryKeyEntity])], }).compile(); @@ -23,14 +23,14 @@ describe('MultiplePrimaryKey - ReadOne', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); describe('READ_ONE', () => { let entity: MultiplePrimaryKeyEntity; - beforeEach(async () => { + beforeAll(async () => { entity = (await service.getAll())?.[0]; }); diff --git a/spec/multiple-primary-key/multiple-primary-key.controller.recover.spec.ts b/spec/multiple-primary-key/multiple-primary-key.controller.recover.spec.ts index d94fd6c..655e23a 100644 --- a/spec/multiple-primary-key/multiple-primary-key.controller.recover.spec.ts +++ b/spec/multiple-primary-key/multiple-primary-key.controller.recover.spec.ts @@ -12,7 +12,7 @@ describe('MultiplePrimaryKey - Recover', () => { let service: MultiplePrimaryKeyService; let entities: MultiplePrimaryKeyEntity[]; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [MultiplePrimaryKeyModule, TestHelper.getTypeOrmMysqlModule([MultiplePrimaryKeyEntity])], }).compile(); @@ -26,7 +26,7 @@ describe('MultiplePrimaryKey - Recover', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -41,20 +41,15 @@ describe('MultiplePrimaryKey - Recover', () => { const { uuid1, uuid2 } = entities[0]; await request(app.getHttpServer()).get(`/base/${uuid1}/${uuid2}`).expect(HttpStatus.OK); - // Delete await request(app.getHttpServer()).delete(`/base/${uuid1}/${uuid2}`).expect(HttpStatus.OK); - // getOne -> NotFOUND await request(app.getHttpServer()).get(`/base/${uuid1}/${uuid2}`).expect(HttpStatus.NOT_FOUND); - // getMany -> idκ°€ μ—†λ‹€. const { body } = await request(app.getHttpServer()).get('/base').expect(HttpStatus.OK); expect(body.data.some((entity: MultiplePrimaryKeyEntity) => entity.uuid1 === uuid1 && entity.uuid2 === uuid2)).toBeFalsy(); - // Recover await request(app.getHttpServer()).post(`/base/${uuid1}/${uuid2}/recover`).expect(HttpStatus.CREATED); - // GetOne -> OK await request(app.getHttpServer()).get(`/base/${uuid1}/${uuid2}`).expect(HttpStatus.OK); }); }); diff --git a/spec/multiple-primary-key/multiple-primary-key.controller.upsert.spec.ts b/spec/multiple-primary-key/multiple-primary-key.controller.upsert.spec.ts index 5ebdee3..ff1f4d6 100644 --- a/spec/multiple-primary-key/multiple-primary-key.controller.upsert.spec.ts +++ b/spec/multiple-primary-key/multiple-primary-key.controller.upsert.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('MultiplePrimaryKey - Upsert', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [MultiplePrimaryKeyModule, TestHelper.getTypeOrmMysqlModule([MultiplePrimaryKeyEntity])], }).compile(); @@ -18,7 +18,7 @@ describe('MultiplePrimaryKey - Upsert', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/pagination/pagination.interceptor.spec.ts b/spec/pagination/pagination.interceptor.spec.ts index f21cab4..042d4b1 100644 --- a/spec/pagination/pagination.interceptor.spec.ts +++ b/spec/pagination/pagination.interceptor.spec.ts @@ -1,6 +1,5 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { PaginationModule } from './pagination.module'; @@ -14,7 +13,7 @@ describe('Pagination with interceptor', () => { describe('with ReadMany Interceptor', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ PaginationModule({ @@ -36,17 +35,21 @@ describe('Pagination with interceptor', () => { app = moduleFixture.createNestApplication(); const service: BaseService = moduleFixture.get(BaseService); - await Promise.all(_.range(100).map((number) => service.repository.save(service.repository.create({ name: `name-${number}` })))); + await Promise.all( + Array.from({ length: 100 }, (_, index) => index).map((number) => + service.repository.save(service.repository.create({ name: `name-${number}` })), + ), + ); await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); it('should be returned deleted entities each interceptor soft-deleted option', async () => { - const deleteIdList: number[] = _.range(1, 101).filter((number) => number % 2 === 0); + const deleteIdList: number[] = Array.from({ length: 100 }, (_, index) => index + 1).filter((number) => number % 2 === 0); const { body: responseBodyBeforeDelete } = await request(app.getHttpServer()) .get(`/${PaginationType.CURSOR}`) .expect(HttpStatus.OK); @@ -64,8 +67,11 @@ describe('Pagination with interceptor', () => { const { body: responseBodyAfterDelete, - }: { body: { data: Array<{ id: number; deletedAt?: unknown }>; metadata: { nextCursor: unknown; query: unknown } } } = - await request(app.getHttpServer()).get(`/${PaginationType.CURSOR}`).expect(HttpStatus.OK); + }: { body: { data: Array<{ id: number; deletedAt?: unknown }>; metadata: { nextCursor: unknown } } } = await request( + app.getHttpServer(), + ) + .get(`/${PaginationType.CURSOR}`) + .expect(HttpStatus.OK); const { body: offsetResponseBodyAfterDelete } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .expect(HttpStatus.OK); @@ -89,13 +95,12 @@ describe('Pagination with interceptor', () => { .get(`/${PaginationType.CURSOR}`) .query({ nextCursor: responseBodyAfterDelete.metadata.nextCursor, - query: responseBodyAfterDelete.metadata.query, }) .expect(HttpStatus.OK); const { body: offsetNextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ - query: offsetResponseBodyAfterDelete.metadata.query, + nextCursor: offsetResponseBodyAfterDelete.metadata.nextCursor, offset: offsetResponseBodyAfterDelete.metadata.offset, }) .expect(HttpStatus.OK); @@ -114,7 +119,7 @@ describe('Pagination with interceptor', () => { describe('without ReadMany Interceptor', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ PaginationModule({ @@ -136,11 +141,15 @@ describe('Pagination with interceptor', () => { app = moduleFixture.createNestApplication(); const service: BaseService = moduleFixture.get(BaseService); - await Promise.all(_.range(100).map((number) => service.repository.save(service.repository.create({ name: `name-${number}` })))); + await Promise.all( + Array.from({ length: 100 }, (_, index) => index).map((number) => + service.repository.save(service.repository.create({ name: `name-${number}` })), + ), + ); await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/pagination/pagination.spec.ts b/spec/pagination/pagination.spec.ts index 81f7bcd..f7b47b7 100644 --- a/spec/pagination/pagination.spec.ts +++ b/spec/pagination/pagination.spec.ts @@ -1,6 +1,5 @@ import { ConsoleLogger, HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { PaginationModule } from './pagination.module'; @@ -14,14 +13,14 @@ describe('Pagination', () => { const totalCount = 100; const defaultLimit = 20; - beforeEach(async () => { + beforeAll(async () => { const logger = new ConsoleLogger(); logger.setLogLevels(['error']); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ PaginationModule({ - cursor: { readMany: { paginationType: 'cursor' } }, - offset: { readMany: { paginationType: 'offset' } }, + cursor: { readMany: { paginationType: 'cursor' }, search: { paginationType: 'cursor' } }, + offset: { readMany: { paginationType: 'offset' }, search: { paginationType: 'offset' } }, }), ], }) @@ -30,13 +29,19 @@ describe('Pagination', () => { app = moduleFixture.createNestApplication(); service = moduleFixture.get(BaseService); + await app.init(); + }); + + beforeEach(async () => { + await service.repository.delete({}); await Promise.all( - _.range(totalCount).map((number) => service.repository.save(service.repository.create({ name: `name-${number}` }))), + Array.from({ length: totalCount }, (_, index) => index).map((number) => + service.repository.save(service.repository.create({ name: `name-${number}` })), + ), ); - await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -67,22 +72,20 @@ describe('Pagination', () => { expect(cursorResponseBody.metadata).toEqual({ nextCursor: expect.any(String), limit: defaultLimit, - query: expect.any(String), total: 1, }); - expect(offsetResponseBody.metadata).toEqual({ page: 1, pages: 1, total: 1, offset: 1, query: expect.any(String) }); + expect(offsetResponseBody.metadata).toEqual({ page: 1, pages: 1, total: 1, offset: 1, nextCursor: expect.any(String) }); const { body: nextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.CURSOR}`) .query({ nextCursor: cursorResponseBody.metadata.nextCursor, - query: cursorResponseBody.metadata.query, }) .expect(HttpStatus.OK); const { body: offsetNextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ - query: offsetResponseBody.metadata.query, + nextCursor: offsetResponseBody.metadata.nextCursor, offset: offsetResponseBody.metadata.offset, }) .expect(HttpStatus.OK); @@ -91,8 +94,7 @@ describe('Pagination', () => { expect(nextResponseBody.data).toHaveLength(0); expect(nextResponseBody.metadata.nextCursor).not.toEqual(cursorResponseBody.metadata.nextCursor); expect(nextResponseBody.metadata.limit).toEqual(20); - expect(nextResponseBody.metadata.query).toEqual(cursorResponseBody.metadata.query); - expect(offsetNextResponseBody.metadata).toEqual({ page: 1, pages: 1, total: 1, offset: 1, query: expect.any(String) }); + expect(offsetNextResponseBody.metadata).toEqual({ page: 1, pages: 1, total: 1, offset: 1, nextCursor: expect.any(String) }); }); describe('Cursor', () => { @@ -103,7 +105,6 @@ describe('Pagination', () => { expect(cursorBody.metadata).toEqual({ nextCursor: expect.any(String), limit: defaultLimit, - query: expect.any(String), total: totalCount, }); }); @@ -115,7 +116,6 @@ describe('Pagination', () => { .get(`/${PaginationType.CURSOR}`) .query({ nextCursor: firstResponseBody.metadata.nextCursor, - query: firstResponseBody.metadata.query, }) .expect(HttpStatus.OK); @@ -125,7 +125,6 @@ describe('Pagination', () => { expect(nextResponseBody.metadata).toEqual({ nextCursor: expect.any(String), limit: defaultLimit, - query: expect.any(String), total: totalCount - defaultLimit, }); @@ -137,7 +136,7 @@ describe('Pagination', () => { it('should be keep query condition to next request', async () => { await Promise.all( - _.range(100).map((_n) => + Array.from({ length: 100 }).map((_) => request(app.getHttpServer()).post(`/${PaginationType.CURSOR}`).send({ name: 'same name' }).expect(HttpStatus.CREATED), ), ); @@ -153,7 +152,6 @@ describe('Pagination', () => { expect(responseBody.metadata).toEqual({ nextCursor: expect.any(String), limit: defaultLimit, - query: expect.any(String), total: totalCount, }); @@ -161,7 +159,6 @@ describe('Pagination', () => { .get(`/${PaginationType.CURSOR}`) .query({ nextCursor: responseBody.metadata.nextCursor, - query: responseBody.metadata.query, }) .expect(HttpStatus.OK); @@ -169,7 +166,6 @@ describe('Pagination', () => { expect(nextResponseBody.metadata).toEqual({ nextCursor: expect.any(String), limit: defaultLimit, - query: responseBody.metadata.query, total: totalCount - defaultLimit, }); expect(nextResponseBody.metadata.nextCursor).not.toEqual(responseBody.metadata.nextCursor); @@ -181,19 +177,6 @@ describe('Pagination', () => { expect((responseBody.data as Array<{ id: string }>).some(({ id }) => nextDataIds.has(id))).not.toBeTruthy(); }); - it('should throw when offset pagination query provided', async () => { - const { body: responseBody } = await request(app.getHttpServer()).get(`/${PaginationType.OFFSET}`).expect(HttpStatus.OK); - - await request(app.getHttpServer()) - .get(`/${PaginationType.CURSOR}`) - .query({ - query: responseBody.metadata.query, - offset: responseBody.metadata.offset, - limit: 15, - }) - .expect(HttpStatus.UNPROCESSABLE_ENTITY); - }); - it('should be calculate the number of entities', async () => { const { body: { metadata: metadataAll }, @@ -216,16 +199,20 @@ describe('Pagination', () => { it('should return 20 entities as default', async () => { const { body } = await request(app.getHttpServer()).get(`/${PaginationType.OFFSET}`).expect(HttpStatus.OK); expect(body.data).toHaveLength(defaultLimit); - expect(body.metadata).toEqual({ page: 1, pages: 5, total: 100, offset: defaultLimit, query: expect.any(String) }); + expect(body.metadata).toEqual({ page: 1, pages: 5, total: 100, offset: defaultLimit, nextCursor: expect.any(String) }); + + const { body: searchBody } = await request(app.getHttpServer()).post(`/${PaginationType.OFFSET}/search`).expect(HttpStatus.OK); + expect(searchBody.data).toHaveLength(defaultLimit); + expect(searchBody.metadata).toEqual({ page: 1, pages: 5, total: 100, offset: defaultLimit, nextCursor: expect.any(String) }); }); - it('should return next page from offset', async () => { + it('should return next page from offset on readMany', async () => { const { body: firstResponseBody } = await request(app.getHttpServer()).get(`/${PaginationType.OFFSET}`).expect(HttpStatus.OK); const { body: nextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ - query: firstResponseBody.metadata.query, + nextCursor: firstResponseBody.metadata.nextCursor, offset: firstResponseBody.metadata.offset, }) .expect(HttpStatus.OK); @@ -235,7 +222,7 @@ describe('Pagination', () => { pages: 5, total: 100, offset: defaultLimit, - query: expect.any(String), + nextCursor: expect.any(String), }); expect(nextResponseBody.metadata).toEqual({ @@ -243,31 +230,119 @@ describe('Pagination', () => { pages: 5, total: 100, offset: defaultLimit * 2, - query: expect.any(String), + nextCursor: expect.any(String), + }); + }); + + it('should return next page from offset on search', async () => { + const { body: firstResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.OFFSET}/search`) + .expect(HttpStatus.OK); + + const { body: nextResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.OFFSET}/search`) + .send({ + nextCursor: firstResponseBody.metadata.nextCursor, + offset: firstResponseBody.metadata.offset, + }) + .expect(HttpStatus.OK); + + expect(firstResponseBody.metadata).toEqual({ + page: 1, + pages: 5, + total: 100, + offset: defaultLimit, + nextCursor: expect.any(String), + }); + + expect(nextResponseBody.metadata).toEqual({ + page: 2, + pages: 5, + total: 100, + offset: defaultLimit * 2, + nextCursor: expect.any(String), }); }); it('should be keep query condition to next request', async () => { await Promise.all( - _.range(100).map((_n) => + Array.from({ length: 100 }).map((_) => request(app.getHttpServer()).post(`/${PaginationType.OFFSET}`).send({ name: 'same name' }).expect(HttpStatus.CREATED), ), ); - const { body: responseBody } = await request(app.getHttpServer()) + // readMany + const { body: readManyResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ name: 'same name' }) .expect(HttpStatus.OK); - const { body: nextResponseBody } = await request(app.getHttpServer()) + const { body: readManyNextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ - query: responseBody.metadata.query, - offset: responseBody.metadata.offset, + nextCursor: readManyResponseBody.metadata.nextCursor, + offset: readManyResponseBody.metadata.offset, }) .expect(HttpStatus.OK); - expect(nextResponseBody.metadata).toHaveProperty('query', responseBody.metadata.query); + expect(readManyResponseBody.metadata).toEqual({ + page: 1, + pages: 5, + total: 100, + offset: 20, + nextCursor: expect.any(String), + }); + expect(readManyNextResponseBody.metadata).toEqual({ + page: 2, + pages: 5, + total: 100, + offset: 40, + nextCursor: expect.any(String), + }); + + // search + const { body: searchResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.OFFSET}/search`) + .send({ where: [{ name: { operator: '=', operand: 'same name' } }] }) + .expect(HttpStatus.OK); + const searchDataSet = new Set(); + for (const data of searchResponseBody.data) { + searchDataSet.add(data.id); + expect(data.name).toEqual('same name'); + } + + const { body: searchNextResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.OFFSET}/search`) + .send({ nextCursor: searchResponseBody.metadata.nextCursor, offset: searchResponseBody.metadata.offset }) + .expect(HttpStatus.OK); + expect(searchNextResponseBody.metadata).toEqual({ + page: 2, + pages: 5, + total: 100, + offset: 40, + nextCursor: expect.any(String), + }); + + for (const data of searchNextResponseBody.data) { + expect(data.name).toEqual('same name'); + expect(searchDataSet.has(data.id)).not.toBeTruthy(); + } + + const { body: searchNextNextResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.OFFSET}/search`) + .send({ nextCursor: searchNextResponseBody.metadata.nextCursor, offset: searchNextResponseBody.metadata.offset }) + .expect(HttpStatus.OK); + expect(searchNextNextResponseBody.metadata).toEqual({ + page: 3, + pages: 5, + total: 100, + offset: 60, + nextCursor: expect.any(String), + }); + for (const data of searchNextResponseBody.data) { + expect(data.name).toEqual('same name'); + expect(searchDataSet.has(data.id)).not.toBeTruthy(); + } }); it('should be able to set limit only at first request', async () => { @@ -277,12 +352,12 @@ describe('Pagination', () => { .query({ limit }) .expect(HttpStatus.OK); expect(responseBody.data).toHaveLength(limit); - expect(responseBody.metadata).toEqual({ page: 1, pages: 7, total: 100, offset: limit, query: expect.any(String) }); + expect(responseBody.metadata).toEqual({ page: 1, pages: 7, total: 100, offset: limit, nextCursor: expect.any(String) }); const { body: nextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ - query: responseBody.metadata.query, + nextCursor: responseBody.metadata.nextCursor, offset: responseBody.metadata.offset, limit, }) @@ -293,7 +368,7 @@ describe('Pagination', () => { pages: 7, total: 100, offset: limit * 2, - query: responseBody.metadata.query, + nextCursor: expect.any(String), }); }); @@ -303,7 +378,7 @@ describe('Pagination', () => { const { body: nextResponseBody } = await request(app.getHttpServer()) .get(`/${PaginationType.OFFSET}`) .query({ - query: firstResponseBody.metadata.query, + nextCursor: firstResponseBody.metadata.nextCursor, offset: firstResponseBody.metadata.offset, limit, }) @@ -311,13 +386,19 @@ describe('Pagination', () => { expect(firstResponseBody.data).toHaveLength(defaultLimit); expect(nextResponseBody.data).toHaveLength(limit); - expect(firstResponseBody.metadata).toEqual({ page: 1, pages: 5, total: 100, offset: defaultLimit, query: expect.any(String) }); + expect(firstResponseBody.metadata).toEqual({ + page: 1, + pages: 5, + total: 100, + offset: defaultLimit, + nextCursor: expect.any(String), + }); expect(nextResponseBody.metadata).toEqual({ page: 2, pages: Math.ceil(100 / limit), total: 100, offset: defaultLimit + limit, - query: firstResponseBody.metadata.query, + nextCursor: expect.any(String), }); const lastOneOfFirstResponse = firstResponseBody.data.pop(); diff --git a/spec/param-option/param-option.entity-key.spec.ts b/spec/param-option/param-option.entity-key.spec.ts index dd9ca57..05f0d62 100644 --- a/spec/param-option/param-option.entity-key.spec.ts +++ b/spec/param-option/param-option.entity-key.spec.ts @@ -1,16 +1,15 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { DynamicCrudModule } from '../dynamic-crud.module'; import { TestHelper } from '../test.helper'; -describe('Params Option - entity의 keyλ₯Ό params으둜 μ‚¬μš©ν•˜λŠ” 경우', () => { +describe('Params Option - When using the entity`s key as params', () => { let app: INestApplication; const param = 'name'; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ DynamicCrudModule({ @@ -26,7 +25,7 @@ describe('Params Option - entity의 keyλ₯Ό params으둜 μ‚¬μš©ν•˜λŠ” 경우', () await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -45,7 +44,7 @@ describe('Params Option - entity의 keyλ₯Ό params으둜 μ‚¬μš©ν•˜λŠ” 경우', () const names = ['name1', 'name1', 'name2', 'name2', 'name3', 'name1']; await Promise.all( - _.range(0, names.length).map((index) => + Array.from({ length: names.length }, (_, index) => index).map((index) => request(app.getHttpServer()).post('/base').send({ name: names[index] }).expect(HttpStatus.CREATED), ), ); diff --git a/spec/param-option/param-option.non-entity-key.spec.ts b/spec/param-option/param-option.non-entity-key.spec.ts index d26628a..07b4ffb 100644 --- a/spec/param-option/param-option.non-entity-key.spec.ts +++ b/spec/param-option/param-option.non-entity-key.spec.ts @@ -1,6 +1,5 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { DynamicCrudModule } from '../dynamic-crud.module'; @@ -10,7 +9,7 @@ describe('Params Option - used as params instead of key of entity', () => { let app: INestApplication; const param = 'unknownProperty'; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ DynamicCrudModule({ @@ -26,7 +25,7 @@ describe('Params Option - used as params instead of key of entity', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -45,7 +44,7 @@ describe('Params Option - used as params instead of key of entity', () => { const names = ['name1', 'name1', 'name2', 'name2', 'name3', 'name1']; await Promise.all( - _.range(0, names.length).map((index) => + Array.from({ length: names.length }, (_, index) => index).map((index) => request(app.getHttpServer()).post('/base').send({ name: names[index] }).expect(HttpStatus.CREATED), ), ); diff --git a/spec/param-option/param-option.with-interceptor.spec.ts b/spec/param-option/param-option.with-interceptor.spec.ts index 05e3842..9fba432 100644 --- a/spec/param-option/param-option.with-interceptor.spec.ts +++ b/spec/param-option/param-option.with-interceptor.spec.ts @@ -1,6 +1,5 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { ParamsInterceptor } from './params.interceptor'; @@ -11,7 +10,7 @@ describe('Params Option - changing params to Interceptor', () => { let app: INestApplication; const param = 'custom'; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ DynamicCrudModule({ @@ -27,7 +26,7 @@ describe('Params Option - changing params to Interceptor', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -46,7 +45,7 @@ describe('Params Option - changing params to Interceptor', () => { const names = ['name1', 'name1', 'name2', 'name2', 'name3', 'name1']; await Promise.all( - _.range(0, names.length).map((index) => + Array.from({ length: names.length }, (_, index) => index).map((index) => request(app.getHttpServer()).post('/base').send({ name: names[index] }).expect(HttpStatus.CREATED), ), ); diff --git a/spec/pgsql/pgsql.spec.ts b/spec/pgsql/pgsql.spec.ts index fbd04e0..5e61966 100644 --- a/spec/pgsql/pgsql.spec.ts +++ b/spec/pgsql/pgsql.spec.ts @@ -59,7 +59,7 @@ class TestModule {} describe('Search complex conditions', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([TestEntity])], }).compile(); @@ -79,7 +79,7 @@ describe('Search complex conditions', () => { ); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/read-many/read-many.controller.spec.ts b/spec/read-many/read-many.controller.spec.ts index 2ab3392..152920f 100644 --- a/spec/read-many/read-many.controller.spec.ts +++ b/spec/read-many/read-many.controller.spec.ts @@ -1,6 +1,5 @@ import { ConsoleLogger, HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { ReadManyModule } from './read-many.module'; @@ -13,7 +12,7 @@ describe('ReadMany - Options', () => { let service: BaseService; const defaultLimit = 20; - beforeEach(async () => { + beforeAll(async () => { const logger = new ConsoleLogger(); logger.setLogLevels(['error']); const moduleFixture: TestingModule = await Test.createTestingModule({ @@ -24,12 +23,16 @@ describe('ReadMany - Options', () => { app = moduleFixture.createNestApplication(); service = moduleFixture.get(BaseService); - await Promise.all(_.range(100).map((number) => service.repository.save(service.repository.create({ name: `name-${number}` })))); + await Promise.all( + Array.from({ length: 100 }, (_, index) => index).map((number) => + service.repository.save(service.repository.create({ name: `name-${number}` })), + ), + ); await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -107,11 +110,9 @@ describe('ReadMany - Options', () => { }) .expect(HttpStatus.OK); - expect(nextResponse.metadata.query).toEqual(firstResponse.metadata.query); - expect(secondNextResponse.metadata.query).toEqual(firstResponse.metadata.query); - expect(nextResponse.metadata.nextCursor).not.toEqual(firstResponse.metadata.nextCursor); - expect(secondNextResponse.metadata.nextCursor).not.toEqual(nextResponse.metadata.nextCursor); + expect(secondNextResponse.metadata.nextCursor).toEqual(expect.any(String)); + expect(secondNextResponse.metadata.nextCursor).not.toEqual(firstResponse.metadata.nextCursor); expect(nextResponse.metadata.total).toEqual(firstResponse.metadata.total - nextResponse.metadata.limit); expect(secondNextResponse.metadata.total).toEqual(nextResponse.metadata.total - secondNextResponse.metadata.limit); diff --git a/spec/relation-entities/README.ko.md b/spec/relation-entities/README.ko.md new file mode 100644 index 0000000..d38191a --- /dev/null +++ b/spec/relation-entities/README.ko.md @@ -0,0 +1,13 @@ +### Relation Test Case + +λ‹€μŒμ˜ Entityκ°€ μžˆμŠ΅λ‹ˆλ‹€. + +- writerEntity: μž‘μ„±μž 정보λ₯Ό κ΄€λ¦¬ν•©λ‹ˆλ‹€. +- categoryEntity: μ§ˆλ¬ΈκΈ€μ˜ μ’…λ₯˜λ₯Ό κ΄€λ¦¬ν•©λ‹ˆλ‹€. +- questionEntity: μ§ˆλ¬ΈκΈ€μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. +- CommentEntity: μ§ˆλ¬ΈκΈ€μ— μΆ”κ°€λ˜λŠ” λŒ“κΈ€μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. + +EntityλŠ” μ•„λž˜μ™€ 같이 κ΄€κ³„λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€. + +questionEntity --ManyToOne-- writerEntity, categoryEntity +
  γ„΄OneToMany- CommentEntity --ManyToOne-- writerEntity diff --git a/spec/relation-entities/README.md b/spec/relation-entities/README.md index d38191a..40585b0 100644 --- a/spec/relation-entities/README.md +++ b/spec/relation-entities/README.md @@ -1,13 +1,13 @@ ### Relation Test Case -λ‹€μŒμ˜ Entityκ°€ μžˆμŠ΅λ‹ˆλ‹€. +There are the fours entities -- writerEntity: μž‘μ„±μž 정보λ₯Ό κ΄€λ¦¬ν•©λ‹ˆλ‹€. -- categoryEntity: μ§ˆλ¬ΈκΈ€μ˜ μ’…λ₯˜λ₯Ό κ΄€λ¦¬ν•©λ‹ˆλ‹€. -- questionEntity: μ§ˆλ¬ΈκΈ€μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. -- CommentEntity: μ§ˆλ¬ΈκΈ€μ— μΆ”κ°€λ˜λŠ” λŒ“κΈ€μ„ κ΄€λ¦¬ν•©λ‹ˆλ‹€. +- writerEntity: Manages writer information. +- categoryEntity: Manages the category of the question. +- questionEntity: Manages a question. +- CommentEntity: Manages comments added to a question. -EntityλŠ” μ•„λž˜μ™€ 같이 κ΄€κ³„λ˜μ–΄μžˆμŠ΅λ‹ˆλ‹€. +Entities are related as follows. questionEntity --ManyToOne-- writerEntity, categoryEntity
  γ„΄OneToMany- CommentEntity --ManyToOne-- writerEntity diff --git a/spec/relation-entities/relation-entities-disable-relations.spec.ts b/spec/relation-entities/relation-entities-disable-relations.spec.ts index 35a0612..a8f5ea1 100644 --- a/spec/relation-entities/relation-entities-disable-relations.spec.ts +++ b/spec/relation-entities/relation-entities-disable-relations.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('disable relation option', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ RelationEntitiesModule({ @@ -30,7 +30,7 @@ describe('disable relation option', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/relation-entities/relation-entities-interceptor.spec.ts b/spec/relation-entities/relation-entities-interceptor.spec.ts index e9d1468..7e18b53 100644 --- a/spec/relation-entities/relation-entities-interceptor.spec.ts +++ b/spec/relation-entities/relation-entities-interceptor.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('relation interceptor', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ RelationEntitiesModule({ @@ -28,7 +28,7 @@ describe('relation interceptor', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/relation-entities/relation-entities-read.spec.ts b/spec/relation-entities/relation-entities-read.spec.ts index e9a9c6b..5bd7c7e 100644 --- a/spec/relation-entities/relation-entities-read.spec.ts +++ b/spec/relation-entities/relation-entities-read.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('Relation Entities Read', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ RelationEntitiesModule({ @@ -25,7 +25,7 @@ describe('Relation Entities Read', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -190,7 +190,7 @@ describe('Relation Entities Read', () => { const { body: commentListBodyNext } = await request(app.getHttpServer()) .get('/comment') - .query({ nextCursor: commentListBody.metadata.nextCursor }) + .query({ nextCursor: commentListBody.metadata.query }) .expect(HttpStatus.OK); expect(commentListBodyNext.data).toHaveLength(1); diff --git a/spec/relation-entities/relation-entities-search.spec.ts b/spec/relation-entities/relation-entities-search.spec.ts index 04f4d85..950f7b7 100644 --- a/spec/relation-entities/relation-entities-search.spec.ts +++ b/spec/relation-entities/relation-entities-search.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('Relation Entities Search', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ RelationEntitiesModule({ @@ -23,7 +23,7 @@ describe('Relation Entities Search', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/relation-entities/relation-entities-seleted-relations.spec.ts b/spec/relation-entities/relation-entities-seleted-relations.spec.ts index c5eb4b5..d28e417 100644 --- a/spec/relation-entities/relation-entities-seleted-relations.spec.ts +++ b/spec/relation-entities/relation-entities-seleted-relations.spec.ts @@ -8,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('disable relation option', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ RelationEntitiesModule({ @@ -31,7 +31,7 @@ describe('disable relation option', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/relation-entities/relation-entities.spec.ts b/spec/relation-entities/relation-entities.spec.ts index a481516..f0b9162 100644 --- a/spec/relation-entities/relation-entities.spec.ts +++ b/spec/relation-entities/relation-entities.spec.ts @@ -7,7 +7,7 @@ import { TestHelper } from '../test.helper'; describe('Relation Entities Routes', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ RelationEntitiesModule({ @@ -22,7 +22,7 @@ describe('Relation Entities Routes', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/request-interceptor/request-interceptor.spec.ts b/spec/request-interceptor/request-interceptor.spec.ts index 1c2e25c..1c899f7 100644 --- a/spec/request-interceptor/request-interceptor.spec.ts +++ b/spec/request-interceptor/request-interceptor.spec.ts @@ -11,7 +11,7 @@ describe('Request Interceptor', () => { let app: INestApplication; let service: BaseService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [RequestInterceptorModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -23,7 +23,7 @@ describe('Request Interceptor', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/response-interceptor/response-interceptor.spec.ts b/spec/response-interceptor/response-interceptor.spec.ts index a8144e1..c6d2d89 100644 --- a/spec/response-interceptor/response-interceptor.spec.ts +++ b/spec/response-interceptor/response-interceptor.spec.ts @@ -11,7 +11,7 @@ describe('Response Interceptor', () => { let app: INestApplication; let service: BaseService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [BaseModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -23,7 +23,7 @@ describe('Response Interceptor', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/search-json-column/json.module.ts b/spec/search-json-column/json.module.ts index 36a3762..1c6aa9c 100644 --- a/spec/search-json-column/json.module.ts +++ b/spec/search-json-column/json.module.ts @@ -1,10 +1,11 @@ /* eslint-disable max-classes-per-file */ import { Controller, Injectable, Module } from '@nestjs/common'; import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { IsOptional } from 'class-validator'; import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, Repository } from 'typeorm'; import { Address, Person } from './interface'; -import { Crud, CrudService, CrudController } from '../../src'; +import { Crud, CrudService, CrudController, GROUP } from '../../src'; @Entity('json_column_entity') export class JsonColumnEntity extends BaseEntity { @@ -15,9 +16,11 @@ export class JsonColumnEntity extends BaseEntity { colors: string[]; @Column({ type: 'json', nullable: true }) + @IsOptional({ groups: [GROUP.SEARCH] }) friends: Person[]; @Column({ type: 'json' }) + @IsOptional({ groups: [GROUP.SEARCH] }) address: Address; } diff --git a/spec/search-json-column/jsonb.module.ts b/spec/search-json-column/jsonb.module.ts index f91ac59..0ec1b15 100644 --- a/spec/search-json-column/jsonb.module.ts +++ b/spec/search-json-column/jsonb.module.ts @@ -1,10 +1,11 @@ /* eslint-disable max-classes-per-file */ import { Controller, Injectable, Module } from '@nestjs/common'; import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; +import { IsOptional } from 'class-validator'; import { BaseEntity, Column, Entity, PrimaryGeneratedColumn, Repository } from 'typeorm'; import { Address, Person } from './interface'; -import { Crud, CrudService, CrudController } from '../../src'; +import { Crud, CrudService, CrudController, GROUP } from '../../src'; @Entity('jsonb_column_entity') export class JsonbColumnEntity extends BaseEntity { @@ -12,12 +13,15 @@ export class JsonbColumnEntity extends BaseEntity { id: number; @Column({ type: 'jsonb' }) + @IsOptional({ groups: [GROUP.SEARCH] }) colors: string[]; @Column({ type: 'jsonb', nullable: true }) + @IsOptional({ groups: [GROUP.SEARCH] }) friends: Person[]; @Column({ type: 'json' }) + @IsOptional({ groups: [GROUP.SEARCH] }) address: Address; } diff --git a/spec/search-json-column/search-json.mysql.spec.ts b/spec/search-json-column/search-json.mysql.spec.ts index 2786878..fed8d91 100644 --- a/spec/search-json-column/search-json.mysql.spec.ts +++ b/spec/search-json-column/search-json.mysql.spec.ts @@ -1,7 +1,8 @@ import 'mysql2'; -import { INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; import { fixtures } from './fixture'; import { JsonColumnEntity, JsonColumnModule, JsonColumnService } from './json.module'; @@ -11,7 +12,7 @@ describe('Search JSON column - MySQL', () => { let app: INestApplication; let service: JsonColumnService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [JsonColumnModule, TestHelper.getTypeOrmMysqlModule([JsonColumnEntity])], }).compile(); @@ -24,27 +25,34 @@ describe('Search JSON column - MySQL', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); describe('[JSON_CONTAINS operator] Whether JSON document contains specific object at path', () => { it('should search entities that meets operation for array column', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ friends: { operator: 'JSON_CONTAINS', operand: '{ "firstName": "Taylor" }' } }] }, - relations: [], - }); + const { + body: { data }, + } = await request(app.getHttpServer()) + .post('/json/search') + .send({ where: [{ friends: { operator: 'JSON_CONTAINS', operand: '{ "firstName": "Taylor" }' } }] }) + .expect(HttpStatus.OK); expect(data).toHaveLength(1); - const { data: data2 } = await service.reservedSearch({ - requestSearchDto: { where: [{ friends: { operator: 'JSON_CONTAINS', operand: '{ "gender": "Male" }' } }] }, - relations: [], - }); + const { + body: { data: data2 }, + } = await request(app.getHttpServer()) + .post('/json/search') + .send({ where: [{ friends: { operator: 'JSON_CONTAINS', operand: '{ "gender": "Male" }' } }] }) + .expect(HttpStatus.OK); expect(data2).toHaveLength(2); - const { data: data3 } = await service.reservedSearch({ - requestSearchDto: { + const { + body: { data: data3 }, + } = await request(app.getHttpServer()) + .post('/json/search') + .send({ where: [ { friends: { @@ -53,25 +61,28 @@ describe('Search JSON column - MySQL', () => { }, }, ], - }, - relations: [], - }); + }) + .expect(HttpStatus.OK); expect(data3).toHaveLength(1); }); it('should search entities that meets operation for object column', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ address: { operator: 'JSON_CONTAINS', operand: '{ "city": "Bali" }' } }] }, - relations: [], - }); + const { + body: { data }, + } = await request(app.getHttpServer()) + .post('/json/search') + .send({ where: [{ address: { operator: 'JSON_CONTAINS', operand: '{ "city": "Bali" }' } }] }) + .expect(HttpStatus.OK); expect(data).toHaveLength(1); }); it('should return empty array when no record matches', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ friends: { operator: 'JSON_CONTAINS', operand: '{ "firstName": "Donghyuk" }' } }] }, - relations: [], - }); + const { + body: { data }, + } = await request(app.getHttpServer()) + .post('/json/search') + .send({ where: [{ friends: { operator: 'JSON_CONTAINS', operand: '{ "firstName": "Donghyuk" }' } }] }) + .expect(HttpStatus.OK); expect(data).toHaveLength(0); }); }); diff --git a/spec/search-json-column/search-jsonb-postgresql.spec.ts b/spec/search-json-column/search-jsonb-postgresql.spec.ts index d48f52e..5d1a878 100644 --- a/spec/search-json-column/search-jsonb-postgresql.spec.ts +++ b/spec/search-json-column/search-jsonb-postgresql.spec.ts @@ -1,7 +1,8 @@ import 'pg'; -import { INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import request from 'supertest'; import { fixtures } from './fixture'; import { JsonbColumnEntity, JsonbColumnModule, JsonbColumnService } from './jsonb.module'; @@ -11,7 +12,7 @@ describe('Search JSONB column - PostgreSQL', () => { let app: INestApplication; let service: JsonbColumnService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [JsonbColumnModule, TestHelper.getTypeOrmPgsqlModule([JsonbColumnEntity])], }).compile(); @@ -24,57 +25,71 @@ describe('Search JSONB column - PostgreSQL', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); describe('[? operator] Does the string exist as a top-level key within the JSON value?', () => { it('should search entities that meets operation', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ colors: { operator: '?', operand: 'Orange' } }] }, - relations: [], - }); + const { + body: { data }, + } = await request(app.getHttpServer()) + .post('/jsonb/search') + .send({ where: [{ colors: { operator: '?', operand: 'Orange' } }] }) + .expect(HttpStatus.OK); expect(data).toHaveLength(2); }); it('should return empty array when no record matches', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ colors: { operator: '?', operand: 'Gold' } }] }, - relations: [], - }); + const { + body: { data }, + } = await request(app.getHttpServer()) + .post('/jsonb/search') + .send({ where: [{ colors: { operator: '?', operand: 'Gold' } }] }) + .expect(HttpStatus.OK); expect(data).toHaveLength(0); }); }); describe('[@> operator] Does the left JSON value contain the right JSON path/value entries at the top level?', () => { it('should search entities that meets operation', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ friends: { operator: '@>', operand: '[{ "firstName": "Taylor" }]' } }] }, - relations: [], - }); + const { + body: { data }, + } = await request(app.getHttpServer()) + .post('/jsonb/search') + .send({ where: [{ friends: { operator: '@>', operand: '[{ "firstName": "Taylor" }]' } }] }) + .expect(HttpStatus.OK); expect(data).toHaveLength(1); - const { data: data2 } = await service.reservedSearch({ - requestSearchDto: { where: [{ friends: { operator: '@>', operand: '[{ "gender": "Male" }]' } }] }, - relations: [], - }); + const { + body: { data: data2 }, + } = await request(app.getHttpServer()) + .post('/jsonb/search') + .send({ where: [{ friends: { operator: '@>', operand: '[{ "gender": "Male" }]' } }] }) + .expect(HttpStatus.OK); expect(data2).toHaveLength(2); - const { data: data3 } = await service.reservedSearch({ - requestSearchDto: { + const { + body: { data: data3 }, + } = await request(app.getHttpServer()) + .post('/jsonb/search') + .send({ where: [{ friends: { operator: '@>', operand: '[{ "lastName": "Bon", "email": "mbon2@pagesperso-orange.fr"}]' } }], - }, - relations: [], - }); + }) + .expect(HttpStatus.OK); expect(data3).toHaveLength(1); }); it('should return empty array when no record matches', async () => { - const { data } = await service.reservedSearch({ - requestSearchDto: { where: [{ friends: { operator: '@>', operand: '[{ "firstName": "Donghyuk" }]' } }] }, - relations: [], - }); + const { + body: { data: data }, + } = await request(app.getHttpServer()) + .post('/jsonb/search') + .send({ + where: [{ friends: { operator: '@>', operand: '[{ "firstName": "Donghyuk" }]' } }], + }) + .expect(HttpStatus.OK); expect(data).toHaveLength(0); }); }); diff --git a/spec/search/module.ts b/spec/search/module.ts index df11554..3733f04 100644 --- a/spec/search/module.ts +++ b/spec/search/module.ts @@ -39,6 +39,7 @@ export class TestService extends CrudService { limitOfTake: 100, }, }, + logging: true, }) @Controller('base') export class TestController implements CrudController { diff --git a/spec/search/search-complex-condition.spec.ts b/spec/search/search-complex-condition.spec.ts index 46d843d..f2f053c 100644 --- a/spec/search/search-complex-condition.spec.ts +++ b/spec/search/search-complex-condition.spec.ts @@ -4,7 +4,6 @@ import { Controller, Injectable, Module } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { InjectRepository, TypeOrmModule } from '@nestjs/typeorm'; import { IsOptional } from 'class-validator'; -import _ from 'lodash'; import request from 'supertest'; import { Entity, BaseEntity, Repository, PrimaryColumn, Column, ObjectLiteral } from 'typeorm'; @@ -51,7 +50,7 @@ class TestModule {} describe('Search complex conditions', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmPgsqlModule([TestEntity])], }).compile(); @@ -59,7 +58,7 @@ describe('Search complex conditions', () => { await app.init(); await Promise.all( - _.range(10).map((no) => + Array.from({ length: 10 }, (_, index) => index).map((no) => request(app.getHttpServer()) .post('/base') .send({ @@ -71,7 +70,7 @@ describe('Search complex conditions', () => { ); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/search/search-cursor-pagination.spec.ts b/spec/search/search-cursor-pagination.spec.ts index 7fa954e..1fd0f63 100644 --- a/spec/search/search-cursor-pagination.spec.ts +++ b/spec/search/search-cursor-pagination.spec.ts @@ -1,6 +1,5 @@ -import { INestApplication, HttpStatus } from '@nestjs/common'; +import { INestApplication, HttpStatus, ConsoleLogger } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { TestEntity, TestModule, TestService } from './module'; @@ -11,10 +10,14 @@ describe('Search Cursor Pagination', () => { let app: INestApplication; let service: TestService; - beforeEach(async () => { + beforeAll(async () => { + const logger = new ConsoleLogger(); + logger.setLogLevels(['error']); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmMysqlModule([TestEntity])], - }).compile(); + }) + .setLogger(logger) + .compile(); app = moduleFixture.createNestApplication(); service = moduleFixture.get(TestService); @@ -34,22 +37,23 @@ describe('Search Cursor Pagination', () => { * - when index is [25-49], is has null */ await Promise.all( - _.range(25).map((no: number) => + Array.from({ length: 25 }, (_, index) => index).map((no: number) => service.repository.save( service.repository.create({ col1: `col${no % 2 === 0 ? '0' : '1'}_${no}`, col2: no, col3: 50 - no }), ), ), ); await Promise.all( - _.range(25, 50).map((no: number) => + Array.from({ length: 25 }, (_, index) => index + 25).map((no: number) => service.repository.save(service.repository.create({ col1: `col${no % 2 === 0 ? '0' : '1'}_${no}`, col2: no })), ), ); + expect(await TestEntity.count()).toEqual(50); await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -57,33 +61,55 @@ describe('Search Cursor Pagination', () => { it('should fetch with nextCursor', async () => { const searchRequestBody = { where: [{ col2: { operator: '<', operand: 40 } }], order: { col1: 'DESC' }, take: 10 }; - const firstResponse = await request(app.getHttpServer()).post('/base/search').send(searchRequestBody); - expect(firstResponse.statusCode).toEqual(HttpStatus.OK); - expect(firstResponse.body.data).toHaveLength(10); - - const lastEntity = PaginationHelper.deserialize(firstResponse.body.metadata.nextCursor); + const { body: firstResponse } = await request(app.getHttpServer()) + .post('/base/search') + .send(searchRequestBody) + .expect(HttpStatus.OK); + expect(firstResponse.data).toHaveLength(10); + + const preCondition: Record = PaginationHelper.deserialize(firstResponse.metadata.nextCursor); + expect(preCondition).toEqual({ + where: expect.any(String), + nextCursor: expect.any(String), + total: expect.any(Number), + }); + const lastEntity = PaginationHelper.deserialize(preCondition.nextCursor as string); expect(lastEntity).toEqual({ col1: 'col1_29' }); - const preCondition = PaginationHelper.deserialize(firstResponse.body.metadata.query); - expect(preCondition).toEqual({ ...searchRequestBody, withDeleted: false }); - - const secondResponse = await request(app.getHttpServer()) + expect(PaginationHelper.deserialize(preCondition.where as string)).toEqual({ + where: [ + { + col2: { operator: '<', operand: 40 }, + }, + ], + order: { col1: 'DESC' }, + take: 10, + withDeleted: false, + }); + + const { body: secondResponse } = await request(app.getHttpServer()) .post('/base/search') - .send({ nextCursor: firstResponse.body.metadata.nextCursor, query: firstResponse.body.metadata.query }); + .send({ nextCursor: firstResponse.metadata.nextCursor }) + .expect(HttpStatus.OK); - expect(secondResponse.statusCode).toEqual(HttpStatus.OK); - expect(secondResponse.body.data).toHaveLength(10); - const firstDataCol1: Set = new Set(firstResponse.body.data.map((d: { col1: string }) => d.col1)); + expect(secondResponse.data).toHaveLength(10); + const firstDataCol1: Set = new Set(firstResponse.data.map((d: { col1: string }) => d.col1)); - for (const nextData of secondResponse.body.data) { + for (const nextData of secondResponse.data) { expect(firstDataCol1.has(nextData.col1)).not.toBeTruthy(); } - const nextLastEntity = PaginationHelper.deserialize(secondResponse.body.metadata.nextCursor); + const nextPreCondition: Record = PaginationHelper.deserialize(secondResponse.metadata.nextCursor); + expect(nextPreCondition).toEqual({ + where: expect.any(String), + nextCursor: expect.any(String), + total: expect.any(Number), + }); + const nextLastEntity = PaginationHelper.deserialize(nextPreCondition.nextCursor as string); expect(nextLastEntity).toEqual({ col1: 'col1_1' }); - const nextPreCondition = PaginationHelper.deserialize(secondResponse.body.metadata.query); - expect(nextPreCondition).toEqual( - _.merge(searchRequestBody, { where: [{ col1: { operand: lastEntity.col1, operator: '<' } }] }, { withDeleted: false }), - ); + expect(PaginationHelper.deserialize(nextPreCondition.where as string)).toEqual({ + ...searchRequestBody, + withDeleted: false, + }); }); it('should be less than limitOfTake', async () => { @@ -107,4 +133,109 @@ describe('Search Cursor Pagination', () => { } = await request(app.getHttpServer()).post('/base/search').send({ take: 13 }).expect(HttpStatus.OK); expect(customData).toHaveLength(13); }); + + it('should fetch with nextCursor', async () => { + const searchRequestBody = { where: [{ col2: { operator: '<', operand: 40 } }], order: { col1: 'DESC' }, take: 10 }; + + const { body: firstResponse } = await request(app.getHttpServer()) + .post('/base/search') + .send(searchRequestBody) + .expect(HttpStatus.OK); + expect(firstResponse.data).toHaveLength(10); + + const preCondition: Record = PaginationHelper.deserialize(firstResponse.metadata.nextCursor); + expect(preCondition).toEqual({ + where: expect.any(String), + nextCursor: expect.any(String), + total: expect.any(Number), + }); + const lastEntity = PaginationHelper.deserialize(preCondition.nextCursor as string); + expect(lastEntity).toEqual({ col1: 'col1_29' }); + expect(PaginationHelper.deserialize(preCondition.where as string)).toEqual({ + where: [ + { + col2: { operator: '<', operand: 40 }, + }, + ], + order: { col1: 'DESC' }, + take: 10, + withDeleted: false, + }); + + const { body: secondResponse } = await request(app.getHttpServer()) + .post('/base/search') + .send({ nextCursor: firstResponse.metadata.nextCursor }) + .expect(HttpStatus.OK); + + expect(secondResponse.data).toHaveLength(10); + const firstDataCol1: Set = new Set(firstResponse.data.map((d: { col1: string }) => d.col1)); + + for (const nextData of secondResponse.data) { + expect(firstDataCol1.has(nextData.col1)).not.toBeTruthy(); + } + + const nextPreCondition: Record = PaginationHelper.deserialize(secondResponse.metadata.nextCursor); + expect(nextPreCondition).toEqual({ + where: expect.any(String), + nextCursor: expect.any(String), + total: expect.any(Number), + }); + const nextLastEntity = PaginationHelper.deserialize(nextPreCondition.nextCursor as string); + expect(nextLastEntity).toEqual({ col1: 'col1_1' }); + expect(PaginationHelper.deserialize(nextPreCondition.where as string)).toEqual({ + ...searchRequestBody, + withDeleted: false, + }); + }); + + it('should be guaranteed keySet, If the primary key is used as a conditional', async () => { + const { body } = await request(app.getHttpServer()) + .post('/base/search') + .send({ where: [{ col1: { operator: 'LIKE', operand: 'col1%' } }], order: { col1: 'ASC' } }) + .expect(HttpStatus.OK); + + expect(body).toEqual({ + data: [ + { col1: 'col1_1', col2: 1, col3: 49 }, + { col1: 'col1_11', col2: 11, col3: 39 }, + { col1: 'col1_13', col2: 13, col3: 37 }, + { col1: 'col1_15', col2: 15, col3: 35 }, + { col1: 'col1_17', col2: 17, col3: 33 }, + ], + metadata: { + limit: 5, + total: 25, + nextCursor: expect.any(String), + }, + }); + + const { body: nextBody } = await request(app.getHttpServer()) + .post('/base/search') + .send({ nextCursor: body.metadata.nextCursor }) + .expect(HttpStatus.OK); + expect(nextBody).toEqual({ + data: [ + { col1: 'col1_19', col2: 19, col3: 31 }, + { col1: 'col1_21', col2: 21, col3: 29 }, + { col1: 'col1_23', col2: 23, col3: 27 }, + { col1: 'col1_25', col2: 25, col3: null }, + { col1: 'col1_27', col2: 27, col3: null }, + ], + metadata: { + limit: 5, + total: 20, + nextCursor: expect.any(String), + }, + }); + }); + + it('should be use empty body', async () => { + const { body } = await request(app.getHttpServer()).post('/base/search').send({}).expect(HttpStatus.OK); + expect(body.data).toBeDefined(); + expect(body.metadata).toBeDefined(); + + const { body: emptyWhere } = await request(app.getHttpServer()).post('/base/search').send({ where: [] }).expect(HttpStatus.OK); + expect(emptyWhere.data).toBeDefined(); + expect(emptyWhere.metadata).toBeDefined(); + }); }); diff --git a/spec/search/search-query-operator.spec.ts b/spec/search/search-query-operator.spec.ts index 81be1ec..fbdc75d 100644 --- a/spec/search/search-query-operator.spec.ts +++ b/spec/search/search-query-operator.spec.ts @@ -1,8 +1,6 @@ -/* eslint-disable @typescript-eslint/quotes */ -/* eslint-disable no-useless-escape */ -import { INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; +import request from 'supertest'; import { TestEntity, TestModule, TestService } from './module'; import { RequestSearchDto } from '../../src/lib/dto/request-search.dto'; @@ -10,14 +8,13 @@ import { TestHelper } from '../test.helper'; describe('Search Query Operator', () => { let app: INestApplication; - let service: TestService; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [TestModule, TestHelper.getTypeOrmMysqlModule([TestEntity])], }).compile(); app = moduleFixture.createNestApplication(); - service = moduleFixture.get(TestService); + const service: TestService = moduleFixture.get(TestService); /** * 10 entities are created for the test. @@ -35,14 +32,14 @@ describe('Search Query Operator', () => { * - when index is [5-9], is has null */ await Promise.all( - _.range(5).map((no: number) => + Array.from({ length: 5 }, (_, index) => index).map((no: number) => service.repository.save( service.repository.create({ col1: `col${no % 2 === 0 ? '0' : '1'}_${no}`, col2: no, col3: 10 - no }), ), ), ); await Promise.all( - _.range(5, 10).map((no: number) => + Array.from({ length: 5 }, (_, index) => index + 5).map((no: number) => service.repository.save(service.repository.create({ col1: `col${no % 2 === 0 ? '0' : '1'}_${no}`, col2: no })), ), ); @@ -50,7 +47,7 @@ describe('Search Query Operator', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -63,20 +60,19 @@ describe('Search Query Operator', () => { ]; for (const requestSearchDto of requestSearchDtoList) { - const { data, metadata } = await service.reservedSearch({ - requestSearchDto, - relations: [], - }); + const { + body: { data, metadata }, + } = await request(app.getHttpServer()).post('/base/search').send(requestSearchDto).expect(HttpStatus.OK); expect(data).toHaveLength(5); expect(Object.keys(data[0])).toEqual(expect.arrayContaining(requestSearchDto.select as unknown[])); expect(metadata.nextCursor).toBeDefined(); - expect(metadata.query).toBeDefined(); expect(metadata.total).toEqual(10); } }); it('should query condition with RequestSearchDto when `where` is provided', async () => { + const take = 100; const fixtures: Array<[RequestSearchDto, number[]]> = [ [{ where: [{ col2: { operator: '=', operand: 5 } }] }, [5]], [{ where: [{ col2: { operator: '=', operand: 5, not: true } }] }, [0, 1, 2, 3, 4, 6, 7, 8, 9]], @@ -112,17 +108,22 @@ describe('Search Query Operator', () => { [{ where: [{ col3: { operator: 'NULL', not: true } }] }, [0, 1, 2, 3, 4]], ]; for (const [requestSearchDto, expected] of fixtures) { - const { data, metadata } = await service.reservedSearch({ requestSearchDto, relations: [] }); - const col2Values = data.map((d) => d.col2); + const { + body: { data, metadata }, + } = await request(app.getHttpServer()) + .post('/base/search') + .send({ ...requestSearchDto, take }) + .expect(HttpStatus.OK); + const col2Values = (data as TestEntity[]).map((d) => d.col2); expect(col2Values).toHaveLength(expected.length); expect(col2Values).toEqual(expect.arrayContaining(expected)); expect(metadata.nextCursor).toBeDefined(); - expect(metadata.query).toBeDefined(); } }); it('nested complex where condition test', async () => { + const take = 100; const fixtures: Array<[RequestSearchDto, number[]]> = [ [{ where: [{ col2: { operator: 'BETWEEN', operand: [3, 5] } }], order: { col1: 'ASC' } }, [3, 4, 5]], [ @@ -167,13 +168,17 @@ describe('Search Query Operator', () => { ], ]; for (const [requestSearchDto, expected] of fixtures) { - const { data, metadata } = await service.reservedSearch({ requestSearchDto, relations: [] }); - const col2Values = data.map((d) => d.col2); + const { + body: { data, metadata }, + } = await request(app.getHttpServer()) + .post('/base/search') + .send({ ...requestSearchDto, take }) + .expect(HttpStatus.OK); + const col2Values = (data as TestEntity[]).map((d) => d.col2); expect(col2Values).toHaveLength(expected.length); expect(col2Values).toEqual(expect.arrayContaining(expected)); expect(metadata.nextCursor).toBeDefined(); - expect(metadata.query).toBeDefined(); } }); }); diff --git a/spec/soft-delete-and-recover/soft-delete-and-recover.controller.spec.ts b/spec/soft-delete-and-recover/soft-delete-and-recover.controller.spec.ts index 5b8e775..90474e1 100644 --- a/spec/soft-delete-and-recover/soft-delete-and-recover.controller.spec.ts +++ b/spec/soft-delete-and-recover/soft-delete-and-recover.controller.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('Soft-delete and recover test', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [SoftDeleteAndRecoverModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -18,7 +18,11 @@ describe('Soft-delete and recover test', () => { await app.init(); }); - afterEach(async () => { + beforeEach(async () => { + await BaseEntity.delete({}); + }); + + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/sub-path/sub-path-more-than-one-parent.spec.ts b/spec/sub-path/sub-path-more-than-one-parent.spec.ts index f710c14..23aa07a 100644 --- a/spec/sub-path/sub-path-more-than-one-parent.spec.ts +++ b/spec/sub-path/sub-path-more-than-one-parent.spec.ts @@ -1,6 +1,5 @@ import { INestApplication, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; import { SubPathModule } from './sub-path.module'; @@ -9,7 +8,7 @@ import { TestHelper } from '../test.helper'; describe('Subpath - more then one parent parameter', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [SubPathModule()], }).compile(); @@ -25,7 +24,7 @@ describe('Subpath - more then one parent parameter', () => { * | parent1 | 1 | writer5, 7, 9 | */ await Promise.all( - _.range(0, 10).map((parentNo) => + Array.from({ length: 10 }, (_, index) => index).map((parentNo) => request(app.getHttpServer()) .post(`/parent${parentNo % 2 === 0 ? '0' : '1'}/sub/${parentNo < 5 ? 0 : 1}/child`) .send({ name: `writer${parentNo}` }) @@ -34,7 +33,7 @@ describe('Subpath - more then one parent parameter', () => { ); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -89,7 +88,7 @@ describe('Subpath - more then one parent parameter', () => { }); it('should meet conditions of parent params - search', async () => { - const operand = _.range(0, 10).map((parentNo) => `writer${parentNo}`); + const operand = Array.from({ length: 10 }, (_, index) => index).map((parentNo) => `writer${parentNo}`); for (const parentName of ['parent0', 'parent1']) { for (const subId of [0, 1]) { const { body } = await request(app.getHttpServer()) diff --git a/spec/sub-path/sub-path-one-parent.spec.ts b/spec/sub-path/sub-path-one-parent.spec.ts index 1d93f6d..624dfe7 100644 --- a/spec/sub-path/sub-path-one-parent.spec.ts +++ b/spec/sub-path/sub-path-one-parent.spec.ts @@ -1,23 +1,26 @@ import { INestApplication, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import request from 'supertest'; +import { DepthOneEntity } from './depth-one.entity'; import { SubPathModule } from './sub-path.module'; import { TestHelper } from '../test.helper'; describe('Subpath - one parent parameter', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [SubPathModule()], }).compile(); app = moduleFixture.createNestApplication(); await app.init(); + }); + beforeEach(async () => { + await DepthOneEntity.delete({}); await Promise.all( - _.range(0, 5).map((parentNo) => + Array.from({ length: 5 }, (_, index) => index).map((parentNo) => request(app.getHttpServer()) .post(`/parent${parentNo % 2 === 0 ? '0' : '1'}/child`) .send({ name: `writer${parentNo}` }) @@ -26,7 +29,7 @@ describe('Subpath - one parent parameter', () => { ); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); @@ -65,7 +68,7 @@ describe('Subpath - one parent parameter', () => { }); it('should meet conditions of parent params - search', async () => { - const operand = _.range(0, 5).map((parentNo) => `writer${parentNo}`); + const operand = Array.from({ length: 5 }, (_, index) => index).map((parentNo) => `writer${parentNo}`); const { body } = await request(app.getHttpServer()) .post('/parent0/child/search') .send({ diff --git a/spec/sub-path/sub-path.spec.ts b/spec/sub-path/sub-path.spec.ts index 0f4e0c0..2d0f79f 100644 --- a/spec/sub-path/sub-path.spec.ts +++ b/spec/sub-path/sub-path.spec.ts @@ -1,6 +1,5 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import _ from 'lodash'; import { SubPathModule } from './sub-path.module'; import { TestHelper } from '../test.helper'; @@ -8,7 +7,7 @@ import { TestHelper } from '../test.helper'; describe('Subpath', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [SubPathModule()], }).compile(); @@ -16,7 +15,7 @@ describe('Subpath', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/swagger-decorator/swagger-decorator.spec.ts b/spec/swagger-decorator/swagger-decorator.spec.ts index 51974c0..3574931 100644 --- a/spec/swagger-decorator/swagger-decorator.spec.ts +++ b/spec/swagger-decorator/swagger-decorator.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('SwaggerDecorator', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [SwaggerDecoratorModule, TestHelper.getTypeOrmMysqlModule([BaseEntity])], }).compile(); @@ -19,7 +19,7 @@ describe('SwaggerDecorator', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/test.helper.ts b/spec/test.helper.ts index b8ee4ad..8b4ef6c 100644 --- a/spec/test.helper.ts +++ b/spec/test.helper.ts @@ -88,7 +88,7 @@ export class TestHelper { if (!route.root?.operationId) { return summary; } - summary[route.root.operationId] = route; + summary[`${route.root.method} ${route.root.path}`] = route; return summary; }, {} as Record); } diff --git a/spec/unique-key/unique.controller.create.mysql.spec.ts b/spec/unique-key/unique.controller.create.mysql.spec.ts index 50bf96b..c49e94a 100644 --- a/spec/unique-key/unique.controller.create.mysql.spec.ts +++ b/spec/unique-key/unique.controller.create.mysql.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('UniqueController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [UniqueModule, TestHelper.getTypeOrmMysqlModule([UniqueEntity])], }).compile(); @@ -18,7 +18,7 @@ describe('UniqueController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/unique-key/unique.controller.create.pgsql.spec.ts b/spec/unique-key/unique.controller.create.pgsql.spec.ts index d454af6..0fae5d0 100644 --- a/spec/unique-key/unique.controller.create.pgsql.spec.ts +++ b/spec/unique-key/unique.controller.create.pgsql.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('UniqueController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [UniqueModule, TestHelper.getTypeOrmPgsqlModule([UniqueEntity])], }).compile(); @@ -18,7 +18,7 @@ describe('UniqueController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/spec/unique-key/unique.controller.create.spec.ts b/spec/unique-key/unique.controller.create.spec.ts index 50bf96b..c49e94a 100644 --- a/spec/unique-key/unique.controller.create.spec.ts +++ b/spec/unique-key/unique.controller.create.spec.ts @@ -9,7 +9,7 @@ import { TestHelper } from '../test.helper'; describe('UniqueController', () => { let app: INestApplication; - beforeEach(async () => { + beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [UniqueModule, TestHelper.getTypeOrmMysqlModule([UniqueEntity])], }).compile(); @@ -18,7 +18,7 @@ describe('UniqueController', () => { await app.init(); }); - afterEach(async () => { + afterAll(async () => { await TestHelper.dropTypeOrmEntityTables(); await app?.close(); }); diff --git a/src/lib/abstract/abstract.pagination.spec.ts b/src/lib/abstract/abstract.pagination.spec.ts new file mode 100644 index 0000000..7989032 --- /dev/null +++ b/src/lib/abstract/abstract.pagination.spec.ts @@ -0,0 +1,26 @@ +import { AbstractPaginationRequest } from './abstract.pagination'; + +describe('AbstractPaginationRequest', () => { + class PaginationRequest extends AbstractPaginationRequest { + nextTotal(_dataLength?: number | undefined): number { + throw new Error('Method not implemented.'); + } + } + it('should do nothing when where is undefined', () => { + const paginationRequest = new PaginationRequest(); + paginationRequest.setWhere(undefined); + expect(paginationRequest.where).toBeUndefined(); + + paginationRequest.setWhere('where'); + expect(paginationRequest.where).toEqual('where'); + + paginationRequest.setWhere(undefined); + expect(paginationRequest.where).toEqual('where'); + }); + + it('should do nothing when query is invalid', () => { + const paginationRequest = new PaginationRequest(); + paginationRequest.setQuery('invalid'); + expect(paginationRequest.isNext).toBeFalsy(); + }); +}); diff --git a/src/lib/abstract/abstract.pagination.ts b/src/lib/abstract/abstract.pagination.ts new file mode 100644 index 0000000..8fe0d21 --- /dev/null +++ b/src/lib/abstract/abstract.pagination.ts @@ -0,0 +1,77 @@ +import { Expose } from 'class-transformer'; +import { IsString, IsOptional } from 'class-validator'; + +import { PaginationType } from '../interface'; + +interface PaginationQuery { + where: string; + nextCursor: string; + total: number; +} +const encoding = 'base64'; + +export interface PaginationAbstractResponse { + data: T[]; +} + +export abstract class AbstractPaginationRequest { + private _isNext: boolean = false; + private _where: string; + private _total: number; + private _nextCursor: string; + + type: PaginationType; + + @Expose({ name: 'nextCursor' }) + @IsString() + @IsOptional() + query: string; + + setWhere(where: string | undefined) { + if (!where) { + return; + } + this._where = where; + } + + makeQuery(total: number, nextCursor: string): string { + return Buffer.from( + JSON.stringify({ + where: this._where, + nextCursor, + total, + }), + ).toString(encoding); + } + + setQuery(query: string): void { + try { + const paginationType: PaginationQuery = JSON.parse(Buffer.from(query, encoding).toString()); + this._where = paginationType.where; + this._total = paginationType.total; + this._nextCursor = paginationType.nextCursor; + + this._isNext = true; + } catch { + // + } + } + + protected get total() { + return this._total; + } + + get where() { + return this._where; + } + + get isNext() { + return this._isNext && this.total != null; + } + + get nextCursor() { + return this._nextCursor; + } + + abstract nextTotal(dataLength?: number): number; +} diff --git a/src/lib/abstract/abstract.request.interceptor.spec.ts b/src/lib/abstract/abstract.request.interceptor.spec.ts index b169ea0..16ad7ec 100644 --- a/src/lib/abstract/abstract.request.interceptor.spec.ts +++ b/src/lib/abstract/abstract.request.interceptor.spec.ts @@ -18,7 +18,7 @@ describe('RequestAbstractInterceptor', () => { id: number; } - beforeEach(() => { + beforeAll(() => { class FooInterceptor extends RequestAbstractInterceptor { constructor(crudLogger: CrudLogger) { super(crudLogger); diff --git a/src/lib/abstract/index.ts b/src/lib/abstract/index.ts index f60ab01..1db2b4d 100644 --- a/src/lib/abstract/index.ts +++ b/src/lib/abstract/index.ts @@ -1 +1,2 @@ export * from './abstract.request.interceptor'; +export * from './abstract.pagination'; diff --git a/src/lib/capitalize-first-letter.ts b/src/lib/capitalize-first-letter.ts new file mode 100644 index 0000000..abe673b --- /dev/null +++ b/src/lib/capitalize-first-letter.ts @@ -0,0 +1 @@ +export const capitalizeFirstLetter = (raw: string) => `${raw.charAt(0).toUpperCase()}${raw.slice(1)}`; diff --git a/src/lib/crud.policy.ts b/src/lib/crud.policy.ts index 034368f..f8cf0ea 100644 --- a/src/lib/crud.policy.ts +++ b/src/lib/crud.policy.ts @@ -1,5 +1,6 @@ import { HttpStatus, NestInterceptor, RequestMethod, Type } from '@nestjs/common'; +import { capitalizeFirstLetter } from './capitalize-first-letter'; import { ReadOneRequestInterceptor, CreateRequestInterceptor } from './interceptor'; import { DeleteRequestInterceptor } from './interceptor/delete-request.interceptor'; import { ReadManyRequestInterceptor } from './interceptor/read-many-request.interceptor'; @@ -8,7 +9,6 @@ import { SearchRequestInterceptor } from './interceptor/search-request.intercept import { UpdateRequestInterceptor } from './interceptor/update-request.interceptor'; import { UpsertRequestInterceptor } from './interceptor/upsert-request.interceptor'; import { CrudOptions, Method, PrimaryKey, FactoryOption, Sort, PaginationType } from './interface'; -import { capitalizeFirstLetter } from './util'; type CrudMethodPolicy = { [Method.READ_ONE]: MethodPolicy; @@ -54,12 +54,11 @@ const metaProperties = (paginationType: PaginationType) => pages: { type: 'number', example: 1 }, total: { type: 'number', example: 100 }, offset: { type: 'number', example: 20 }, - query: { type: 'string', example: 'queryToken' }, + nextCursor: { type: 'string', example: 'cursorToken' }, } : { total: { type: 'number', example: 100 }, limit: { type: 'number', example: 20 }, - query: { type: 'string', example: 'queryToken' }, nextCursor: { type: 'string', example: 'cursorToken' }, }; /** diff --git a/src/lib/crud.route.factory.ts b/src/lib/crud.route.factory.ts index 33616d4..88e5113 100644 --- a/src/lib/crud.route.factory.ts +++ b/src/lib/crud.route.factory.ts @@ -13,6 +13,7 @@ import { DECORATORS } from '@nestjs/swagger/dist/constants'; import { BaseEntity, getMetadataArgsStorage } from 'typeorm'; import { MetadataUtils } from 'typeorm/metadata-builder/MetadataUtils'; +import { capitalizeFirstLetter } from './capitalize-first-letter'; import { CRUD_ROUTE_ARGS } from './constants'; import { CRUD_POLICY } from './crud.policy'; import { RequestSearchDto } from './dto/request-search.dto'; @@ -24,7 +25,6 @@ import { CrudOptions, CrudReadOneRequest, CrudRecoverRequest, - CrudSearchRequest, CrudUpdateOneRequest, FactoryOption, Method, @@ -34,7 +34,6 @@ import { } from './interface'; import { CrudLogger } from './provider/crud-logger'; import { CrudReadManyRequest } from './request'; -import { capitalizeFirstLetter, isSomeEnum } from './util'; export class CrudRouteFactory { private crudLogger: CrudLogger; @@ -52,7 +51,11 @@ export class CrudRouteFactory { this.entityInformation(crudOptions.entity); const paginationType = crudOptions.routes?.readMany?.paginationType ?? CRUD_POLICY[Method.READ_MANY].default.paginationType; - const isPaginationType = isSomeEnum(PaginationType); + const isPaginationType = ( + >(enumType: TEnum) => + (nextCursor: unknown): nextCursor is TEnum[keyof TEnum] => + Object.values(enumType).includes(nextCursor as TEnum[keyof TEnum]) + )(PaginationType); if (!isPaginationType(paginationType)) { throw new TypeError(`invalid PaginationType ${paginationType}`); } @@ -117,8 +120,8 @@ export class CrudRouteFactory { } protected search(controllerMethodName: string) { - this.targetPrototype[controllerMethodName] = function reservedSearch(crudSearchRequest: CrudSearchRequest) { - return this.crudService.reservedSearch(crudSearchRequest); + this.targetPrototype[controllerMethodName] = function reservedReadMany(crudReadManyRequest: CrudReadManyRequest) { + return this.crudService.reservedReadMany(crudReadManyRequest); }; } diff --git a/src/lib/crud.service.spec.ts b/src/lib/crud.service.spec.ts index 6ce970a..30d45d3 100644 --- a/src/lib/crud.service.spec.ts +++ b/src/lib/crud.service.spec.ts @@ -14,7 +14,7 @@ describe('CrudService', () => { const crudService = new CrudService(mockRepository as unknown as Repository); const mockEntity = { id: 1, name: 'name1' }; - beforeEach(() => { + beforeAll(() => { mockRepository.findOne.mockResolvedValueOnce(mockEntity); }); @@ -50,4 +50,17 @@ describe('CrudService', () => { ).rejects.toThrow(ConflictException); }); }); + + describe('reservedReadMany', () => { + it('should log error and throw error when error occurred', async () => { + const mockRepository = { + metadata: { + primaryColumns: [{ propertyName: 'id' }], + }, + find: jest.fn(), + }; + const crudService = new CrudService(mockRepository as unknown as Repository); + await expect(crudService.reservedReadMany({ key: 'value', array: [{ key: 'value' }] } as any)).rejects.toThrow(Error); + }); + }); }); diff --git a/src/lib/crud.service.ts b/src/lib/crud.service.ts index 7f0beaf..0757a59 100644 --- a/src/lib/crud.service.ts +++ b/src/lib/crud.service.ts @@ -1,6 +1,6 @@ import { ConflictException, Logger, NotFoundException } from '@nestjs/common'; import _ from 'lodash'; -import { BaseEntity, DeepPartial, FindManyOptions, FindOptionsOrder, FindOptionsSelect, FindOptionsWhere, Repository } from 'typeorm'; +import { BaseEntity, DeepPartial, FindOptionsSelect, FindOptionsWhere, Repository } from 'typeorm'; import { CrudReadOneRequest, @@ -9,14 +9,10 @@ import { CrudUpsertRequest, CrudRecoverRequest, PaginationResponse, - CrudSearchRequest, isCrudCreateManyRequest, CrudCreateOneRequest, CrudCreateManyRequest, - CursorPaginationResponse, } from './interface'; -import { PaginationHelper } from './provider/pagination.helper'; -import { TypeOrmQueryBuilderHelper } from './provider/typeorm-query-builder.helper'; import { CrudReadManyRequest } from './request'; export class CrudService { @@ -26,56 +22,27 @@ export class CrudService { this.primaryKey = this.repository.metadata.primaryColumns?.map((columnMetadata) => columnMetadata.propertyName) ?? []; } - readonly reservedSearch = async (crudSearchRequest: CrudSearchRequest): Promise> => { - const { requestSearchDto, relations } = crudSearchRequest; - - const limit = requestSearchDto.take; - const findManyOptions: FindManyOptions = { - select: requestSearchDto.select, - where: - Array.isArray(requestSearchDto.where) && requestSearchDto.where.length > 0 - ? requestSearchDto.where.map((queryFilter, index) => - TypeOrmQueryBuilderHelper.queryFilterToFindOptionsWhere(queryFilter, index), - ) - : undefined, - withDeleted: requestSearchDto.withDeleted, - take: limit, - order: requestSearchDto.order as FindOptionsOrder, - relations, - }; - const [data, total] = await Promise.all([ - this.repository.find({ ...findManyOptions }), - this.repository.count({ - select: findManyOptions.select, - where: findManyOptions.where, - withDeleted: findManyOptions.withDeleted, - }), - ]); - const nextCursor = PaginationHelper.serialize(_.pick(data.at(-1), this.primaryKey) as FindOptionsWhere); - - return { - data, - metadata: { - nextCursor, - limit: limit!, - total, - query: PaginationHelper.serialize((requestSearchDto ?? {}) as FindOptionsWhere), - }, - }; - }; - readonly reservedReadMany = async (crudReadManyRequest: CrudReadManyRequest): Promise> => { try { - const [entities, total] = await Promise.all([ - this.repository.find({ ...crudReadManyRequest.findOptions }), - this.repository.count({ - where: crudReadManyRequest.findOptions.where, - withDeleted: crudReadManyRequest.findOptions.withDeleted, - }), - ]); + const { entities, total } = await (async () => { + const findEntities = this.repository.find({ ...crudReadManyRequest.findOptions }); + + if (crudReadManyRequest.pagination.isNext) { + const entities = await findEntities; + return { entities, total: crudReadManyRequest.pagination.nextTotal(entities.length) }; + } + const [entities, total] = await Promise.all([ + findEntities, + this.repository.count({ + where: crudReadManyRequest.findOptions.where, + withDeleted: crudReadManyRequest.findOptions.withDeleted, + }), + ]); + return { entities, total }; + })(); return crudReadManyRequest.toResponse(entities, total); } catch (error) { - Logger.error(crudReadManyRequest.toString()); + Logger.error(JSON.stringify(crudReadManyRequest)); Logger.error(error); throw error; } @@ -126,7 +93,7 @@ export class CrudService { }) .then(async (entity: T | null) => { const upsertEntity = entity ?? this.repository.create(crudUpsertRequest.params as unknown as DeepPartial); - if (!_.isNil(_.get(upsertEntity, 'deletedAt'))) { + if ('deletedAt' in upsertEntity && upsertEntity.deletedAt != null) { throw new ConflictException('it has been deleted'); } diff --git a/src/lib/dto/index.ts b/src/lib/dto/index.ts new file mode 100644 index 0000000..723325e --- /dev/null +++ b/src/lib/dto/index.ts @@ -0,0 +1,6 @@ +export * from './pagination-cursor.dto'; +export * from './pagination-offset.dto'; +export * from './params.dto'; +export * from './request-fields.dto'; +export * from './request-search.dto'; +export * from './request.dto'; diff --git a/src/lib/dto/pagination-cursor.dto.ts b/src/lib/dto/pagination-cursor.dto.ts index 7f9aeb3..969bd38 100644 --- a/src/lib/dto/pagination-cursor.dto.ts +++ b/src/lib/dto/pagination-cursor.dto.ts @@ -1,18 +1,10 @@ -import { Expose } from 'class-transformer'; -import { IsOptional, IsString } from 'class-validator'; +import { AbstractPaginationRequest } from '../abstract'; +import { PaginationType } from '../interface'; -import { PaginationRequestAbstract, PaginationType } from '../interface'; - -export class PaginationCursorDto implements PaginationRequestAbstract { +export class PaginationCursorDto extends AbstractPaginationRequest { type: PaginationType.CURSOR = PaginationType.CURSOR; - @Expose({ name: 'nextCursor' }) - @IsString() - @IsOptional() - nextCursor?: string; - - @Expose({ name: 'query' }) - @IsString() - @IsOptional() - query: string; + nextTotal(dataLength: number): number { + return this.total - dataLength; + } } diff --git a/src/lib/dto/pagination-offset.dto.spec.ts b/src/lib/dto/pagination-offset.dto.spec.ts index e5a6fbe..cb89e66 100644 --- a/src/lib/dto/pagination-offset.dto.spec.ts +++ b/src/lib/dto/pagination-offset.dto.spec.ts @@ -18,18 +18,18 @@ describe('PaginationOffsetDto', () => { expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 100 }))).toEqual([]); expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 100, offset: 20 }))).toEqual([]); - expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 100, offset: 20, query: 'queryToken' }))).toEqual([]); + expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 100, offset: 20, nextCursor: 'queryToken' }))).toEqual([]); }); it('should be positive number', () => { expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 10 }))).toEqual([]); expect(validateSync(plainToInstance(PaginationOffsetDto, { offset: 10 }))).toEqual([]); - expect(validateSync(plainToInstance(PaginationOffsetDto, { query: 10 }))).toEqual([ + expect(validateSync(plainToInstance(PaginationOffsetDto, { nextCursor: 10 }))).toEqual([ { children: [], constraints: { isString: 'query must be a string' }, property: 'query', - target: { query: 10, type: 'offset' }, + target: { query: 10, type: 'offset', _isNext: false }, value: 10, }, ]); @@ -38,7 +38,7 @@ describe('PaginationOffsetDto', () => { it('should be character type', () => { expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 'a' }))).toEqual([ { - target: { limit: Number.NaN, type: 'offset' }, + target: { limit: Number.NaN, type: 'offset', _isNext: false }, value: Number.NaN, property: 'limit', children: [], @@ -51,7 +51,7 @@ describe('PaginationOffsetDto', () => { expect(validateSync(plainToInstance(PaginationOffsetDto, { offset: 'b' }))).toEqual([ { - target: { offset: Number.NaN, type: 'offset' }, + target: { offset: Number.NaN, type: 'offset', _isNext: false }, value: Number.NaN, property: 'offset', children: [], @@ -66,7 +66,7 @@ describe('PaginationOffsetDto', () => { expect(validateSync(plainToInstance(PaginationOffsetDto, { limit: 101 }))).toEqual([ { - target: { limit: 101, type: 'offset' }, + target: { limit: 101, type: 'offset', _isNext: false }, value: 101, property: 'limit', children: [], @@ -77,9 +77,9 @@ describe('PaginationOffsetDto', () => { }); it('should be allowed zero', () => { - expect(validateByPaginationOffsetDto({ limit: 1 })).toEqual({ limit: 1, type: 'offset' }); - expect(validateByPaginationOffsetDto({ limit: 0 })).toEqual({ limit: 0, type: 'offset' }); - expect(validateByPaginationOffsetDto({ limit: -1 })).toEqual({ limit: 0, type: 'offset' }); - expect(validateByPaginationOffsetDto({})).toEqual({ type: 'offset' }); + expect(validateByPaginationOffsetDto({ limit: 1 })).toEqual({ limit: 1, type: 'offset', _isNext: false }); + expect(validateByPaginationOffsetDto({ limit: 0 })).toEqual({ limit: 0, type: 'offset', _isNext: false }); + expect(validateByPaginationOffsetDto({ limit: -1 })).toEqual({ limit: 0, type: 'offset', _isNext: false }); + expect(validateByPaginationOffsetDto({})).toEqual({ type: 'offset', _isNext: false }); }); }); diff --git a/src/lib/dto/pagination-offset.dto.ts b/src/lib/dto/pagination-offset.dto.ts index 96bf544..e84b082 100644 --- a/src/lib/dto/pagination-offset.dto.ts +++ b/src/lib/dto/pagination-offset.dto.ts @@ -1,9 +1,10 @@ import { Expose, Transform, Type } from 'class-transformer'; -import { IsNumber, IsOptional, IsPositive, IsString, Max } from 'class-validator'; +import { IsNumber, IsOptional, IsPositive, Max } from 'class-validator'; -import { PaginationRequestAbstract, PaginationType } from '../interface'; +import { AbstractPaginationRequest } from '../abstract'; +import { PaginationType } from '../interface'; -export class PaginationOffsetDto implements PaginationRequestAbstract { +export class PaginationOffsetDto extends AbstractPaginationRequest { type: PaginationType.OFFSET = PaginationType.OFFSET; @Expose({ name: 'limit' }) @@ -20,8 +21,7 @@ export class PaginationOffsetDto implements PaginationRequestAbstract { @IsOptional() offset?: number; - @Expose({ name: 'query' }) - @IsString() - @IsOptional() - query?: string; + nextTotal(): number { + return this.total; + } } diff --git a/src/lib/dto/request.dto.ts b/src/lib/dto/request.dto.ts index a1c1554..3af0363 100644 --- a/src/lib/dto/request.dto.ts +++ b/src/lib/dto/request.dto.ts @@ -5,8 +5,8 @@ import { getMetadataStorage, MetadataStorage } from 'class-validator'; import { ValidationMetadata } from 'class-validator/types/metadata/ValidationMetadata'; import { BaseEntity } from 'typeorm'; +import { capitalizeFirstLetter } from '../capitalize-first-letter'; import { Method } from '../interface'; -import { capitalizeFirstLetter } from '../util'; export function CreateRequestDto(parentClass: typeof BaseEntity, group: Method) { const propertyNamesAppliedValidation = getPropertyNamesFromMetadata(parentClass, group); diff --git a/src/lib/interceptor/read-many-request.interceptor.spec.ts b/src/lib/interceptor/read-many-request.interceptor.spec.ts index 98b133d..265fad2 100644 --- a/src/lib/interceptor/read-many-request.interceptor.spec.ts +++ b/src/lib/interceptor/read-many-request.interceptor.spec.ts @@ -7,7 +7,6 @@ import { BaseEntity } from 'typeorm'; import { ReadManyRequestInterceptor } from './read-many-request.interceptor'; import { CUSTOM_REQUEST_OPTIONS } from '../constants'; -import { PaginationType } from '../interface'; import { ExecutionContextHost } from '../provider'; import { CrudLogger } from '../provider/crud-logger'; @@ -33,17 +32,6 @@ describe('ReadManyRequestInterceptor', () => { }).not.toThrowError(); }); - it('should be able to return pagination for GET_MORE type', () => { - const Interceptor = ReadManyRequestInterceptor({ entity: {} as typeof BaseEntity }, { relations: [], logger: new CrudLogger() }); - const interceptor = new Interceptor(); - - expect(interceptor.getPaginationRequest(PaginationType.CURSOR, { key: 'value', nextCursor: 'token' })).toEqual({ - nextCursor: 'token', - query: btoa('{}'), - type: 'cursor', - }); - }); - it('should be able to fields validation from entity columns', async () => { class QueryDto extends BaseEntity { @IsString({ groups: ['readMany'] }) @@ -79,6 +67,20 @@ describe('ReadManyRequestInterceptor', () => { await expect(interceptor.validateQuery({ col1: 1 })).rejects.toThrow(UnprocessableEntityException); await expect(interceptor.validateQuery({ col2: '1' })).rejects.toThrow(UnprocessableEntityException); await expect(interceptor.validateQuery({ col1: 1, col2: '2', col3: 1 })).rejects.toThrow(UnprocessableEntityException); + + // should be able to ignore limit and offset + expect(await interceptor.validateQuery({ col1: 1, col2: 2, limit: 3 })).toEqual({ + col1: '1', + col2: 2, + }); + expect(await interceptor.validateQuery({ col1: 1, col2: 2, offset: 10 })).toEqual({ + col1: '1', + col2: 2, + }); + expect(await interceptor.validateQuery({ col1: 1, col2: 2, limit: 3, offset: 10 })).toEqual({ + col1: '1', + col2: 2, + }); }); it('should be get relation values per each condition', () => { @@ -107,15 +109,4 @@ describe('ReadManyRequestInterceptor', () => { expect(interceptorWithFalseOptions.getRelations({ relations: ['table'] })).toEqual(['table']); expect(interceptorWithFalseOptions.getRelations({})).toEqual([]); }); - - it('should be validate pagination query', () => { - const Interceptor = ReadManyRequestInterceptor({ entity: {} as typeof BaseEntity }, { relations: [], logger: new CrudLogger() }); - const interceptor = new Interceptor(); - - interceptor.getPaginationRequest(PaginationType.CURSOR, undefined as any); - expect(() => interceptor.getPaginationRequest(PaginationType.CURSOR, { nextCursor: 3 })).toThrowError(UnprocessableEntityException); - - interceptor.getPaginationRequest(PaginationType.OFFSET, undefined as any); - expect(() => interceptor.getPaginationRequest(PaginationType.OFFSET, { limit: 200 })).toThrowError(UnprocessableEntityException); - }); }); diff --git a/src/lib/interceptor/read-many-request.interceptor.ts b/src/lib/interceptor/read-many-request.interceptor.ts index e4c1c41..979be94 100644 --- a/src/lib/interceptor/read-many-request.interceptor.ts +++ b/src/lib/interceptor/read-many-request.interceptor.ts @@ -1,17 +1,17 @@ import { CallHandler, ExecutionContext, mixin, NestInterceptor, UnprocessableEntityException } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; -import { validate, validateSync } from 'class-validator'; +import { validate } from 'class-validator'; import { Request } from 'express'; import _ from 'lodash'; import { Observable } from 'rxjs'; +import { FindOptionsWhere, LessThan, MoreThan } from 'typeorm'; import { CustomReadManyRequestOptions } from './custom-request.interceptor'; import { RequestAbstractInterceptor } from '../abstract'; import { CRUD_ROUTE_ARGS, CUSTOM_REQUEST_OPTIONS } from '../constants'; import { CRUD_POLICY } from '../crud.policy'; -import { PaginationCursorDto } from '../dto/pagination-cursor.dto'; -import { PaginationOffsetDto } from '../dto/pagination-offset.dto'; -import { CrudOptions, FactoryOption, Method, Sort, GROUP, PaginationType, PaginationRequest } from '../interface'; +import { CrudOptions, FactoryOption, Method, Sort, GROUP, PaginationType } from '../interface'; +import { PaginationHelper } from '../provider'; import { CrudReadManyRequest } from '../request'; const method = Method.READ_MANY; @@ -26,35 +26,37 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti const readManyOptions = crudOptions.routes?.[method] ?? {}; const customReadManyRequestOptions: CustomReadManyRequestOptions = req[CUSTOM_REQUEST_OPTIONS]; - const paginationType = (readManyOptions.paginationType ?? CRUD_POLICY[method].default?.paginationType) as PaginationType; + const paginationType = (readManyOptions.paginationType ?? CRUD_POLICY[method].default.paginationType) as PaginationType; if (req.params) { Object.assign(req.query, req.params); } - const pagination = this.getPaginationRequest(paginationType, req.query); + const pagination = PaginationHelper.getPaginationRequest(paginationType, req.query); const query = await (async () => { - if ( - (pagination.type === PaginationType.CURSOR && !_.isNil(pagination['nextCursor'])) || - (pagination.type === PaginationType.OFFSET && (!_.isNil(pagination['offset']) || !_.isNil(pagination['limit']))) - ) { + if (PaginationHelper.isNextPage(pagination)) { + pagination.setQuery(pagination.query ?? btoa('{}')); return {}; } - return this.validateQuery(req.query); + const query = await this.validateQuery(req.query); + pagination.setWhere(PaginationHelper.serialize(query)); + return query; })(); + const crudReadManyRequest: CrudReadManyRequest = new CrudReadManyRequest() .setPrimaryKey(factoryOption.primaryKeys ?? []) .setPagination(pagination) .setWithDeleted( _.isBoolean(customReadManyRequestOptions?.softDeleted) ? customReadManyRequestOptions.softDeleted - : crudOptions.routes?.[method]?.softDelete ?? (CRUD_POLICY[method].default.softDeleted as boolean), + : crudOptions.routes?.[method]?.softDelete ?? CRUD_POLICY[method].default.softDeleted, ) .setWhere(query) .setTake(readManyOptions.numberOfTake ?? CRUD_POLICY[method].default.numberOfTake) - .setSort(readManyOptions.sort ? Sort[readManyOptions.sort] : (CRUD_POLICY[method].default.sort as Sort)) + .setSort(readManyOptions.sort ? Sort[readManyOptions.sort] : CRUD_POLICY[method].default.sort) .setRelations(this.getRelations(customReadManyRequestOptions)) + .setDeserialize(this.deserialize) .generate(); this.crudLogger.logRequest(req, crudReadManyRequest.toString()); @@ -63,30 +65,18 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti return next.handle(); } - getPaginationRequest(paginationType: PaginationType, query: Record): PaginationRequest { - const plain = query ?? {}; - const transformed = - paginationType === PaginationType.OFFSET - ? plainToInstance(PaginationOffsetDto, plain, { excludeExtraneousValues: true }) - : plainToInstance(PaginationCursorDto, plain, { excludeExtraneousValues: true }); - const [error] = validateSync(transformed, { stopAtFirstError: true }); - - if (error) { - throw new UnprocessableEntityException(error); - } - - if (transformed.type === PaginationType.CURSOR && transformed.nextCursor && !transformed.query) { - transformed.query = btoa('{}'); - } - - return transformed; - } - async validateQuery(query: Record) { if (_.isNil(query)) { return {}; } + if ('limit' in query) { + delete query.limit; + } + if ('offset' in query) { + delete query.offset; + } + const transformed = plainToInstance(crudOptions.entity, query, { groups: [GROUP.READ_MANY] }); const errorList = await validate(transformed, { groups: [GROUP.READ_MANY], @@ -115,6 +105,21 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti } return factoryOption.relations; } + + deserialize({ pagination, findOptions, sort }: CrudReadManyRequest): FindOptionsWhere { + if (pagination.type === PaginationType.OFFSET) { + return PaginationHelper.deserialize(pagination.where); + } + const query: Record = PaginationHelper.deserialize(pagination.where); + const lastObject: Record = PaginationHelper.deserialize(pagination.nextCursor); + + const operator = (key: keyof T) => ((findOptions.order?.[key] ?? sort) === Sort.DESC ? LessThan : MoreThan); + + for (const [key, value] of Object.entries(lastObject)) { + query[key] = operator(key as keyof T)(value); + } + return query as FindOptionsWhere; + } } return mixin(MixinInterceptor); diff --git a/src/lib/interceptor/search-request.interceptor.spec.ts b/src/lib/interceptor/search-request.interceptor.spec.ts index 53fb833..527f98b 100644 --- a/src/lib/interceptor/search-request.interceptor.spec.ts +++ b/src/lib/interceptor/search-request.interceptor.spec.ts @@ -23,7 +23,7 @@ describe('SearchRequestInterceptor', () => { } let interceptor: any; - beforeEach(() => { + beforeAll(() => { const Interceptor = SearchRequestInterceptor( { entity: TestEntity }, { @@ -106,9 +106,10 @@ describe('SearchRequestInterceptor', () => { }); }); - it('should throw when query filter is not an array or empty', async () => { - await expect(interceptor.validateBody({ where: [] })).rejects.toThrow(UnprocessableEntityException); + it('should throw when query filter is not an array', async () => { + await expect(interceptor.validateBody({ where: {} })).rejects.toThrow(UnprocessableEntityException); await expect(interceptor.validateBody({ where: [null] })).rejects.toThrow(UnprocessableEntityException); + await expect(interceptor.validateBody({ where: [undefined] })).rejects.toThrow(UnprocessableEntityException); }); it('should throw when invalid query filter is given', async () => { diff --git a/src/lib/interceptor/search-request.interceptor.ts b/src/lib/interceptor/search-request.interceptor.ts index 2c285ae..7e0d81e 100644 --- a/src/lib/interceptor/search-request.interceptor.ts +++ b/src/lib/interceptor/search-request.interceptor.ts @@ -4,7 +4,7 @@ import { validate } from 'class-validator'; import { Request } from 'express'; import _ from 'lodash'; import { Observable } from 'rxjs'; -import { BaseEntity } from 'typeorm'; +import { BaseEntity, FindOptionsOrder, FindOptionsWhere, LessThan, MoreThan, And, FindOperator } from 'typeorm'; import { CustomSearchRequestOptions } from './custom-request.interceptor'; import { RequestAbstractInterceptor } from '../abstract'; @@ -12,9 +12,10 @@ import { CRUD_ROUTE_ARGS, CUSTOM_REQUEST_OPTIONS } from '../constants'; import { CRUD_POLICY } from '../crud.policy'; import { CreateParamsDto } from '../dto/params.dto'; import { RequestSearchDto } from '../dto/request-search.dto'; -import { CrudOptions, CrudSearchRequest, FactoryOption, GROUP, Method, Sort } from '../interface'; +import { CrudOptions, FactoryOption, GROUP, Method, PaginationType, Sort } from '../interface'; import { operatorBetween, operatorIn, operatorNull, operatorList, OperatorUnion } from '../interface/query-operation.interface'; -import { PaginationHelper } from '../provider'; +import { PaginationHelper, TypeOrmQueryBuilderHelper } from '../provider'; +import { CrudReadManyRequest } from '../request'; const method = Method.SEARCH; export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption: FactoryOption) { @@ -25,7 +26,7 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption async intercept(context: ExecutionContext, next: CallHandler): Promise> { const req: Record = context.switchToHttp().getRequest(); - + const searchOptions = crudOptions.routes?.[method] ?? {}; const customSearchRequestOptions: CustomSearchRequestOptions = req[CUSTOM_REQUEST_OPTIONS]; if (req.params && req.body?.where && Array.isArray(req.body.where)) { @@ -37,14 +38,48 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption _.merge(queryFilter, paramsCondition); } } - const requestSearchDto = await this.validateBody(req.body); - const crudSearchRequest: CrudSearchRequest = { - requestSearchDto, - relations: customSearchRequestOptions?.relations ?? factoryOption.relations, - }; + 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) { + pagination.setQuery(pagination.query ?? btoa('{}')); + return PaginationHelper.deserialize>(pagination.where); + } + const searchBody = await this.validateBody(req.body); + pagination.setWhere(PaginationHelper.serialize((searchBody ?? {}) as FindOptionsWhere)); + return searchBody; + })(); + + pagination.query = + pagination.query ?? + PaginationHelper.serialize((requestSearchDto.where ?? {}) as FindOptionsWhere); + const where: + | Array> + | (FindOptionsWhere & Partial) = + Array.isArray(requestSearchDto.where) && requestSearchDto.where.length > 0 + ? requestSearchDto.where.map((queryFilter, index) => + TypeOrmQueryBuilderHelper.queryFilterToFindOptionsWhere(queryFilter, index), + ) + : {}; - this.crudLogger.logRequest(req, crudSearchRequest); - req[CRUD_ROUTE_ARGS] = crudSearchRequest; + const crudReadManyRequest: CrudReadManyRequest = new CrudReadManyRequest() + .setPrimaryKey(factoryOption.primaryKeys ?? []) + .setPagination(pagination) + .setSelect(requestSearchDto.select) + .setWhere(where) + .setTake(requestSearchDto.take ?? CRUD_POLICY[method].default.numberOfTake) + .setOrder(requestSearchDto.order as FindOptionsOrder, CRUD_POLICY[method].default.sort) + .setWithDeleted( + requestSearchDto.withDeleted ?? crudOptions.routes?.[method]?.softDelete ?? CRUD_POLICY[method].default.softDeleted, + ) + .setRelations(customSearchRequestOptions?.relations ?? factoryOption.relations) + .setDeserialize(this.deserialize) + .generate(); + + this.crudLogger.logRequest(req, crudReadManyRequest.toString()); + req[CRUD_ROUTE_ARGS] = crudReadManyRequest; return next.handle(); } @@ -58,10 +93,6 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption const requestSearchDto = plainToInstance(RequestSearchDto, body); const searchOptions = crudOptions.routes?.[method] ?? {}; - if ('nextCursor' in requestSearchDto) { - return this.validatePagination(requestSearchDto); - } - if ('select' in requestSearchDto) { this.validateSelect(requestSearchDto.select); } @@ -80,6 +111,10 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption requestSearchDto.withDeleted = searchOptions.softDelete ?? (CRUD_POLICY[method].default.softDeleted as boolean); } + if ('take' in requestSearchDto) { + this.validateTake(requestSearchDto.take, searchOptions.limitOfTake); + } + requestSearchDto.take = 'take' in requestSearchDto ? this.validateTake(requestSearchDto.take, searchOptions.limitOfTake) @@ -88,33 +123,6 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption return requestSearchDto; } - validatePagination(requestSearchDto: RequestSearchDto): RequestSearchDto { - if (typeof requestSearchDto.nextCursor !== 'string') { - throw new UnprocessableEntityException('nextCursor should be String type'); - } - if ('query' in requestSearchDto && typeof requestSearchDto.query !== 'string') { - throw new UnprocessableEntityException('query should be String type'); - } - const preCondition = PaginationHelper.deserialize>(requestSearchDto.query); - const lastObject: Record = PaginationHelper.deserialize(requestSearchDto.nextCursor); - preCondition.where = preCondition.where ?? [{}]; - - const cursorCondition = Object.entries(lastObject).reduce( - (queryFilter, [key, operand]) => ({ - ...queryFilter, - [key]: { - operator: _.get(preCondition.order, key, CRUD_POLICY[method].default.sort) === Sort.DESC ? '<' : '>', - operand, - }, - }), - {}, - ); - for (const queryFilter of preCondition.where) { - _.merge(queryFilter, cursorCondition); - } - return preCondition; - } - validateSelect(select: RequestSearchDto['select']): void { if (!Array.isArray(select)) { throw new UnprocessableEntityException('select must be array type'); @@ -126,7 +134,7 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption } async validateQueryFilterList(value: unknown): Promise { - if (!Array.isArray(value) || value.length === 0) { + if (!Array.isArray(value)) { throw new UnprocessableEntityException('incorrect query format'); } for (const queryFilter of value) { @@ -241,6 +249,36 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption } return takeNumber; } + + deserialize({ pagination, findOptions, sort }: CrudReadManyRequest): Array> { + const where = findOptions.where as Array>; + if (pagination.type === PaginationType.OFFSET) { + return where; + } + + const lastObject: Record = PaginationHelper.deserialize(pagination.nextCursor); + + const operator = (key: keyof T) => ((findOptions.order?.[key] ?? sort) === Sort.DESC ? LessThan : MoreThan); + + const cursorCondition: Record> = Object.entries(lastObject).reduce( + (queryFilter, [key, operand]) => ({ + ...queryFilter, + [key]: operator(key as keyof T)(operand), + }), + {}, + ); + for (const queryFilter of where) { + for (const [key, operation] of Object.entries(cursorCondition)) { + _.merge( + queryFilter, + key in queryFilter + ? { [key]: And(operation, (queryFilter as Record>)[key]) } + : { [key]: operation }, + ); + } + } + return where; + } } return mixin(MixinInterceptor); diff --git a/src/lib/interface/decorator-option.interface.ts b/src/lib/interface/decorator-option.interface.ts index c2bd928..9a07f6a 100644 --- a/src/lib/interface/decorator-option.interface.ts +++ b/src/lib/interface/decorator-option.interface.ts @@ -101,6 +101,11 @@ export interface CrudOptions { softDelete?: boolean; } & Omit; [Method.SEARCH]?: { + /** + * Type of pagination to use. Currently 'offset' and 'cursor' are supported. + * @default PaginationType.CURSOR + */ + paginationType?: PaginationType | `${PaginationType}`; /** * Default number of entities should be taken. See `crud.policy.ts` for more details. * @default 20 diff --git a/src/lib/interface/pagination.interface.ts b/src/lib/interface/pagination.interface.ts index 64ff3d3..72420f6 100644 --- a/src/lib/interface/pagination.interface.ts +++ b/src/lib/interface/pagination.interface.ts @@ -1,3 +1,4 @@ +import { PaginationAbstractResponse } from '../abstract'; import { PaginationCursorDto } from '../dto/pagination-cursor.dto'; import { PaginationOffsetDto } from '../dto/pagination-offset.dto'; @@ -10,21 +11,13 @@ export const PAGINATION_SWAGGER_QUERY: Record { - data: T[]; -} - export interface CursorPaginationResponse extends PaginationAbstractResponse { metadata: { - query: string; limit: number; total: number; nextCursor: string; @@ -34,22 +27,25 @@ export interface CursorPaginationResponse extends PaginationAbstractResponse< export interface OffsetPaginationResponse extends PaginationAbstractResponse { metadata: { /** - * ν˜„μž¬ page 번호 + * Current page number */ page: number; /** - * 전체 page 개수 + * Total page count */ pages: number; /** - * 전체 데이터 개수 + * Total data count */ total: number; /** - * ν•œ νŽ˜μ΄μ§€μ˜ 데이터 μ΅œλŒ€ 개수 + * Maximum number of data on a page */ offset: number; - query: string; + /** + * cursor token for next page + */ + nextCursor: string; }; } diff --git a/src/lib/provider/pagination.helper.spec.ts b/src/lib/provider/pagination.helper.spec.ts index 13a5f92..5dab63d 100644 --- a/src/lib/provider/pagination.helper.spec.ts +++ b/src/lib/provider/pagination.helper.spec.ts @@ -1,4 +1,7 @@ +import { UnprocessableEntityException } from '@nestjs/common'; + import { PaginationHelper } from './pagination.helper'; +import { PaginationType } from '../interface'; describe('Pagination Helper', () => { it('should serialize entity', () => { @@ -14,4 +17,56 @@ describe('Pagination Helper', () => { const cursor = 'malformed'; expect(PaginationHelper.deserialize(cursor)).toEqual({}); }); + + it('should be able to return pagination for GET_MORE type', () => { + expect(PaginationHelper.getPaginationRequest(PaginationType.CURSOR, { key: 'value', nextCursor: 'token' })).toEqual({ + query: 'token', + type: 'cursor', + _isNext: false, + }); + + expect(PaginationHelper.getPaginationRequest(PaginationType.CURSOR, { key: 'value' })).toEqual({ + query: undefined, + type: 'cursor', + _isNext: false, + }); + + expect(PaginationHelper.getPaginationRequest(PaginationType.CURSOR, { query: 'query' })).toEqual({ + query: undefined, + type: 'cursor', + _isNext: false, + }); + }); + + it('should be validate pagination query', () => { + expect(PaginationHelper.getPaginationRequest(PaginationType.CURSOR, undefined as any)).toEqual({ + type: 'cursor', + query: undefined, + _isNext: false, + }); + + expect(() => PaginationHelper.getPaginationRequest(PaginationType.CURSOR, { nextCursor: 3 })).toThrowError( + UnprocessableEntityException, + ); + + expect(PaginationHelper.getPaginationRequest(PaginationType.OFFSET, undefined as any)).toEqual({ + type: 'offset', + limit: undefined, + offset: undefined, + query: undefined, + _isNext: false, + }); + + expect(() => PaginationHelper.getPaginationRequest(PaginationType.OFFSET, { nextCursor: 3 })).toThrowError( + UnprocessableEntityException, + ); + + expect(() => PaginationHelper.getPaginationRequest(PaginationType.OFFSET, { limit: 200 })).toThrowError( + UnprocessableEntityException, + ); + }); + + it('should be return empty object when nextCursor is undefined', () => { + expect(PaginationHelper.deserialize(undefined as any)).toEqual({}); + }); }); diff --git a/src/lib/provider/pagination.helper.ts b/src/lib/provider/pagination.helper.ts index 5c9717f..1973b2e 100644 --- a/src/lib/provider/pagination.helper.ts +++ b/src/lib/provider/pagination.helper.ts @@ -1,9 +1,15 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { validateSync } from 'class-validator'; import { FindOptionsWhere } from 'typeorm'; +import { PaginationCursorDto, PaginationOffsetDto, RequestSearchDto } from '../dto'; +import { PaginationRequest, PaginationType } from '../interface'; + const encoding = 'base64'; export class PaginationHelper { - static serialize(entity: FindOptionsWhere): string { + static serialize(entity: FindOptionsWhere | Array> | RequestSearchDto | Record): string { return Buffer.from(JSON.stringify(entity)).toString(encoding); } @@ -17,4 +23,29 @@ export class PaginationHelper { return {} as T; } } + + static getPaginationRequest(paginationType: PaginationType, query: Record): PaginationRequest { + const plain = query ?? {}; + const transformed = + paginationType === PaginationType.OFFSET + ? plainToInstance(PaginationOffsetDto, plain, { excludeExtraneousValues: true }) + : plainToInstance(PaginationCursorDto, plain, { excludeExtraneousValues: true }); + const [error] = validateSync(transformed, { stopAtFirstError: true }); + + if (error) { + throw new UnprocessableEntityException(error); + } + + return transformed; + } + + /** + * [EN] Check if the request is requesting the next page. + * [KR] Request μš”μ²­μ΄ λ‹€μŒ νŽ˜μ΄μ§€λ₯Ό μš”μ²­ν•˜λŠ”μ§€ ν™•μΈν•©λ‹ˆλ‹€. + * @param paginationRequest + * @returns boolean + */ + static isNextPage(paginationRequest: PaginationRequest): boolean { + return paginationRequest.query != null; + } } diff --git a/src/lib/request/read-many.request.ts b/src/lib/request/read-many.request.ts index 5b1d8a0..03cd450 100644 --- a/src/lib/request/read-many.request.ts +++ b/src/lib/request/read-many.request.ts @@ -1,18 +1,25 @@ import _ from 'lodash'; -import { FindManyOptions, FindOptionsWhere, LessThan, MoreThan } from 'typeorm'; +import { FindManyOptions, FindOptionsOrder, FindOptionsSelect, FindOptionsSelectByString, FindOptionsWhere } from 'typeorm'; import { CRUD_POLICY } from '../crud.policy'; import { Method, PaginationRequest, PaginationResponse, PaginationType, PrimaryKey, Sort } from '../interface'; import { PaginationHelper } from '../provider'; +type Where = FindOptionsWhere | Array>; export class CrudReadManyRequest { private _primaryKeys: PrimaryKey[] = []; - private _findOptions: FindManyOptions & { where: FindOptionsWhere; take: number } = { + private _findOptions: FindManyOptions & { + where: Where; + take: number; + order: FindOptionsOrder; + } = { where: {}, take: CRUD_POLICY[Method.READ_MANY].default.numberOfTake, + order: {}, }; private _sort: Sort; private _pagination: PaginationRequest; + private _deserialize: (crudReadManyRequest: CrudReadManyRequest) => Where; get primaryKeys() { return this._primaryKeys; @@ -24,6 +31,10 @@ export class CrudReadManyRequest { return this._pagination; } + get sort() { + return this._sort; + } + setPagination(pagination: PaginationRequest): this { this._pagination = pagination; return this; @@ -39,7 +50,13 @@ export class CrudReadManyRequest { return this; } - setWhere(where: FindOptionsWhere & Partial): this { + // TODO: FindOptionsSelectByString is deprecated. + setSelect(select: FindOptionsSelect | FindOptionsSelectByString | undefined): this { + this._findOptions.select = select; + return this; + } + + setWhere(where: (FindOptionsWhere & Partial) | Array>): this { this._findOptions.where = where; return this; } @@ -55,73 +72,71 @@ export class CrudReadManyRequest { return this; } + setOrder(order: FindOptionsOrder, sort: Sort): this { + this._sort = sort; + this._findOptions.order = order; + return this; + } + setRelations(relations: string[] | undefined): this { this._findOptions.relations = relations; return this; } + setDeserialize(deserialize: (crudReadManyRequest: CrudReadManyRequest) => FindOptionsWhere | Array>): this { + this._deserialize = deserialize; + return this; + } + generate(): this { if (this.pagination.type === PaginationType.OFFSET) { if (this.pagination.limit != null) { this._findOptions.take = this.pagination.limit; } if (Number.isFinite(this.pagination.offset)) { - this._findOptions.where = PaginationHelper.deserialize(this.pagination.query); + this._findOptions.where = this._deserialize(this); this._findOptions.skip = this.pagination.offset; } } if (this.pagination.type === PaginationType.CURSOR && this.pagination.nextCursor) { - this._findOptions.where = this.paginateCursorWhereByNextCursor(); + this._findOptions.where = this._deserialize(this); } return this; } toString(): string { - return JSON.stringify(this); + return JSON.stringify(_.omit(this, ['_deserialize'])); } toResponse(data: T[], total: number): PaginationResponse { + const nextCursor = PaginationHelper.serialize( + _.pick( + data.at(-1), + this.primaryKeys.map(({ name }) => name), + ) as FindOptionsWhere, + ); if (this.pagination.type === PaginationType.OFFSET) { return { data, metadata: { page: this.pagination.offset ? Math.floor(this.pagination.offset / this.findOptions.take) + 1 : 1, pages: total ? Math.ceil(total / this.findOptions.take) : 1, - total, offset: (this.pagination.offset ?? 0) + data.length, - query: this.pagination.query ?? PaginationHelper.serialize(this.findOptions.where), + total, + nextCursor: this.pagination.makeQuery(total, nextCursor), }, }; } + return { data, metadata: { - nextCursor: PaginationHelper.serialize( - _.pick( - data.at(-1), - this.primaryKeys.map(({ name }) => name), - ) as FindOptionsWhere, - ), limit: this.findOptions.take, total, - query: this.pagination.query ?? PaginationHelper.serialize(this.findOptions.where), + nextCursor: this.pagination.makeQuery(total, nextCursor), }, }; } - - private paginateCursorWhereByNextCursor(): FindOptionsWhere { - if (this.pagination.type !== PaginationType.CURSOR) { - return {}; - } - const query: Record = PaginationHelper.deserialize(this.pagination.query); - const lastObject: Record = PaginationHelper.deserialize(this.pagination.nextCursor); - - const operator = this._sort === Sort.DESC ? LessThan : MoreThan; - for (const [key, value] of Object.entries(lastObject)) { - query[key] = operator(value); - } - return query as FindOptionsWhere; - } } diff --git a/src/lib/util.ts b/src/lib/util.ts deleted file mode 100644 index 248f232..0000000 --- a/src/lib/util.ts +++ /dev/null @@ -1,6 +0,0 @@ -export const capitalizeFirstLetter = (raw: string) => `${raw.charAt(0).toUpperCase()}${raw.slice(1)}`; - -export const isSomeEnum = - >(enumType: TEnum) => - (nextCursor: unknown): nextCursor is TEnum[keyof TEnum] => - Object.values(enumType).includes(nextCursor as TEnum[keyof TEnum]);