diff --git a/spec/pagination/pagination.spec.ts b/spec/pagination/pagination.spec.ts index 228d3c6..2143f4d 100644 --- a/spec/pagination/pagination.spec.ts +++ b/spec/pagination/pagination.spec.ts @@ -246,6 +246,37 @@ describe('Pagination', () => { expect(lastOneOfFirstResponse.type).toEqual(0); expect(firstOneOfNextResponse.type).toEqual(1); }); + + it('should return items queried based on request when nextCursor is invalid', async () => { + // readMany + const { body: responseBody } = await request(app.getHttpServer()) + .get(`/${PaginationType.CURSOR}`) + .query({ + type: 1, + nextCursor: 'invalid', + }) + .expect(HttpStatus.OK); + + expect(responseBody.data).toHaveLength(20); + expect(responseBody.metadata).toEqual({ + nextCursor: expect.any(String), + limit: 20, + total: 50, + }); + + // search + const { body: searchResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.CURSOR}/search`) + .send({ where: [{ type: { operator: '=', operand: 1 } }], nextCursor: 'invalid' }) + .expect(HttpStatus.OK); + + expect(searchResponseBody.data).toHaveLength(20); + expect(searchResponseBody.metadata).toEqual({ + nextCursor: expect.any(String), + limit: 20, + total: 50, + }); + }); }); describe('Offset', () => { @@ -536,5 +567,37 @@ describe('Pagination', () => { expect(lastOneOfFirstResponse.type).toEqual(0); expect(firstOneOfNextResponse.type).toEqual(1); }); + + it('should return items queried based on request when nextCursor is invalid', async () => { + // readMany + const { body: readManyResponseBody } = await request(app.getHttpServer()) + .get(`/${PaginationType.OFFSET}`) + .query({ + nextCursor: 'invalid', + type: 1, + }) + .expect(HttpStatus.OK); + + expect(readManyResponseBody.metadata).toEqual({ + page: 1, + pages: 3, + total: 50, + offset: 20, + nextCursor: expect.any(String), + }); + + // search + const { body: searchResponseBody } = await request(app.getHttpServer()) + .post(`/${PaginationType.OFFSET}/search`) + .send({ where: [{ type: { operator: '=', operand: 1 } }], nextCursor: 'invalid' }) + .expect(HttpStatus.OK); + expect(searchResponseBody.metadata).toEqual({ + page: 1, + pages: 3, + total: 50, + offset: 20, + nextCursor: expect.any(String), + }); + }); }); }); diff --git a/src/lib/abstract/abstract.pagination.spec.ts b/src/lib/abstract/abstract.pagination.spec.ts index 0a63df1..aa5cd4b 100644 --- a/src/lib/abstract/abstract.pagination.spec.ts +++ b/src/lib/abstract/abstract.pagination.spec.ts @@ -25,7 +25,8 @@ describe('AbstractPaginationRequest', () => { it('should do nothing when query is invalid', () => { const paginationRequest = new PaginationRequest(); - paginationRequest.setQuery('invalid'); + const isQueryValid = paginationRequest.setQuery('invalid'); + expect(isQueryValid).toEqual(false); expect(paginationRequest.isNext).toBeFalsy(); }); }); diff --git a/src/lib/abstract/abstract.pagination.ts b/src/lib/abstract/abstract.pagination.ts index cf589af..468c030 100644 --- a/src/lib/abstract/abstract.pagination.ts +++ b/src/lib/abstract/abstract.pagination.ts @@ -44,17 +44,25 @@ export abstract class AbstractPaginationRequest { ).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 { - // + setQuery(query: string): boolean { + const paginationQuery: PaginationQuery | null = (() => { + try { + return JSON.parse(Buffer.from(query, encoding).toString()); + } catch { + return null; + } + })(); + if (paginationQuery == null) { + return false; } + + this._where = paginationQuery.where; + this._total = paginationQuery.total; + this._nextCursor = paginationQuery.nextCursor; + + this._isNext = true; + + return true; } protected get total(): number { diff --git a/src/lib/abstract/abstract.request.interceptor.ts b/src/lib/abstract/abstract.request.interceptor.ts index 30ba376..f6271dd 100644 --- a/src/lib/abstract/abstract.request.interceptor.ts +++ b/src/lib/abstract/abstract.request.interceptor.ts @@ -43,7 +43,7 @@ export abstract class RequestAbstractInterceptor { crudOptions: CrudOptions, method: Exclude, ): Author | undefined { - const author = crudOptions?.routes?.[method]?.author; + const author = crudOptions.routes?.[method]?.author; if (!author) { return; diff --git a/src/lib/dto/request-search.dto.ts b/src/lib/dto/request-search.dto.ts index e779e01..fd37344 100644 --- a/src/lib/dto/request-search.dto.ts +++ b/src/lib/dto/request-search.dto.ts @@ -4,7 +4,7 @@ import { Sort } from '../interface'; import { QueryFilter, operators } from '../interface/query-operation.interface'; export class RequestSearchDto { - @ApiPropertyOptional({ description: 'select fields', isArray: true, type: [String] }) + @ApiPropertyOptional({ description: 'select fields', isArray: true, type: String }) select?: Array>; @ApiPropertyOptional({ @@ -39,12 +39,9 @@ export class RequestSearchDto { @ApiPropertyOptional({ description: 'withDeleted', type: Boolean }) withDeleted?: boolean; - @ApiPropertyOptional({ description: 'take', type: Number }) + @ApiPropertyOptional({ description: 'take', type: Number, example: 20 }) take?: number; @ApiPropertyOptional({ description: 'Use to search the next page', type: String }) nextCursor?: string; - - @ApiPropertyOptional({ description: 'Use to search the next page under the same conditions', type: String }) - query?: string; } diff --git a/src/lib/interceptor/read-many-request.interceptor.ts b/src/lib/interceptor/read-many-request.interceptor.ts index bed8c0b..7dd21e0 100644 --- a/src/lib/interceptor/read-many-request.interceptor.ts +++ b/src/lib/interceptor/read-many-request.interceptor.ts @@ -42,8 +42,10 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti const query = await (async () => { if (PaginationHelper.isNextPage(pagination)) { - pagination.setQuery(pagination.query ?? btoa('{}')); - return {}; + const isQueryValid = pagination.setQuery(pagination.query); + if (isQueryValid) { + return {}; + } } const query = await this.validateQuery(req.query); pagination.setWhere(PaginationHelper.serialize(query)); @@ -83,6 +85,9 @@ export function ReadManyRequestInterceptor(crudOptions: CrudOptions, factoryOpti if ('offset' in query) { delete query.offset; } + if ('nextCursor' in query) { + delete query.nextCursor; + } const transformed = plainToInstance(crudOptions.entity as ClassConstructor, query, { groups: [GROUP.READ_MANY], diff --git a/src/lib/interceptor/search-request.interceptor.ts b/src/lib/interceptor/search-request.interceptor.ts index 0bade61..ccd5acc 100644 --- a/src/lib/interceptor/search-request.interceptor.ts +++ b/src/lib/interceptor/search-request.interceptor.ts @@ -51,8 +51,10 @@ export function SearchRequestInterceptor(crudOptions: CrudOptions, factoryOption const requestSearchDto = await (async () => { if (isNextPage) { - pagination.setQuery(pagination.query ?? btoa('{}')); - return PaginationHelper.deserialize>(pagination.where); + const isQueryValid = pagination.setQuery(pagination.query); + if (isQueryValid) { + return PaginationHelper.deserialize>(pagination.where); + } } const searchBody = await this.validateBody(req.body); pagination.setWhere(PaginationHelper.serialize((searchBody ?? {}) as FindOptionsWhere));