From bc523dc06702ef6f450eab975757eaeabc5d7b6c Mon Sep 17 00:00:00 2001 From: phertyameen Date: Sat, 26 Jul 2025 18:05:46 +0100 Subject: [PATCH 1/2] feat(users): add AchievementsController and GET /users/:id/achievements endpoint --- src/achievement/achievement.controller.ts | 55 ++++++++++------ src/achievement/achievement.module.ts | 3 +- .../entities/achievement.entity.ts | 14 ++++- .../providers/achievement.service.ts | 10 ++- .../providers/find-by-user-id-provider.ts | 21 +++++++ .../find-by-user-idn-provider.spec.ts | 63 +++++++++++++++++++ 6 files changed, 143 insertions(+), 23 deletions(-) create mode 100644 src/achievement/providers/find-by-user-id-provider.ts create mode 100644 src/achievement/providers/find-by-user-idn-provider.spec.ts diff --git a/src/achievement/achievement.controller.ts b/src/achievement/achievement.controller.ts index 094f8ff..fd88be4 100644 --- a/src/achievement/achievement.controller.ts +++ b/src/achievement/achievement.controller.ts @@ -2,26 +2,45 @@ import { Controller, Get, Param } from '@nestjs/common'; import { Repository } from 'typeorm'; import { UserAchievement } from './entities/user-achievement.entity'; import { InjectRepository } from '@nestjs/typeorm'; +import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger'; +import { Achievement } from './entities/achievement.entity'; +import { AchievementService } from './providers/achievement.service'; @Controller('achievements') export class AchievementController { - constructor( - @InjectRepository(UserAchievement) - private readonly userAchievementRepo: Repository - ) {} -@Get('/users/:id/achievements') -public async getUserAchievements(@Param('id') userId: string) { - const unlocked = await this.userAchievementRepo.find({ - where: { user: { id: userId } }, - relations: ['achievement'], - }); + constructor( + @InjectRepository(UserAchievement) + private readonly userAchievementRepo: Repository, - return unlocked.map((ua) => ({ - id: ua.achievement.id, - title: ua.achievement.title, - description: ua.achievement.description, - iconUrl: ua.achievement.iconUrl, - unlockedAt: ua.unlockedAt, - })); -} + private readonly achievementsService: AchievementService + ) {} + @Get('/users/:id/achievements') + public async getUserAchievements(@Param('id') userId: string) { + const unlocked = await this.userAchievementRepo.find({ + where: { user: { id: userId } }, + relations: ['achievement'], + }); + + return unlocked.map((ua) => ({ + id: ua.achievement.id, + title: ua.achievement.title, + description: ua.achievement.description, + iconUrl: ua.achievement.iconUrl, + unlockedAt: ua.unlockedAt, + })); + } + + @Get() + @ApiOperation({ summary: 'Get unlocked achievements for a user' }) + @ApiParam({ name: 'id', description: 'User ID' }) + @ApiResponse({ + status: 200, + description: 'List of achievements', + type: [Achievement], + }) + async getAchievements( + @Param('id') userId: string, + ): Promise[]> { + return this.achievementsService.findByID(userId); + } } diff --git a/src/achievement/achievement.module.ts b/src/achievement/achievement.module.ts index 34ed361..e6f8f99 100644 --- a/src/achievement/achievement.module.ts +++ b/src/achievement/achievement.module.ts @@ -8,11 +8,12 @@ import { Achievement } from './entities/achievement.entity'; import { LeaderboardEntry } from 'src/leaderboard/entities/leaderboard.entity'; import { Badge } from 'src/badge/entities/badge.entity'; import { User } from 'src/users/user.entity'; +import { FindByUserIdProvider } from './providers/find-by-user-id-provider'; @Module({ imports: [TypeOrmModule.forFeature([Achievement, UserAchievement, LeaderboardEntry, Badge, User])], controllers: [AchievementController], - providers: [AchievementService, AchievementUnlockerProvider], + providers: [AchievementService, AchievementUnlockerProvider, FindByUserIdProvider], exports: [AchievementService] }) export class AchievementModule {} diff --git a/src/achievement/entities/achievement.entity.ts b/src/achievement/entities/achievement.entity.ts index 62d845d..fedfbc2 100644 --- a/src/achievement/entities/achievement.entity.ts +++ b/src/achievement/entities/achievement.entity.ts @@ -1,4 +1,11 @@ -import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn } from "typeorm"; +import { User } from 'src/users/user.entity'; +import { + Column, + CreateDateColumn, + Entity, + ManyToOne, + PrimaryGeneratedColumn, +} from 'typeorm'; @Entity('achievements') export class Achievement { @@ -8,6 +15,9 @@ export class Achievement { @Column() title: string; + @ManyToOne(() => User, { eager: true }) + user: User; + @Column() description: string; @@ -16,4 +26,4 @@ export class Achievement { @CreateDateColumn() createdAt: Date; -} \ No newline at end of file +} diff --git a/src/achievement/providers/achievement.service.ts b/src/achievement/providers/achievement.service.ts index c0b3aa3..32eeb1b 100644 --- a/src/achievement/providers/achievement.service.ts +++ b/src/achievement/providers/achievement.service.ts @@ -1,14 +1,20 @@ import { Injectable } from '@nestjs/common'; import { AchievementUnlockerProvider } from './achievement-unlocker.service'; import { User } from 'src/users/user.entity'; +import { FindByUserIdProvider } from './find-by-user-id-provider'; @Injectable() export class AchievementService { constructor( - private readonly achievementUnlockerProvider: AchievementUnlockerProvider + private readonly achievementUnlockerProvider: AchievementUnlockerProvider, + private readonly fndByUserIdProvider: FindByUserIdProvider, ) {} public async achievementUnlocker(user: User) { - return this,this.achievementUnlockerProvider.unlockAchievementsForUser(user) + return this.achievementUnlockerProvider.unlockAchievementsForUser(user) + } + + public async findByID(userId: string) { + return this.fndByUserIdProvider.findByUserId(userId) } } diff --git a/src/achievement/providers/find-by-user-id-provider.ts b/src/achievement/providers/find-by-user-id-provider.ts new file mode 100644 index 0000000..502d47e --- /dev/null +++ b/src/achievement/providers/find-by-user-id-provider.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Achievement } from '../entities/achievement.entity'; +import { Repository } from 'typeorm'; + +@Injectable() +export class FindByUserIdProvider { + constructor( + @InjectRepository(Achievement) + private readonly achievementRepo: Repository, + ) {} + + async findByUserId(userId: string) { + return this.achievementRepo.find({ + where: { user: { id: userId } }, + relations: ['user'], + select: ['id', 'title', 'iconUrl', 'createdAt'], + order: { createdAt: 'DESC' }, + }); + } +} diff --git a/src/achievement/providers/find-by-user-idn-provider.spec.ts b/src/achievement/providers/find-by-user-idn-provider.spec.ts new file mode 100644 index 0000000..cbf3848 --- /dev/null +++ b/src/achievement/providers/find-by-user-idn-provider.spec.ts @@ -0,0 +1,63 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Achievement } from '../entities/achievement.entity'; +import { Repository } from 'typeorm'; +import { AchievementService } from './achievement.service'; + +const mockAchievements = [ + { + id: '1', + title: 'First Achievement', + iconUrl: 'http://example.com/icon1.svg', + unlockedAt: new Date('2024-01-01T00:00:00.000Z'), + user: { id: 'user123' }, + }, + { + id: '2', + title: 'Second Achievement', + iconUrl: 'http://example.com/icon2.svg', + unlockedAt: new Date('2024-02-01T00:00:00.000Z'), + user: { id: 'user123' }, + }, +]; + +describe('AchievementsService', () => { + let service: AchievementService; + let repo: Repository; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AchievementService, + { + provide: getRepositoryToken(Achievement), + useValue: { + find: jest.fn().mockResolvedValue(mockAchievements), + }, + }, + ], + }).compile(); + + service = module.get(AchievementService); + repo = module.get>(getRepositoryToken(Achievement)); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findByUserId', () => { + it('should return achievements for a user', async () => { + const result = await service.findByID('user123'); + + expect(repo.find).toHaveBeenCalledWith({ + where: { user: { id: 'user123' } }, + relations: ['user'], + select: ['id', 'title', 'iconUrl', 'unlockedAt'], + order: { unlockedAt: 'DESC' }, + }); + + expect(result).toEqual(mockAchievements); + }); + }); +}); \ No newline at end of file From 7d4eaca2059938aaf4d7b278190f09dd37706953 Mon Sep 17 00:00:00 2001 From: phertyameen Date: Sat, 26 Jul 2025 18:15:28 +0100 Subject: [PATCH 2/2] feat(users): add AchievementsController and GET /users/:id/achievements endpoint --- .../find-by-user-idn-provider.spec.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/achievement/providers/find-by-user-idn-provider.spec.ts b/src/achievement/providers/find-by-user-idn-provider.spec.ts index cbf3848..342cbca 100644 --- a/src/achievement/providers/find-by-user-idn-provider.spec.ts +++ b/src/achievement/providers/find-by-user-idn-provider.spec.ts @@ -2,33 +2,33 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { Achievement } from '../entities/achievement.entity'; import { Repository } from 'typeorm'; -import { AchievementService } from './achievement.service'; +import { FindByUserIdProvider } from './find-by-user-id-provider'; const mockAchievements = [ { id: '1', title: 'First Achievement', iconUrl: 'http://example.com/icon1.svg', - unlockedAt: new Date('2024-01-01T00:00:00.000Z'), + createdAt: new Date('2024-01-01T00:00:00.000Z'), user: { id: 'user123' }, }, { id: '2', title: 'Second Achievement', iconUrl: 'http://example.com/icon2.svg', - unlockedAt: new Date('2024-02-01T00:00:00.000Z'), + createdAt: new Date('2024-02-01T00:00:00.000Z'), user: { id: 'user123' }, }, ]; -describe('AchievementsService', () => { - let service: AchievementService; +describe('FindByUserIdProvider', () => { + let provider: FindByUserIdProvider; let repo: Repository; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - AchievementService, + FindByUserIdProvider, { provide: getRepositoryToken(Achievement), useValue: { @@ -38,23 +38,23 @@ describe('AchievementsService', () => { ], }).compile(); - service = module.get(AchievementService); + provider = module.get(FindByUserIdProvider); repo = module.get>(getRepositoryToken(Achievement)); }); it('should be defined', () => { - expect(service).toBeDefined(); + expect(provider).toBeDefined(); }); describe('findByUserId', () => { - it('should return achievements for a user', async () => { - const result = await service.findByID('user123'); + it('should return a list of achievements for the given user ID', async () => { + const result = await provider.findByUserId('user123'); expect(repo.find).toHaveBeenCalledWith({ where: { user: { id: 'user123' } }, relations: ['user'], - select: ['id', 'title', 'iconUrl', 'unlockedAt'], - order: { unlockedAt: 'DESC' }, + select: ['id', 'title', 'iconUrl', 'createdAt'], + order: { createdAt: 'DESC' }, }); expect(result).toEqual(mockAchievements);