From e42d15bf996349bebcadd570ad6229bd5c548434 Mon Sep 17 00:00:00 2001 From: yonghyuk_kim Date: Fri, 5 Dec 2025 01:37:15 +0900 Subject: [PATCH 1/3] feat: add fetchRaw --- README.md | 15 ++++ src/__tests__/sale.entity.ts | 19 +++++ src/paginate.spec.ts | 145 +++++++++++++++++++++++++++++++++++ src/paginate.ts | 25 ++++-- 4 files changed, 199 insertions(+), 5 deletions(-) create mode 100644 src/__tests__/sale.entity.ts 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..99b87aede --- /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..970cf7833 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').groupBy('sale.itemName') + 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..f32a2217e 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('') From 721d661343fb74ad5415f3dffbb38b1dd16c6534 Mon Sep 17 00:00:00 2001 From: yonghyuk_kim Date: Fri, 5 Dec 2025 01:46:46 +0900 Subject: [PATCH 2/3] fix: normalizes line endings Ensures consistent line endings across the project by replacing CRLF with LF. This improves collaboration and prevents potential cross-platform compatibility issues. --- src/__tests__/sale.entity.ts | 38 ++++++++++++++++++------------------ src/paginate.ts | 6 +++--- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/__tests__/sale.entity.ts b/src/__tests__/sale.entity.ts index 99b87aede..622a562b0 100644 --- a/src/__tests__/sale.entity.ts +++ b/src/__tests__/sale.entity.ts @@ -1,19 +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 -} +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.ts b/src/paginate.ts index f32a2217e..0a9e2f642 100644 --- a/src/paginate.ts +++ b/src/paginate.ts @@ -915,12 +915,12 @@ export async function paginate( totalItems = await queryBuilder.getCount() } else if (isPaginated && config.paginationType !== PaginationType.CURSOR) { if (config.fetchRaw) { - items = await queryBuilder.getRawMany(); + items = await queryBuilder.getRawMany() if (config.buildCountQuery) { - totalItems = await config.buildCountQuery(queryBuilder.clone()).getCount(); + totalItems = await config.buildCountQuery(queryBuilder.clone()).getCount() } else { - totalItems = await queryBuilder.getCount(); + totalItems = await queryBuilder.getCount() } } else { if (config.buildCountQuery) { From f4fac7be9bde96774d5e89d53057e94ffa6c7fd0 Mon Sep 17 00:00:00 2001 From: yonghyuk_kim Date: Fri, 5 Dec 2025 02:01:45 +0900 Subject: [PATCH 3/3] fix: test failure --- src/paginate.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paginate.spec.ts b/src/paginate.spec.ts index 970cf7833..1a37f2fbe 100644 --- a/src/paginate.spec.ts +++ b/src/paginate.spec.ts @@ -3549,7 +3549,7 @@ describe('paginate', () => { fetchRaw: true, buildCountQuery: (qb) => { qb.orderBy().limit().offset().take().skip() - qb.select('sale.itemName').groupBy('sale.itemName') + qb.select('sale.itemName').distinct(true) return qb }, }