diff --git a/src/__tests__/sale.entity.ts b/src/__tests__/sale.entity.ts new file mode 100644 index 00000000..622a562b --- /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 ce6dc2b2..a45584de 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.0, + totalPrice: 1200.0, + }), + saleRepo.create({ + itemName: 'Smartphone', + quantity: 2, + unitPrice: 600.0, + totalPrice: 1200.0, + }), + saleRepo.create({ + itemName: 'Headphones', + quantity: 1, + unitPrice: 150.0, + totalPrice: 150.0, + }), + saleRepo.create({ + itemName: 'Laptop', + quantity: 1, + unitPrice: 1200.0, + totalPrice: 1200.0, + }), + saleRepo.create({ + itemName: 'Mouse', + quantity: 3, + unitPrice: 20.0, + totalPrice: 60.0, + }), + saleRepo.create({ + itemName: 'Keyboard', + quantity: 2, + unitPrice: 80.0, + totalPrice: 160.0, + }), + saleRepo.create({ + itemName: 'Monitor', + quantity: 1, + unitPrice: 300.0, + totalPrice: 300.0, + }), + saleRepo.create({ + itemName: 'Laptop', + quantity: 1, + unitPrice: 1200.0, + totalPrice: 1200.0, + }), + saleRepo.create({ + itemName: 'Printer', + quantity: 1, + unitPrice: 250.0, + totalPrice: 250.0, + }), + saleRepo.create({ + itemName: 'Smartphone', + quantity: 1, + unitPrice: 600.0, + totalPrice: 600.0, + }), + saleRepo.create({ + itemName: 'Monitor', + quantity: 2, + unitPrice: 300.0, + totalPrice: 600.0, + }), + saleRepo.create({ + itemName: 'Headphones', + quantity: 1, + unitPrice: 150.0, + totalPrice: 150.0, + }), + saleRepo.create({ + itemName: 'Keyboard', + quantity: 1, + unitPrice: 80.0, + totalPrice: 80.0, + }), + saleRepo.create({ + itemName: 'Laptop Stand', + quantity: 2, + unitPrice: 50.0, + totalPrice: 100.0, + }), + saleRepo.create({ + itemName: 'Smartphone', + quantity: 1, + unitPrice: 600.0, + totalPrice: 600.0, + }), + ]) }) if (process.env.DB === 'postgres') { @@ -3049,6 +3146,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') { @@ -3178,5 +3319,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) + }) } }) diff --git a/src/paginate.ts b/src/paginate.ts index 1307db5b..5773f8f1 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 { @@ -212,7 +214,6 @@ export async function paginate( let [items, totalItems]: [T[], number] = [[], 0] const queryBuilder = isRepository(repo) ? repo.createQueryBuilder('__root') : repo - if (isRepository(repo) && !config.relations && config.loadEagerRelations === true) { if (!config.relations) { FindOptionsUtils.joinEagerRelations(queryBuilder, queryBuilder.alias, repo.metadata) @@ -388,11 +389,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 +474,23 @@ 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 sql = qb.orderBy().limit().offset().take().skip().getQuery() + + const result = await qb + .createQueryBuilder() + .select('COUNT(*)', 'total_rows') + .from(`(${sql})`, 'query_count') + .setParameters(qb.getParameters()) + .getRawOne() + + return Number(result.total_rows) +}