Skip to content

Commit

Permalink
Merge branch 'main' into lynn-chat
Browse files Browse the repository at this point in the history
  • Loading branch information
shishirbychapur authored Nov 3, 2024
2 parents 69fbce5 + ce210c3 commit e9799ae
Show file tree
Hide file tree
Showing 19 changed files with 817 additions and 6 deletions.
2 changes: 2 additions & 0 deletions backend/collaboration-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/language-data": "^6.5.1",
"axios": "^1.7.7",
"bcrypt": "^5.1.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
Expand All @@ -37,6 +38,7 @@
"devDependencies": {
"@repo/eslint-config": "*",
"@repo/request-types": "*",
"@repo/submission-types": "*",
"@repo/typescript-config": "*",
"@testcontainers/mongodb": "^10.13.1",
"@types/bcrypt": "^5.0.2",
Expand Down
32 changes: 32 additions & 0 deletions backend/collaboration-service/src/controllers/collab.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { ValidationError } from 'class-validator'
import { Request, Response } from 'express'
import { ITypedBodyRequest } from '@repo/request-types'
import { SubmissionRequestDto, SubmissionResponseDto } from '@repo/submission-types'
import { CollabDto } from '../types/CollabDto'
import { createSession, getSessionById } from '../models/collab.repository'
import judgeZero from '../services/judgezero.service'
import config from '../common/config.util'

export async function createSessionRequest(request: ITypedBodyRequest<CollabDto>, response: Response): Promise<void> {
const collabDto = CollabDto.fromRequest(request)
Expand Down Expand Up @@ -62,3 +65,32 @@ export async function getChatHistory(request: Request, response: Response): Prom
// Send retrieved data
response.status(200).json(session.chatHistory).send()
}

export async function submitCode(request: ITypedBodyRequest<SubmissionRequestDto>, response: Response): Promise<void> {
const submissionRequestDto = SubmissionRequestDto.fromRequest(request)
const requestErrors = await submissionRequestDto.validate()

if (requestErrors.length) {
const errorMessages = requestErrors.flatMap((error: ValidationError) => Object.values(error.constraints))
response.status(400).json(errorMessages).send()
return
}

const res = await judgeZero.post(config.JUDGE_ZERO_SUBMIT_CONFIG, submissionRequestDto)

if (!res) {
response.status(400).json('Failed to submit code. Please try again.').send()
return
}

const submissionResponseDto = SubmissionResponseDto.fromResponse(res)
const responseErrors = await submissionResponseDto.validate()

if (responseErrors.length) {
const errorMessages = requestErrors.flatMap((error: ValidationError) => Object.values(error.constraints))
response.status(400).json(errorMessages).send()
return
}

response.status(200).json(submissionResponseDto).send()
}
3 changes: 2 additions & 1 deletion backend/collaboration-service/src/routes/collab.routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from 'express'
import passport from 'passport'
import { createSessionRequest, getChatHistory, getSession } from '../controllers/collab.controller'
import { createSessionRequest, getChatHistory, getSession, submitCode } from '../controllers/collab.controller'

const router = Router()

Expand All @@ -10,5 +10,6 @@ router.use(passport.authenticate('jwt', { session: false }))
router.put('/', createSessionRequest)
router.get('/:id', getSession)
router.get('/chat/:id', getChatHistory)
router.post('/submit', submitCode)

export default router
41 changes: 41 additions & 0 deletions backend/collaboration-service/src/services/judgezero.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios'
import config from '../common/config.util'
import { SubmissionRequestDto } from '@repo/submission-types'
import logger from '../common/logger.util'

class JudgeZero {
private axiosInstance: AxiosInstance

constructor(baseURL: string) {
this.axiosInstance = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
},
})

// Request Interceptor
this.axiosInstance.interceptors.request.use((error) => {
logger.error(`[Judge-Zero] Failed to send Judge Zero API request: ${error}`)
return Promise.reject(error)
})

// Response Interceptor
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse) => response,
(error) => {
logger.error(`[Judge-Zero] Error receving Judge Zero API response: ${error}`)
return Promise.reject(error)
}
)
}

public async post(url: string, data?: SubmissionRequestDto): Promise<AxiosResponse> {
const response = await this.axiosInstance.post(url, data)
return response
}
}

const judgeZero = new JudgeZero(config.JUDGE_ZERO_URL)

