Skip to content

Commit

Permalink
Merge pull request #1262 from G4EVA-dev/feat-implement-pagination-on-…
Browse files Browse the repository at this point in the history
…findAllInvitations-Endpoint

Feat: Implement pagination on find all invitations endpoint
  • Loading branch information
TheCodeGhinux authored Mar 2, 2025
2 parents 3acd2f5 + ae23b2f commit a0322c4
Show file tree
Hide file tree
Showing 7 changed files with 252 additions and 24 deletions.
11 changes: 1 addition & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -414,19 +414,10 @@ todo.txt
# Docker compose
docker-compose.yml
data/
data/
docker-compose.yml
package-lock.json
.dev.env


package-lock.json
# Docker compose
docker-compose.yml
data/
.dev.env

/compose/compose.yaml
compose/compose.yaml

data/
docker-compose.yml
Expand Down
17 changes: 14 additions & 3 deletions src/modules/invite/dto/all-invitations-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
import { InviteDto } from './invite.dto';
import { ApiProperty } from '@nestjs/swagger';

export class PaginatedInvitationsDto {
@ApiProperty({ type: [InviteDto] })
invitations: InviteDto[];

@ApiProperty({ example: 250, description: 'Total number of invitations' })
total: number;
}

export class FindAllInvitationsResponseDto {
@ApiProperty({ example: 'success' })
status: string;

@ApiProperty({ example: 200 })
status_code: number;

@ApiProperty({ example: 'Successfully fetched invites' })
@ApiProperty({ example: 'Invitations retrieved successfully' })
message: string;

@ApiProperty({ type: [InviteDto] })
data: InviteDto[];
@ApiProperty({ type: PaginatedInvitationsDto })
data: PaginatedInvitationsDto;
}
29 changes: 29 additions & 0 deletions src/modules/invite/dto/pagination-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsInt, Min } from 'class-validator';
import { Type } from 'class-transformer';

