Skip to content

Commit

Permalink
Merge pull request #256 from nottherealalanturing/leadership
Browse files Browse the repository at this point in the history
added contribution leadership endpoints
  • Loading branch information
Ibinola authored Feb 28, 2025
2 parents f12c01c + 4f6943e commit 37d2b83
Show file tree
Hide file tree
Showing 30 changed files with 1,720 additions and 0 deletions.
65 changes: 65 additions & 0 deletions backend/src/leadership/contribution-leaderboard.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Module } from "@nestjs/common"
import { TypeOrmModule } from "@nestjs/typeorm"
import { ContributionLeaderboardController } from "./controllers/contribution-leaderboard.controller"
import { ContributionLeaderboardService } from "./services/contribution-leaderboard.service"
import { ContributionService } from "./services/contribution.service"
import { UserContributionService } from "./services/user-contribution.service"
import { ContributionEntity } from "./entities/contribution.entity"
import { UserContributionEntity } from "./entities/user-contribution.entity"
import { ContributionTypeEntity } from "./entities/contribution-type.entity"
import { ContributionRepository } from "./repositories/contribution.repository"
import { UserContributionRepository } from "./repositories/user-contribution.repository"
import { ContributionTypeRepository } from "./repositories/contribution-type.repository"
import { CacheModule } from "@nestjs/cache-manager"
import { ScheduleModule } from "@nestjs/schedule"
import { ContributionCacheService } from "./services/contribution-cache.service"
import { ContributionSchedulerService } from "./services/contribution-scheduler.service"
import { ContributionEventEmitter } from "./events/contribution-event.emitter"
import { ContributionEventListener } from "./events/contribution-event.listener"
import { ContributionMetricsService } from "./services/contribution-metrics.service"
import { ContributionLoggerService } from "./services/contribution-logger.service"
import { ContributionValidationService } from "./services/contribution-validation.service"
import { ContributionAchievementService } from "./services/contribution-achievement.service"
import { AchievementEntity } from "./entities/achievement.entity"
import { UserAchievementEntity } from "./entities/user-achievement.entity"
import { AchievementRepository } from "./repositories/achievement.repository"
import { UserAchievementRepository } from "./repositories/user-achievement.repository"

@Module({
imports: [
TypeOrmModule.forFeature([
ContributionEntity,
UserContributionEntity,
ContributionTypeEntity,
AchievementEntity,
UserAchievementEntity,
]),
CacheModule.register({
ttl: 60 * 60 * 1000, // 1 hour cache
max: 100, // maximum number of items in cache
}),
ScheduleModule.forRoot(),
],
controllers: [ContributionLeaderboardController],
providers: [
ContributionLeaderboardService,
ContributionService,
UserContributionService,
ContributionRepository,
UserContributionRepository,
ContributionTypeRepository,
ContributionCacheService,
ContributionSchedulerService,
ContributionEventEmitter,
ContributionEventListener,
ContributionMetricsService,
ContributionLoggerService,
ContributionValidationService,
ContributionAchievementService,
AchievementRepository,
UserAchievementRepository,
],
exports: [ContributionLeaderboardService, ContributionService, UserContributionService],
})
export class ContributionLeaderboardModule {}

Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {
Controller,
Get,
Post,
Body,
Query,
Param,
UseInterceptors,
CacheInterceptor,
HttpStatus,
HttpCode,
ValidationPipe,
ParseIntPipe,
DefaultValuePipe,
Logger,
} from "@nestjs/common"
import { ApiTags, ApiOperation, ApiResponse, ApiQuery, ApiParam, ApiBody } from "@nestjs/swagger"
import type { ContributionLeaderboardService } from "../services/contribution-leaderboard.service"
import type { GetLeaderboardDto } from "../dto/get-leaderboard.dto"
import { CreateContributionDto } from "../dto/create-contribution.dto"
import { LeaderboardResponseDto } from "../dto/leaderboard-response.dto"
import { TimeRangeEnum } from "../enums/time-range.enum"
import { ContributionTypeEnum } from "../enums/contribution-type.enum"
import type { ContributionEventEmitter } from "../events/contribution-event.emitter"
import type { ContributionLoggerService } from "../services/contribution-logger.service"

