diff --git a/package.json b/package.json index 81128db6..a6218e50 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,10 @@ "paginate" ], "devDependencies": { - "@nestjs/common": "^9.0.5", - "@nestjs/core": "9.4.3", - "@nestjs/testing": "^9.0.5", - "@nestjs/typeorm": "^7.1.0", + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "@nestjs/testing": "^9.0.0", + "@nestjs/typeorm": "^9.0.0", "@types/jest": "^27.0.0", "@types/node": "^20.9.0", "coveralls": "^3.0.5", @@ -29,7 +29,7 @@ "mysql": "^2.17.1", "prettier": "^3.1.0", "reflect-metadata": "^0.1.13", - "rxjs": "^6.5.2", + "rxjs": "^7.1.0", "ts-jest": "^26.4.4", "ts-node": "^10.0.0", "typeorm": "0.3.17", diff --git a/src/__tests__/base-orm-config.ts b/src/__tests__/base-orm-config.ts index 590e56bf..ba383d73 100644 --- a/src/__tests__/base-orm-config.ts +++ b/src/__tests__/base-orm-config.ts @@ -1,9 +1,10 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm'; +import { TestPivotEntity } from './test-pivot.entity'; import { TestRelatedEntity } from './test-related.entity'; import { TestEntity } from './test.entity'; export const baseOrmConfigs: TypeOrmModuleOptions = { - entities: [TestEntity, TestRelatedEntity], + entities: [TestEntity, TestRelatedEntity, TestPivotEntity], host: 'localhost', port: 3306, type: 'mysql', diff --git a/src/__tests__/paginate-raw-and-entities.spec.ts b/src/__tests__/paginate-raw-and-entities.spec.ts index f5686640..86171f79 100644 --- a/src/__tests__/paginate-raw-and-entities.spec.ts +++ b/src/__tests__/paginate-raw-and-entities.spec.ts @@ -4,6 +4,7 @@ import { Connection, QueryRunner, SelectQueryBuilder } from 'typeorm'; import { paginateRawAndEntities } from '../paginate'; import { Pagination } from '../pagination'; import { baseOrmConfigs } from './base-orm-config'; +import { TestPivotEntity } from './test-pivot.entity'; import { TestEntity } from './test.entity'; describe('Test paginateRawAndEntities function', () => { @@ -46,6 +47,16 @@ describe('Test paginateRawAndEntities function', () => { }) .execute(); } + + for (let i = 1; i <= 3; i++) { + await queryBuilder + .insert() + .into(TestPivotEntity) + .values({ + id: i, + }) + .execute(); + } }); afterAll(async () => { diff --git a/src/__tests__/paginate-raw.spec.ts b/src/__tests__/paginate-raw.spec.ts index c3ea05d4..13ec329e 100644 --- a/src/__tests__/paginate-raw.spec.ts +++ b/src/__tests__/paginate-raw.spec.ts @@ -4,6 +4,7 @@ import { Connection, QueryRunner, SelectQueryBuilder } from 'typeorm'; import { paginateRaw } from '../paginate'; import { Pagination } from '../pagination'; import { baseOrmConfigs } from './base-orm-config'; +import { TestPivotEntity } from './test-pivot.entity'; import { TestEntity } from './test.entity'; interface RawQueryResult { @@ -48,6 +49,16 @@ describe('Test paginateRaw function', () => { }) .execute(); } + + for (let i = 1; i <= 3; i++) { + await queryBuilder + .insert() + .into(TestPivotEntity) + .values({ + id: i, + }) + .execute(); + } }); afterAll(async () => { diff --git a/src/__tests__/paginate.query.builder.spec.ts b/src/__tests__/paginate.query.builder.spec.ts index 93cfd352..532b3e5a 100644 --- a/src/__tests__/paginate.query.builder.spec.ts +++ b/src/__tests__/paginate.query.builder.spec.ts @@ -5,8 +5,9 @@ import { paginate } from './../paginate'; import { Pagination } from '../pagination'; import { baseOrmConfigs } from './base-orm-config'; import { TestEntity } from './test.entity'; -import { PaginationTypeEnum } from '../interfaces'; +import { CountQueryTypeEnum, PaginationTypeEnum } from '../interfaces'; import { TestRelatedEntity } from './test-related.entity'; +import { TestPivotEntity } from './test-pivot.entity'; describe('Paginate with queryBuilder', () => { let app: TestingModule; @@ -14,6 +15,7 @@ describe('Paginate with queryBuilder', () => { let runner: QueryRunner; let queryBuilder: SelectQueryBuilder; let testRelatedQueryBuilder: SelectQueryBuilder; + let testPivotQueryBuilder: SelectQueryBuilder; beforeEach(async () => { app = await Test.createTestingModule({ @@ -32,6 +34,10 @@ describe('Paginate with queryBuilder', () => { TestRelatedEntity, 'tr', ); + testPivotQueryBuilder = runner.manager.createQueryBuilder( + TestPivotEntity, + 'tp', + ); }); afterEach(() => { @@ -48,7 +54,7 @@ describe('Paginate with queryBuilder', () => { const result = await paginate(queryBuilder, { limit: 10, page: 1, - paginationType: PaginationTypeEnum.LIMIT_AND_OFFSET, + paginationType: PaginationTypeEnum.TAKE_AND_SKIP, }); expect(result).toBeInstanceOf(Pagination); }); @@ -113,4 +119,43 @@ describe('Paginate with queryBuilder', () => { expect(result).toBeInstanceOf(Pagination); expect(result.meta.totalItems).toEqual(10); }); + + it('Can paginate with countQueryType set to ENTITY', async () => { + const pivot = (await testPivotQueryBuilder + .where('tp.id = :id', { id: 1 }) + .getOne()) as TestPivotEntity; + + const testOne = await runner.manager + .getRepository(TestEntity) + .findOne({ where: { id: 1 } }); + const testTwo = await runner.manager + .getRepository(TestEntity) + .findOne({ where: { id: 2 } }); + + if (testOne) { + testOne.testPivots = [pivot]; + } + + if (testTwo) { + testTwo.testPivots = [pivot]; + } + + await runner.manager.save([testOne, testTwo]); + + const qb = queryBuilder.innerJoinAndSelect( + 't.testPivots', + 'tp', + 't_tp.testPivotId = :id', + { id: 1 }, + ); + + const result = await paginate(qb, { + limit: 10, + page: 1, + countQueryType: CountQueryTypeEnum.ENTITY, + }); + + expect(result).toBeInstanceOf(Pagination); + expect(result.meta.totalItems).toEqual(2); + }); }); diff --git a/src/__tests__/test-pivot.entity.ts b/src/__tests__/test-pivot.entity.ts new file mode 100644 index 00000000..0b0ffac9 --- /dev/null +++ b/src/__tests__/test-pivot.entity.ts @@ -0,0 +1,11 @@ +import { Entity, ManyToMany, PrimaryColumn } from 'typeorm'; +import { TestEntity } from './test.entity'; + +@Entity() +export class TestPivotEntity { + @PrimaryColumn() + id: number; + + @ManyToMany(() => TestEntity, (tests) => tests.testPivots) + tests: TestEntity[]; +} diff --git a/src/__tests__/test.entity.ts b/src/__tests__/test.entity.ts index 64a0f484..7bba665a 100644 --- a/src/__tests__/test.entity.ts +++ b/src/__tests__/test.entity.ts @@ -1,4 +1,5 @@ -import { PrimaryColumn, Entity, OneToMany } from 'typeorm'; +import { PrimaryColumn, Entity, OneToMany, ManyToMany, JoinTable } from 'typeorm'; +import { TestPivotEntity } from './test-pivot.entity'; import { TestRelatedEntity } from './test-related.entity'; @Entity() @@ -8,4 +9,18 @@ export class TestEntity { @OneToMany(() => TestRelatedEntity, (related) => related.test) related: TestRelatedEntity[]; + + @ManyToMany(() => TestPivotEntity, (testPivots) => testPivots.tests) + @JoinTable({ + name: 'test_test_pivot', + joinColumn: { + name: 'testId', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'testPivotId', + referencedColumnName: 'id', + }, + }) + testPivots: TestPivotEntity[]; } diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 659a91e4..9ce98aef 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -3,6 +3,11 @@ export enum PaginationTypeEnum { TAKE_AND_SKIP = 'take', } +export enum CountQueryTypeEnum { + RAW = 'raw', + ENTITY = 'entity', +} + export interface IPaginationOptions { /** * @default 10 @@ -43,6 +48,12 @@ export interface IPaginationOptions { */ countQueries?: boolean; + /** + * @default CountQueryTypeEnum.RAW + * Used for count query with countQuery(builder, cacheOptions) which is RAW or builder.getCount() which is ENTITY + */ + countQueryType?: CountQueryTypeEnum; + /** * @default false * @link https://orkhan.gitbook.io/typeorm/docs/caching diff --git a/src/paginate.ts b/src/paginate.ts index 2e59e09b..470b169f 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -7,6 +7,7 @@ import { } from 'typeorm'; import { Pagination } from './pagination'; import { + CountQueryTypeEnum, IPaginationMeta, IPaginationOptions, PaginationTypeEnum, @@ -51,8 +52,15 @@ export async function paginateRaw< queryBuilder: SelectQueryBuilder, options: IPaginationOptions, ): Promise> { - const [page, limit, route, paginationType, countQueries, cacheOption] = - resolveOptions(options); + const [ + page, + limit, + route, + paginationType, + countQueries, + countQueryType, + cacheOption, + ] = resolveOptions(options); const promises: [Promise, Promise | undefined] = [ (paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET @@ -65,7 +73,10 @@ export async function paginateRaw< ]; if (countQueries) { - promises[1] = countQuery(queryBuilder, cacheOption); + promises[1] = + countQueryType === CountQueryTypeEnum.RAW + ? countQuery(queryBuilder, cacheOption) + : queryBuilder.cache(cacheOption).getCount(); } const [items, total] = await Promise.all(promises); @@ -88,8 +99,15 @@ export async function paginateRawAndEntities< queryBuilder: SelectQueryBuilder, options: IPaginationOptions, ): Promise<[Pagination, Partial[]]> { - const [page, limit, route, paginationType, countQueries, cacheOption] = - resolveOptions(options); + const [ + page, + limit, + route, + paginationType, + countQueries, + countQueryType, + cacheOption, + ] = resolveOptions(options); const promises: [ Promise<{ entities: T[]; raw: T[] }>, @@ -105,7 +123,10 @@ export async function paginateRawAndEntities< ]; if (countQueries) { - promises[1] = countQuery(queryBuilder, cacheOption); + promises[1] = + countQueryType === CountQueryTypeEnum.RAW + ? countQuery(queryBuilder, cacheOption) + : queryBuilder.cache(cacheOption).getCount(); } const [itemObject, total] = await Promise.all(promises); @@ -126,7 +147,15 @@ export async function paginateRawAndEntities< function resolveOptions( options: IPaginationOptions, -): [number, number, string, PaginationTypeEnum, boolean, TypeORMCacheType] { +): [ + number, + number, + string, + PaginationTypeEnum, + boolean, + CountQueryTypeEnum, + TypeORMCacheType, +] { const page = resolveNumericOption(options, 'page', DEFAULT_PAGE); const limit = resolveNumericOption(options, 'limit', DEFAULT_LIMIT); const route = options.route; @@ -134,9 +163,18 @@ function resolveOptions( options.paginationType || PaginationTypeEnum.LIMIT_AND_OFFSET; const countQueries = typeof options.countQueries !== 'undefined' ? options.countQueries : true; + const countQueryType = options.countQueryType || CountQueryTypeEnum.RAW; const cacheQueries = options.cacheQueries || false; - return [page, limit, route, paginationType, countQueries, cacheQueries]; + return [ + page, + limit, + route, + paginationType, + countQueries, + countQueryType, + cacheQueries, + ]; } function resolveNumericOption( @@ -208,8 +246,15 @@ async function paginateQueryBuilder( queryBuilder: SelectQueryBuilder, options: IPaginationOptions, ): Promise> { - const [page, limit, route, paginationType, countQueries, cacheOption] = - resolveOptions(options); + const [ + page, + limit, + route, + paginationType, + countQueries, + countQueryType, + cacheOption, + ] = resolveOptions(options); const promises: [Promise, Promise | undefined] = [ (PaginationTypeEnum.LIMIT_AND_OFFSET === paginationType @@ -222,7 +267,10 @@ async function paginateQueryBuilder( ]; if (countQueries) { - promises[1] = countQuery(queryBuilder, cacheOption); + promises[1] = + countQueryType === CountQueryTypeEnum.RAW + ? countQuery(queryBuilder, cacheOption) + : queryBuilder.cache(cacheOption).getCount(); } const [items, total] = await Promise.all(promises);