From 9ca4019c99ff7cb2c748328ff6fd5998f764e615 Mon Sep 17 00:00:00 2001 From: Oka Prinarjaya Date: Wed, 11 Sep 2024 12:44:16 +0700 Subject: [PATCH 1/4] Feature: Add support for getRawMany(), and flexible row count --- src/__tests__/sale.entity.ts | 19 +++++ src/paginate.spec.ts | 140 +++++++++++++++++++++++++++++++++++ src/paginate.ts | 33 ++++++++- 3 files changed, 188 insertions(+), 4 deletions(-) create mode 100644 src/__tests__/sale.entity.ts diff --git a/src/__tests__/sale.entity.ts b/src/__tests__/sale.entity.ts new file mode 100644 index 00000000..822ad8d0 --- /dev/null +++ b/src/__tests__/sale.entity.ts @@ -0,0 +1,19 @@ +import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity({ name: 'sales' }) +export class SaleEntity { + @PrimaryGeneratedColumn() + id: number + + @Column({ name: 'item_name', nullable: false }) + itemName: string + + @Column({ nullable: false }) + quantity: number + + @Column({ nullable: false, type: 'decimal', precision: 10, scale: 2 }) + unitPrice: number + + @Column({ nullable: false, type: 'decimal', precision: 10, scale: 2 }) + totalPrice: number +} diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index 13253d8e..f2b62997 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -21,6 +21,7 @@ import { parseFilterToken, } from './filter' import { PaginateConfig, Paginated, PaginationLimit, paginate } from './paginate' +import { SaleEntity } from './__tests__/sale.entity' const isoStringToDate = (isoString) => new Date(isoString) @@ -33,6 +34,7 @@ describe('paginate', () => { let toyShopAddressRepository: Repository let catHomeRepo: Repository let catHomePillowRepo: Repository + let saleRepo: Repository let cats: CatEntity[] let catToys: CatToyEntity[] let catToysWithoutShop: CatToyEntity[] @@ -54,6 +56,7 @@ describe('paginate', () => { CatHomeEntity, CatHomePillowEntity, ToyShopEntity, + SaleEntity, process.env.DB === 'postgres' ? CatHairEntity : undefined, ], } @@ -98,6 +101,7 @@ describe('paginate', () => { catHomePillowRepo = dataSource.getRepository(CatHomePillowEntity) toyShopRepo = dataSource.getRepository(ToyShopEntity) toyShopAddressRepository = dataSource.getRepository(ToyShopAddressEntity) + saleRepo = dataSource.getRepository(SaleEntity) cats = await catRepo.save([ catRepo.create({ @@ -203,6 +207,99 @@ describe('paginate', () => { catHairRepo.create({ name: 'none' }), ]) } + + await saleRepo.save([ + saleRepo.create({ + itemName: 'Laptop', + quantity: 1, + unitPrice: 1200.00, + totalPrice: 1200.00 + }), + saleRepo.create({ + itemName: 'Smartphone', + quantity: 2, + unitPrice: 600.00, + totalPrice: 1200.00 + }), + saleRepo.create({ + itemName: 'Headphones', + quantity: 1, + unitPrice: 150.00, + totalPrice: 150.00 + }), + saleRepo.create({ + itemName: 'Laptop', + quantity: 1, + unitPrice: 1200.00, + totalPrice: 1200.00 + }), + saleRepo.create({ + itemName: 'Mouse', + quantity: 3, + unitPrice: 20.00, + totalPrice: 60.00 + }), + saleRepo.create({ + itemName: 'Keyboard', + quantity: 2, + unitPrice: 80.00, + totalPrice: 160.00 + }), + saleRepo.create({ + itemName: 'Monitor', + quantity: 1, + unitPrice: 300.00, + totalPrice: 300.00 + }), + saleRepo.create({ + itemName: 'Laptop', + quantity: 1, + unitPrice: 1200.00, + totalPrice: 1200.00 + }), + saleRepo.create({ + itemName: 'Printer', + quantity: 1, + unitPrice: 250.00, + totalPrice: 250.00 + }), + saleRepo.create({ + itemName: 'Smartphone', + quantity: 1, + unitPrice: 600.00, + totalPrice: 600.00 + }), + saleRepo.create({ + itemName: 'Monitor', + quantity: 2, + unitPrice: 300.00, + totalPrice: 600.00 + }), + saleRepo.create({ + itemName: 'Headphones', + quantity: 1, + unitPrice: 150.00, + totalPrice: 150.00 + }), + saleRepo.create({ + itemName: 'Keyboard', + quantity: 1, + unitPrice: 80.00, + totalPrice: 80.00 + }), + saleRepo.create({ + itemName: 'Laptop Stand', + quantity: 2, + unitPrice: 50.00, + totalPrice: 100.00 + }), + saleRepo.create({ + itemName: 'Smartphone', + quantity: 1, + unitPrice: 600.00, + totalPrice: 600.00 + }) + ]) }) if (process.env.DB === 'postgres') { @@ -2798,6 +2895,49 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC') }) + it('should return correct amount of data with complete correct columns in each data item when data queried using manual sql aggregates functions', async () => { + const salesQuery = saleRepo + .createQueryBuilder('sale') + .select([ + 'sale.itemName as itemName', + 'SUM(sale.totalPrice) as totalSales', + 'COUNT(*) as numberOfSales' + ]) + .groupBy('sale.itemName') + + const config: PaginateConfig = { + sortableColumns: ['itemName'], + getDataAsRaw: true, + rowCountAsItIs: true + } + + const query: PaginateQuery = { + path: '', + limit: 3, + page: 1 + } + + const result = await paginate(query, salesQuery, config) + + type MyRow = { + itemName: string + totalSales: number + numberOfSales: number + } + + result.data.forEach(data => { + const sale = data as unknown as MyRow; + expect(sale.itemName).toBeDefined() + expect(sale.totalSales).toBeDefined() + expect(sale.numberOfSales).toBeDefined() + }) + + // expect(countAsItIs).toEqual(8) + expect(result.data.length).toEqual(3) + expect(result.meta.currentPage).toEqual(1) + expect(result.meta.itemsPerPage).toEqual(3) + }); + describe('should return result based on date column filter', () => { it('with $not and $null operators', async () => { const config: PaginateConfig = { diff --git a/src/paginate.ts b/src/paginate.ts index 1307db5b..88c7924a 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -90,6 +90,8 @@ export interface PaginateConfig { origin?: string ignoreSearchByInQueryParam?: boolean ignoreSelectInQueryParam?: boolean + getDataAsRaw?: boolean + rowCountAsItIs?: boolean } export enum PaginationLimit { @@ -388,11 +390,21 @@ export async function paginate( } if (query.limit === PaginationLimit.COUNTER_ONLY) { - totalItems = await queryBuilder.getCount() - } else if (isPaginated) { - ;[items, totalItems] = await queryBuilder.getManyAndCount() + totalItems = await getCount(queryBuilder, config) } else { - items = await queryBuilder.getMany() + if (!isPaginated && !config.getDataAsRaw) { + items = await queryBuilder.getMany() + } + if (!isPaginated && config.getDataAsRaw) { + items = await queryBuilder.getRawMany() + } + if (isPaginated && !config.getDataAsRaw) { + ;[items, totalItems] = await queryBuilder.getManyAndCount() + } + if (isPaginated && config.getDataAsRaw) { + items = await queryBuilder.getRawMany() + totalItems = await getCount(queryBuilder, config) + } } const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('') @@ -463,3 +475,16 @@ export async function paginate( return Object.assign(new Paginated(), results) } + +export async function getCount(qb: SelectQueryBuilder, config: PaginateConfig): Promise { + if (!config.rowCountAsItIs) { + return qb.getCount(); + } + + const result = await qb.createQueryBuilder() + .select('COUNT(*)', 'total_rows') + .from(`(${qb.getSql()})`, 'query_count') + .getRawOne() + + return result.total_rows; +} From fab979e171e9ea8ac911a30a8e2b722a0f8ef89e Mon Sep 17 00:00:00 2001 From: Oka Prinarjaya Date: Wed, 11 Sep 2024 13:48:58 +0700 Subject: [PATCH 2/4] Feature: Add support for getRawMany(), and flexible row count - update 1 --- src/__tests__/sale.entity.ts | 2 +- src/paginate.spec.ts | 85 +++++++++++++++++------------------- src/paginate.ts | 17 +++++--- 3 files changed, 53 insertions(+), 51 deletions(-) diff --git a/src/__tests__/sale.entity.ts b/src/__tests__/sale.entity.ts index 822ad8d0..622a562b 100644 --- a/src/__tests__/sale.entity.ts +++ b/src/__tests__/sale.entity.ts @@ -1,4 +1,4 @@ -import { Column, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm' @Entity({ name: 'sales' }) export class SaleEntity { diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index f2b62997..a36e2986 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -212,93 +212,93 @@ describe('paginate', () => { saleRepo.create({ itemName: 'Laptop', quantity: 1, - unitPrice: 1200.00, - totalPrice: 1200.00 + unitPrice: 1200.0, + totalPrice: 1200.0, }), saleRepo.create({ itemName: 'Smartphone', quantity: 2, - unitPrice: 600.00, - totalPrice: 1200.00 + unitPrice: 600.0, + totalPrice: 1200.0, }), saleRepo.create({ itemName: 'Headphones', quantity: 1, - unitPrice: 150.00, - totalPrice: 150.00 + unitPrice: 150.0, + totalPrice: 150.0, }), saleRepo.create({ itemName: 'Laptop', quantity: 1, - unitPrice: 1200.00, - totalPrice: 1200.00 + unitPrice: 1200.0, + totalPrice: 1200.0, }), saleRepo.create({ itemName: 'Mouse', quantity: 3, - unitPrice: 20.00, - totalPrice: 60.00 + unitPrice: 20.0, + totalPrice: 60.0, }), saleRepo.create({ itemName: 'Keyboard', quantity: 2, - unitPrice: 80.00, - totalPrice: 160.00 + unitPrice: 80.0, + totalPrice: 160.0, }), saleRepo.create({ itemName: 'Monitor', quantity: 1, - unitPrice: 300.00, - totalPrice: 300.00 + unitPrice: 300.0, + totalPrice: 300.0, }), saleRepo.create({ itemName: 'Laptop', quantity: 1, - unitPrice: 1200.00, - totalPrice: 1200.00 + unitPrice: 1200.0, + totalPrice: 1200.0, }), saleRepo.create({ itemName: 'Printer', quantity: 1, - unitPrice: 250.00, - totalPrice: 250.00 + unitPrice: 250.0, + totalPrice: 250.0, }), saleRepo.create({ itemName: 'Smartphone', quantity: 1, - unitPrice: 600.00, - totalPrice: 600.00 + unitPrice: 600.0, + totalPrice: 600.0, }), saleRepo.create({ itemName: 'Monitor', quantity: 2, - unitPrice: 300.00, - totalPrice: 600.00 + unitPrice: 300.0, + totalPrice: 600.0, }), saleRepo.create({ itemName: 'Headphones', quantity: 1, - unitPrice: 150.00, - totalPrice: 150.00 + unitPrice: 150.0, + totalPrice: 150.0, }), saleRepo.create({ itemName: 'Keyboard', quantity: 1, - unitPrice: 80.00, - totalPrice: 80.00 + unitPrice: 80.0, + totalPrice: 80.0, }), saleRepo.create({ itemName: 'Laptop Stand', quantity: 2, - unitPrice: 50.00, - totalPrice: 100.00 + unitPrice: 50.0, + totalPrice: 100.0, }), saleRepo.create({ itemName: 'Smartphone', quantity: 1, - unitPrice: 600.00, - totalPrice: 600.00 - }) + unitPrice: 600.0, + totalPrice: 600.0, + }), ]) }) @@ -2897,24 +2897,20 @@ describe('paginate', () => { it('should return correct amount of data with complete correct columns in each data item when data queried using manual sql aggregates functions', async () => { const salesQuery = saleRepo - .createQueryBuilder('sale') - .select([ - 'sale.itemName as itemName', - 'SUM(sale.totalPrice) as totalSales', - 'COUNT(*) as numberOfSales' - ]) - .groupBy('sale.itemName') + .createQueryBuilder('sale') + .select(['sale.itemName as itemName', 'SUM(sale.totalPrice) as totalSales', 'COUNT(*) as numberOfSales']) + .groupBy('sale.itemName') const config: PaginateConfig = { sortableColumns: ['itemName'], getDataAsRaw: true, - rowCountAsItIs: true + rowCountAsItIs: true, } const query: PaginateQuery = { path: '', limit: 3, - page: 1 + page: 1, } const result = await paginate(query, salesQuery, config) @@ -2925,18 +2921,19 @@ describe('paginate', () => { numberOfSales: number } - result.data.forEach(data => { - const sale = data as unknown as MyRow; + result.data.forEach((data) => { + const sale = data as unknown as MyRow expect(sale.itemName).toBeDefined() expect(sale.totalSales).toBeDefined() expect(sale.numberOfSales).toBeDefined() }) - // expect(countAsItIs).toEqual(8) + expect(result.meta.totalItems).toEqual(8) + expect(result.meta.totalPages).toEqual(3) expect(result.data.length).toEqual(3) expect(result.meta.currentPage).toEqual(1) expect(result.meta.itemsPerPage).toEqual(3) - }); + }) describe('should return result based on date column filter', () => { it('with $not and $null operators', async () => { diff --git a/src/paginate.ts b/src/paginate.ts index 88c7924a..61552f3e 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -214,6 +214,7 @@ export async function paginate( let [items, totalItems]: [T[], number] = [[], 0] const queryBuilder = isRepository(repo) ? repo.createQueryBuilder('__root') : repo + const queryBuilderCloned = new SelectQueryBuilder(queryBuilder) if (isRepository(repo) && !config.relations && config.loadEagerRelations === true) { if (!config.relations) { @@ -390,7 +391,7 @@ export async function paginate( } if (query.limit === PaginationLimit.COUNTER_ONLY) { - totalItems = await getCount(queryBuilder, config) + totalItems = await getCount(queryBuilderCloned, config) } else { if (!isPaginated && !config.getDataAsRaw) { items = await queryBuilder.getMany() @@ -403,7 +404,7 @@ export async function paginate( } if (isPaginated && config.getDataAsRaw) { items = await queryBuilder.getRawMany() - totalItems = await getCount(queryBuilder, config) + totalItems = await getCount(queryBuilderCloned, config) } } @@ -476,15 +477,19 @@ export async function paginate( return Object.assign(new Paginated(), results) } -export async function getCount(qb: SelectQueryBuilder, config: PaginateConfig): Promise { +export async function getCount( + qb: SelectQueryBuilder, + config: PaginateConfig +): Promise { if (!config.rowCountAsItIs) { - return qb.getCount(); + return qb.getCount() } - const result = await qb.createQueryBuilder() + const result = await qb + .createQueryBuilder() .select('COUNT(*)', 'total_rows') .from(`(${qb.getSql()})`, 'query_count') .getRawOne() - return result.total_rows; + return Number(result.total_rows) } From c2de9b5cdde939c876769f66f58df390055b3d97 Mon Sep 17 00:00:00 2001 From: Oka Prinarjaya Date: Wed, 11 Sep 2024 14:07:23 +0700 Subject: [PATCH 3/4] Feature: Add support for getRawMany(), and flexible row count - update 2 --- src/paginate.spec.ts | 128 +++++++++++++++++++++++++++++-------------- 1 file changed, 88 insertions(+), 40 deletions(-) diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index a36e2986..b068ce76 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -2895,46 +2895,6 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=id:ASC') }) - it('should return correct amount of data with complete correct columns in each data item when data queried using manual sql aggregates functions', async () => { - const salesQuery = saleRepo - .createQueryBuilder('sale') - .select(['sale.itemName as itemName', 'SUM(sale.totalPrice) as totalSales', 'COUNT(*) as numberOfSales']) - .groupBy('sale.itemName') - - const config: PaginateConfig = { - sortableColumns: ['itemName'], - getDataAsRaw: true, - rowCountAsItIs: true, - } - - const query: PaginateQuery = { - path: '', - limit: 3, - page: 1, - } - - const result = await paginate(query, salesQuery, config) - - type MyRow = { - itemName: string - totalSales: number - numberOfSales: number - } - - result.data.forEach((data) => { - const sale = data as unknown as MyRow - expect(sale.itemName).toBeDefined() - expect(sale.totalSales).toBeDefined() - expect(sale.numberOfSales).toBeDefined() - }) - - expect(result.meta.totalItems).toEqual(8) - expect(result.meta.totalPages).toEqual(3) - expect(result.data.length).toEqual(3) - expect(result.meta.currentPage).toEqual(1) - expect(result.meta.itemsPerPage).toEqual(3) - }) - describe('should return result based on date column filter', () => { it('with $not and $null operators', async () => { const config: PaginateConfig = { @@ -3185,6 +3145,50 @@ describe('paginate', () => { expect(result.links.current).toBe(`?page=1&limit=20&sortBy=id:ASC&search=brown`) }) }) + + it('should return correct amount of data with complete correct columns in each data item when data queried using manual sql aggregates functions', async () => { + const salesQuery = saleRepo + .createQueryBuilder('sale') + .select([ + 'sale.itemName as itemName', + 'SUM(sale.totalPrice) as totalSales', + 'COUNT(*) as numberOfSales', + ]) + .groupBy('sale.itemName') + + const config: PaginateConfig = { + sortableColumns: ['itemName'], + getDataAsRaw: true, + rowCountAsItIs: true, + } + + const query: PaginateQuery = { + path: '', + limit: 3, + page: 1, + } + + const result = await paginate(query, salesQuery, config) + + type MyRow = { + itemname: string + totalsales: number + numberofsales: number + } + + result.data.forEach((data) => { + const sale = data as unknown as MyRow + expect(sale.itemname).toBeDefined() + expect(sale.totalsales).toBeDefined() + expect(sale.numberofsales).toBeDefined() + }) + + expect(result.meta.totalItems).toEqual(8) + expect(result.meta.totalPages).toEqual(3) + expect(result.data.length).toEqual(3) + expect(result.meta.currentPage).toEqual(1) + expect(result.meta.itemsPerPage).toEqual(3) + }) } if (process.env.DB !== 'postgres') { @@ -3314,5 +3318,49 @@ describe('paginate', () => { expect(result.links.current).toBe('?page=1&limit=20&sortBy=home.countCat:ASC') }) }) + + it('should return correct amount of data with complete correct columns in each data item when data queried using manual sql aggregates functions', async () => { + const salesQuery = saleRepo + .createQueryBuilder('sale') + .select([ + 'sale.itemName as itemName', + 'SUM(sale.totalPrice) as totalSales', + 'COUNT(*) as numberOfSales', + ]) + .groupBy('sale.itemName') + + const config: PaginateConfig = { + sortableColumns: ['itemName'], + getDataAsRaw: true, + rowCountAsItIs: true, + } + + const query: PaginateQuery = { + path: '', + limit: 3, + page: 1, + } + + const result = await paginate(query, salesQuery, config) + + type MyRow = { + itemName: string + totalSales: number + numberOfSales: number + } + + result.data.forEach((data) => { + const sale = data as unknown as MyRow + expect(sale.itemName).toBeDefined() + expect(sale.totalSales).toBeDefined() + expect(sale.numberOfSales).toBeDefined() + }) + + expect(result.meta.totalItems).toEqual(8) + expect(result.meta.totalPages).toEqual(3) + expect(result.data.length).toEqual(3) + expect(result.meta.currentPage).toEqual(1) + expect(result.meta.itemsPerPage).toEqual(3) + }) } }) From b6d4478a414e04c43607abb7422f756ec4e99438 Mon Sep 17 00:00:00 2001 From: Oka Prinarjaya Date: Sat, 21 Sep 2024 17:10:52 +0700 Subject: [PATCH 4/4] Cancelling to use queryBuilder object cloning strategy --- src/paginate.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/paginate.ts b/src/paginate.ts index 61552f3e..5773f8f1 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -214,8 +214,6 @@ export async function paginate( let [items, totalItems]: [T[], number] = [[], 0] const queryBuilder = isRepository(repo) ? repo.createQueryBuilder('__root') : repo - const queryBuilderCloned = new SelectQueryBuilder(queryBuilder) - if (isRepository(repo) && !config.relations && config.loadEagerRelations === true) { if (!config.relations) { FindOptionsUtils.joinEagerRelations(queryBuilder, queryBuilder.alias, repo.metadata) @@ -391,7 +389,7 @@ export async function paginate( } if (query.limit === PaginationLimit.COUNTER_ONLY) { - totalItems = await getCount(queryBuilderCloned, config) + totalItems = await getCount(queryBuilder, config) } else { if (!isPaginated && !config.getDataAsRaw) { items = await queryBuilder.getMany() @@ -404,7 +402,7 @@ export async function paginate( } if (isPaginated && config.getDataAsRaw) { items = await queryBuilder.getRawMany() - totalItems = await getCount(queryBuilderCloned, config) + totalItems = await getCount(queryBuilder, config) } } @@ -485,10 +483,13 @@ export async function getCount( return qb.getCount() } + const sql = qb.orderBy().limit().offset().take().skip().getQuery() + const result = await qb .createQueryBuilder() .select('COUNT(*)', 'total_rows') - .from(`(${qb.getSql()})`, 'query_count') + .from(`(${sql})`, 'query_count') + .setParameters(qb.getParameters()) .getRawOne() return Number(result.total_rows)