@ApiTags("Contribution Leaderboard")
@Controller("contribution-leaderboard")
export class ContributionLeaderboardController {
private readonly logger = new Logger(ContributionLeaderboardController.name)

constructor(
private readonly leaderboardService: ContributionLeaderboardService,
private readonly eventEmitter: ContributionEventEmitter,
private readonly loggerService: ContributionLoggerService,
) {}

@Get()
@UseInterceptors(CacheInterceptor)
@ApiOperation({ summary: "Get contribution leaderboard" })
@ApiQuery({ name: "timeRange", enum: TimeRangeEnum, required: false })
@ApiQuery({ name: "contributionType", enum: ContributionTypeEnum, required: false })
@ApiQuery({ name: "page", type: Number, required: false })
@ApiQuery({ name: "limit", type: Number, required: false })
@ApiResponse({ status: 200, description: "Return leaderboard data", type: LeaderboardResponseDto })
async getLeaderboard(
@Query('timeRange', new DefaultValuePipe(TimeRangeEnum.ALL_TIME)) timeRange: TimeRangeEnum,
@Query('contributionType') contributionType: ContributionTypeEnum,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
): Promise<LeaderboardResponseDto> {
this.logger.log(
`Getting leaderboard for timeRange: ${timeRange}, contributionType: ${contributionType}, page: ${page}, limit: ${limit}`,
)

const dto: GetLeaderboardDto = {
timeRange,
contributionType,
pagination: { page, limit },
}

this.loggerService.logLeaderboardRequest(dto)

return this.leaderboardService.getLeaderboard(dto)
}

@Post('contributions')
@HttpCode(HttpStatus.CREATED)
@ApiOperation({ summary: 'Record a new contribution' })
@ApiBody({ type: CreateContributionDto })
@ApiResponse({ status: 201, description: 'Contribution recorded successfully' })
async recordContribution(
@Body(new ValidationPipe({ transform: true })) createContributionDto: CreateContributionDto,
): Promise<void> {
this.logger.log(`Recording contribution for user: ${createContributionDto.userId}, type: ${createContributionDto.type}`);

await this.leaderboardService.recordContribution(createContributionDto);

// Emit event for other parts of the system
this.eventEmitter.emitContributionCreated(createContributionDto);

this.loggerService.logContributionCreated(createContributionDto);
}

@Get("users/:userId/contributions")
@ApiOperation({ summary: "Get contributions for a specific user" })
@ApiParam({ name: "userId", type: String })
@ApiQuery({ name: "timeRange", enum: TimeRangeEnum, required: false })
@ApiQuery({ name: "page", type: Number, required: false })
@ApiQuery({ name: "limit", type: Number, required: false })
async getUserContributions(
@Param('userId') userId: string,
@Query('timeRange', new DefaultValuePipe(TimeRangeEnum.ALL_TIME)) timeRange: TimeRangeEnum,
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
this.logger.log(
`Getting contributions for user: ${userId}, timeRange: ${timeRange}, page: ${page}, limit: ${limit}`,
)

return this.leaderboardService.getUserContributions(userId, timeRange, { page, limit })
}

@Get("contribution-types")
@ApiOperation({ summary: "Get all contribution types" })
async getContributionTypes() {
return this.leaderboardService.getContributionTypes()
}

@Get('statistics')
@ApiOperation({ summary: 'Get contribution statistics' })
@ApiQuery({ name: 'timeRange', enum: TimeRangeEnum, required: false })
async getStatistics(
@Query('timeRange', new DefaultValuePipe(TimeRangeEnum.ALL_TIME)) timeRange: TimeRangeEnum,
) {
return this.leaderboardService.getStatistics(timeRange);
}
}

21 changes: 21 additions & 0 deletions backend/src/leadership/dto/create-contribution.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IsNotEmpty, IsString, IsOptional, IsNumber, IsObject, Min } from "class-validator"

export class CreateContributionDto {
@IsNotEmpty()
@IsString()
userId: string

@IsNotEmpty()
@IsString()
type: string

@IsOptional()
@IsNumber()
@Min(0)
points?: number

@IsOptional()
@IsObject()
metadata?: Record<string, any>
}

20 changes: 20 additions & 0 deletions backend/src/leadership/dto/get-leaderboard.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { IsEnum, IsOptional, ValidateNested } from "class-validator"
import { Type } from "class-transformer"
import { TimeRangeEnum } from "../enums/time-range.enum"
import { ContributionTypeEnum } from "../enums/contribution-type.enum"
import { PaginationDto } from "./pagination.dto"

