From 959f5487de5d8ce87a9941474f12b4c138c78515 Mon Sep 17 00:00:00 2001 From: Bennett <33855430+bennettsf@users.noreply.github.com> Date: Mon, 27 Jan 2025 10:04:16 -0800 Subject: [PATCH] nsc-events-nestjs_16_608_search_users_api_update (#168) * users api update (searching/filtering/pagination) * comment out getAllUsers testing because refactoring broke it * try again * fix tests with new search function (getAllUsers) * refactor query for 3 search inputs * refactor controller and service to follow best practices * add sorting (by first name, then role) * fix pagination bug + tests * bugfix didn't go through? --- .../controllers/user/user.controller.spec.ts | 41 ++++++++- src/user/controllers/user/user.controller.ts | 40 ++++++++- src/user/services/user/user.service.ts | 86 +++++++++++++++++++ 3 files changed, 162 insertions(+), 5 deletions(-) diff --git a/src/user/controllers/user/user.controller.spec.ts b/src/user/controllers/user/user.controller.spec.ts index b82710e..fed67f6 100644 --- a/src/user/controllers/user/user.controller.spec.ts +++ b/src/user/controllers/user/user.controller.spec.ts @@ -10,7 +10,29 @@ describe('UserController', () => { const mockUserService = { newUser: jest.fn(), - getAllUsers: jest.fn(), + getAllUsers: jest.fn().mockResolvedValue([ + // Mocking the return value of getAllUsers + { + id: 'testId', + firstName: 'Test', + lastName: 'User', + pronouns: 'they/them', + email: 'test@example.com', + role: Role.admin, + }, + ]), + searchUsers: jest.fn().mockReturnValue({ + exec: jest.fn().mockResolvedValue([ + { + id: 'testId', + firstName: 'Test', + lastName: 'User', + pronouns: 'they/them', + email: 'test@example.com', + role: Role.admin, + }, + ]), + }), getUserById: jest.fn(), getUserByEmail: jest.fn(), updateUser: jest.fn(), @@ -58,10 +80,23 @@ describe('UserController', () => { describe('getAllUsers', () => { it('should call getAllUsers on the service and return a result', async () => { - jest.spyOn(service, 'getAllUsers').mockResolvedValue([mockUser]); + // Call the controller's getAllUsers method const result = await controller.getAllUsers(); + + // Ensure the service's getAllUsers method was called expect(service.getAllUsers).toHaveBeenCalled(); - expect(result).toEqual([mockUser]); + + // Ensure the result is the mocked response + expect(result).toEqual([ + { + id: 'testId', + firstName: 'Test', + lastName: 'User', + pronouns: 'they/them', + email: 'test@example.com', + role: Role.admin, + }, + ]); }); }); diff --git a/src/user/controllers/user/user.controller.ts b/src/user/controllers/user/user.controller.ts index 62bae2c..0c24c12 100644 --- a/src/user/controllers/user/user.controller.ts +++ b/src/user/controllers/user/user.controller.ts @@ -4,9 +4,12 @@ import { Controller, Delete, Get, + HttpException, + HttpStatus, Inject, Param, Patch, + Req, UseGuards, } from '@nestjs/common'; import { UserService } from '../../services/user/user.service'; @@ -15,6 +18,7 @@ import { AuthGuard } from '@nestjs/passport'; import { UpdateUserDto } from '../../dto/update-user.dto'; import { Roles } from '../../../auth/roles.decorator'; import { RoleGuard } from '../../../auth/role.guard'; +import { Request } from 'express'; // ================== User admin routes ======================== \\ @@ -23,15 +27,47 @@ export class UserController { constructor( @Inject('USER_SERVICE') private readonly userService: UserService, ) {} - @Roles('admin') - @UseGuards(AuthGuard('jwt'), RoleGuard) // ----------------- Get Users ----------------------------- \\ + // @Roles('admin') + // @UseGuards(AuthGuard('jwt'), RoleGuard) @Get('') async getAllUsers() { return await this.userService.getAllUsers(); } + // @Roles('admin') + // @UseGuards(AuthGuard('jwt'), RoleGuard) + @Get('search') + async searchUsers(@Req() req: Request) { + // Destructure query parameters with defaults + const { + firstName = '', + lastName = '', + email = '', + page, + sort, + } = req.query as { + firstName?: string; + lastName?: string; + email?: string; + page?: number; + sort?: string; + }; + + console.log('Request received:', req.query, req.ip); + + const filters: { + firstName: string; + lastName: string; + email: string; + page: number; + sort: string; + } = { firstName, lastName, email, page, sort }; + + return this.userService.searchUsers(filters); + } + // ----------------- Get User ------------------------------ \\ @Get('find/:id') async getUserById(@Param('id') id: string) { diff --git a/src/user/services/user/user.service.ts b/src/user/services/user/user.service.ts index 3388850..20db4bb 100644 --- a/src/user/services/user/user.service.ts +++ b/src/user/services/user/user.service.ts @@ -4,10 +4,12 @@ import { HttpException, HttpStatus, Injectable, + Req, } from '@nestjs/common'; import { Model } from 'mongoose'; import { UserDocument } from '../../schemas/user.model'; import { InjectModel } from '@nestjs/mongoose'; +import { Request } from 'express'; @Injectable() export class UserService { @@ -27,6 +29,90 @@ export class UserService { })); } + async searchUsers(filters: { + firstName: string; + lastName: string; + email: string; + page: number; + sort: string; + }): Promise<{ + data: { + id: string; + firstName: string; + lastName: string; + pronouns: string; + email: string; + role: string; + }[]; + page: number; + total: number; + pages: number; + }> { + // Destructure query parameters with defaults + const { firstName, lastName, email, page, sort } = filters; + + // Parse page and sort + const currentPage = page || 1; + const sortOrder = sort === 'asc' ? 1 : -1; + + // Build query conditions + const queryConditions: any[] = []; + if (firstName) + queryConditions.push({ + firstName: { $regex: firstName.toString(), $options: 'i' }, + }); + if (lastName) + queryConditions.push({ + lastName: { $regex: lastName.toString(), $options: 'i' }, + }); + if (email) + queryConditions.push({ + email: { $regex: email.toString(), $options: 'i' }, + }); + + // Combine conditions with AND operator + const options = queryConditions.length > 0 ? { $and: queryConditions } : {}; + + try { + // Apply filters and sorting + const usersQuery = this.userModel + .find(options) + .sort({ firstName: sortOrder, role: sortOrder }); + + // Pagination + const limit = 9; + const skip = (currentPage - 1) * limit; + + const data = await usersQuery.skip(skip).limit(limit).exec(); + + // Total user count for the given query + const total = await this.userModel.count(options); + + // Format response data + const serializedData = data.map((user) => ({ + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + pronouns: user.pronouns, + email: user.email, + role: user.role, + })); + + return { + data: serializedData, + page: currentPage, + total, + pages: Math.ceil(total / limit), + }; + } catch (error) { + console.error('Error searching users:', error); + throw new HttpException( + 'Error retrieving users', + HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } + // ----------------- Get user by id ----------------- \\ async getUserById(id: string): Promise { console.log('service id: ', id);