diff --git a/backend/collaboration-service/package.json b/backend/collaboration-service/package.json index 5be024595b..91e1fdfeae 100644 --- a/backend/collaboration-service/package.json +++ b/backend/collaboration-service/package.json @@ -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", @@ -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", diff --git a/backend/collaboration-service/src/controllers/collab.controller.ts b/backend/collaboration-service/src/controllers/collab.controller.ts index 2a1db1d330..e8dcc0745f 100644 --- a/backend/collaboration-service/src/controllers/collab.controller.ts +++ b/backend/collaboration-service/src/controllers/collab.controller.ts @@ -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, response: Response): Promise { const collabDto = CollabDto.fromRequest(request) @@ -47,3 +50,32 @@ export async function getSession(request: Request, response: Response): Promise< // Send retrieved data response.status(200).json(session).send() } + +export async function submitCode(request: ITypedBodyRequest, response: Response): Promise { + 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() +} diff --git a/backend/collaboration-service/src/routes/collab.routes.ts b/backend/collaboration-service/src/routes/collab.routes.ts index fd055761e1..da4b9f2c18 100644 --- a/backend/collaboration-service/src/routes/collab.routes.ts +++ b/backend/collaboration-service/src/routes/collab.routes.ts @@ -1,6 +1,6 @@ import { Router } from 'express' import passport from 'passport' -import { createSessionRequest, getSession } from '../controllers/collab.controller' +import { createSessionRequest, getSession, submitCode } from '../controllers/collab.controller' const router = Router() @@ -9,5 +9,6 @@ router.use(passport.authenticate('jwt', { session: false })) // To change this route to enable retrival of sessions with pagination router.put('/', createSessionRequest) router.get('/:id', getSession) +router.post('/submit', submitCode) export default router diff --git a/backend/collaboration-service/src/services/judgezero.service.ts b/backend/collaboration-service/src/services/judgezero.service.ts new file mode 100644 index 0000000000..ec58e58535 --- /dev/null +++ b/backend/collaboration-service/src/services/judgezero.service.ts @@ -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 { + const response = await this.axiosInstance.post(url, data) + return response + } +} + +const judgeZero = new JudgeZero(config.JUDGE_ZERO_URL) + +export default judgeZero diff --git a/backend/collaboration-service/src/types/Config.ts b/backend/collaboration-service/src/types/Config.ts index 0f304c17c4..a0ef650e53 100644 --- a/backend/collaboration-service/src/types/Config.ts +++ b/backend/collaboration-service/src/types/Config.ts @@ -24,6 +24,13 @@ 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, @@ -31,7 +38,9 @@ export class Config { 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' @@ -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 { @@ -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! ) } diff --git a/docker-compose.local.yml b/docker-compose.local.yml index d4fc985d82..4c916bca54 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -21,6 +21,99 @@ services: - question-service - matching-service command: sh -c "envsubst < /etc/nginx/templates/nginx.conf.template > /etc/nginx/nginx.conf && nginx -g 'daemon off;'" + + # collaboration-service: + # container_name: collaboration-service + # port: + # - 3008:3008 + # build: + # context: . + # dockerfile: ./backend/collaboration-service/Dockerfile + # environment: + # - NODE_ENV=development + # - PORT=3006 + # - DB_URL=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@collab-db:27017/collaboration-service?authSource=admin + # - ACCESS_TOKEN_PUBLIC_KEY=${ACCESS_TOKEN_PUBLIC_KEY} + # - ACCESS_TOKEN_PRIVATE_KEY=${ACCESS_TOKEN_PRIVATE_KEY} + # - USER_SERVICE_URL=http://user-service:3002 + # - QUESTION_SERVICE_URL=http://question-service:3004 + # - MATCHING_SERVICE_URL=http://matching-service:3006 + # - JUDGE_ZERO_URL=http://judgezero-server:2358 + # - JUDGE_ZERO_SUBMIT_CONFIG=${JUDGE_ZERO_SUBMIT_CONFIG} + # restart: always + # networks: + # - backend-network + # depends_on: + # - judgezero-server + # - judgezero-workers + # - collab-db + + # collab-db: + # container_name: collab-db + # image: mongo:8.0.0 + # environment: + # - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER} + # - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD} + # ports: + # - '27087:27017' + # volumes: + # - 'collab_data:/data/db' + # networks: + # - backend-network + # command: mongod --quiet --logpath /dev/null --auth + + judgezero-server: + image: judge0/judge0:1.13.1 + volumes: + - ./judge0.conf:/judge0.conf:ro + ports: + - 2358:2358 + privileged: true + restart: always + depends_on: + judgezero-db: + condition: service_healthy + judgezero-redis: + condition: service_healthy + + judgezero-workers: + image: judge0/judge0:1.13.1 + command: ["./scripts/workers"] + volumes: + - ./judge0.conf:/judge0.conf:ro + privileged: true + restart: always + depends_on: + judgezero-db: + condition: service_healthy + judgezero-redis: + condition: service_healthy + + judgezero-db: + image: postgres:16.2 + env_file: judge0.conf + volumes: + - judgezero_data:/var/lib/postgresql/data/ + restart: always + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 10s + retries: 5 + start_period: 5s + + judgezero-redis: + image: redis:7.2.4 + command: [ + "bash", "-c", + 'docker-entrypoint.sh --appendonly no --requirepass "$$REDIS_PASSWORD"' + ] + env_file: judge0.conf + restart: always + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + retries: 5 + start_period: 5s matching-service: container_name: matching-service @@ -142,6 +235,8 @@ volumes: matching_data: rabbitmq_data: rabbitmq_log: + collab_data: + judgezero_data: # Define a network, which allows containers to communicate # with each other, by using their container name as a hostname diff --git a/judge0.conf b/judge0.conf new file mode 100644 index 0000000000..afbd87c89e --- /dev/null +++ b/judge0.conf @@ -0,0 +1,358 @@ +################################################################################ +# Judge0 Configuration File +################################################################################ +# Judge0 is a highly configurable which allows you to use it for many +# different use-cases. Please, before deploying Judge0 take a look at this +# configuration file that is divided in several logical sections that will help +# you understand what can you do with Judge0. +# +# This default configuration file is designed to work out of the box for you and +# you can start with it when deploying Judge0 on your server. +# +# If you have any questions please don't hasitate to send an email or open an +# issue on the GitHub page of the project. + + +################################################################################ +# Judge0 Server Configuration +################################################################################ +# Enable or disable Judge0 Telemetry. +# Read more about it here: https://github.com/judge0/judge0/blob/master/TELEMETRY.md +# Default: true +JUDGE0_TELEMETRY_ENABLE= + +# Automatically restart Judge0 server if it fails to start. +# Default: 10 +RESTART_MAX_TRIES= + +# Maintenance mode is a mode in which clients cannot +# create or delete submissions while maintenance is enabled. +# Default: false +MAINTENANCE_MODE= + +# Set custom maintenance message that will be returned to clients +# who try to create or delete submisions. +# Default: Judge0 is currently in maintenance. +MAINTENANCE_MESSAGE= + +# If enabled user can request to synchronically wait for +# submission result on submission create. +# Default: true, i.e. user can request to wait for the result +ENABLE_WAIT_RESULT= + +# If enabled user is allowed to set custom compiler options. +# Default: true +ENABLE_COMPILER_OPTIONS= + +# List language names, separated by space, for which setting compiler options is allowed. +# Note that ENABLE_COMPILER_OPTIONS has higher priority, so this option will be +# ignored if setting compiler options is disabled with ENABLE_COMPILER_OPTIONS. +# For example, ALLOWED_LANGUAGES_FOR_COMPILER_OPTIONS="C C++ Java" would only +# allow setting compiler options for languages C, C++ and Java. +# Default: empty - for every compiled language setting compiler options is allowed. +ALLOWED_LANGUAGES_FOR_COMPILER_OPTIONS= + +# If enabled user is allowed to set custom command line arguments. +# Default: true +ENABLE_COMMAND_LINE_ARGUMENTS= + +# If enabled autorized users can delete a submission. +# Default: false +ENABLE_SUBMISSION_DELETE= + +# If enabled user can GET and POST batched submissions. +# Default: true +ENABLE_BATCHED_SUBMISSIONS= + +# Maximum number of submissions that can be created or get in a batch. +# Default: 20 +MAX_SUBMISSION_BATCH_SIZE= + +# If enabled user can use callbacks. +# Default: true +ENABLE_CALLBACKS= + +# Maximum number of callback tries before giving up. +# Default: 3 +CALLBACKS_MAX_TRIES= + +# Timeout callback call after this many seconds. +# Default: 5 +CALLBACKS_TIMEOUT= + +# If enabled user can preset additional files in the sandbox. +# Default: true +ENABLE_ADDITIONAL_FILES= + +# Duration (in seconds) of submission cache. Decimal numbers are allowed. +# Set to 0 to turn of submission caching. Note that this does not apply to +# batched submissions. +# Default: 1 +SUBMISSION_CACHE_DURATION= + +# If true the documentation page will be used as a homepage, otherwise, the +# homepage will be empty. You can always access the documentation page via /docs. +# Default: false +USE_DOCS_AS_HOMEPAGE= + + +################################################################################ +# Judge0 Workers Configuration +################################################################################ +# Specify polling frequency in seconds. Decimal numbers are allowed. +# Default: 0.1 +INTERVAL= + +# Specify how many parallel workers to run. +# Default: 2*nproc (https://linux.die.net/man/1/nproc) +COUNT= + +# Specify maximum queue size. Represents maximum number of submissions that +# can wait in the queue at once. If request for new submission comes and the +# queue if full then submission will be rejected. +# Default: 100 +MAX_QUEUE_SIZE= + + +################################################################################ +# Judge0 Server Access Configuration +################################################################################ +# Allow only specified origins. +# If left blank, then all origins will be allowed (denoted with '*'). +# Example: +# ALLOW_ORIGIN="www.judge0.com judge0.com www.example.com blog.example.com" +ALLOW_ORIGIN= + +# Disallow only specified origins. +# If left blank, then no origin will be disallowed. +# Example: +# DISALLOW_ORIGIN="www.judge0.com judge0.com www.example.com blog.example.com" +DISALLOW_ORIGIN= + +# Allow only specified IP addresses. +# If left blank, then all IP addresses will be allowed. +# Example: +# ALLOW_IP="192.168.10.10 96.239.226.228 208.23.207.242" +ALLOW_IP= + +# Disallow only specified IP addresses. +# If left blank, then no IP addresses will be disallowed. +# Example: +# DISALLOW_IP="192.168.10.10 96.239.226.228 208.23.207.242" +DISALLOW_IP= + + +################################################################################ +# Judge0 Authentication Configuration +################################################################################ +# You can protect your API with (AUTHN_HEADER, AUTHN_TOKEN) pair. +# Each request then needs to have this pair either in headers or +# query parameters. For example let AUTHN_HEADER=X-Judge0-Token and +# AUTHN_TOKEN=mySecretToken. Then user should authenticate by sending this +# in headers or query parameters in each request, e.g.: +# https://api.judge0.com/system_info?X-Judge0-Token=mySecretToken + +# Specify authentication header name. +# Default: X-Auth-Token +AUTHN_HEADER= + +# Specify valid authentication tokens. +# Default: empty - authentication is disabled +AUTHN_TOKEN= + + +################################################################################ +# Judge0 Authorization Configuration +################################################################################ +# Protected API calls can be issued with (AUTHZ_HEADER, AUTHZ_TOKEN) pair. +# To see exactly which API calls are protected with authorization tokens +# please read the docs at https://api.judge0.com. +# API authorization ensures that only specified users call protected API calls. +# For example let AUTHZ_HEADER=X-Judge0-User and AUTHZ_TOKEN=mySecretToken. +# Then user should authorize be sending this in headers or query parameters in +# each request, e.g.: https://api.judge0.com/system_info?X-Judge0-User=mySecretToken +# Note that if you enabled authentication, then user should also send valid +# authentication token. + +# Specify authorization header name. +# Default: X-Auth-User +AUTHZ_HEADER= + +# Specify valid authorization tokens. +# Default: empty - authorization is disabled, protected API calls cannot be issued +AUTHZ_TOKEN= + + +################################################################################ +# Redis Configuration +################################################################################ +# Specify Redis host +# Default: localhost +REDIS_HOST=redis + +# Specify Redis port. +# Default: 6379 +REDIS_PORT= + +# Specify Redis password. Cannot be blank. +# Default: NO DEFAULT! MUST BE SET! +REDIS_PASSWORD=adJhsMh8rJQFGKujDfqh4dEvkEaVjdC8 + + +################################################################################ +# PostgreSQL Configuration +################################################################################ +# Specify Postgres host. +# Default: localhost +POSTGRES_HOST=db + +# Specify Postgres port. +# Default: 5432 +POSTGRES_PORT= + +# Name of the database to use. Used only in production. +# Default: postgres +POSTGRES_DB=judge0 + +# User who can access this database. Used only in production. +# Default: postgres +POSTGRES_USER=judge0 + +# Password of the user. Cannot be blank. Used only in production. +# Default: NO DEFAULT, YOU MUST SET YOUR PASSWORD +POSTGRES_PASSWORD=vTY9mG6tNAUj5jBWTUBN4vwujAznbdtc + + +################################################################################ +# Submission Configuration +################################################################################ +# Judge0 uses isolate as an sandboxing environment. +# Almost all of the options you see here can be mapped to one of the options +# that isolate provides. For more information about these options please +# check for the isolate documentation here: +# https://raw.githubusercontent.com/ioi/isolate/master/isolate.1.txt + +# Default runtime limit for every program (in seconds). Decimal numbers are allowed. +# Time in which the OS assigns the processor to different tasks is not counted. +# Default: 5 +CPU_TIME_LIMIT=2 + +# Maximum custom CPU_TIME_LIMIT. +# Default: 15 +MAX_CPU_TIME_LIMIT= + +# When a time limit is exceeded, wait for extra time (in seconds), before +# killing the program. This has the advantage that the real execution time +# is reported, even though it slightly exceeds the limit. +# Default: 1 +CPU_EXTRA_TIME= + +# Maximum custom CPU_EXTRA_TIME. +# Default: 5 +MAX_CPU_EXTRA_TIME= + +# Limit wall-clock time in seconds. Decimal numbers are allowed. +# This clock measures the time from the start of the program to its exit, +# so it does not stop when the program has lost the CPU or when it is waiting +# for an external event. We recommend to use CPU_TIME_LIMIT as the main limit, +# but set WALL_TIME_LIMIT to a much higher value as a precaution against +# sleeping programs. +# Default: 10 +WALL_TIME_LIMIT= + +# Maximum custom WALL_TIME_LIMIT. +# Default: 20 +MAX_WALL_TIME_LIMIT= + +# Limit address space of the program in kilobytes. +# Default: 128000 +MEMORY_LIMIT=128000 + +# Maximum custom MEMORY_LIMIT. +# Default: 512000 +MAX_MEMORY_LIMIT= + +# Limit process stack in kilobytes. +# Default: 64000 +STACK_LIMIT= + +# Maximum custom STACK_LIMIT. +# Default: 128000 +MAX_STACK_LIMIT= + +# Maximum number of processes and/or threads program can create. +# Default: 60 +MAX_PROCESSES_AND_OR_THREADS= + +# Maximum custom MAX_PROCESSES_AND_OR_THREADS. +# Default: 120 +MAX_MAX_PROCESSES_AND_OR_THREADS= + +# If true then CPU_TIME_LIMIT will be used as per process and thread. +# Default: false, i.e. CPU_TIME_LIMIT is set as a total limit for all processes and threads. +ENABLE_PER_PROCESS_AND_THREAD_TIME_LIMIT= + +# If false, user won't be able to set ENABLE_PER_PROCESS_AND_THREAD_TIME_LIMIT. +# Default: true +ALLOW_ENABLE_PER_PROCESS_AND_THREAD_TIME_LIMIT= + +# If true then MEMORY_LIMIT will be used as per process and thread. +# Default: false, i.e. MEMORY_LIMIT is set as a total limit for all processes and threads. +ENABLE_PER_PROCESS_AND_THREAD_MEMORY_LIMIT= + +# If false, user won't be able to set ENABLE_PER_PROCESS_AND_THREAD_MEMORY_LIMIT. +# Default: true +ALLOW_ENABLE_PER_PROCESS_AND_THREAD_MEMORY_LIMIT= + +# Limit size of files created (or modified) by the program in kilobytes. +# Default: 1024 +MAX_FILE_SIZE= + +# Maximum custom MAX_FILE_SIZE. +# Default: 4096 +MAX_MAX_FILE_SIZE= + +# Run each program this many times and take average of time and memory. +# Default: 1 +NUMBER_OF_RUNS= + +# Maximum custom NUMBER_OF_RUNS. +# Default: 20 +MAX_NUMBER_OF_RUNS= + +# Redirect stderr to stdout. +# Default: false +REDIRECT_STDERR_TO_STDOUT= + +# Maximum total size (in kilobytes) of extracted files from additional files archive. +# Default: 10240, i.e. maximum of 10MB in total can be extracted. +MAX_EXTRACT_SIZE= + +# If false, user won't be able to set ENABLE_NETWORK. +# Default: true, i.e. allow user to permit or deny network calls from the submission. +ALLOW_ENABLE_NETWORK= + +# If true submission will by default be able to do network calls. +# Default: false, i.e. programs cannot do network calls. +ENABLE_NETWORK= + + +################################################################################ +# Rails Configuration +################################################################################ +# Specify Rails environment: production or development +# Default: production +RAILS_ENV= + +# Specify maximum number of concurrent Rails threads. +# Default: nproc (https://linux.die.net/man/1/nproc) +RAILS_MAX_THREADS= + +# Specify how many processes will be created for handing requests. Each process +# will aditionally create RAILS_MAX_THREADS threads. +# Default: 2 +RAILS_SERVER_PROCESSES= + +# Secret key base for production, if not set it will be randomly generated +# Default: randomly generated +SECRET_KEY_BASE= diff --git a/package-lock.json b/package-lock.json index 6a3229865e..8945bcd300 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,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", @@ -50,6 +51,7 @@ "devDependencies": { "@repo/eslint-config": "*", "@repo/request-types": "*", + "@repo/submission-types": "*", "@repo/typescript-config": "*", "@testcontainers/mongodb": "^10.13.1", "@types/bcrypt": "^5.0.2", @@ -3911,6 +3913,10 @@ "resolved": "packages/request-types", "link": true }, + "node_modules/@repo/submission-types": { + "resolved": "packages/submission-types", + "link": true + }, "node_modules/@repo/typescript-config": { "resolved": "packages/typescript-config", "link": true @@ -15537,6 +15543,20 @@ "@types/express": "^4.17.21" } }, + "packages/submission-types": { + "name": "@repo/submission-types", + "version": "0.0.0", + "dependencies": { + "axios": "^1.7.7", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@repo/request-types": "*", + "@repo/typescript-config": "*" + } + }, "packages/typescript-config": { "name": "@repo/typescript-config", "version": "0.0.0" diff --git a/packages/request-types/src/ITypedBodyResponse.ts b/packages/request-types/src/ITypedBodyResponse.ts new file mode 100644 index 0000000000..81b0bcdda8 --- /dev/null +++ b/packages/request-types/src/ITypedBodyResponse.ts @@ -0,0 +1,5 @@ +import type { Response } from 'express' + +export interface ITypedBodyResponse extends Response { + body: T +} diff --git a/packages/request-types/src/index.ts b/packages/request-types/src/index.ts index 2b5e37936e..4fb1442291 100644 --- a/packages/request-types/src/index.ts +++ b/packages/request-types/src/index.ts @@ -1,3 +1,4 @@ export * from './IAuthenticatedRequest' export * from './IPaginationRequest' export * from './ITypedBodyRequest' +export * from './ITypedBodyResponse' diff --git a/packages/submission-types/package.json b/packages/submission-types/package.json new file mode 100644 index 0000000000..f000bd4dd6 --- /dev/null +++ b/packages/submission-types/package.json @@ -0,0 +1,20 @@ +{ + "name": "@repo/submission-types", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "tsc" + }, + "main": "./dist/index.js", + "types": "./src/index.ts", + "dependencies": { + "axios": "^1.7.7", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "reflect-metadata": "^0.2.2" + }, + "devDependencies": { + "@repo/request-types": "*", + "@repo/typescript-config": "*" + } +} diff --git a/packages/submission-types/src/SubmissionRequestDto.ts b/packages/submission-types/src/SubmissionRequestDto.ts new file mode 100644 index 0000000000..6a43ba17fa --- /dev/null +++ b/packages/submission-types/src/SubmissionRequestDto.ts @@ -0,0 +1,34 @@ +import { ITypedBodyRequest } from '@repo/request-types' +import { IsNotEmpty, IsNumber, IsString, validate, ValidationError } from 'class-validator' + +export class SubmissionRequestDto { + @IsNumber() + @IsNotEmpty() + language_id: number + + @IsString() + source_code: string + + @IsString() + stdin: string + + @IsString() + expected_output: string + + constructor(language_id: number, source_code: string, expected_output: string, stdin?: string) { + this.language_id = language_id + this.source_code = source_code + this.stdin = stdin || '' + this.expected_output = expected_output + } + + static fromRequest({ + body: { language_id, source_code, stdin, expected_output }, + }: ITypedBodyRequest): SubmissionRequestDto { + return new SubmissionRequestDto(language_id, source_code, stdin, expected_output) + } + + async validate(): Promise { + return validate(this) + } +} diff --git a/packages/submission-types/src/SubmissionResponseDto.ts b/packages/submission-types/src/SubmissionResponseDto.ts new file mode 100644 index 0000000000..4e2de30303 --- /dev/null +++ b/packages/submission-types/src/SubmissionResponseDto.ts @@ -0,0 +1,75 @@ +import { IsNumber, IsOptional, IsString, validate, ValidateNested, ValidationError } from 'class-validator' +import { Type } from 'class-transformer' +import { AxiosResponse } from 'axios' +import 'reflect-metadata' + +class StatusDto { + @IsNumber() + id: number + + @IsString() + description: string +} + +export class SubmissionResponseDto { + @IsOptional() + @IsString() + stdout: string | null + + @IsOptional() + @IsString() + time: string | null + + @IsOptional() + @IsNumber() + memory: number | null + + @IsOptional() + @IsString() + stderr: string | null + + @IsString() + token: string + + @IsOptional() + @IsString() + compile_output: string | null + + @IsOptional() + @IsString() + message: string | null + + @ValidateNested() + @Type(() => StatusDto) + status: StatusDto + + constructor( + stdout: string | null, + time: string, + memory: number | null, + stderr: string | null, + token: string, + compile_output: string | null, + message: string | null, + status: StatusDto + ) { + this.stdout = stdout + this.time = time + this.memory = memory + this.stderr = stderr + this.token = token + this.compile_output = compile_output + this.message = message + this.status = status + } + + static fromResponse({ + data: { stdout, time, memory, stderr, token, compile_output, message, status }, + }: AxiosResponse): SubmissionResponseDto { + return new SubmissionResponseDto(stdout, time, memory, stderr, token, compile_output, message, status) + } + + async validate(): Promise { + return validate(this) + } +} diff --git a/packages/submission-types/src/index.ts b/packages/submission-types/src/index.ts new file mode 100644 index 0000000000..cbe639280a --- /dev/null +++ b/packages/submission-types/src/index.ts @@ -0,0 +1,2 @@ +export * from './SubmissionRequestDto' +export * from './SubmissionResponseDto' diff --git a/packages/submission-types/tsconfig.json b/packages/submission-types/tsconfig.json new file mode 100644 index 0000000000..59781395e3 --- /dev/null +++ b/packages/submission-types/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@repo/typescript-config/tsconfig-service.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["src/**/*"] +}