export class GetLeaderboardDto {
@IsEnum(TimeRangeEnum)
@IsOptional()
timeRange: TimeRangeEnum = TimeRangeEnum.ALL_TIME

@IsEnum(ContributionTypeEnum)
@IsOptional()
contributionType?: ContributionTypeEnum

@ValidateNested()
@Type(() => PaginationDto)
pagination: PaginationDto
}

20 changes: 20 additions & 0 deletions backend/src/leadership/dto/leaderboard-entry.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export class LeaderboardEntryDto {
rank: number
userId: string
username: string
avatarUrl?: string
totalPoints: number
contributionCounts: {
submissions: number
edits: number
approvals: number
total: number
}
achievements?: Array<{
id: string
name: string
icon: string
}>
lastContributionDate: Date
}

16 changes: 16 additions & 0 deletions backend/src/leadership/dto/leaderboard-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { TimeRangeEnum } from "../enums/time-range.enum"
import type { ContributionTypeEnum } from "../enums/contribution-type.enum"
import type { LeaderboardEntryDto } from "./leaderboard-entry.dto"

export class LeaderboardResponseDto {
timeRange: TimeRangeEnum
contributionType?: ContributionTypeEnum
pagination: {
page: number
limit: number
totalItems: number
totalPages: number
}
entries: LeaderboardEntryDto[]
}

18 changes: 18 additions & 0 deletions backend/src/leadership/dto/pagination.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { IsNumber, IsOptional, Min, Max } from "class-validator"
import { Type } from "class-transformer"

export class PaginationDto {
@IsNumber()
@IsOptional()
@Min(1)
@Type(() => Number)
page = 1

@IsNumber()
@IsOptional()
@Min(1)
@Max(100)
@Type(() => Number)
limit = 10
}

29 changes: 29 additions & 0 deletions backend/src/leadership/entities/achievement.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity, Column, PrimaryColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"

@Entity("contribution_leaderboard_achievements")
export class AchievementEntity {
@PrimaryColumn()
id: string

@Column()
name: string

@Column()
description: string

@Column({ nullable: true })
icon: string

@Column({ type: "int" })
threshold: number

@Column()
type: string

@CreateDateColumn({ name: "created_at" })
createdAt: Date

@UpdateDateColumn({ name: "updated_at" })
updatedAt: Date
}

26 changes: 26 additions & 0 deletions backend/src/leadership/entities/contribution-type.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"

@Entity("contribution_leaderboard_contribution_types")
export class ContributionTypeEntity {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ unique: true })
name: string

@Column({ type: "int", default: 1 })
defaultPoints: number

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

@Column({ nullable: true })
icon: string

@CreateDateColumn({ name: "created_at" })
createdAt: Date

@UpdateDateColumn({ name: "updated_at" })
updatedAt: Date
}

29 changes: 29 additions & 0 deletions backend/src/leadership/entities/contribution.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn, UpdateDateColumn } from "typeorm"

@Entity("contribution_leaderboard_contributions")
export class ContributionEntity {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ name: "user_id" })
@Index()
userId: string

@Column({ name: "contribution_type_id" })
@Index()
contributionTypeId: string

@Column({ type: "int", default: 1 })
points: number

@Column({ type: "jsonb", default: {} })
metadata: Record<string, any>

@CreateDateColumn({ name: "created_at" })
@Index()
createdAt: Date

@UpdateDateColumn({ name: "updated_at" })
updatedAt: Date
}

22 changes: 22 additions & 0 deletions backend/src/leadership/entities/user-achievement.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Entity, Column, PrimaryGeneratedColumn, Index, CreateDateColumn } from "typeorm"

@Entity("contribution_leaderboard_user_achievements")
export class UserAchievementEntity {
@PrimaryGeneratedColumn("uuid")
id: string

@Column({ name: "user_id" })
@Index()
userId: string

@Column({ name: "achievement_id" })
@Index()
achievementId: string

@Column({ name: "awarded_at", type: "timestamp" })
awardedAt: Date

@CreateDateColumn({ name: "created_at" })
createdAt: Date
}

Loading

0 comments on commit 37d2b83

Please sign in to comment.