export default judgeZero
17 changes: 15 additions & 2 deletions backend/collaboration-service/src/types/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,23 @@ export class Config {
@IsUrl({ require_tld: false })
MATCHING_SERVICE_URL: string

@IsUrl({ require_tld: false })
JUDGE_ZERO_URL: string

@IsString()
@IsNotEmpty()
JUDGE_ZERO_SUBMIT_CONFIG: string

constructor(
NODE_ENV: string,
PORT: string,
DB_URL: string,
ACCESS_TOKEN_PUBLIC_KEY: string,
USER_SERVICE_URL: string,
QUESTION_SERVICE_URL: string,
MATCHING_SERVICE_URL: string
MATCHING_SERVICE_URL: string,
JUDGE_ZERO_URL: string,
JUDGE_ZERO_SUBMIT_CONFIG: string
) {
this.NODE_ENV = NODE_ENV ?? 'development'
this.PORT = PORT ?? '3006'
Expand All @@ -40,6 +49,8 @@ export class Config {
this.USER_SERVICE_URL = USER_SERVICE_URL
this.QUESTION_SERVICE_URL = QUESTION_SERVICE_URL
this.MATCHING_SERVICE_URL = MATCHING_SERVICE_URL
this.JUDGE_ZERO_URL = JUDGE_ZERO_URL
this.JUDGE_ZERO_SUBMIT_CONFIG = JUDGE_ZERO_SUBMIT_CONFIG
}

static fromEnv(env: { [key: string]: string | undefined }): Config {
Expand All @@ -50,7 +61,9 @@ export class Config {
env.ACCESS_TOKEN_PUBLIC_KEY!,
env.USER_SERVICE_URL!,
env.QUESTION_SERVICE_URL!,
env.MATCHING_SERVICE_URL!
env.MATCHING_SERVICE_URL!,
env.JUDGE_ZERO_URL!,
env.JUDGE_ZERO_SUBMIT_CONFIG!
)
}

Expand Down
47 changes: 46 additions & 1 deletion backend/matching-service/src/controllers/matching.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ITypedBodyRequest } from '@repo/request-types'
import { IPaginationRequest, ITypedBodyRequest } from '@repo/request-types'
import { WebSocketMessageType } from '@repo/ws-types'
import { randomUUID } from 'crypto'
import { Response } from 'express'
Expand All @@ -8,8 +8,12 @@ import { IMatch } from '@repo/user-types'
import { MatchDto } from '../types/MatchDto'
import {
createMatch,
findMatchCount,
findPaginatedMatches,
findPaginatedMatchesWithSort,
getMatchByUserIdandMatchId,
isUserInMatch,
isValidSort,
updateMatchCompletion,
} from '../models/matching.repository'
import { getRandomQuestion } from '../services/matching.service'
Expand Down Expand Up @@ -101,3 +105,44 @@ export async function updateCompletion(
}
response.status(200).send('MATCH_COMPLETED')
}

export async function handleGetPaginatedSessions(request: IPaginationRequest, response: Response): Promise<void> {
const page = parseInt(request.query.page)
const limit = parseInt(request.query.limit)

if (isNaN(page) || isNaN(limit) || page <= 0 || limit <= 0) {
response.status(400).json('INVALID_PAGINATION').send()
return
}
const start = (page - 1) * limit
const sortBy = request.query.sortBy?.split(',').map((sort) => sort.split(':')) ?? []
const isSortsValid = sortBy.every(
(sort: string[]) => sort.at(0) && sort.at(1) && isValidSort(sort.at(0)!, sort.at(1)!)
)

if (!isSortsValid) {
response.status(400).json('INVALID_SORT').send()
return
}
const count = await findMatchCount()
let matches: IMatch[]

if (sortBy.length) {
matches = await findPaginatedMatchesWithSort(start, limit, sortBy)
} else {
matches = await findPaginatedMatches(start, limit)
}

const nextPage = start + limit < count ? page + 1 : null

response.status(200).json({
pagination: {
currentPage: page,
nextPage,
totalPages: Math.ceil(count / limit),
totalItems: count,
limit,
},
sessions: matches.map(MatchDto.fromModel),
})
}
31 changes: 30 additions & 1 deletion backend/matching-service/src/models/matching.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Model, model } from 'mongoose'
import { Model, model, SortOrder } from 'mongoose'
import matchSchema from './matching.model'
import { IMatch } from '@repo/user-types'
import { MatchDto } from '../types/MatchDto'
Expand Down Expand Up @@ -28,3 +28,32 @@ export async function updateMatchCompletion(matchId: string): Promise<boolean> {
const match = await matchModel.updateOne({ _id: matchId }, { isCompleted: true })
return !!match
}

export async function findPaginatedMatches(start: number, limit: number): Promise<IMatch[]> {
return matchModel.find().limit(limit).skip(start)
}

export async function findPaginatedMatchesWithSort(
start: number,
limit: number,
sortBy: string[][]
): Promise<IMatch[]> {
return matchModel
.find()
.sort(sortBy.map(([key, order]): [string, SortOrder] => [key, order as SortOrder]))
.limit(limit)
.skip(start)
}

export function getSortKeysAndOrders(): { keys: string[]; orders: string[] } {
return { keys: ['complexity'], orders: ['asc', 'desc'] }
}

export function isValidSort(key: string, order: string): boolean {
const { keys, orders } = getSortKeysAndOrders()
return orders.includes(order) && keys.includes(key)
}

export async function findMatchCount(): Promise<number> {
return matchModel.countDocuments()
}
8 changes: 7 additions & 1 deletion backend/matching-service/src/routes/matching.routes.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { Router } from 'express'
import passport from 'passport'
import { generateWS, getMatchDetails, updateCompletion } from '../controllers/matching.controller'
import {
generateWS,
getMatchDetails,
handleGetPaginatedSessions,
updateCompletion,
} from '../controllers/matching.controller'

const router = Router()

router.put('/', updateCompletion)
router.use(passport.authenticate('jwt', { session: false }))
router.post('/', generateWS)
router.get('/:id', getMatchDetails)
router.get('/', handleGetPaginatedSessions)

export default router
24 changes: 24 additions & 0 deletions backend/matching-service/src/types/MatchDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ export class MatchDto {
)
}

static fromModel({
user1Id,
user1Name,
user2Id,
user2Name,
complexity,
category,
question,
isCompleted,
createdAt,
}: IMatch): MatchDto {
return new MatchDto(
user1Id,
user1Name,
user2Id,
user2Name,
complexity,
category,
question,
isCompleted,
createdAt
)
}

async validate(): Promise<ValidationError[]> {
return validate(this)
}
Expand Down
Loading

0 comments on commit e9799ae

Please sign in to comment.