export class PaginationQueryDto {
@ApiProperty({
description: 'Page number (starts at 1)',
required: false,
default: 1,
type: Number,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page?: number = 1;

@ApiProperty({
description: 'Number of items per page',
required: false,
default: 10,
type: Number,
})
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
limit?: number = 10;
}
7 changes: 5 additions & 2 deletions src/modules/invite/invite.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Body,
Res,
UseGuards,
Query, // Added this import
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { InviteService } from './invite.service';
Expand All @@ -25,6 +26,7 @@ import { SendInvitationsResponseDto } from './dto/send-invitations-response.dto'
import { AcceptInviteDto } from './dto/accept-invite.dto';
import { OwnershipGuard } from '@guards/authorization.guard';
import * as SYS_MSG from '@shared/constants/SystemMessages';
import { PaginationQueryDto } from './dto/pagination-query.dto'; // Added this import
@ApiBearerAuth()
@ApiTags('Organisation Invites')
@Controller('organizations')
Expand All @@ -43,10 +45,11 @@ export class InviteController {
type: ErrorResponseDto,
})
@Get('invites')
async findAllInvitations() {
const allInvites = await this.inviteService.findAllInvitations();
async findAllInvitations(@Query() paginationQuery: PaginationQueryDto) {
const allInvites = await this.inviteService.findAllInvitations(paginationQuery.page, paginationQuery.limit);
return allInvites;
}

@ApiOperation({ summary: 'Get All Pending Invitations' })
@ApiResponse({
status: 200,
Expand Down
31 changes: 26 additions & 5 deletions src/modules/invite/invite.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,28 @@ export class InviteService {
throw new InternalServerErrorException(`Internal server error: ${error.message}`);
}
}
async findAllInvitations(): Promise<{ status_code: number; message: string; data: InviteDto[] }> {

async findAllInvitations(
page: number = 1,
limit: number = 10
): Promise<{
status: string;
status_code: number;
message: string;
data: { invitations: InviteDto[]; total: number };
}> {
try {
const invites = await this.inviteRepository.find();
// Calculate skip value for pagination
const skip = (page - 1) * limit;

// Get paginated invites
const [invites, total] = await this.inviteRepository.findAndCount({
skip,
take: limit,
});

const allInvites: InviteDto[] = invites.map(invite => {
// Map to DTOs
const invitesDto: InviteDto[] = invites.map(invite => {
return {
token: invite.token,
id: invite.id,
Expand All @@ -68,9 +85,13 @@ export class InviteService {
});

const responseData = {
status: 'success',
status_code: HttpStatus.OK,
message: 'Successfully fetched invites',
data: allInvites,
message: 'Invitations retrieved successfully',
data: {
invitations: invitesDto,
total,
},
};

return responseData;
Expand Down
22 changes: 18 additions & 4 deletions src/modules/invite/tests/invite.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('InviteService', () => {
save: jest.fn(),
findOneBy: jest.fn(),
update: jest.fn(),
findAndCount: jest.fn(),
},
},
{
Expand Down Expand Up @@ -163,15 +164,26 @@ describe('InviteService', () => {
});

it('should fetch all invites', async () => {
jest.spyOn(repository, 'find').mockResolvedValue(mockInvites);
const expectedInvites = mockInvites.map(invite => {
const { created_at, updated_at, ...rest } = invite;
return rest;
});

jest.spyOn(repository, 'findAndCount').mockResolvedValue([mockInvites, mockInvites.length]);

const result = await service.findAllInvitations();

expect(result).toEqual({
status_code: 200,
message: 'Successfully fetched invites',
data: mockInvitesResponse,
status: 'success',
status_code: HttpStatus.OK,
message: 'Invitations retrieved successfully',
data: {
invitations: expectedInvites,
total: mockInvites.length,
},
});

expect(repository.findAndCount).toHaveBeenCalled();
});

it('should throw an internal server error if an exception occurs', async () => {
Expand All @@ -195,6 +207,8 @@ describe('InviteService', () => {
isGeneric: invite.isGeneric,
organisation: invite.organisation,
email: invite.email,
created_at: new Date(),
updated_at: new Date(),
})),
});
});
Expand Down
159 changes: 159 additions & 0 deletions src/modules/invite/tests/test-invite.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Test, TestingModule } from '@nestjs/testing';
import { InviteService } from '../invite.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Invite } from '../entities/invite.entity';
import { Organisation } from '@modules/organisations/entities/organisations.entity';
import { User } from '@modules/user/entities/user.entity';
import { HttpStatus, InternalServerErrorException } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
import { EmailService } from '@modules/email/email.service';
import { ConfigService } from '@nestjs/config';
import { OrganisationsService } from '@modules/organisations/organisations.service';
import { Repository, EntityManager } from 'typeorm';

describe('InviteService', () => {
let service: InviteService;
let inviteRepository: jest.Mocked<Repository<Invite>>;

const mockInviteRepository = {
find: jest.fn(),
findOne: jest.fn(),
findAndCount: jest.fn(),
create: jest.fn(),
save: jest.fn(),
};

const mockOrganisationRepository = {
findOne: jest.fn(),
};

const mockUserRepository = {
findOne: jest.fn(),
};

const mockMailerService = {
sendMail: jest.fn(),
};

const mockEmailService = {
getTemplate: jest.fn(),
};

const mockConfigService = {
get: jest.fn(),
};

const mockOrganisationsService = {
addOrganisationMember: jest.fn(),
};

// Add a mock for EntityManager
const mockEntityManager = {
transaction: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
InviteService,
{ provide: getRepositoryToken(Invite), useValue: mockInviteRepository },
{ provide: getRepositoryToken(Organisation), useValue: mockOrganisationRepository },
{ provide: getRepositoryToken(User), useValue: mockUserRepository },
{ provide: MailerService, useValue: mockMailerService },
{ provide: EmailService, useValue: mockEmailService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: OrganisationsService, useValue: mockOrganisationsService },
{ provide: EntityManager, useValue: mockEntityManager }, // Add this line
],
}).compile();

service = module.get<InviteService>(InviteService);
inviteRepository = module.get(getRepositoryToken(Invite));
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('findAllInvitations', () => {
it('should return paginated invitations with default parameters', async () => {
const mockInvites = [
{
id: '1',
token: 'token1',
isAccepted: false,
isGeneric: true,
organisation: { id: 'org1', name: 'Org 1' },
email: '[email protected]',
},
];

mockInviteRepository.findAndCount.mockResolvedValue([mockInvites, 1]);

const result = await service.findAllInvitations();

expect(mockInviteRepository.findAndCount).toHaveBeenCalledWith({
skip: 0,
take: 10,
});

expect(result).toEqual({
status: 'success',
status_code: HttpStatus.OK,
message: 'Invitations retrieved successfully',
data: {
invitations: expect.any(Array),
total: 1,
},
});
expect(result.data.invitations).toHaveLength(1);
});

it('should return paginated invitations with custom parameters', async () => {
const mockInvites = [
{
id: '1',
token: 'token1',
isAccepted: false,
isGeneric: true,
organisation: { id: 'org1', name: 'Org 1' },
email: '[email protected]',
},
{
id: '2',
token: 'token2',
isAccepted: true,
isGeneric: false,
organisation: { id: 'org2', name: 'Org 2' },
email: '[email protected]',
},
];

mockInviteRepository.findAndCount.mockResolvedValue([mockInvites, 10]);

const result = await service.findAllInvitations(2, 5);

expect(mockInviteRepository.findAndCount).toHaveBeenCalledWith({
skip: 5,
take: 5,
});

expect(result).toEqual({
status: 'success',
status_code: HttpStatus.OK,
message: 'Invitations retrieved successfully',
data: {
invitations: expect.any(Array),
total: 10,
},
});
expect(result.data.invitations).toHaveLength(2);
});

it('should handle errors and throw InternalServerErrorException', async () => {
mockInviteRepository.findAndCount.mockRejectedValue(new Error('Database error'));

await expect(service.findAllInvitations()).rejects.toThrow(InternalServerErrorException);
});
});
});

0 comments on commit a0322c4

Please sign in to comment.