diff --git a/README.md b/README.md index 27b40cefd..6f164169d 100644 --- a/README.md +++ b/README.md @@ -435,6 +435,21 @@ const paginateConfig: PaginateConfig { * ``` */ buildCountQuery: (qb: SelectQueryBuilder) => SelectQueryBuilder, + + /** + * Required: false + * Type: boolean + * Default: false + * Description: When true, paginate() will use QueryBuilder.getRawMany() instead + * of getMany() to retrieve items. + * + * Useful when: + * - You need raw SQL output such as computed or aggregated fields. + * + * Notes: + * - Column names in the response will follow SQL aliases. + */ + fetchRaw: false, } ```` diff --git a/src/__tests__/sale.entity.ts b/src/__tests__/sale.entity.ts new file mode 100644 index 000000000..622a562b0 --- /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 b1ac53d86..1a37f2fbe 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -11,6 +11,7 @@ import { CatToyEntity } from './__tests__/cat-toy.entity' import { CatEntity, CutenessLevel } from './__tests__/cat.entity' import { ToyShopAddressEntity } from './__tests__/toy-shop-address.entity' import { ToyShopEntity } from './__tests__/toy-shop.entity' +import { SaleEntity } from './__tests__/sale.entity' import { PaginateQuery } from './decorator' import { FilterComparator, @@ -45,6 +46,7 @@ describe('paginate', () => { let catHomeRepo: Repository let catHomePillowRepo: Repository let catHomePillowBrandRepo: Repository + let saleRepo: Repository let cats: CatEntity[] let catToys: CatToyEntity[] let catToysWithoutShop: CatToyEntity[] @@ -70,6 +72,7 @@ describe('paginate', () => { CatHomePillowEntity, CatHomePillowBrandEntity, ToyShopEntity, + SaleEntity, process.env.DB === 'postgres' ? CatHairEntity : undefined, ], } @@ -115,6 +118,7 @@ describe('paginate', () => { catHomePillowBrandRepo = dataSource.getRepository(CatHomePillowBrandEntity) toyShopRepo = dataSource.getRepository(ToyShopEntity) toyShopAddressRepository = dataSource.getRepository(ToyShopAddressEntity) + saleRepo = dataSource.getRepository(SaleEntity) cats = await catRepo.save([ catRepo.create({ @@ -259,6 +263,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') { @@ -3436,6 +3533,54 @@ describe('paginate', () => { expect(result.data).toStrictEqual([catHairs[0], catHairs[1]]) 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'], + fetchRaw: true, + buildCountQuery: (qb) => { + qb.orderBy().limit().offset().take().skip() + qb.select('sale.itemName').distinct(true) + return qb + }, + } + + 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 204b6ac0d..0a9e2f642 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -104,6 +104,7 @@ export interface PaginateConfig { defaultJoinMethod?: JoinMethod joinMethods?: Partial> buildCountQuery?: (qb: SelectQueryBuilder) => SelectQueryBuilder + fetchRaw?: boolean } export enum PaginationLimit { @@ -913,14 +914,28 @@ export async function paginate( if (query.limit === PaginationLimit.COUNTER_ONLY) { totalItems = await queryBuilder.getCount() } else if (isPaginated && config.paginationType !== PaginationType.CURSOR) { - if (config.buildCountQuery) { - items = await queryBuilder.getMany() - totalItems = await config.buildCountQuery(queryBuilder.clone()).getCount() + if (config.fetchRaw) { + items = await queryBuilder.getRawMany() + + if (config.buildCountQuery) { + totalItems = await config.buildCountQuery(queryBuilder.clone()).getCount() + } else { + totalItems = await queryBuilder.getCount() + } } else { - ;[items, totalItems] = await queryBuilder.getManyAndCount() + if (config.buildCountQuery) { + items = await queryBuilder.getMany() + totalItems = await config.buildCountQuery(queryBuilder.clone()).getCount() + } else { + ;[items, totalItems] = await queryBuilder.getManyAndCount() + } } } else { - items = await queryBuilder.getMany() + if (config.fetchRaw) { + items = await queryBuilder.getRawMany() + } else { + items = await queryBuilder.getMany() + } } const sortByQuery = sortBy.map((order) => `&sortBy=${order.join(':')}`).join('')