Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ POSTGRES_PASSWORD=postgres
############################
AZURE_ACCOUNT_NAME=<account_name>
AZURE_ACCOUNT_KEY=<account_key>

############################
# Azure Service Bus
############################
AZURE_SERVICE_BUS_CONNECTION_STRING=<connection_string>
AZURE_SERVICE_BUS_QUEUE_NAME=code-execution
1 change: 1 addition & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"typescript-eslint": "^8.57.1"
},
"dependencies": {
"@azure/service-bus": "^7.9.5",
"@azure/storage-blob": "^12.20.0",
"@dotenvx/dotenvx": "^1.57.0",
"bcrypt": "^6.0.0",
Expand Down
78 changes: 78 additions & 0 deletions packages/backend/src/controllers/submission.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { RequestHandler } from "express";
import { ApiResponse } from "../utils/ApiResponse";
import { SubmissionServiceInstance } from "../services/submission.service";
import { Submission } from "../models/submission.model";
import { UserJWTPayload } from "../types";
import {
CreateSubmissionRequest,
GetSubmissionsQuery,
UpdateSubmissionRequest,
} from "../dtos/submission.dto";

export type SubmissionIdParam = {
submissionId: string;
};

class SubmissionController {
createSubmission: RequestHandler<
unknown,
ApiResponse<Submission>,
CreateSubmissionRequest,
unknown,
UserJWTPayload
> = async (req, res) => {
const submission = await SubmissionServiceInstance.createSubmission(
req.body,
res.locals.userId,
);
res
.status(201)
.json(
new ApiResponse(201, "Submission created successfully", submission),
);
};

getSubmissionById: RequestHandler<
SubmissionIdParam,
ApiResponse<Submission>
> = async (req, res) => {
const submissionId = parseInt(req.params.submissionId, 10);
const submission =
await SubmissionServiceInstance.getSubmissionById(submissionId);
res
.status(200)
.json(
new ApiResponse(200, "Submission fetched successfully", submission),
);
};

getSubmissions: RequestHandler<
unknown,
ApiResponse<{
submissions: Submission[];
totalSubmissions: number;
totalPages: number;
currentPage: number;
}>,
unknown,
GetSubmissionsQuery
> = async (req, res) => {
const result = await SubmissionServiceInstance.getSubmissions(req.query);
res
.status(200)
.json(new ApiResponse(200, "Submissions fetched successfully", result));
};

updateSubmission: RequestHandler<
unknown,
ApiResponse,
UpdateSubmissionRequest
> = async (req, res) => {
await SubmissionServiceInstance.updateSubmission(req.body);
res
.status(200)
.json(new ApiResponse(200, "Submission updated successfully"));
};
}

export const SubmissionControllerInstance = new SubmissionController();
2 changes: 2 additions & 0 deletions packages/backend/src/db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Kysely, PostgresDialect } from "kysely";
import { Logger } from "../utils/logger";
import { UserTable } from "../models/user.model";
import { ProblemTable } from "../models/problem.model";
import { SubmissionTable } from "../models/submission.model";

