From c85fe79b4225cace3a3f18859722d5c5d17e2a0f Mon Sep 17 00:00:00 2001 From: Md Azizul Hakim Date: Thu, 24 Oct 2024 01:18:12 +0600 Subject: [PATCH 1/2] bug fix --- src/modules/role/role.service.spec.ts | 2 +- src/modules/user/types.d.ts | 5 +++++ src/modules/user/user.service.ts | 10 +++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/modules/role/role.service.spec.ts b/src/modules/role/role.service.spec.ts index 7d4d3ed..c7b249c 100644 --- a/src/modules/role/role.service.spec.ts +++ b/src/modules/role/role.service.spec.ts @@ -139,7 +139,7 @@ describe('RoleService', () => { expect(mockEntityManager.removeAndFlush).not.toHaveBeenCalled(); }); - it('should delete a system role', async () => { + it('should delete a role', async () => { await service.delete(4); expect(mockRoleRepository.findOne).toHaveBeenCalledWith(4); diff --git a/src/modules/user/types.d.ts b/src/modules/user/types.d.ts index ea53a4e..4a7a061 100644 --- a/src/modules/user/types.d.ts +++ b/src/modules/user/types.d.ts @@ -11,3 +11,8 @@ interface UserResponse { roles?: string[]; permissions?: string[]; } + +interface UserPaginatedList { + data: Partial[]; + meta: PaginatedMeta; +} diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index 0bb905a..b8bfb32 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -17,10 +17,6 @@ import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; import { UserTransformer } from './transformer/user.transformer'; -interface UserPaginatedList { - data: Partial[]; - meta: PaginatedMeta; -} @Injectable() export class UserService { // List of role ids that can't be assigned (ex: SuperAdmin) @@ -252,6 +248,10 @@ export class UserService { userId: number, roleIds: number[], ): Promise> { + //filter out unassignable roles + const roleIdsToAssign = roleIds.filter( + (roleId) => !this.UNASSIGNABLE_ROLE_IDS.includes(roleId), + ); const user = await this.userRepository.findOne( { id: userId }, { @@ -261,7 +261,7 @@ export class UserService { if (!user) { throw new NotFoundException('User not found'); } - const roles = await this.em.find(Role, { id: { $in: roleIds } }); + const roles = await this.em.find(Role, { id: { $in: roleIdsToAssign } }); user.roles.set(roles); await this.em.flush(); From c67aa6d913738c122b30ec2deff38032b011dd93 Mon Sep 17 00:00:00 2001 From: Md Azizul Hakim Date: Fri, 25 Oct 2024 02:20:02 +0600 Subject: [PATCH 2/2] filter and pagination rework with service --- ims-nest-api-starter.postman_collection.json | 16 +- package.json | 13 +- src/common/controllers/base.controller.ts | 4 +- src/modules/misc/filter.service.spec.ts | 190 +++++++++++++++++++ src/modules/misc/filter.service.ts | 105 ++++++++++ src/modules/misc/misc.module.ts | 6 +- src/modules/misc/pagination.service.spec.ts | 100 ++++++++++ src/modules/misc/pagination.service.ts | 35 ++++ src/modules/misc/types.d.ts | 39 ++++ src/modules/user/user.controller.spec.ts | 4 +- src/modules/user/user.controller.ts | 6 +- src/modules/user/user.service.spec.ts | 32 +++- src/modules/user/user.service.ts | 47 ++--- src/types.d.ts | 4 + src/utils/helper.ts | 2 +- types.d.ts | 28 --- 16 files changed, 551 insertions(+), 80 deletions(-) create mode 100644 src/modules/misc/filter.service.spec.ts create mode 100644 src/modules/misc/filter.service.ts create mode 100644 src/modules/misc/pagination.service.spec.ts create mode 100644 src/modules/misc/pagination.service.ts create mode 100644 src/modules/misc/types.d.ts create mode 100644 src/types.d.ts delete mode 100644 types.d.ts diff --git a/ims-nest-api-starter.postman_collection.json b/ims-nest-api-starter.postman_collection.json index d5d009b..cd5e03f 100644 --- a/ims-nest-api-starter.postman_collection.json +++ b/ims-nest-api-starter.postman_collection.json @@ -5,7 +5,7 @@ "description": "# IMS-NEST-API-STARTER API Documentation\n\n## Introduction\n\nWelcome to the api documentation.\n`ims-nest-api-starter` is a backend API starter template using [NestJS](https://nestjs.com/) and [MikroORM](https://mikro-orm.io/) designed for scalable applications. This starter includes authentication, authorization, user management, role management, and role/permission-based access", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "29444436", - "_collection_link": "https://innovix-matrix-system.postman.co/workspace/c0ff4f3a-4d52-490d-81f4-c33d60e51246/collection/29444436-0f10bdbf-368f-47c9-8a59-e7d1586fae52?action=share&source=collection_link&creator=29444436" + "_collection_link": "https://innovix-matrix-system.postman.co/workspace/Innovix-Matrix-System-Workspace~c0ff4f3a-4d52-490d-81f4-c33d60e51246/collection/29444436-0f10bdbf-368f-47c9-8a59-e7d1586fae52?action=share&source=collection_link&creator=29444436" }, "item": [ { @@ -284,7 +284,7 @@ "query": [ { "key": "search", - "value": "trevor", + "value": "john", "description": "search my name, email", "disabled": true }, @@ -301,6 +301,16 @@ { "key": "perPage", "value": "10" + }, + { + "key": "orderBy", + "value": "createdAt", + "disabled": true + }, + { + "key": "orderDirection", + "value": "desc", + "disabled": true } ] } @@ -467,7 +477,7 @@ "variable": [ { "key": "id", - "value": "48" + "value": "4" } ] } diff --git a/package.json b/package.json index 8c0ad5f..dda3a03 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,6 @@ "xsecurity:install": "cross-env CLI_PATH=./dist/cli.js npx nestjs-command xsecurity:install", "prepare": "husky", "create:module": "cross-env CLI_PATH=./dist/cli.js npx nestjs-command create:module" - }, "dependencies": { "@faker-js/faker": "^9.0.3", @@ -97,15 +96,19 @@ "ts" ], "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", + "testRegex": "modules/.*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, "collectCoverageFrom": [ - "**/*.(t|j)s" + "modules/**/*.(t|j)s", + "!**/*.d.ts" ], "coverageDirectory": "../coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "modulePathIgnorePatterns": [ + "types.d.ts" + ] }, "mikro-orm": { "useTsNode": true, @@ -113,4 +116,4 @@ "./src/config/mikro-orm.config.ts" ] } -} +} \ No newline at end of file diff --git a/src/common/controllers/base.controller.ts b/src/common/controllers/base.controller.ts index 882b12d..dae6968 100644 --- a/src/common/controllers/base.controller.ts +++ b/src/common/controllers/base.controller.ts @@ -4,7 +4,7 @@ import { Response } from 'express'; export class BaseController { sendPaginatedResponse( data: any[], - pageData: PaginatedMeta, + pageData: PaginationMeta, message = '', statusCode = HttpStatus.OK, res: Response, @@ -18,8 +18,6 @@ export class BaseController { currentPage: pageData.currentPage, from: pageData.from, lastPage: pageData.lastPage, - // links: pageData.links || [], - // path: pageData.path || '', perPage: pageData.perPage, to: pageData.to, total: pageData.total, diff --git a/src/modules/misc/filter.service.spec.ts b/src/modules/misc/filter.service.spec.ts new file mode 100644 index 0000000..30aa78e --- /dev/null +++ b/src/modules/misc/filter.service.spec.ts @@ -0,0 +1,190 @@ +import { EntityRepository } from '@mikro-orm/core'; +import { Test, TestingModule } from '@nestjs/testing'; +import { FilterService } from './filter.service'; +import { MiscModule } from './misc.module'; +import { PaginationService } from './pagination.service'; + +describe('FilterService', () => { + let service: FilterService; + let mockPaginationService: jest.Mocked; + let mockRepository: jest.Mocked>; + + beforeEach(async () => { + mockRepository = { + findAndCount: jest.fn(), + } as any; + + mockPaginationService = { + buildPaginationOptions: jest.fn().mockReturnValue({ + limit: 10, + offset: 0, + }), + buildPaginationMeta: jest.fn().mockReturnValue({ + currentPage: 1, + from: 1, + lastPage: 1, + perPage: 10, + to: 10, + total: 10, + }), + } as any; + + const module: TestingModule = await Test.createTestingModule({ + imports: [MiscModule], + providers: [ + FilterService, + { + provide: PaginationService, + useValue: mockPaginationService, + }, + ], + }).compile(); + + service = module.get(FilterService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('filter', () => { + it('should return paginated results with default parameters', async () => { + const mockData = [{ id: 1, name: 'Test' }]; + mockRepository.findAndCount.mockResolvedValue([mockData, 1]); + + const result = await service.filter( + mockRepository, + {}, + ['name'], + ['roles'], + ); + + expect(result).toEqual({ + data: mockData, + meta: expect.any(Object), + }); + expect(mockRepository.findAndCount).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + limit: 10, + offset: 0, + orderBy: { createdAt: 'ASC' }, + }), + ); + }); + + it('should apply search filters correctly', async () => { + const mockData = [{ id: 1, name: 'Test' }]; + mockRepository.findAndCount.mockResolvedValue([mockData, 1]); + + await service.filter( + mockRepository, + { + search: 'test', + searchFields: ['name', 'email'], + }, + ['name'], + ['roles'], + ); + + expect(mockRepository.findAndCount).toHaveBeenCalledWith( + { + $or: [ + { name: { $ilike: '%test%' } }, + { email: { $ilike: '%test%' } }, + ], + }, + expect.any(Object), + ); + }); + + it('should apply select filters correctly', async () => { + const mockData = [{ id: 1, name: 'Test' }]; + mockRepository.findAndCount.mockResolvedValue([mockData, 1]); + + await service.filter( + mockRepository, + { + selectFields: [{ status: 'active' }], + }, + ['name'], + ['roles'], + ); + + expect(mockRepository.findAndCount).toHaveBeenCalledWith( + { status: 'active' }, + expect.any(Object), + ); + }); + + it('should apply ordering correctly', async () => { + const mockData = [{ id: 1, name: 'Test' }]; + mockRepository.findAndCount.mockResolvedValue([mockData, 1]); + + await service.filter( + mockRepository, + { + orderBy: 'name', + orderDirection: 'DESC', + }, + ['name'], + ['roles'], + ); + + expect(mockRepository.findAndCount).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + orderBy: { name: 'DESC' }, + }), + ); + }); + + it('should use default ordering when invalid order field is provided', async () => { + const mockData = [{ id: 1, name: 'Test' }]; + mockRepository.findAndCount.mockResolvedValue([mockData, 1]); + + await service.filter( + mockRepository, + { + orderBy: 'invalid_field', + orderDirection: 'DESC', + }, + ['name'], + ['roles'], + ); + + expect(mockRepository.findAndCount).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining({ + orderBy: { createdAt: 'ASC' }, + }), + ); + }); + + it('should combine search and select filters with $and', async () => { + const mockData = [{ id: 1, name: 'Test' }]; + mockRepository.findAndCount.mockResolvedValue([mockData, 1]); + + await service.filter( + mockRepository, + { + search: 'test', + searchFields: ['name'], + selectFields: [{ status: 'active' }], + }, + ['name'], + ['roles'], + ); + + expect(mockRepository.findAndCount).toHaveBeenCalledWith( + { + $and: [ + { $or: [{ name: { $ilike: '%test%' } }] }, + { status: 'active' }, + ], + }, + expect.any(Object), + ); + }); + }); +}); diff --git a/src/modules/misc/filter.service.ts b/src/modules/misc/filter.service.ts new file mode 100644 index 0000000..2c92fb3 --- /dev/null +++ b/src/modules/misc/filter.service.ts @@ -0,0 +1,105 @@ +import { + EntityRepository, + FilterQuery, + OrderDefinition, + Populate, +} from '@mikro-orm/core'; +import { Injectable } from '@nestjs/common'; +import { PaginationService } from './pagination.service'; + +@Injectable() +export class FilterService { + constructor(private readonly paginationService: PaginationService) {} + + async filter( + repository: EntityRepository, + params: FilterWithPaginationParams, + validOrderFields: string[] = [], + populate: string[] = [], + ): Promise> { + const where = this.buildWhereClause( + params.search, + params.searchFields, + params.selectFields, + ); + + const orderBy = this.buildOrderOptions( + validOrderFields, + params.orderBy, + params.orderDirection, + ); + + const { limit, offset } = this.paginationService.buildPaginationOptions({ + page: params.page, + perPage: params.perPage, + }); + + const [results, totalCount] = await repository.findAndCount(where, { + populate: populate as unknown as Populate, + limit, + offset, + orderBy, + }); + + const meta = this.paginationService.buildPaginationMeta( + params.page || 1, + params.perPage || 10, + totalCount, + ); + + return { + data: results, + meta, + }; + } + + private buildWhereClause( + search?: string, + searchFields?: string[], + selectFields?: Array<{ [key: string]: boolean | number | string }>, + ): FilterQuery { + if (!search && (!selectFields || selectFields.length === 0)) { + return {} as FilterQuery; + } + + const conditions: any[] = []; + + if (search && searchFields?.length > 0) { + const searchConditions = searchFields.map((field) => ({ + [field]: { $ilike: `%${search}%` }, + })); + conditions.push({ $or: searchConditions }); + } + + if (selectFields?.length > 0) { + selectFields.forEach((field) => { + conditions.push(field); + }); + } + + if (conditions.length > 1) { + return { $and: conditions } as FilterQuery; + } + + if (conditions.length === 1) { + return conditions[0] as FilterQuery; + } + + return {} as FilterQuery; + } + + private buildOrderOptions( + validOrderFields: string[], + orderBy?: string, + orderDirection: 'ASC' | 'DESC' = 'ASC', + ): OrderDefinition { + const defaultOrderField = 'createdAt'; + const defaultOrderDirection = 'ASC' as const; + + if (orderBy && validOrderFields.includes(orderBy)) { + return { [orderBy]: orderDirection } as OrderDefinition; + } + + return { [defaultOrderField]: defaultOrderDirection } as OrderDefinition; + } +} diff --git a/src/modules/misc/misc.module.ts b/src/modules/misc/misc.module.ts index 32af494..061deea 100644 --- a/src/modules/misc/misc.module.ts +++ b/src/modules/misc/misc.module.ts @@ -1,10 +1,12 @@ import { Module } from '@nestjs/common'; +import { FilterService } from './filter.service'; +import { PaginationService } from './pagination.service'; import { PasswordService } from './password.service'; @Module({ imports: [], controllers: [], - providers: [PasswordService], - exports: [PasswordService], + providers: [PasswordService, PaginationService, FilterService], + exports: [PasswordService, PaginationService, FilterService], }) export class MiscModule {} diff --git a/src/modules/misc/pagination.service.spec.ts b/src/modules/misc/pagination.service.spec.ts new file mode 100644 index 0000000..4404e82 --- /dev/null +++ b/src/modules/misc/pagination.service.spec.ts @@ -0,0 +1,100 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PaginationService } from './pagination.service'; + +describe('PaginationService', () => { + let service: PaginationService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [PaginationService], + }).compile(); + + service = module.get(PaginationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('buildPaginationOptions', () => { + it('should return default pagination options when no params provided', () => { + const result = service.buildPaginationOptions({}); + expect(result).toEqual({ + limit: 10, + offset: 0, + }); + }); + + it('should calculate correct offset for given page and perPage', () => { + const result = service.buildPaginationOptions({ + page: 2, + perPage: 15, + }); + expect(result).toEqual({ + limit: 15, + offset: 15, // (page-1) * perPage = (2-1) * 15 = 15 + }); + }); + + it('should handle negative page numbers by using default values', () => { + const result = service.buildPaginationOptions({ + page: -1, + perPage: 10, + }); + expect(result).toEqual({ + limit: 10, + offset: 0, + }); + }); + }); + + describe('buildPaginationMeta', () => { + it('should calculate correct pagination meta for first page', () => { + const result = service.buildPaginationMeta(1, 10, 25); + expect(result).toEqual({ + currentPage: 1, + from: 1, + lastPage: 3, + perPage: 10, + to: 10, + total: 25, + }); + }); + + it('should calculate correct pagination meta for last page', () => { + const result = service.buildPaginationMeta(3, 10, 25); + expect(result).toEqual({ + currentPage: 3, + from: 21, + lastPage: 3, + perPage: 10, + to: 25, + total: 25, + }); + }); + + it('should handle case when total is less than perPage', () => { + const result = service.buildPaginationMeta(1, 10, 5); + expect(result).toEqual({ + currentPage: 1, + from: 1, + lastPage: 1, + perPage: 10, + to: 5, + total: 5, + }); + }); + + it('should handle zero total count', () => { + const result = service.buildPaginationMeta(1, 10, 0); + expect(result).toEqual({ + currentPage: 1, + from: 1, + lastPage: 0, + perPage: 10, + to: 0, + total: 0, + }); + }); + }); +}); diff --git a/src/modules/misc/pagination.service.ts b/src/modules/misc/pagination.service.ts new file mode 100644 index 0000000..7591103 --- /dev/null +++ b/src/modules/misc/pagination.service.ts @@ -0,0 +1,35 @@ +import { Injectable } from '@nestjs/common'; +@Injectable() +export class PaginationService { + buildPaginationOptions(params: PaginatedParams): { + limit: number; + offset: number; + } { + const page = Math.abs(params?.page || 1); + const perPage = Math.abs(params.perPage || 10); + + return { + limit: perPage, + offset: (page - 1) * perPage, + }; + } + + buildPaginationMeta( + page: number, + perPage: number, + totalCount: number, + ): PaginationMeta { + const totalPages = Math.ceil(totalCount / perPage); + const from = (page - 1) * perPage + 1; + const to = Math.min(page * perPage, totalCount); + + return { + currentPage: page, + from, + lastPage: totalPages, + perPage, + to, + total: totalCount, + }; + } +} diff --git a/src/modules/misc/types.d.ts b/src/modules/misc/types.d.ts new file mode 100644 index 0000000..01a59e6 --- /dev/null +++ b/src/modules/misc/types.d.ts @@ -0,0 +1,39 @@ +interface FilterParams { + search?: string; + searchFields?: string[]; + selectFields?: Array<{ [key: string]: boolean | number | string }>; + orderBy?: string; + orderDirection?: 'ASC' | 'DESC'; +} + +interface PaginatedParams { + page?: number; + perPage?: number; + path?: string; +} + +interface FilterWithPaginationParams extends PaginatedParams, FilterParams {} + +interface PaginationMeta { + currentPage: number; + from: number; + lastPage: number; + perPage: number; + to: number; + total: number; +} + +interface PaginatedResult { + data: T[]; + meta: PaginationMeta; +} + +interface PaginationLinks { + url: string; + label: string; + active: boolean; +} +interface PaginationMetaWithLinks extends PaginationMeta { + path?: string; + links?: PaginationLinks[]; +} diff --git a/src/modules/user/user.controller.spec.ts b/src/modules/user/user.controller.spec.ts index 3539305..c6b7ea3 100644 --- a/src/modules/user/user.controller.spec.ts +++ b/src/modules/user/user.controller.spec.ts @@ -1,3 +1,4 @@ +import { HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Response } from 'express'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; @@ -11,7 +12,6 @@ import { RoleAssignDto } from './dto/role-assign.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { UserController } from './user.controller'; import { UserService } from './user.service'; -import { HttpStatus } from '@nestjs/common'; describe('UserController', () => { let controller: UserController; @@ -201,7 +201,7 @@ describe('UserController', () => { }; userService.findAll.mockResolvedValue(mockPaginatedResponse); - await controller.findAll(1, 10, '', undefined, mockResponse); + await controller.findAll(1, 10, 'ASC', 'createdAt', '', true, mockResponse); expect(userService.findAll).toHaveBeenCalled(); expect(mockResponse.status).toHaveBeenCalledWith(HttpStatus.OK); diff --git a/src/modules/user/user.controller.ts b/src/modules/user/user.controller.ts index c5d0dd8..dc10148 100644 --- a/src/modules/user/user.controller.ts +++ b/src/modules/user/user.controller.ts @@ -122,6 +122,8 @@ export class UserController extends BaseController { async findAll( @Query('page', ParseIntPipe) page: number = 1, @Query('perPage', ParseIntPipe) perPage: number = 10, + @Query('orderDirection') orderDirection: 'ASC' | 'DESC' = 'ASC', + @Query('orderBy') orderBy: string = 'createdAt', @Query('search') search: string = '', @Query('isActive') isActive: boolean, @Res() res: Response, @@ -131,9 +133,11 @@ export class UserController extends BaseController { if (isActive !== undefined) { selectFields.push({ isActive: isActive }); } - const params: PaginatedParams = { + const params: FilterWithPaginationParams = { page, perPage, + orderBy, + orderDirection, search, searchFields, selectFields, diff --git a/src/modules/user/user.service.spec.ts b/src/modules/user/user.service.spec.ts index e784ce2..63665f0 100644 --- a/src/modules/user/user.service.spec.ts +++ b/src/modules/user/user.service.spec.ts @@ -2,11 +2,11 @@ import { Collection, EntityManager, EntityRepository } from '@mikro-orm/core'; import { getRepositoryToken } from '@mikro-orm/nestjs'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { User } from './entities/user.entity'; import { createMockCollection } from '../../mocks/collection.mock'; import { MiscModule } from '../misc/misc.module'; import { Permission } from '../permission/entities/permission.entity'; import { Role } from '../role/entities/role.entity'; +import { User } from './entities/user.entity'; import { UserTransformer } from './transformer/user.transformer'; import { UserService } from './user.service'; @@ -84,6 +84,22 @@ describe('UserService', () => { roles: roleCollection, permissions: permissionCollection, }), + findAndCount: jest.fn().mockReturnValue([ + [ + { + id: 1, + name: 'John Doe', + email: '1q3U8@example.com', + isActive: true, + lastLoginAt: new Date(), + createdAt: new Date(), + updatedAt: new Date(), + roles: roleCollection, + permissions: permissionCollection, + }, + ], + 1, + ]), create: jest.fn().mockImplementation((dto) => ({ ...dto, id: 1, // Assign an ID for the mock user @@ -134,6 +150,20 @@ describe('UserService', () => { expect(user.isActive).toEqual(createUserDto.isActive); }); + it('should find all users', async () => { + const params: FilterWithPaginationParams = { + page: 1, + perPage: 10, + orderBy: 'name', + orderDirection: 'ASC', + }; + const users = await service.findAll(params); + expect(mockUserRepository.findAndCount).toHaveBeenCalled(); + expect(users.data.length).toEqual(1); + expect(users.meta.total).toEqual(1); + expect(users.meta.lastPage).toEqual(1); + }); + it('should find a user by ID', async () => { const userId = 1; const user = await service.findOne(userId); diff --git a/src/modules/user/user.service.ts b/src/modules/user/user.service.ts index b8bfb32..20e4622 100644 --- a/src/modules/user/user.service.ts +++ b/src/modules/user/user.service.ts @@ -16,6 +16,7 @@ import { ChangeSelfPasswordDto } from './dto/reset-self-password.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; import { UserTransformer } from './transformer/user.transformer'; +import { FilterService } from '../misc/filter.service'; @Injectable() export class UserService { @@ -28,6 +29,7 @@ export class UserService { private readonly userRepository: EntityRepository, private readonly em: EntityManager, private readonly passwordService: PasswordService, + private readonly filterService: FilterService, private readonly userTransformer: UserTransformer, ) {} @@ -46,45 +48,22 @@ export class UserService { } // Find all users - async findAll(params: PaginatedParams): Promise { - const { page, perPage, search, searchFields, selectFields } = params; - const where: any = {}; - //map search fields - if (search && searchFields && searchFields.length > 0) { - where.$or = searchFields.map((field) => ({ - [field]: { $ilike: `%${search}%` }, //case-insensitive partial match - })); - } - // map filters - if (selectFields && selectFields.length > 0) { - selectFields.forEach((field) => { - Object.assign(where, field); - }); - } - const [users, totalCount] = await this.userRepository.findAndCount(where, { - populate: ['roles'], - limit: perPage, - offset: (page - 1) * perPage, - }); - const mappedUsers = this.userTransformer.transformMany(users, { + async findAll( + params: FilterWithPaginationParams, + ): Promise { + const { data, meta } = await this.filterService.filter( + this.userRepository, + params, + ['id', 'name', 'email', 'createdAt'], + ['roles'], + ); + const mappedUsers = this.userTransformer.transformMany(data, { loadRelations: true, }); - const totalPages = Math.ceil(totalCount / perPage); - const from = (page - 1) * perPage + 1; - const to = Math.min(page * perPage, totalCount); - - const pageData = { - currentPage: page, - from: from, - lastPage: totalPages, - perPage: perPage, - to: to, - total: totalCount, - }; return { data: mappedUsers, - meta: pageData, + meta, }; } diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..eec6a61 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,4 @@ +interface DataTransformer { + transform(entity: T): R; + transformMany(entities: T[]): R[]; +} diff --git a/src/utils/helper.ts b/src/utils/helper.ts index 913ca2d..3849a21 100644 --- a/src/utils/helper.ts +++ b/src/utils/helper.ts @@ -4,7 +4,7 @@ export const getPaginationLinks = ( page: number, totalPages: number, path: string, -): PaginatedLinks[] => { +): PaginationLinks[] => { const links = [ { url: page > 1 ? `${path}?page=${page - 1}` : null, diff --git a/types.d.ts b/types.d.ts deleted file mode 100644 index c588dc4..0000000 --- a/types.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -interface DataTransformer { - transform(entity: T): R; - transformMany(entities: T[]): R[]; -} -interface PaginatedLinks { - url: string; - label: string; - active: boolean; -} -interface PaginatedMeta { - currentPage: number; - from: number; - to: number; - perPage: number; - lastPage: number; - total: number; - path?: string; - links?: PaginatedLinks[]; -} - -interface PaginatedParams { - page: number; - perPage: number; - path?: string; - search?: string; - searchFields?: string[]; - selectFields?: Array<{ [key: string]: boolean | number | string }>; -}