Skip to content

Commit

Permalink
Merge pull request #1246 from Ng1n3/feat-language-endpoint
Browse files Browse the repository at this point in the history
feat: implement get all languages by User Id
  • Loading branch information
incredible-phoenix246 authored Feb 28, 2025
2 parents 43a7d74 + f48cfcf commit 5b93595
Show file tree
Hide file tree
Showing 8 changed files with 313 additions and 10 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,8 @@ todo.txt
.vscode/


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

7 changes: 6 additions & 1 deletion src/modules/languages/entities/language.entity.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { Entity, Column, PrimaryGeneratedColumn, ManyToMany, JoinTable } from 'typeorm';
import { AbstractBaseEntity } from '../../../entities/base.entity';
import { User } from '@modules/user/entities/user.entity';

@Entity()
export class Language extends AbstractBaseEntity {
Expand All @@ -11,4 +12,8 @@ export class Language extends AbstractBaseEntity {

@Column({ nullable: true })
description: string;

@ManyToMany(() => User, user => user.languages)
@JoinTable()
users?: User[];
}
18 changes: 15 additions & 3 deletions src/modules/languages/languages.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Controller, Post, Body, Get, Patch, Param, Res } from '@nestjs/common';
import { Controller, Post, Body, Get, Patch, Param, Res, Request, Response } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBody, ApiBearerAuth } from '@nestjs/swagger';
import { CreateLanguageDto, UpdateLanguageDto } from './dto/create-language.dto';
import { LanguagesService } from './languages.service';
import { Response } from 'express';

@ApiTags('Languages')
@Controller('languages')
Expand All @@ -25,11 +24,24 @@ export class LanguagesController {
@ApiOperation({ summary: 'Get all supported languages' })
@ApiResponse({ status: 200, description: 'List of supported languages.' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
async getLanguages(@Res() response: Response): Promise<any> {
async getLanguages(@Response() response): Promise<any> {
const result = await this.languagesService.getSupportedLanguages();
return response.status(result.status_code).json(result);
}

@Get(':id')
@ApiBearerAuth()
@ApiOperation({ summary: 'Get all languages from a userId' })
@ApiResponse({ status: 200, description: 'List of languages by the user.' })
@ApiResponse({ status: 401, description: 'Unauthorized' })
@ApiResponse({ status: 400, description: 'Invalid UserId' })
@ApiResponse({ status: 404, description: 'No languages found for the specified user' })
@ApiResponse({ status: 500, description: 'Internal server error' })
async getLanguagesByUserId(@Param('id') id: string, @Request() req, @Response() response): Promise<any> {
const result = await this.languagesService.getLanguagesById(id, req.user);
return response.status(result.status_code).json(result);
}

@Patch(':id')
@ApiBearerAuth()
@ApiOperation({ summary: 'Update a language' })
Expand Down
3 changes: 2 additions & 1 deletion src/modules/languages/languages.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Language } from './entities/language.entity';
import { LanguagesService } from './languages.service';
import { LanguagesController } from './languages.controller';
import { User } from '@modules/user/entities/user.entity';

@Module({
imports: [TypeOrmModule.forFeature([Language])],
imports: [TypeOrmModule.forFeature([Language, User])],
controllers: [LanguagesController],
providers: [LanguagesService],
})
Expand Down
58 changes: 58 additions & 0 deletions src/modules/languages/languages.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
BadRequestException,
ConflictException,
ForbiddenException,
HttpStatus,
Injectable,
InternalServerErrorException,
Expand All @@ -10,6 +12,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Language } from './entities/language.entity';
import { CreateLanguageDto, UpdateLanguageDto } from './dto/create-language.dto';
import { User } from '@modules/user/entities/user.entity';
import { isUUID } from 'class-validator';

@Injectable()
export class LanguagesService {
Expand Down Expand Up @@ -100,4 +104,58 @@ export class LanguagesService {
});
}
}
async getLanguagesById(userId: string, user: User): Promise<any> {
try {
if (!isUUID(userId)) {
throw new BadRequestException('Invalid user Id');
}

if (user.id !== userId) {
throw new ForbiddenException({
status_code: HttpStatus.FORBIDDEN,
message: 'You are not authorized to access this resource',
});
}

const languages = await this.languageRepository
.createQueryBuilder('language')
.innerJoin('language.users', 'user')
.where('user.id = :userId', { userId })
.getMany();

if (!languages || languages.length === 0) {
throw new NotFoundException({
status_code: HttpStatus.NOT_FOUND,
message: 'Languages associated with this user not found',
});
}

const formattedLanguages = languages.map(language => ({
id: language.id,
language: language.language,
description: language.description,
code: language.code,
}));

return {
status: 'OK',
status_code: HttpStatus.OK,
message: 'Languages fetched successfully',
data: formattedLanguages,
};
} catch (error) {
if (
error instanceof NotFoundException ||
error instanceof ForbiddenException ||
error instanceof BadRequestException
) {
throw error;
}
Logger.error('LanguagesServiceError ~ getLanguagesById ~', error);
throw new InternalServerErrorException({
message: 'An error occurred',
status_code: HttpStatus.INTERNAL_SERVER_ERROR,
});
}
}
}
98 changes: 97 additions & 1 deletion src/modules/languages/tests/languages.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Language } from '../entities/language.entity';
import { CreateLanguageDto } from '../dto/create-language.dto';
import { HttpException, HttpStatus } from '@nestjs/common';
import { BadRequestException, ForbiddenException, HttpException, HttpStatus, NotFoundException } from '@nestjs/common';
import { User } from '@modules/user/entities/user.entity';

