From a3a68997e9aaebbc667c0a049e6ebbfd44005e24 Mon Sep 17 00:00:00 2001 From: "phat.do" Date: Sat, 11 Jan 2025 17:58:39 +0700 Subject: [PATCH] feat: re-routing latest page have items --- README.md | 51 ++++-- .../paginate-raw-and-entities.spec.ts | 13 ++ src/__tests__/paginate-raw.spec.ts | 15 +- src/__tests__/paginate.query.builder.spec.ts | 26 +++ src/interfaces/index.ts | 6 + src/paginate.ts | 168 ++++++++++++++---- 6 files changed, 221 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e5ce6ee1..a2b609f4 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ Pagination helper method for TypeORM repositories or queryBuilders with strict t ```bash $ yarn add nestjs-typeorm-paginate ``` + or + ```bash $ npm i nestjs-typeorm-paginate ``` @@ -91,7 +93,13 @@ export class CatService { ##### Controller ```ts -import { Controller, DefaultValuePipe, Get, ParseIntPipe, Query } from '@nestjs/common'; +import { + Controller, + DefaultValuePipe, + Get, + ParseIntPipe, + Query, +} from '@nestjs/common'; import { CatService } from './cat.service'; import { CatEntity } from './cat.entity'; import { Pagination } from 'nestjs-typeorm-paginate'; @@ -180,7 +188,6 @@ export class CatsController { `links.next`: A URL for the next page to call | `""` (blank) if no page to call `links.last`: A URL for the last page to call | `""` (blank) if no `route` is defined - > Do note that `links.first` may not have the 'page' query param defined ## Find Parameters @@ -210,7 +217,7 @@ import { Entity, OneToMany } from 'typeorm'; @Entity() export class CatEntity { - @OneToMany(t => TigerKingEntity, tigerKing.cats, { + @OneToMany((t) => TigerKingEntity, tigerKing.cats, { eager: true, }) tigerKings: TigerKingEntity[]; @@ -268,12 +275,11 @@ Let's assume there's a joined table that matches each cat with its cat toys. And we want to bring how many toys each cat has. ```typescript - const queryBuilder = this.repository .createQueryBuilder<{ type: string; totalLives: string }>('cat') - .leftJoinAndSelect('cat.toys', 'toys') - .addSelect('COUNT(toys)::INTEGER', 'toyCount') - .groupBy('cat.name'); + .leftJoinAndSelect('cat.toys', 'toys') + .addSelect('COUNT(toys)::INTEGER', 'toyCount') + .groupBy('cat.name'); ``` This will allow us to get the paginated cats information with the additional raw query to build our actual response value. @@ -329,7 +335,6 @@ The rawResults array will look something like this: If you wanted to alter the meta data that is returned from the pagination object. Then use the `metaTransformer` in the options like so ```ts - class CustomPaginationMeta { constructor( public readonly count: number, @@ -337,33 +342,41 @@ class CustomPaginationMeta { ) {} } -return paginate(this.repository, { +return paginate(this.repository, { page, limit, - metaTransformer: (meta: IPaginationMeta): CustomPaginationMeta => new CustomPaginationMeta( - meta.itemCount, - meta.totalItems, - ), - }); + metaTransformer: (meta: IPaginationMeta): CustomPaginationMeta => + new CustomPaginationMeta(meta.itemCount, meta.totalItems), +}); ``` This will result in the above returning `CustomPaginationMeta` in the `meta` property instead of the default `IPaginationMeta`. - ## Custom links query params labels If you want to alter the `limit` and/or `page` labels in meta links, then use `routingLabels` in the options like so ```ts - -return paginate(this.repository, { +return paginate(this.repository, { page, limit, routingLabels: { limitLabel: 'page-size', // default: limit pageLabel: 'current-page', //default: page - } - }); + }, +}); ``` This will result links like `http://example.com/something?current-page=1&page-size=3`. + +## Custom re-routing latest page have items + +If you just want to return latest has items when the parameter `page` was over than `page` calculated in the database at the moment, then use `routingLatest` as the options. Make sure you not set countQueries to `false`. It will be ignored when pagination is disabled and `routingLatest` option won't be affect anymore. + +```ts +return paginate(this.repository, { + page, + limit, + routingLatest: true, +}); +``` diff --git a/src/__tests__/paginate-raw-and-entities.spec.ts b/src/__tests__/paginate-raw-and-entities.spec.ts index f5686640..35ec900f 100644 --- a/src/__tests__/paginate-raw-and-entities.spec.ts +++ b/src/__tests__/paginate-raw-and-entities.spec.ts @@ -186,4 +186,17 @@ describe('Test paginateRawAndEntities function', () => { expect(result.meta.totalItems).toBe(undefined); expect(result.meta.totalPages).toBe(undefined); }); + + it('Can routing to latest page have items', async () => { + const [result] = await paginateRawAndEntities(queryBuilder, { + limit: 10, + page: 2, + countQueries: true, + routingLatest: true, + }); + + expect(result).toBeInstanceOf(Pagination); + expect(result.meta.totalItems).toEqual(10); + expect(result.meta.currentPage).toEqual(1); + }); }); diff --git a/src/__tests__/paginate-raw.spec.ts b/src/__tests__/paginate-raw.spec.ts index c3ea05d4..365f71f6 100644 --- a/src/__tests__/paginate-raw.spec.ts +++ b/src/__tests__/paginate-raw.spec.ts @@ -51,7 +51,7 @@ describe('Test paginateRaw function', () => { }); afterAll(async () => { - await queryBuilder.delete(); + queryBuilder.delete(); await app.close(); }); @@ -180,4 +180,17 @@ describe('Test paginateRaw function', () => { expect(result.meta.totalItems).toBe(undefined); expect(result.meta.totalPages).toBe(undefined); }); + + it('Can routing to latest page have items', async () => { + const result = await paginateRaw(queryBuilder, { + limit: 10, + page: 2, + countQueries: true, + routingLatest: true, + }); + + expect(result).toBeInstanceOf(Pagination); + expect(result.meta.totalItems).toEqual(10); + expect(result.meta.currentPage).toEqual(1); + }); }); diff --git a/src/__tests__/paginate.query.builder.spec.ts b/src/__tests__/paginate.query.builder.spec.ts index 93cfd352..390e33a1 100644 --- a/src/__tests__/paginate.query.builder.spec.ts +++ b/src/__tests__/paginate.query.builder.spec.ts @@ -113,4 +113,30 @@ describe('Paginate with queryBuilder', () => { expect(result).toBeInstanceOf(Pagination); expect(result.meta.totalItems).toEqual(10); }); + + it('Can routing to latest page have items', async () => { + await testRelatedQueryBuilder + .createQueryBuilder() + .insert() + .into(TestRelatedEntity) + .values([ + { id: 1, testId: 1 }, + { id: 2, testId: 1 }, + { id: 3, testId: 1 }, + ]) + .execute(); + + const qb = queryBuilder.leftJoinAndSelect('t.related', 'r'); + + const result = await paginate(qb, { + limit: 15, + page: 2, + routingLatest: true, + countQueries: true, + }); + + expect(result).toBeInstanceOf(Pagination); + expect(result.meta.totalItems).toEqual(10); + expect(result.meta.currentPage).toEqual(1); + }); }); diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 659a91e4..a4d809db 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -50,6 +50,12 @@ export interface IPaginationOptions { * Enables or disables query result caching. */ cacheQueries?: TypeORMCacheType; + + /** + * @default false + * Enables or disables routing to the latest page have items + */ + routingLatest?: boolean; } export type TypeORMCacheType = diff --git a/src/paginate.ts b/src/paginate.ts index 2e59e09b..ff8af6b6 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -51,24 +51,48 @@ export async function paginateRaw< queryBuilder: SelectQueryBuilder, options: IPaginationOptions, ): Promise> { - const [page, limit, route, paginationType, countQueries, cacheOption] = - resolveOptions(options); + let [ + page, + limit, + route, + paginationType, + countQueries, + cacheOption, + routingLatest, + ] = resolveOptions(options); - const promises: [Promise, Promise | undefined] = [ - (paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET - ? queryBuilder.limit(limit).offset((page - 1) * limit) - : queryBuilder.take(limit).skip((page - 1) * limit) - ) - .cache(cacheOption) - .getRawMany(), + const promises: [Promise | undefined | number, Promise] = [ undefined, + Promise.resolve([]), ]; - if (countQueries) { - promises[1] = countQuery(queryBuilder, cacheOption); + // To re-routing latest page have items need to set countQueries & routingLabels to true + if (routingLatest && countQueries) { + let total = await countQuery(queryBuilder, cacheOption); + + // Recalculate the latest page that have items + page = + total / Number(limit) < Number(page) + ? Math.ceil(total / Number(limit)) + : +page; + + promises[0] = Promise.resolve(total); } - const [items, total] = await Promise.all(promises); + // Avoid duplicate count query + if (countQueries && !routingLatest) { + promises[0] = countQuery(queryBuilder, cacheOption); + } + + promises[1] = ( + paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET + ? queryBuilder.limit(limit).offset((page - 1) * limit) + : queryBuilder.take(limit).skip((page - 1) * limit) + ) + .cache(cacheOption) + .getRawMany(); + + const [total, items] = await Promise.all(promises); return createPaginationObject({ items, @@ -88,27 +112,54 @@ export async function paginateRawAndEntities< queryBuilder: SelectQueryBuilder, options: IPaginationOptions, ): Promise<[Pagination, Partial[]]> { - const [page, limit, route, paginationType, countQueries, cacheOption] = - resolveOptions(options); + let [ + page, + limit, + route, + paginationType, + countQueries, + cacheOption, + routingLatest, + ] = resolveOptions(options); const promises: [ - Promise<{ entities: T[]; raw: T[] }>, Promise | undefined, + Promise<{ entities: T[]; raw: T[] }>, ] = [ - (paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET - ? queryBuilder.limit(limit).offset((page - 1) * limit) - : queryBuilder.take(limit).skip((page - 1) * limit) - ) - .cache(cacheOption) - .getRawAndEntities(), undefined, + Promise.resolve({ + entities: [], + raw: [], + }), ]; - if (countQueries) { - promises[1] = countQuery(queryBuilder, cacheOption); + // To re-routing latest page have items need to set countQueries & routingLabels to true + if (routingLatest && countQueries) { + let total = await countQuery(queryBuilder, cacheOption); + + // Recalculate the latest page that have items + page = + total / Number(limit) < Number(page) + ? Math.ceil(total / Number(limit)) + : +page; + + promises[0] = Promise.resolve(total); } - const [itemObject, total] = await Promise.all(promises); + // Avoid duplicate count query + if (countQueries && !routingLatest) { + promises[0] = countQuery(queryBuilder, cacheOption); + } + + promises[1] = ( + paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET + ? queryBuilder.limit(limit).offset((page - 1) * limit) + : queryBuilder.take(limit).skip((page - 1) * limit) + ) + .cache(cacheOption) + .getRawAndEntities(); + + const [total, itemObject] = await Promise.all(promises); return [ createPaginationObject({ @@ -126,7 +177,15 @@ export async function paginateRawAndEntities< function resolveOptions( options: IPaginationOptions, -): [number, number, string, PaginationTypeEnum, boolean, TypeORMCacheType] { +): [ + number, + number, + string, + PaginationTypeEnum, + boolean, + TypeORMCacheType, + boolean, +] { const page = resolveNumericOption(options, 'page', DEFAULT_PAGE); const limit = resolveNumericOption(options, 'limit', DEFAULT_LIMIT); const route = options.route; @@ -135,8 +194,17 @@ function resolveOptions( const countQueries = typeof options.countQueries !== 'undefined' ? options.countQueries : true; const cacheQueries = options.cacheQueries || false; + const routingLatest = options.routingLatest || false; - return [page, limit, route, paginationType, countQueries, cacheQueries]; + return [ + page, + limit, + route, + paginationType, + countQueries, + cacheQueries, + routingLatest, + ]; } function resolveNumericOption( @@ -208,24 +276,48 @@ async function paginateQueryBuilder( queryBuilder: SelectQueryBuilder, options: IPaginationOptions, ): Promise> { - const [page, limit, route, paginationType, countQueries, cacheOption] = - resolveOptions(options); + let [ + page, + limit, + route, + paginationType, + countQueries, + cacheOption, + routingLatest, + ] = resolveOptions(options); - const promises: [Promise, Promise | undefined] = [ - (PaginationTypeEnum.LIMIT_AND_OFFSET === paginationType - ? queryBuilder.limit(limit).offset((page - 1) * limit) - : queryBuilder.take(limit).skip((page - 1) * limit) - ) - .cache(cacheOption) - .getMany(), + const promises: [Promise | undefined | number, Promise] = [ undefined, + Promise.resolve([]), ]; - if (countQueries) { - promises[1] = countQuery(queryBuilder, cacheOption); + // To re-routing latest page have items need to set countQueries & routingLabels to true + if (routingLatest && countQueries) { + let total = await countQuery(queryBuilder, cacheOption); + + // Recalculate the latest page that have items + page = + total / Number(limit) < Number(page) + ? Math.ceil(total / Number(limit)) + : +page; + + promises[0] = Promise.resolve(total); } - const [items, total] = await Promise.all(promises); + // Avoid duplicate count query + if (countQueries && !routingLatest) { + promises[0] = countQuery(queryBuilder, cacheOption); + } + + promises[1] = ( + PaginationTypeEnum.LIMIT_AND_OFFSET === paginationType + ? queryBuilder.limit(limit).offset((page - 1) * limit) + : queryBuilder.take(limit).skip((page - 1) * limit) + ) + .cache(cacheOption) + .getMany(); + + const [total, items] = await Promise.all(promises); return createPaginationObject({ items,