// Create the Postgres dialect for Kysely
// Reference: https://kysely.dev/docs/getting-started#instantiation
Expand All @@ -23,6 +24,7 @@ const dialect = new PostgresDialect({
export interface Database {
users: UserTable;
problems: ProblemTable;
submissions: SubmissionTable;
}

// Instantiate Database
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { sql, type Kysely } from "kysely";

// `any`(or unknown) is required here since migrations should be frozen in time
// Migrations should never depend on the current code of your app
// because they need to work even when the app changes
// For more info, see: https://kysely.dev/docs/migrations
export async function up(db: Kysely<unknown>): Promise<void> {
await sql`CREATE TYPE submission_status AS ENUM (
'Queued',
'Compile Error',
'Runtime Error',
'Time Limit Error',
'Wrong Answer',
'Successful',
'Server Error'
)`.execute(db);

await db.schema
.createTable("submissions")
.addColumn("submission_id", "serial", (col) => col.primaryKey())
.addColumn("user_id", "integer", (col) =>
col.notNull().references("users.user_id").onDelete("cascade"),
)
.addColumn("problem_id", "text", (col) => col.notNull())
.addColumn("language", "varchar(50)", (col) => col.notNull())
.addColumn("status", sql`submission_status`, (col) =>
col.notNull().defaultTo(sql`'Queued'::submission_status`),
)
.addColumn("runtime", "float8")
.addColumn("memory_used", "float8")
.addColumn("test_cases_passed", "integer")
.addColumn("total_test_cases", "integer")
.addColumn("error_message", "text")
.addColumn("submission_key", "text", (col) => col.notNull())
.addColumn("created_at", "timestamptz", (col) =>
col.notNull().defaultTo(sql`now()`),
)
.addColumn("executed_at", "timestamptz")
.execute();
}

// `any`(or unknown) is required here since migrations should be frozen in time
// Migrations should never depend on the current code of your app
// because they need to work even when the app changes
// For more info, see: https://kysely.dev/docs/migrations
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable("submissions").execute();
await sql`DROP TYPE submission_status`.execute(db);
}
53 changes: 53 additions & 0 deletions packages/backend/src/dtos/submission.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { z } from "zod";
import { Language, SubmissionStatus } from "../models/submission.model";
import { UserIdDto } from "./user.dto";

////////////////////////////////////////////
// Common Dtos
////////////////////////////////////////////
export const SubmissionIdDto = z.number().int().positive();
export const ProblemIdDto = z.string().uuid();
export const LanguageDto = z.enum(Language);
export const SubmissionStatusDto = z.enum(SubmissionStatus);
export const SubmissionKeyDto = z.string().min(1);

////////////////////////////////////////////
// Create Submission Request Dto
////////////////////////////////////////////
export const CreateSubmissionRequestDto = z.object({
problem_id: ProblemIdDto,
language: LanguageDto,
submission_key: SubmissionKeyDto,
});
export type CreateSubmissionRequest = z.infer<
typeof CreateSubmissionRequestDto
>;

////////////////////////////////////////////
// Update Submission Request Dto
// Called by the code execution worker after running the submission
////////////////////////////////////////////
export const UpdateSubmissionRequestDto = z.object({
submission_id: SubmissionIdDto,
status: SubmissionStatusDto,
runtime: z.number().nonnegative().nullish(),
memory_used: z.number().nonnegative().nullish(),
test_cases_passed: z.number().int().nonnegative().nullish(),
total_test_cases: z.number().int().nonnegative().nullish(),
error_message: z.string().nullish(),
executed_at: z.string().datetime().nullish(),
});
export type UpdateSubmissionRequest = z.infer<
typeof UpdateSubmissionRequestDto
>;

////////////////////////////////////////////
// Get Submissions Query Dto
////////////////////////////////////////////
export const GetSubmissionsQueryDto = z.object({
user_id: UserIdDto.nullish(),
problem_id: ProblemIdDto.nullish(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(50).default(10),
});
export type GetSubmissionsQuery = z.infer<typeof GetSubmissionsQueryDto>;
69 changes: 69 additions & 0 deletions packages/backend/src/models/submission.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
ColumnType,
Generated,
Insertable,
Selectable,
Updateable,
} from "kysely";
import { UserTable } from "./user.model";

////////////////////////////////////////////
// Submission Status
////////////////////////////////////////////
export const SubmissionStatus = {
Queued: "Queued",
CompileError: "Compile Error",
RuntimeError: "Runtime Error",
TimeLimitError: "Time Limit Error",
WrongAnswer: "Wrong Answer",
Successful: "Successful",
ServerError: "Server Error",
} as const;
export type SubmissionStatus =
(typeof SubmissionStatus)[keyof typeof SubmissionStatus];

////////////////////////////////////////////
// Programming Languages
////////////////////////////////////////////
export const Language = {
Python: "Python",
Cpp: "Cpp",
} as const;
export type Language = (typeof Language)[keyof typeof Language];

////////////////////////////////////////////
// Submission Table Definition
////////////////////////////////////////////
export interface SubmissionTable {
submission_id: Generated<number>;
user_id: UserTable["user_id"];
/**
* UUID of the problem being submitted
*/
problem_id: string;
language: Language;
status: ColumnType<
SubmissionStatus,
SubmissionStatus | undefined,
SubmissionStatus
>;
runtime: number | null;
memory_used: number | null;
test_cases_passed: number | null;
total_test_cases: number | null;
error_message: string | null;
/**
* Azure Blob Storage key for the submitted code file
*/
submission_key: string;
created_at: ColumnType<Date, never, never>;
executed_at: ColumnType<
Date | null,
string | null | undefined,
string | null | undefined
>;
}

export type Submission = Selectable<SubmissionTable>;
export type NewSubmission = Insertable<SubmissionTable>;
export type SubmissionUpdate = Updateable<SubmissionTable>;
Loading