const mockLanguageRepository = {
findOne: jest.fn(),
Expand All @@ -16,6 +17,7 @@ const mockLanguageRepository = {

const mockLanguagesService = {
createLanguage: jest.fn(),
getLanguagesById: jest.fn(),
getSupportedLanguages: jest.fn(),
};

Expand Down Expand Up @@ -175,4 +177,98 @@ describe('LanguagesController', () => {
);
});
});

describe('getLanguagesByUserId', () => {
it('should return languages associated with a user', async () => {
const userId = '550e8400-e29b-41d4-a716-446655440000';
const user = { id: 'user-id' } as User;

const languages = [
{ id: '1', language: 'English', code: 'en', description: 'English' },
{ id: '2', language: 'Spanish', code: 'es', description: 'Español' },
];
mockLanguagesService.getLanguagesById.mockResolvedValue({
status: 'OK',
status_code: HttpStatus.OK,
message: 'Languages fetched successfully',
data: languages,
});

const req = { user };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };

await controller.getLanguagesByUserId(userId, req, res as any);

expect(res.status).toHaveBeenCalledWith(HttpStatus.OK);
expect(res.json).toHaveBeenCalledWith({
status: 'OK',
status_code: HttpStatus.OK,
message: 'Languages fetched successfully',
data: languages,
});
});

it('should handle invalid user ID', async () => {
const userId = 'invalid-id';
const user = { id: 'user-id' } as User;

mockLanguagesService.getLanguagesById.mockRejectedValue(new BadRequestException('Invalid user Id'));

const req = { user };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };

await expect(controller.getLanguagesByUserId(userId, req, res as any)).rejects.toThrow(BadRequestException);
});

it('should handle unauthorized access', async () => {
const userId = '550e8400-e29b-41d4-a716-446655440000';
const user = { id: 'different-user-id' } as User;

mockLanguagesService.getLanguagesById.mockRejectedValue(
new ForbiddenException({
status_code: HttpStatus.FORBIDDEN,
message: 'You are not authorized to access this resource',
})
);

const req = { user };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };

await expect(controller.getLanguagesByUserId(userId, req, res as any)).rejects.toThrow(ForbiddenException);
});

it('should handle no languages found for the user', async () => {
const userId = '550e8400-e29b-41d4-a716-446655440000';
const user = { id: userId } as User;

mockLanguagesService.getLanguagesById.mockRejectedValue(
new NotFoundException({
status_code: HttpStatus.NOT_FOUND,
message: 'Languages associated with this user not found',
})
);

const req = { user };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };

await expect(controller.getLanguagesByUserId(userId, req, res as any)).rejects.toThrow(NotFoundException);
});

it('should handle no languages found for the user', async () => {
const userId = '550e8400-e29b-41d4-a716-446655440000';
const user = { id: userId } as User;

mockLanguagesService.getLanguagesById.mockRejectedValue(
new NotFoundException({
status_code: HttpStatus.NOT_FOUND,
message: 'Languages associated with this user not found',
})
);

const req = { user };
const res = { status: jest.fn().mockReturnThis(), json: jest.fn() };

await expect(controller.getLanguagesByUserId(userId, req, res as any)).rejects.toThrow(NotFoundException);
});
});
});
Loading

0 comments on commit 5b93595

Please sign in to comment.