├── uploads
└── test.txt
├── dist
├── utils
│ ├── CustomInterfaces
│ │ ├── CustomRequest.js
│ │ ├── ExamInterface.js
│ │ ├── QuestionInterfaces.js
│ │ └── ParticipantDataInterfaces.js
│ ├── catchAsync.js
│ ├── response-handler.js
│ ├── cloudinary.js
│ ├── multer.js
│ ├── questionFunctions.js
│ └── examFunctions.js
├── errors
│ └── custom-error.js
├── middlewares
│ └── auth.js
├── routes
│ └── api
│ │ ├── utils
│ │ └── routes.js
│ │ ├── user
│ │ └── routes.js
│ │ └── exam
│ │ └── routes.js
├── server.js
├── controllers
│ ├── utilsController.js
│ ├── userController.js
│ └── examController.js
└── database
│ └── dbConnection.js
├── prisma
├── migrations
│ ├── migration_lock.toml
│ ├── 20220406082854_init
│ │ └── migration.sql
│ ├── 20220406211848_init
│ │ └── migration.sql
│ ├── 20220406082433_init
│ │ └── migration.sql
│ └── 20220406091007_init
│ │ └── migration.sql
└── schema.prisma
├── src
├── utils
│ ├── CustomInterfaces
│ │ ├── CustomRequest.ts
│ │ ├── ExamInterface.ts
│ │ ├── QuestionInterfaces.ts
│ │ └── ParticipantDataInterfaces.ts
│ ├── catchAsync.ts
│ ├── response-handler.ts
│ ├── multer.ts
│ ├── cloudinary.ts
│ ├── questionFunctions.ts
│ └── examFunctions.ts
├── errors
│ └── custom-error.ts
├── routes
│ └── api
│ │ ├── utils
│ │ └── routes.ts
│ │ ├── user
│ │ └── routes.ts
│ │ └── exam
│ │ └── routes.ts
├── database
│ ├── prisma-client.ts
│ └── dbConnection.ts
├── middlewares
│ └── auth.ts
├── controllers
│ ├── utilsController.ts
│ ├── userController.ts
│ └── examController.ts
└── server.ts
├── .prettierrc
├── .gitignore
├── README.md
├── package.json
├── web.config
├── .github
└── workflows
│ └── master_exam-simulation.yml
└── tsconfig.json
/uploads/test.txt:
--------------------------------------------------------------------------------
1 | :)
--------------------------------------------------------------------------------
/dist/utils/CustomInterfaces/CustomRequest.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 |
--------------------------------------------------------------------------------
/dist/utils/CustomInterfaces/ExamInterface.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 |
--------------------------------------------------------------------------------
/dist/utils/CustomInterfaces/QuestionInterfaces.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "mysql"
--------------------------------------------------------------------------------
/src/utils/CustomInterfaces/CustomRequest.ts:
--------------------------------------------------------------------------------
1 | import { Request } from 'express';
2 | import { JwtPayload } from 'jsonwebtoken';
3 | export interface CustomRequest extends Request {
4 | user?: JwtPayload;
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "es5",
3 | "tabWidth": 2,
4 | "semi": true,
5 | "singleQuote": true,
6 | "printWidth": 150,
7 | "jsxSingleQuote": true,
8 | "bracketSameLine": true,
9 | "bracketSpacing": true
10 | }
11 |
--------------------------------------------------------------------------------
/dist/utils/catchAsync.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.default = (fn) => {
4 | return (req, res, next) => {
5 | fn(req, res, next).catch((err) => next(err));
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/utils/catchAsync.ts:
--------------------------------------------------------------------------------
1 | import { Response, Request, NextFunction } from 'express';
2 |
3 | export default (fn: Function) => {
4 | return (req: Request, res: Response, next: NextFunction) => {
5 | fn(req, res, next).catch((err: Error) => next(err));
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # dependencies
3 | node_modules
4 | /.pnp
5 | .pnp.js
6 |
7 | # testing
8 | /coverage
9 |
10 | # production
11 | /build
12 |
13 | # misc
14 | .DS_Store
15 | .env.local
16 | .env.development.local
17 | .env.test.local
18 | .env.production.local
19 |
20 | npm-debug.log*
21 | yarn-debug.log*
22 | yarn-error.log*
23 |
24 | yarn.lock
25 | package-lock.json
26 | *.env
27 |
--------------------------------------------------------------------------------
/src/errors/custom-error.ts:
--------------------------------------------------------------------------------
1 | export default class CustomError extends Error {
2 | statusCode: number;
3 | status: string;
4 | constructor(message: string, statusCode: number) {
5 | super(message);
6 | this.statusCode = statusCode;
7 | this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
8 | Error.captureStackTrace(this, this.constructor);
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/routes/api/utils/routes.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import * as utilsController from '../../../controllers/utilsController';
3 | import auth from '../../../middlewares/auth';
4 | import upload from '../../../utils/multer';
5 |
6 | const router = express.Router();
7 |
8 | router.post('/upload/images', auth, upload.array('image'), utilsController.uploadImages);
9 |
10 | export default router;
11 |
--------------------------------------------------------------------------------
/dist/errors/custom-error.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | class CustomError extends Error {
4 | constructor(message, statusCode) {
5 | super(message);
6 | this.statusCode = statusCode;
7 | this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
8 | Error.captureStackTrace(this, this.constructor);
9 | }
10 | }
11 | exports.default = CustomError;
12 |
--------------------------------------------------------------------------------
/dist/utils/CustomInterfaces/ParticipantDataInterfaces.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.participantRankingData = void 0;
4 | class participantRankingData {
5 | constructor(id, totalScore, finishTime) {
6 | this.id = id;
7 | this.totalScore = totalScore;
8 | this.finishTime = finishTime;
9 | }
10 | }
11 | exports.participantRankingData = participantRankingData;
12 |
--------------------------------------------------------------------------------
/src/utils/response-handler.ts:
--------------------------------------------------------------------------------
1 | import CustomError from '../errors/custom-error';
2 |
3 | export const SuccessResponse = (data: any, message?: string) => ({
4 | success: true,
5 | status: 'success',
6 | message: message || 'Success',
7 | data,
8 | });
9 |
10 | export const ErrorResponse = (err: Error) => ({
11 | success: false,
12 | status: err instanceof CustomError && `${err.statusCode}`.startsWith('4') ? 'fail' : 'error',
13 | message: err.message,
14 | error: err,
15 | });
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Quizzio: Backend
2 |
3 | Backend for the Exam Simulation project.
4 |
5 | ## Installation
6 |
7 | Install my-project with npm
8 |
9 | ```bash
10 | npm install
11 | npm run dev
12 | ```
13 |
14 | ## Environment Variables
15 |
16 | To run this project, you will need to set the following environment variables in a .env file in the root directory:
17 |
18 | `ACCESS_TOKEN_SECRET`
19 |
20 | `UPLOADS_FOLDER_LOCAL`
21 |
22 | `CLOUDINARY_CLOUD_NAME`
23 |
24 | `CLOUDINARY_API_KEY`
25 |
26 | `CLOUDINARY_API_SECRET`
27 |
28 | ## Authors
29 |
30 | - [@Bihan001](https://www.github.com/Bihan001)
31 |
--------------------------------------------------------------------------------
/src/utils/CustomInterfaces/ExamInterface.ts:
--------------------------------------------------------------------------------
1 | export interface examInterface {
2 | id: string;
3 | userId: string;
4 | image: string;
5 | tags: string;
6 | questions: any;
7 | startTime: number;
8 | duration: number;
9 | ongoing: boolean;
10 | finished: boolean;
11 | }
12 |
13 | export interface Option {
14 | id: number;
15 | data: string;
16 | }
17 |
18 | export interface RootObject {
19 | question: string;
20 | type: string;
21 | options: Option[];
22 | givenOption: any;
23 | correctOption: any;
24 | }
25 |
26 | export interface RootQuestionsObject {
27 | [id: string]: RootObject;
28 | }
29 |
--------------------------------------------------------------------------------
/src/database/prisma-client.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const prisma = new PrismaClient({
4 | errorFormat: 'pretty',
5 | log: [
6 | {
7 | emit: 'event',
8 | level: 'query',
9 | },
10 | {
11 | emit: 'stdout',
12 | level: 'error',
13 | },
14 | {
15 | emit: 'stdout',
16 | level: 'info',
17 | },
18 | {
19 | emit: 'stdout',
20 | level: 'warn',
21 | },
22 | ],
23 | });
24 |
25 | prisma.$on('query', (e) => {
26 | console.log('Query: ' + e.query);
27 | console.log('Params: ' + e.params);
28 | console.log('Duration: ' + e.duration + 'ms');
29 | });
30 |
31 | export default prisma;
32 |
--------------------------------------------------------------------------------
/src/utils/multer.ts:
--------------------------------------------------------------------------------
1 | import multer from 'multer';
2 | require('dotenv').config();
3 |
4 | const fileTypes = ['image/jpeg', 'image/png'];
5 |
6 | const storage = multer.diskStorage({
7 | destination: (req, file, cb) => {
8 | cb(null, process.env.UPLOADS_FOLDER_LOCAL as string);
9 | },
10 | filename: (req, file, cb) => {
11 | cb(null, Date.now().toString() + '-' + file.originalname);
12 | },
13 | });
14 |
15 | const fileFilter = (req: any, file: Express.Multer.File, cb: Function) => {
16 | if (fileTypes.includes(file.mimetype)) {
17 | cb(null, true);
18 | } else {
19 | cb({ message: 'File format not supported' }, false);
20 | }
21 | };
22 |
23 | export default multer({
24 | storage,
25 | fileFilter,
26 | });
27 |
--------------------------------------------------------------------------------
/src/routes/api/user/routes.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import * as userController from '../../../controllers/userController';
3 | import auth from '../../../middlewares/auth';
4 | import upload from '../../../utils/multer';
5 |
6 | const router = express.Router();
7 |
8 | router.post('/login', userController.loginUser);
9 | router.post('/register', userController.registerUser);
10 | router.post('/logout', auth, userController.logoutUser);
11 | router.get('/current', auth, userController.getCurrentUser);
12 | router.get('/exams-hosted', auth, userController.getExamHosted);
13 | router.get('/exams-given', auth, userController.getExamGiven);
14 | router.get('/:id', userController.getuser);
15 | router.patch('/', auth, upload.single('image'), userController.editUser);
16 |
17 | export default router;
18 |
--------------------------------------------------------------------------------
/src/middlewares/auth.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response, NextFunction } from 'express';
2 | import { getDb } from '../database/dbConnection';
3 | import jwt from 'jsonwebtoken';
4 | import bcrypt from 'bcrypt';
5 | import CustomError from '../errors/custom-error';
6 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest';
7 |
8 | export default (req: CustomRequest, res: Response, next: NextFunction) => {
9 | const db = getDb();
10 | const authHeader = req.headers['authorization'] as string;
11 | const token = authHeader?.split(' ')[1] || '';
12 | if (!token) throw new CustomError('Token Not Found!', 403);
13 | jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!, (err, user) => {
14 | if (err || !user) throw new CustomError('Token Invalid!', 403);
15 | req.user = user;
16 | next();
17 | });
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/cloudinary.ts:
--------------------------------------------------------------------------------
1 | import { v2 as cloudinary } from 'cloudinary';
2 | require('dotenv').config();
3 |
4 | cloudinary.config({
5 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME as string,
6 | api_key: process.env.CLOUDINARY_API_KEY as string,
7 | api_secret: process.env.CLOUDINARY_API_SECRET as string,
8 | });
9 |
10 | export default function uploader(filePath: string, folder = 'exam_simulation_images') {
11 | return new Promise((resolve, reject) => {
12 | cloudinary.uploader.upload(
13 | filePath,
14 | {
15 | folder,
16 | resource_type: 'auto',
17 | },
18 | (err, result: any) => {
19 | if (err) {
20 | reject(err);
21 | } else {
22 | resolve({ url: result.secure_url, public_id: result.public_id });
23 | }
24 | }
25 | );
26 | });
27 | }
28 |
--------------------------------------------------------------------------------
/src/utils/CustomInterfaces/QuestionInterfaces.ts:
--------------------------------------------------------------------------------
1 | import { evaluateQuestion } from '../questionFunctions';
2 |
3 | interface questionInterface {
4 | id: string;
5 | question: string;
6 | marks: number;
7 | negMarks: number;
8 | type: string;
9 | }
10 |
11 | export interface mcqInterface extends questionInterface {
12 | correctOption: number[];
13 | options: [
14 | {
15 | id: number;
16 | data: string;
17 | }
18 | ];
19 | }
20 |
21 | export interface multipleOptionsInterface extends questionInterface {
22 | correctOption: number[];
23 | options: [
24 | {
25 | id: number;
26 | data: string;
27 | }
28 | ];
29 | }
30 | export interface fillInTheBlanksInterface extends questionInterface {
31 | correctOption: string;
32 | }
33 |
34 | export interface evaluateQuestionInterface {
35 | [type: string]: any;
36 | }
37 |
--------------------------------------------------------------------------------
/dist/utils/response-handler.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | exports.ErrorResponse = exports.SuccessResponse = void 0;
7 | const custom_error_1 = __importDefault(require("../errors/custom-error"));
8 | const SuccessResponse = (data, message) => ({
9 | success: true,
10 | status: 'success',
11 | message: message || 'Success',
12 | data,
13 | });
14 | exports.SuccessResponse = SuccessResponse;
15 | const ErrorResponse = (err) => ({
16 | success: false,
17 | status: err instanceof custom_error_1.default && `${err.statusCode}`.startsWith('4') ? 'fail' : 'error',
18 | message: err.message,
19 | error: err,
20 | });
21 | exports.ErrorResponse = ErrorResponse;
22 |
--------------------------------------------------------------------------------
/src/database/dbConnection.ts:
--------------------------------------------------------------------------------
1 | import e from 'express';
2 | import mysql from 'mysql2/promise';
3 | import { scheduleOnServerRestart } from '../utils/examFunctions';
4 |
5 | let db: any;
6 | const connectDatabase = async () => {
7 | db = await mysql.createConnection({
8 | host: 'mydbinstance.cyfjjout6pho.ap-south-1.rds.amazonaws.com',
9 | user: 'subho57',
10 | password: 'adminDBpassword123',
11 | port: 3306,
12 | database: 'examSimulation',
13 | multipleStatements: true, // Prevent nested sql statements
14 | connectTimeout: 60 * 60 * 1000,
15 | // debug: true,
16 | });
17 | if (db) {
18 | console.log('Database Connected !');
19 | scheduleOnServerRestart();
20 | } else console.log('Database Not Connected !');
21 | };
22 |
23 | const getDb = () => {
24 | return db;
25 | };
26 |
27 | export default db;
28 | export { connectDatabase, getDb };
29 |
--------------------------------------------------------------------------------
/dist/utils/cloudinary.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | const cloudinary_1 = require("cloudinary");
4 | require('dotenv').config();
5 | cloudinary_1.v2.config({
6 | cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
7 | api_key: process.env.CLOUDINARY_API_KEY,
8 | api_secret: process.env.CLOUDINARY_API_SECRET,
9 | });
10 | function uploader(filePath, folder = 'exam_simulation_images') {
11 | return new Promise((resolve, reject) => {
12 | cloudinary_1.v2.uploader.upload(filePath, {
13 | folder,
14 | resource_type: 'auto',
15 | }, (err, result) => {
16 | if (err) {
17 | reject(err);
18 | }
19 | else {
20 | resolve({ url: result.secure_url, public_id: result.public_id });
21 | }
22 | });
23 | });
24 | }
25 | exports.default = uploader;
26 |
--------------------------------------------------------------------------------
/src/controllers/utilsController.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../utils/catchAsync';
2 | import { SuccessResponse } from '../utils/response-handler';
3 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest';
4 | import CustomError from '../errors/custom-error';
5 | import { Response, Request } from 'express';
6 | import { getDb } from '../database/dbConnection';
7 | import cloudinaryUploader from '../utils/cloudinary';
8 | import fs from 'fs';
9 |
10 | export const uploadImages = catchAsync(async (req: Request, res: Response) => {
11 | const uploader = async (path: string) => await cloudinaryUploader(path);
12 |
13 | const files = req.files as Express.Multer.File[];
14 | const urls = [];
15 | for (const file of files) {
16 | const { path } = file;
17 | const newPath = await uploader(path);
18 | urls.push(newPath);
19 | fs.unlinkSync(path);
20 | }
21 | return res.status(200).json(SuccessResponse({ urls }, 'Successfully uploaded images'));
22 | });
23 |
--------------------------------------------------------------------------------
/dist/utils/multer.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const multer_1 = __importDefault(require("multer"));
7 | require('dotenv').config();
8 | const fileTypes = ['image/jpeg', 'image/png'];
9 | const storage = multer_1.default.diskStorage({
10 | destination: (req, file, cb) => {
11 | cb(null, process.env.UPLOADS_FOLDER_LOCAL);
12 | },
13 | filename: (req, file, cb) => {
14 | cb(null, Date.now().toString() + '-' + file.originalname);
15 | },
16 | });
17 | const fileFilter = (req, file, cb) => {
18 | if (fileTypes.includes(file.mimetype)) {
19 | cb(null, true);
20 | }
21 | else {
22 | cb({ message: 'File format not supported' }, false);
23 | }
24 | };
25 | exports.default = (0, multer_1.default)({
26 | storage,
27 | fileFilter,
28 | });
29 |
--------------------------------------------------------------------------------
/src/utils/CustomInterfaces/ParticipantDataInterfaces.ts:
--------------------------------------------------------------------------------
1 | export interface participantDataInterface {
2 | id: number;
3 | examId: string;
4 | participantId: string;
5 | answers: any;
6 | totalScore: number | null;
7 | finishTime: number;
8 | virtual: boolean;
9 | }
10 |
11 | export interface answerInterface {
12 | type: 'mcq' | 'multipleOptions' | 'fillInTheBlanks';
13 | answer: number[] | string;
14 | }
15 |
16 | export interface answersObjInterface {
17 | [key: string]: answerInterface;
18 | }
19 |
20 | export interface participantRankingInterface {
21 | id: String;
22 | finishTime: number;
23 | totalScore: number;
24 | }
25 |
26 | export class participantRankingData implements participantRankingInterface {
27 | id: String;
28 | finishTime: number;
29 | totalScore: number;
30 |
31 | constructor(id: String, totalScore: number, finishTime: number) {
32 | this.id = id;
33 | this.totalScore = totalScore;
34 | this.finishTime = finishTime;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/dist/middlewares/auth.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const dbConnection_1 = require("../database/dbConnection");
7 | const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
8 | const custom_error_1 = __importDefault(require("../errors/custom-error"));
9 | exports.default = (req, res, next) => {
10 | const db = (0, dbConnection_1.getDb)();
11 | const authHeader = req.headers['authorization'];
12 | const token = (authHeader === null || authHeader === void 0 ? void 0 : authHeader.split(' ')[1]) || '';
13 | if (!token)
14 | throw new custom_error_1.default('Token Not Found!', 403);
15 | jsonwebtoken_1.default.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
16 | if (err || !user)
17 | throw new custom_error_1.default('Token Invalid!', 403);
18 | req.user = user;
19 | next();
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/dist/utils/questionFunctions.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | Object.defineProperty(exports, "__esModule", { value: true });
3 | exports.evaluateQuestion = void 0;
4 | const evaluateMCQ = (question, answer) => {
5 | if (question.correctOption[0] === answer.answer[0])
6 | return question.marks;
7 | return -question.negMarks;
8 | };
9 | const evaluateMultipleOptions = (question, answer) => {
10 | let participantAnsOpts = [...answer.answer].sort();
11 | let correctOptions = [...question.correctOption].sort();
12 | if (correctOptions.every((val, index) => val === participantAnsOpts[index]))
13 | return question.marks;
14 | return -question.negMarks;
15 | };
16 | const evaluateFillInTheBlanks = (question, answer) => {
17 | return question.correctOption.toLowerCase() ===
18 | answer.answer.toString().toLowerCase()
19 | ? question.marks
20 | : -question.negMarks;
21 | };
22 | exports.evaluateQuestion = {
23 | mcq: evaluateMCQ,
24 | multipleOptions: evaluateMultipleOptions,
25 | fillInTheBlanks: evaluateFillInTheBlanks,
26 | };
27 |
--------------------------------------------------------------------------------
/src/routes/api/exam/routes.ts:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import * as examController from '../../../controllers/examController';
3 | import auth from '../../../middlewares/auth';
4 |
5 | const router = express.Router();
6 | router.get('/testing-route', examController.testing);
7 | router.get('/tags', examController.getTags);
8 | router.get('/question-types', examController.getQuestionTypes);
9 | router.get('/create-tables', examController.createTables);
10 | router.post('/create', auth, examController.createExam);
11 | router.get('/evaluate/force', examController.forceEvaluateExam);
12 | router.get('/exam-registered', auth, examController.examRegisterStatus);
13 | router.get('/solution', auth, examController.getExamSolution);
14 | router.get('/scores', auth, examController.getExamScores);
15 | router.patch('/:id', auth, examController.editExam);
16 | router.post('/all', examController.getExams);
17 | router.get('/:id', examController.getExamDetails);
18 | router.get('/:id/start', auth, examController.startExam);
19 | router.post('/register', auth, examController.registerInExam);
20 | router.post('/submit', auth, examController.submitExam);
21 |
22 | export default router;
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "backend",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "./src/server.ts",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "dev": "nodemon src/server.ts",
9 | "start": "node dist/server.js",
10 | "build": "tsc --project .",
11 | "build-prod": "npm install && npm run build",
12 | "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install"
13 | },
14 | "author": "Ankur Saha",
15 | "license": "ISC",
16 | "dependencies": {
17 | "@prisma/client": "3.12.0",
18 | "bcrypt": "^5.0.1",
19 | "cloudinary": "^1.28.1",
20 | "cors": "^2.8.5",
21 | "dotenv": "^10.0.0",
22 | "express": "^4.17.2",
23 | "jsonwebtoken": "^8.5.1",
24 | "multer": "^1.4.4",
25 | "mysql2": "^2.3.3",
26 | "node-schedule": "^2.1.0",
27 | "uuid": "^8.3.2"
28 | },
29 | "devDependencies": {
30 | "@types/bcrypt": "^5.0.0",
31 | "@types/cors": "^2.8.12",
32 | "@types/express": "^4.17.13",
33 | "@types/jsonwebtoken": "^8.5.6",
34 | "@types/multer": "^1.4.7",
35 | "@types/mysql": "^2.15.20",
36 | "@types/node": "^17.0.8",
37 | "@types/node-cron": "^3.0.1",
38 | "@types/node-schedule": "^1.3.2",
39 | "@types/uuid": "^8.3.4",
40 | "nodemon": "^2.0.15",
41 | "prisma": "^3.12.0",
42 | "ts-node": "^10.4.0",
43 | "typescript": "^4.5.4"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/utils/questionFunctions.ts:
--------------------------------------------------------------------------------
1 | import {
2 | mcqInterface,
3 | multipleOptionsInterface,
4 | fillInTheBlanksInterface,
5 | evaluateQuestionInterface,
6 | } from './CustomInterfaces/QuestionInterfaces';
7 | import { examInterface } from './CustomInterfaces/ExamInterface';
8 | import { answerInterface } from './CustomInterfaces/ParticipantDataInterfaces';
9 |
10 | const evaluateMCQ = (
11 | question: mcqInterface,
12 | answer: answerInterface
13 | ): Number => {
14 | if (question.correctOption[0] === answer.answer[0]) return question.marks;
15 | return -question.negMarks;
16 | };
17 |
18 | const evaluateMultipleOptions = (
19 | question: multipleOptionsInterface,
20 | answer: answerInterface
21 | ): Number => {
22 | let participantAnsOpts = [...answer.answer].sort();
23 | let correctOptions = [...question.correctOption].sort();
24 | if (correctOptions.every((val, index) => val === participantAnsOpts[index]))
25 | return question.marks;
26 | return -question.negMarks;
27 | };
28 |
29 | const evaluateFillInTheBlanks = (
30 | question: fillInTheBlanksInterface,
31 | answer: answerInterface
32 | ): Number => {
33 | return question.correctOption.toLowerCase() ===
34 | answer.answer.toString().toLowerCase()
35 | ? question.marks
36 | : -question.negMarks;
37 | };
38 |
39 | export const evaluateQuestion: evaluateQuestionInterface = {
40 | mcq: evaluateMCQ,
41 | multipleOptions: evaluateMultipleOptions,
42 | fillInTheBlanks: evaluateFillInTheBlanks,
43 | };
44 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response, NextFunction } from "express";
2 | import cors from "cors";
3 | import CustomError from "./errors/custom-error";
4 | import { SuccessResponse, ErrorResponse } from "./utils/response-handler";
5 | import examRoutes from "./routes/api/exam/routes";
6 | import userRoutes from "./routes/api/user/routes";
7 | import utilsRoutes from "./routes/api/utils/routes";
8 |
9 | import { connectDatabase } from "./database/dbConnection";
10 |
11 | const app = express();
12 |
13 | app.use(
14 | cors({
15 | credentials: true,
16 | origin: (origin, callback) => {
17 | return callback(null, true);
18 | },
19 | })
20 | );
21 | app.use(express.json());
22 | require("dotenv").config();
23 | app.use("/exam", examRoutes);
24 | app.use("/user", userRoutes);
25 | app.use("/utils", utilsRoutes);
26 |
27 | //connecting database
28 | connectDatabase();
29 | //===================
30 |
31 | // All middlewares goes above this
32 |
33 | app.all("*", (req: Request, res: Response, next: NextFunction) => {
34 | const err = new CustomError("Non-existant route", 404);
35 | next(err);
36 | });
37 |
38 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
39 | if (err instanceof CustomError) {
40 | return res.status(err.statusCode).json(ErrorResponse(err));
41 | }
42 | return res.status(500).json(ErrorResponse(err));
43 | });
44 |
45 | const PORT = process.env.PORT || 5000;
46 | app.listen(PORT, () => {
47 | console.log(`Server started on port ${PORT}`);
48 | });
49 |
--------------------------------------------------------------------------------
/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.github/workflows/master_exam-simulation.yml:
--------------------------------------------------------------------------------
1 | # Docs for the Azure Web Apps Deploy action: https://github.com/Azure/webapps-deploy
2 | # More GitHub Actions for Azure: https://github.com/Azure/actions
3 |
4 | name: Build and deploy Node.js app to Azure Web App - exam-simulation
5 |
6 | on:
7 | push:
8 | branches:
9 | - master
10 | workflow_dispatch:
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 |
19 | - name: Set up Node.js version
20 | uses: actions/setup-node@v1
21 | with:
22 | node-version: '14.x'
23 |
24 | - name: npm install, build
25 | run: npm run build-prod
26 |
27 | - name: Upload artifact for deployment job
28 | uses: actions/upload-artifact@v2
29 | with:
30 | name: node-app
31 | path: .
32 |
33 | deploy:
34 | runs-on: ubuntu-latest
35 | needs: build
36 | environment:
37 | name: 'Production'
38 | url: ${{ steps.deploy-to-webapp.outputs.webapp-url }}
39 |
40 | steps:
41 | - name: Download artifact from build job
42 | uses: actions/download-artifact@v2
43 | with:
44 | name: node-app
45 |
46 | - name: 'Deploy to Azure Web App'
47 | id: deploy-to-webapp
48 | uses: azure/webapps-deploy@v2
49 | with:
50 | app-name: 'exam-simulation'
51 | slot-name: 'Production'
52 | publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_8382F1C3F78845208D2B79C8F0B8FB8B }}
53 | package: .
54 |
--------------------------------------------------------------------------------
/dist/routes/api/utils/routes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5 | }) : (function(o, m, k, k2) {
6 | if (k2 === undefined) k2 = k;
7 | o[k2] = m[k];
8 | }));
9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 | Object.defineProperty(o, "default", { enumerable: true, value: v });
11 | }) : function(o, v) {
12 | o["default"] = v;
13 | });
14 | var __importStar = (this && this.__importStar) || function (mod) {
15 | if (mod && mod.__esModule) return mod;
16 | var result = {};
17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 | __setModuleDefault(result, mod);
19 | return result;
20 | };
21 | var __importDefault = (this && this.__importDefault) || function (mod) {
22 | return (mod && mod.__esModule) ? mod : { "default": mod };
23 | };
24 | Object.defineProperty(exports, "__esModule", { value: true });
25 | const express_1 = __importDefault(require("express"));
26 | const utilsController = __importStar(require("../../../controllers/utilsController"));
27 | const auth_1 = __importDefault(require("../../../middlewares/auth"));
28 | const multer_1 = __importDefault(require("../../../utils/multer"));
29 | const router = express_1.default.Router();
30 | router.post('/upload/images', auth_1.default, multer_1.default.array('image'), utilsController.uploadImages);
31 | exports.default = router;
32 |
--------------------------------------------------------------------------------
/dist/server.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
4 | };
5 | Object.defineProperty(exports, "__esModule", { value: true });
6 | const express_1 = __importDefault(require("express"));
7 | const cors_1 = __importDefault(require("cors"));
8 | const custom_error_1 = __importDefault(require("./errors/custom-error"));
9 | const response_handler_1 = require("./utils/response-handler");
10 | const routes_1 = __importDefault(require("./routes/api/exam/routes"));
11 | const routes_2 = __importDefault(require("./routes/api/user/routes"));
12 | const routes_3 = __importDefault(require("./routes/api/utils/routes"));
13 | const dbConnection_1 = require("./database/dbConnection");
14 | const app = (0, express_1.default)();
15 | app.use((0, cors_1.default)({
16 | credentials: true,
17 | origin: (origin, callback) => {
18 | return callback(null, true);
19 | },
20 | }));
21 | app.use(express_1.default.json());
22 | require("dotenv").config();
23 | app.use("/exam", routes_1.default);
24 | app.use("/user", routes_2.default);
25 | app.use("/utils", routes_3.default);
26 | //connecting database
27 | (0, dbConnection_1.connectDatabase)();
28 | //===================
29 | // All middlewares goes above this
30 | app.all("*", (req, res, next) => {
31 | const err = new custom_error_1.default("Non-existant route", 404);
32 | next(err);
33 | });
34 | app.use((err, req, res, next) => {
35 | if (err instanceof custom_error_1.default) {
36 | return res.status(err.statusCode).json((0, response_handler_1.ErrorResponse)(err));
37 | }
38 | return res.status(500).json((0, response_handler_1.ErrorResponse)(err));
39 | });
40 | const PORT = process.env.PORT || 5000;
41 | app.listen(PORT, () => {
42 | console.log(`Server started on port ${PORT}`);
43 | });
44 |
--------------------------------------------------------------------------------
/dist/controllers/utilsController.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __importDefault = (this && this.__importDefault) || function (mod) {
12 | return (mod && mod.__esModule) ? mod : { "default": mod };
13 | };
14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | exports.uploadImages = void 0;
16 | const catchAsync_1 = __importDefault(require("../utils/catchAsync"));
17 | const response_handler_1 = require("../utils/response-handler");
18 | const cloudinary_1 = __importDefault(require("../utils/cloudinary"));
19 | const fs_1 = __importDefault(require("fs"));
20 | exports.uploadImages = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
21 | const uploader = (path) => __awaiter(void 0, void 0, void 0, function* () { return yield (0, cloudinary_1.default)(path); });
22 | const files = req.files;
23 | const urls = [];
24 | for (const file of files) {
25 | const { path } = file;
26 | const newPath = yield uploader(path);
27 | urls.push(newPath);
28 | fs_1.default.unlinkSync(path);
29 | }
30 | return res.status(200).json((0, response_handler_1.SuccessResponse)({ urls }, 'Successfully uploaded images'));
31 | }));
32 |
--------------------------------------------------------------------------------
/dist/database/dbConnection.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __importDefault = (this && this.__importDefault) || function (mod) {
12 | return (mod && mod.__esModule) ? mod : { "default": mod };
13 | };
14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | exports.getDb = exports.connectDatabase = void 0;
16 | const promise_1 = __importDefault(require("mysql2/promise"));
17 | const examFunctions_1 = require("../utils/examFunctions");
18 | let db;
19 | const connectDatabase = () => __awaiter(void 0, void 0, void 0, function* () {
20 | db = yield promise_1.default.createConnection({
21 | host: 'mydbinstance.cyfjjout6pho.ap-south-1.rds.amazonaws.com',
22 | user: 'subho57',
23 | password: 'adminDBpassword123',
24 | port: 3306,
25 | database: 'examSimulation',
26 | multipleStatements: true,
27 | connectTimeout: 60 * 60 * 1000,
28 | // debug: true,
29 | });
30 | if (db) {
31 | console.log('Database Connected !');
32 | (0, examFunctions_1.scheduleOnServerRestart)();
33 | }
34 | else
35 | console.log('Database Not Connected !');
36 | });
37 | exports.connectDatabase = connectDatabase;
38 | const getDb = () => {
39 | return db;
40 | };
41 | exports.getDb = getDb;
42 | exports.default = db;
43 |
--------------------------------------------------------------------------------
/dist/routes/api/user/routes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5 | }) : (function(o, m, k, k2) {
6 | if (k2 === undefined) k2 = k;
7 | o[k2] = m[k];
8 | }));
9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 | Object.defineProperty(o, "default", { enumerable: true, value: v });
11 | }) : function(o, v) {
12 | o["default"] = v;
13 | });
14 | var __importStar = (this && this.__importStar) || function (mod) {
15 | if (mod && mod.__esModule) return mod;
16 | var result = {};
17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 | __setModuleDefault(result, mod);
19 | return result;
20 | };
21 | var __importDefault = (this && this.__importDefault) || function (mod) {
22 | return (mod && mod.__esModule) ? mod : { "default": mod };
23 | };
24 | Object.defineProperty(exports, "__esModule", { value: true });
25 | const express_1 = __importDefault(require("express"));
26 | const userController = __importStar(require("../../../controllers/userController"));
27 | const auth_1 = __importDefault(require("../../../middlewares/auth"));
28 | const multer_1 = __importDefault(require("../../../utils/multer"));
29 | const router = express_1.default.Router();
30 | router.post('/login', userController.loginUser);
31 | router.post('/register', userController.registerUser);
32 | router.post('/logout', auth_1.default, userController.logoutUser);
33 | router.get('/current', auth_1.default, userController.getCurrentUser);
34 | router.get('/exams-hosted', auth_1.default, userController.getExamHosted);
35 | router.get('/exams-given', auth_1.default, userController.getExamGiven);
36 | router.get('/:id', userController.getuser);
37 | router.patch('/', auth_1.default, multer_1.default.single('image'), userController.editUser);
38 | exports.default = router;
39 |
--------------------------------------------------------------------------------
/dist/routes/api/exam/routes.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3 | if (k2 === undefined) k2 = k;
4 | Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5 | }) : (function(o, m, k, k2) {
6 | if (k2 === undefined) k2 = k;
7 | o[k2] = m[k];
8 | }));
9 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10 | Object.defineProperty(o, "default", { enumerable: true, value: v });
11 | }) : function(o, v) {
12 | o["default"] = v;
13 | });
14 | var __importStar = (this && this.__importStar) || function (mod) {
15 | if (mod && mod.__esModule) return mod;
16 | var result = {};
17 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18 | __setModuleDefault(result, mod);
19 | return result;
20 | };
21 | var __importDefault = (this && this.__importDefault) || function (mod) {
22 | return (mod && mod.__esModule) ? mod : { "default": mod };
23 | };
24 | Object.defineProperty(exports, "__esModule", { value: true });
25 | const express_1 = __importDefault(require("express"));
26 | const examController = __importStar(require("../../../controllers/examController"));
27 | const auth_1 = __importDefault(require("../../../middlewares/auth"));
28 | const router = express_1.default.Router();
29 | router.get('/testing-route', examController.testing);
30 | router.get('/tags', examController.getTags);
31 | router.get('/question-types', examController.getQuestionTypes);
32 | router.get('/create-tables', examController.createTables);
33 | router.post('/create', auth_1.default, examController.createExam);
34 | router.get('/evaluate/force', examController.forceEvaluateExam);
35 | router.get('/exam-registered', auth_1.default, examController.examRegisterStatus);
36 | router.get('/solution', auth_1.default, examController.getExamSolution);
37 | router.get('/scores', auth_1.default, examController.getExamScores);
38 | router.patch('/:id', auth_1.default, examController.editExam);
39 | router.post('/all', examController.getExams);
40 | router.get('/:id', examController.getExamDetails);
41 | router.get('/:id/start', auth_1.default, examController.startExam);
42 | router.post('/register', auth_1.default, examController.registerInExam);
43 | router.post('/submit', auth_1.default, examController.submitExam);
44 | exports.default = router;
45 |
--------------------------------------------------------------------------------
/prisma/migrations/20220406082854_init/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `created_at` on the `ExamParticipant` table. All the data in the column will be lost.
5 | - You are about to drop the column `updated_at` on the `ExamParticipant` table. All the data in the column will be lost.
6 | - You are about to drop the column `created_at` on the `ExamTag` table. All the data in the column will be lost.
7 | - You are about to drop the column `updated_at` on the `ExamTag` table. All the data in the column will be lost.
8 | - You are about to drop the column `created_at` on the `PrivateExamEmail` table. All the data in the column will be lost.
9 | - You are about to drop the column `updated_at` on the `PrivateExamEmail` table. All the data in the column will be lost.
10 | - The primary key for the `QuestionUserAnswer` table will be changed. If it partially fails, the table could be left without primary key constraint.
11 | - You are about to drop the column `id` on the `QuestionUserAnswer` table. All the data in the column will be lost.
12 | - A unique constraint covering the columns `[exam_id,question_id,user_id]` on the table `QuestionUserAnswer` will be added. If there are existing duplicate values, this will fail.
13 | - Added the required column `updated_at` to the `Question` table without a default value. This is not possible if the table is not empty.
14 | - Added the required column `updated_at` to the `QuestionVariant` table without a default value. This is not possible if the table is not empty.
15 |
16 | */
17 | -- AlterTable
18 | ALTER TABLE `ExamParticipant` DROP COLUMN `created_at`,
19 | DROP COLUMN `updated_at`;
20 |
21 | -- AlterTable
22 | ALTER TABLE `ExamTag` DROP COLUMN `created_at`,
23 | DROP COLUMN `updated_at`;
24 |
25 | -- AlterTable
26 | ALTER TABLE `PrivateExamEmail` DROP COLUMN `created_at`,
27 | DROP COLUMN `updated_at`;
28 |
29 | -- AlterTable
30 | ALTER TABLE `Question` ADD COLUMN `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
31 | ADD COLUMN `updated_at` DATETIME(3) NOT NULL;
32 |
33 | -- AlterTable
34 | ALTER TABLE `QuestionUserAnswer` DROP PRIMARY KEY,
35 | DROP COLUMN `id`;
36 |
37 | -- AlterTable
38 | ALTER TABLE `QuestionVariant` ADD COLUMN `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
39 | ADD COLUMN `updated_at` DATETIME(3) NOT NULL;
40 |
41 | -- CreateIndex
42 | CREATE UNIQUE INDEX `QuestionUserAnswer_exam_id_question_id_user_id_key` ON `QuestionUserAnswer`(`exam_id`, `question_id`, `user_id`);
43 |
--------------------------------------------------------------------------------
/prisma/migrations/20220406211848_init/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `Institution` on the `users` table. All the data in the column will be lost.
5 | - You are about to drop the `question_exam_submissions` table. If the table is not empty, all the data it contains will be lost.
6 |
7 | */
8 | -- DropForeignKey
9 | ALTER TABLE `question_exam_submissions` DROP FOREIGN KEY `question_exam_submissions_exam_id_fkey`;
10 |
11 | -- DropForeignKey
12 | ALTER TABLE `question_exam_submissions` DROP FOREIGN KEY `question_exam_submissions_question_id_fkey`;
13 |
14 | -- DropForeignKey
15 | ALTER TABLE `question_exam_submissions` DROP FOREIGN KEY `question_exam_submissions_user_id_fkey`;
16 |
17 | -- AlterTable
18 | ALTER TABLE `users` DROP COLUMN `Institution`,
19 | ADD COLUMN `institution` VARCHAR(191) NULL;
20 |
21 | -- DropTable
22 | DROP TABLE `question_exam_submissions`;
23 |
24 | -- CreateTable
25 | CREATE TABLE `question_submissions` (
26 | `exam_id` INTEGER NOT NULL,
27 | `question_id` INTEGER NOT NULL,
28 | `user_id` INTEGER NOT NULL,
29 | `option` VARCHAR(191) NOT NULL,
30 |
31 | UNIQUE INDEX `question_submissions_exam_id_question_id_user_id_option_key`(`exam_id`, `question_id`, `user_id`, `option`)
32 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
33 |
34 | -- CreateTable
35 | CREATE TABLE `exam_submissions` (
36 | `exam_id` INTEGER NOT NULL,
37 | `question_id` INTEGER NOT NULL,
38 | `user_id` INTEGER NOT NULL,
39 | `score` INTEGER NOT NULL DEFAULT 0,
40 |
41 | UNIQUE INDEX `exam_submissions_exam_id_key`(`exam_id`),
42 | UNIQUE INDEX `exam_submissions_exam_id_question_id_user_id_key`(`exam_id`, `question_id`, `user_id`)
43 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
44 |
45 | -- CreateTable
46 | CREATE TABLE `question_options` (
47 | `question_id` INTEGER NOT NULL,
48 | `option` VARCHAR(191) NOT NULL,
49 |
50 | UNIQUE INDEX `question_options_question_id_option_key`(`question_id`, `option`)
51 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
52 |
53 | -- AddForeignKey
54 | ALTER TABLE `question_submissions` ADD CONSTRAINT `question_submissions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
55 |
56 | -- AddForeignKey
57 | ALTER TABLE `question_submissions` ADD CONSTRAINT `question_submissions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
58 |
59 | -- AddForeignKey
60 | ALTER TABLE `exam_submissions` ADD CONSTRAINT `exam_submissions_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
61 |
62 | -- AddForeignKey
63 | ALTER TABLE `exam_submissions` ADD CONSTRAINT `exam_submissions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
64 |
65 | -- AddForeignKey
66 | ALTER TABLE `exam_submissions` ADD CONSTRAINT `exam_submissions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
67 |
68 | -- AddForeignKey
69 | ALTER TABLE `question_options` ADD CONSTRAINT `question_options_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
70 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | generator client {
5 | provider = "prisma-client-js"
6 | }
7 |
8 | datasource db {
9 | provider = "mysql"
10 | url = env("DATABASE_URL")
11 | }
12 |
13 | model User {
14 | id Int @id @default(autoincrement())
15 | name String
16 | email String @unique
17 | bio String?
18 | dob DateTime
19 | address String? @db.VarChar(255)
20 | country String?
21 | profile_image String?
22 | password String
23 | latest_degree String?
24 | institution String?
25 | gender String?
26 | country_code Int
27 | phone String
28 | exams_created Exam[]
29 | created_at DateTime @default(now())
30 | updated_at DateTime @updatedAt
31 | exams_participated ExamParticipant[]
32 | exam_question_scores ExamSubmission[]
33 |
34 | @@map("users")
35 | }
36 |
37 | model Exam {
38 | id Int @id @default(autoincrement())
39 | creator_id Int
40 | creator User @relation(fields: [creator_id], references: [id])
41 | name String
42 | description String?
43 | banner String?
44 | thumbnail String?
45 | start_time DateTime
46 | duration BigInt
47 | ongoing Boolean @default(false)
48 | is_private Boolean @default(false)
49 | participant_count Int @default(0)
50 | created_at DateTime @default(now())
51 | updated_at DateTime @updatedAt
52 | exam_participants ExamParticipant[]
53 | private_exam_emails PrivateExamEmail[]
54 | exam_tags ExamTag[]
55 | exam_question_scores ExamSubmission[]
56 | exam_questions ExamQuestion[]
57 | exam_questions_submissions QuestionSubmission[]
58 |
59 | @@map("exams")
60 | }
61 |
62 | model ExamParticipant {
63 | exam_id Int
64 | participant_id Int
65 | exam Exam @relation(fields: [exam_id], references: [id])
66 | user User @relation(fields: [participant_id], references: [id])
67 | score Int?
68 | finish_time Int?
69 | rank Int?
70 |
71 | @@unique([exam_id, participant_id])
72 | @@map("exam_participants")
73 | }
74 |
75 | model PrivateExamEmail {
76 | examId Int
77 | exam Exam @relation(fields: [examId], references: [id])
78 | email String
79 |
80 | @@unique([examId, email])
81 | @@map("private_exam_emails")
82 | }
83 |
84 | model Tag {
85 | id Int @id @default(autoincrement())
86 | tag String @unique
87 | exam_tags ExamTag[]
88 | created_at DateTime @default(now())
89 | updated_at DateTime @updatedAt
90 |
91 | @@map("tags")
92 | }
93 |
94 | model ExamTag {
95 | exam_id Int
96 | tag_id Int
97 | exam Exam @relation(fields: [exam_id], references: [id])
98 | tag Tag @relation(fields: [tag_id], references: [id])
99 |
100 | @@unique([exam_id, tag_id])
101 | @@map("exam_tags")
102 | }
103 |
104 | model Question {
105 | id Int @id @default(autoincrement())
106 | question String
107 | variant_id Int
108 | variant QuestionVariant @relation(fields: [variant_id], references: [id])
109 | solution String
110 | user_submissions QuestionSubmission[]
111 | exam_questions ExamQuestion[]
112 | created_at DateTime @default(now())
113 | updated_at DateTime @updatedAt
114 | question_options QuestionOption[]
115 | exam_question_scores ExamSubmission[]
116 |
117 | @@map("questions")
118 | }
119 |
120 | model ExamQuestion {
121 | exam_id Int
122 | question_id Int
123 | exam Exam @relation(fields: [exam_id], references: [id])
124 | question Question @relation(fields: [question_id], references: [id])
125 | mark Int
126 | negative_mark Int
127 |
128 | @@unique([exam_id, question_id])
129 | @@map("exam_questions")
130 | }
131 |
132 | model QuestionVariant {
133 | id Int @id @default(autoincrement())
134 | variant String @unique
135 | questions Question[]
136 | created_at DateTime @default(now())
137 | updated_at DateTime @updatedAt
138 |
139 | @@map("question_variants")
140 | }
141 |
142 | model QuestionSubmission {
143 | exam_id Int
144 | exam Exam @relation(fields: [exam_id], references: [id])
145 | question_id Int
146 | question Question @relation(fields: [question_id], references: [id])
147 | user_id Int
148 | option String
149 |
150 | @@unique([exam_id, question_id, user_id, option])
151 | @@map("question_submissions")
152 | }
153 |
154 | model ExamSubmission {
155 | exam_id Int @unique
156 | exam Exam @relation(fields: [exam_id], references: [id])
157 | question_id Int
158 | question Question @relation(fields: [question_id], references: [id])
159 | user_id Int
160 | user User @relation(fields: [user_id], references: [id])
161 | score Int @default(0)
162 |
163 | @@unique([exam_id, question_id, user_id])
164 | @@map("exam_submissions")
165 | }
166 |
167 | model QuestionOption {
168 | question_id Int
169 | question Question @relation(fields: [question_id], references: [id])
170 | option String
171 |
172 | @@unique([question_id, option])
173 | @@map("question_options")
174 | }
175 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */
4 | /* Basic Options */
5 | // "incremental": true, /* Enable incremental compilation */
6 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
7 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
8 | // "lib": [], /* Specify library files to be included in the compilation. */
9 | // "allowJs": true, /* Allow javascript files to be compiled. */
10 | // "checkJs": true, /* Report errors in .js files. */
11 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
12 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
14 | // "sourceMap": true, /* Generates corresponding '.map' file. */
15 | // "outFile": "./", /* Concatenate and emit output to single file. */
16 | "outDir": "./dist" /* Redirect output structure to the directory. */,
17 | "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
18 | // "composite": true, /* Enable project compilation */
19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
20 | // "removeComments": true, /* Do not emit comments to output. */
21 | // "noEmit": true, /* Do not emit outputs. */
22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
25 |
26 | /* Strict Type-Checking Options */
27 | "strict": true /* Enable all strict type-checking options. */,
28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
29 | // "strictNullChecks": true, /* Enable strict null checks. */
30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
35 |
36 | /* Additional Checks */
37 | // "noUnusedLocals": true, /* Report errors on unused locals. */
38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
41 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
42 |
43 | /* Module Resolution Options */
44 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
48 | // "typeRoots": [], /* List of folders to include type definitions from. */
49 | // "types": [], /* Type declaration files to be included in compilation. */
50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
51 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
54 |
55 | /* Source Map Options */
56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
60 |
61 | /* Experimental Options */
62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
64 |
65 | /* Advanced Options */
66 | "skipLibCheck": true /* Skip type checking of declaration files. */,
67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/controllers/userController.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../utils/catchAsync';
2 | import { SuccessResponse } from '../utils/response-handler';
3 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest';
4 | import CustomError from '../errors/custom-error';
5 | import { Response, Request } from 'express';
6 | import { getDb } from '../database/dbConnection';
7 | import { v4 as uuid } from 'uuid';
8 | import jwt from 'jsonwebtoken';
9 | import bcrypt from 'bcrypt';
10 |
11 | const defaultDp =
12 | 'https://www.tenforums.com/geek/gars/images/2/types/thumb_15951118880user.png';
13 |
14 | export const getCurrentUser = catchAsync(
15 | async (req: CustomRequest, res: Response) => {
16 | return res.status(200).json(SuccessResponse(req.user, 'User found'));
17 | }
18 | );
19 |
20 | export const getuser = catchAsync(async (req: CustomRequest, res: Response) => {
21 | const db = getDb();
22 | const id: String = req.params.id || '';
23 | let query = 'select * from `User` where `id` = ?';
24 | const [rows, fields] = await db.execute(query, [id]);
25 | if (rows && rows.length > 0) {
26 | res.status(200).json(SuccessResponse(rows[0], 'User Found !'));
27 | //console.log(rows);
28 | } else res.status(500).send('User Not Found !');
29 | });
30 |
31 | //Register =====================================
32 | export const registerUser = catchAsync(async (req: Request, res: Response) => {
33 | const db = getDb();
34 | const {
35 | name,
36 | email,
37 | bio,
38 | password,
39 | institution,
40 | phoneNumber,
41 | dob,
42 | address,
43 | gender,
44 | } = req.body;
45 | if (
46 | !name ||
47 | !email ||
48 | !password ||
49 | !institution ||
50 | !phoneNumber ||
51 | !dob ||
52 | !gender
53 | )
54 | throw new CustomError('Some Fields are missing !', 500);
55 | let findByEmail =
56 | 'select count(*) as userExists from `User` where `email`=? limit 1';
57 | let [rows] = await db.execute(findByEmail, [email]);
58 | if (rows[0].userExists)
59 | throw new CustomError('User with this email id already exists !', 500);
60 | let registerUser =
61 | 'insert into `User` (`id`,`name`,`email`,`bio`,`image`,`password`,`institution`,`phoneNumber`,`dob`,`address`,`gender`) values(?,?,?,?,?,?,?,?,?,?,?)';
62 | const salt = await bcrypt.genSalt();
63 | const passwordHash = await bcrypt.hash(password, salt);
64 | let id = uuid();
65 | const result = await db.execute(registerUser, [
66 | id,
67 | name,
68 | email,
69 | bio || ' ',
70 | defaultDp,
71 | passwordHash,
72 | institution,
73 | phoneNumber,
74 | dob,
75 | address || ' ',
76 | gender,
77 | ]);
78 | const user = {
79 | id,
80 | name,
81 | email,
82 | bio,
83 | image: defaultDp,
84 | institution,
85 | phoneNumber,
86 | dob,
87 | address: address || ' ',
88 | gender,
89 | };
90 | const token = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET!);
91 | if (result)
92 | res.status(200).json(SuccessResponse({ token, user }, 'User Inserted !'));
93 | else throw new CustomError('User not inserted!', 500);
94 | });
95 | //==============================================
96 |
97 | //login ========================================
98 | export const loginUser = catchAsync(async (req: Request, res: Response) => {
99 | const db = getDb();
100 | const { email, password } = req.body;
101 | if (!email || !password)
102 | throw new CustomError('Some Fields are missing !', 500);
103 | let findByEmail = 'select * from `User` where `email`=?';
104 | let [rows, fields] = await db.execute(findByEmail, [email]);
105 | if (rows.length != 1) throw new CustomError('No User with this Email!', 500);
106 | const user = rows[0];
107 | const isMatch = await bcrypt.compare(password, user.password);
108 | delete user.password;
109 | if (!isMatch) throw new CustomError("Password does'nt match !", 404);
110 | const token = jwt.sign(user, process.env.ACCESS_TOKEN_SECRET!);
111 | res.status(200).json(SuccessResponse({ token, user }, 'User Logged in!'));
112 | });
113 | //==============================================
114 |
115 | //logout ========================================
116 | export const logoutUser = catchAsync(
117 | async (req: CustomRequest, res: Response) => {
118 | res.status(200).send('Logout Successfull !');
119 | }
120 | );
121 | //================================================
122 |
123 | //edit==========================================
124 | export const editUser = catchAsync(
125 | async (req: CustomRequest, res: Response) => {
126 | const db = getDb();
127 | const userId = req.user?.id;
128 | const { name, bio, institution, phoneNumber, dob, address, image, gender } =
129 | req.body;
130 | if (!name || !institution || !phoneNumber || !dob || !gender)
131 | throw new CustomError('Some Fields are missing !', 500);
132 | let query =
133 | 'update `User` set `name`=?, bio=?, `institution`=?, `phoneNumber`=?, `dob`=?, `address`=?, `image`=?, `gender`=? where `id`=?';
134 | const [rows] = await db.execute(query, [
135 | name,
136 | bio,
137 | institution,
138 | phoneNumber,
139 | dob,
140 | address,
141 | image,
142 | gender,
143 | userId,
144 | ]);
145 | if (!rows.affectedRows) throw new CustomError('User Not updated', 500);
146 | return res.status(200).json(SuccessResponse(req.user, 'User Updated!'));
147 | }
148 | );
149 | //==============================================
150 |
151 | export const getExamHosted = catchAsync(
152 | async (req: CustomRequest, res: Response) => {
153 | const db = getDb();
154 | const userId = req.user?.id;
155 | let query =
156 | 'select id,name,description,image,userId,tags,startTime,duration,ongoing,finished,isPrivate from `Exam` where `userId`=?';
157 | const [rows] = await db.execute(query, [userId]);
158 | res.status(200).json(SuccessResponse(rows, 'The Exam hosted are'));
159 | }
160 | );
161 |
162 | export const getExamGiven = catchAsync(
163 | async (req: CustomRequest, res: Response) => {
164 | const db = getDb();
165 | const userId = req.user?.id;
166 | console.log(req.user);
167 | const query =
168 | 'select e.* from `Exam` as e,`Exam-Participants` as ep where ep.`examId`=e.`id` and ep.`participantId`=?';
169 | const [rows] = await db.execute(query, [userId]);
170 | res.status(200).json(SuccessResponse(rows, 'Exams given are : '));
171 | }
172 | );
173 |
--------------------------------------------------------------------------------
/prisma/migrations/20220406082433_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE `User` (
3 | `id` INTEGER NOT NULL AUTO_INCREMENT,
4 | `name` VARCHAR(191) NOT NULL,
5 | `email` VARCHAR(191) NOT NULL,
6 | `bio` VARCHAR(191) NULL,
7 | `dob` DATETIME(3) NOT NULL,
8 | `address` VARCHAR(255) NULL,
9 | `country` VARCHAR(191) NULL,
10 | `profile_image` VARCHAR(191) NULL,
11 | `password` VARCHAR(191) NOT NULL,
12 | `latest_degree` VARCHAR(191) NULL,
13 | `Institution` VARCHAR(191) NULL,
14 | `gender` VARCHAR(191) NULL,
15 | `country_code` INTEGER NOT NULL,
16 | `phone` VARCHAR(191) NOT NULL,
17 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
18 | `updated_at` DATETIME(3) NOT NULL,
19 |
20 | UNIQUE INDEX `User_email_key`(`email`),
21 | PRIMARY KEY (`id`)
22 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
23 |
24 | -- CreateTable
25 | CREATE TABLE `Exam` (
26 | `id` INTEGER NOT NULL AUTO_INCREMENT,
27 | `creator_id` INTEGER NOT NULL,
28 | `name` VARCHAR(191) NOT NULL,
29 | `description` VARCHAR(191) NULL,
30 | `banner` VARCHAR(191) NULL,
31 | `thumbnail` VARCHAR(191) NULL,
32 | `start_time` DATETIME(3) NOT NULL,
33 | `duration` BIGINT NOT NULL,
34 | `ongoing` BOOLEAN NOT NULL DEFAULT false,
35 | `is_private` BOOLEAN NOT NULL DEFAULT false,
36 | `participant_count` INTEGER NOT NULL DEFAULT 0,
37 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
38 | `updated_at` DATETIME(3) NOT NULL,
39 |
40 | PRIMARY KEY (`id`)
41 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
42 |
43 | -- CreateTable
44 | CREATE TABLE `ExamParticipant` (
45 | `exam_id` INTEGER NOT NULL,
46 | `participant_id` INTEGER NOT NULL,
47 | `score` INTEGER NULL,
48 | `finish_time` INTEGER NULL,
49 | `rank` INTEGER NULL,
50 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
51 | `updated_at` DATETIME(3) NOT NULL,
52 |
53 | UNIQUE INDEX `ExamParticipant_exam_id_participant_id_key`(`exam_id`, `participant_id`)
54 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
55 |
56 | -- CreateTable
57 | CREATE TABLE `PrivateExamEmail` (
58 | `examId` INTEGER NOT NULL,
59 | `email` VARCHAR(191) NOT NULL,
60 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
61 | `updated_at` DATETIME(3) NOT NULL,
62 |
63 | UNIQUE INDEX `PrivateExamEmail_examId_email_key`(`examId`, `email`)
64 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
65 |
66 | -- CreateTable
67 | CREATE TABLE `Tag` (
68 | `id` INTEGER NOT NULL AUTO_INCREMENT,
69 | `tag` VARCHAR(191) NOT NULL,
70 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
71 | `updated_at` DATETIME(3) NOT NULL,
72 |
73 | UNIQUE INDEX `Tag_tag_key`(`tag`),
74 | PRIMARY KEY (`id`)
75 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
76 |
77 | -- CreateTable
78 | CREATE TABLE `ExamTag` (
79 | `exam_id` INTEGER NOT NULL,
80 | `tag_id` INTEGER NOT NULL,
81 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
82 | `updated_at` DATETIME(3) NOT NULL,
83 |
84 | UNIQUE INDEX `ExamTag_exam_id_tag_id_key`(`exam_id`, `tag_id`)
85 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
86 |
87 | -- CreateTable
88 | CREATE TABLE `Question` (
89 | `id` INTEGER NOT NULL AUTO_INCREMENT,
90 | `question` VARCHAR(191) NOT NULL,
91 | `variant_id` INTEGER NOT NULL,
92 | `solution` VARCHAR(191) NOT NULL,
93 |
94 | PRIMARY KEY (`id`)
95 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
96 |
97 | -- CreateTable
98 | CREATE TABLE `ExamQuestion` (
99 | `exam_id` INTEGER NOT NULL,
100 | `question_id` INTEGER NOT NULL,
101 | `mark` INTEGER NOT NULL,
102 | `negative_mark` INTEGER NOT NULL,
103 |
104 | UNIQUE INDEX `ExamQuestion_exam_id_question_id_key`(`exam_id`, `question_id`)
105 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
106 |
107 | -- CreateTable
108 | CREATE TABLE `QuestionVariant` (
109 | `id` INTEGER NOT NULL AUTO_INCREMENT,
110 | `variant` VARCHAR(191) NOT NULL,
111 |
112 | UNIQUE INDEX `QuestionVariant_variant_key`(`variant`),
113 | PRIMARY KEY (`id`)
114 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
115 |
116 | -- CreateTable
117 | CREATE TABLE `QuestionUserAnswer` (
118 | `id` INTEGER NOT NULL AUTO_INCREMENT,
119 | `exam_id` INTEGER NOT NULL,
120 | `question_id` INTEGER NOT NULL,
121 | `user_id` INTEGER NOT NULL,
122 | `given_answer` VARCHAR(191) NOT NULL,
123 | `score` INTEGER NOT NULL DEFAULT 0,
124 |
125 | PRIMARY KEY (`id`)
126 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
127 |
128 | -- AddForeignKey
129 | ALTER TABLE `Exam` ADD CONSTRAINT `Exam_creator_id_fkey` FOREIGN KEY (`creator_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
130 |
131 | -- AddForeignKey
132 | ALTER TABLE `ExamParticipant` ADD CONSTRAINT `ExamParticipant_participant_id_fkey` FOREIGN KEY (`participant_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
133 |
134 | -- AddForeignKey
135 | ALTER TABLE `ExamParticipant` ADD CONSTRAINT `ExamParticipant_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
136 |
137 | -- AddForeignKey
138 | ALTER TABLE `PrivateExamEmail` ADD CONSTRAINT `PrivateExamEmail_examId_fkey` FOREIGN KEY (`examId`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
139 |
140 | -- AddForeignKey
141 | ALTER TABLE `ExamTag` ADD CONSTRAINT `ExamTag_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
142 |
143 | -- AddForeignKey
144 | ALTER TABLE `ExamTag` ADD CONSTRAINT `ExamTag_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `Tag`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
145 |
146 | -- AddForeignKey
147 | ALTER TABLE `Question` ADD CONSTRAINT `Question_variant_id_fkey` FOREIGN KEY (`variant_id`) REFERENCES `QuestionVariant`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
148 |
149 | -- AddForeignKey
150 | ALTER TABLE `ExamQuestion` ADD CONSTRAINT `ExamQuestion_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
151 |
152 | -- AddForeignKey
153 | ALTER TABLE `ExamQuestion` ADD CONSTRAINT `ExamQuestion_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `Question`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
154 |
155 | -- AddForeignKey
156 | ALTER TABLE `QuestionUserAnswer` ADD CONSTRAINT `QuestionUserAnswer_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `User`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
157 |
158 | -- AddForeignKey
159 | ALTER TABLE `QuestionUserAnswer` ADD CONSTRAINT `QuestionUserAnswer_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `Exam`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
160 |
161 | -- AddForeignKey
162 | ALTER TABLE `QuestionUserAnswer` ADD CONSTRAINT `QuestionUserAnswer_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `Question`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
163 |
--------------------------------------------------------------------------------
/dist/controllers/userController.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __importDefault = (this && this.__importDefault) || function (mod) {
12 | return (mod && mod.__esModule) ? mod : { "default": mod };
13 | };
14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | exports.getExamGiven = exports.getExamHosted = exports.editUser = exports.logoutUser = exports.loginUser = exports.registerUser = exports.getuser = exports.getCurrentUser = void 0;
16 | const catchAsync_1 = __importDefault(require("../utils/catchAsync"));
17 | const response_handler_1 = require("../utils/response-handler");
18 | const custom_error_1 = __importDefault(require("../errors/custom-error"));
19 | const dbConnection_1 = require("../database/dbConnection");
20 | const uuid_1 = require("uuid");
21 | const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
22 | const bcrypt_1 = __importDefault(require("bcrypt"));
23 | const defaultDp = 'https://www.tenforums.com/geek/gars/images/2/types/thumb_15951118880user.png';
24 | exports.getCurrentUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
25 | return res.status(200).json((0, response_handler_1.SuccessResponse)(req.user, 'User found'));
26 | }));
27 | exports.getuser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
28 | const db = (0, dbConnection_1.getDb)();
29 | const id = req.params.id || '';
30 | let query = 'select * from `User` where `id` = ?';
31 | const [rows, fields] = yield db.execute(query, [id]);
32 | if (rows && rows.length > 0) {
33 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows[0], 'User Found !'));
34 | //console.log(rows);
35 | }
36 | else
37 | res.status(500).send('User Not Found !');
38 | }));
39 | //Register =====================================
40 | exports.registerUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
41 | const db = (0, dbConnection_1.getDb)();
42 | const { name, email, bio, password, institution, phoneNumber, dob, address, gender, } = req.body;
43 | if (!name ||
44 | !email ||
45 | !password ||
46 | !institution ||
47 | !phoneNumber ||
48 | !dob ||
49 | !gender)
50 | throw new custom_error_1.default('Some Fields are missing !', 500);
51 | let findByEmail = 'select count(*) as userExists from `User` where `email`=? limit 1';
52 | let [rows] = yield db.execute(findByEmail, [email]);
53 | if (rows[0].userExists)
54 | throw new custom_error_1.default('User with this email id already exists !', 500);
55 | let registerUser = 'insert into `User` (`id`,`name`,`email`,`bio`,`image`,`password`,`institution`,`phoneNumber`,`dob`,`address`,`gender`) values(?,?,?,?,?,?,?,?,?,?,?)';
56 | const salt = yield bcrypt_1.default.genSalt();
57 | const passwordHash = yield bcrypt_1.default.hash(password, salt);
58 | let id = (0, uuid_1.v4)();
59 | const result = yield db.execute(registerUser, [
60 | id,
61 | name,
62 | email,
63 | bio || ' ',
64 | defaultDp,
65 | passwordHash,
66 | institution,
67 | phoneNumber,
68 | dob,
69 | address || ' ',
70 | gender,
71 | ]);
72 | const user = {
73 | id,
74 | name,
75 | email,
76 | bio,
77 | image: defaultDp,
78 | institution,
79 | phoneNumber,
80 | dob,
81 | address: address || ' ',
82 | gender,
83 | };
84 | const token = jsonwebtoken_1.default.sign(user, process.env.ACCESS_TOKEN_SECRET);
85 | if (result)
86 | res.status(200).json((0, response_handler_1.SuccessResponse)({ token, user }, 'User Inserted !'));
87 | else
88 | throw new custom_error_1.default('User not inserted!', 500);
89 | }));
90 | //==============================================
91 | //login ========================================
92 | exports.loginUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
93 | const db = (0, dbConnection_1.getDb)();
94 | const { email, password } = req.body;
95 | if (!email || !password)
96 | throw new custom_error_1.default('Some Fields are missing !', 500);
97 | let findByEmail = 'select * from `User` where `email`=?';
98 | let [rows, fields] = yield db.execute(findByEmail, [email]);
99 | if (rows.length != 1)
100 | throw new custom_error_1.default('No User with this Email!', 500);
101 | const user = rows[0];
102 | const isMatch = yield bcrypt_1.default.compare(password, user.password);
103 | delete user.password;
104 | if (!isMatch)
105 | throw new custom_error_1.default("Password does'nt match !", 404);
106 | const token = jsonwebtoken_1.default.sign(user, process.env.ACCESS_TOKEN_SECRET);
107 | res.status(200).json((0, response_handler_1.SuccessResponse)({ token, user }, 'User Logged in!'));
108 | }));
109 | //==============================================
110 | //logout ========================================
111 | exports.logoutUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
112 | res.status(200).send('Logout Successfull !');
113 | }));
114 | //================================================
115 | //edit==========================================
116 | exports.editUser = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
117 | var _a;
118 | const db = (0, dbConnection_1.getDb)();
119 | const userId = (_a = req.user) === null || _a === void 0 ? void 0 : _a.id;
120 | const { name, bio, institution, phoneNumber, dob, address, image, gender } = req.body;
121 | if (!name || !institution || !phoneNumber || !dob || !gender)
122 | throw new custom_error_1.default('Some Fields are missing !', 500);
123 | let query = 'update `User` set `name`=?, bio=?, `institution`=?, `phoneNumber`=?, `dob`=?, `address`=?, `image`=?, `gender`=? where `id`=?';
124 | const [rows] = yield db.execute(query, [
125 | name,
126 | bio,
127 | institution,
128 | phoneNumber,
129 | dob,
130 | address,
131 | image,
132 | gender,
133 | userId,
134 | ]);
135 | if (!rows.affectedRows)
136 | throw new custom_error_1.default('User Not updated', 500);
137 | return res.status(200).json((0, response_handler_1.SuccessResponse)(req.user, 'User Updated!'));
138 | }));
139 | //==============================================
140 | exports.getExamHosted = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
141 | var _b;
142 | const db = (0, dbConnection_1.getDb)();
143 | const userId = (_b = req.user) === null || _b === void 0 ? void 0 : _b.id;
144 | let query = 'select id,name,description,image,userId,tags,startTime,duration,ongoing,finished,isPrivate from `Exam` where `userId`=?';
145 | const [rows] = yield db.execute(query, [userId]);
146 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows, 'The Exam hosted are'));
147 | }));
148 | exports.getExamGiven = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
149 | var _c;
150 | const db = (0, dbConnection_1.getDb)();
151 | const userId = (_c = req.user) === null || _c === void 0 ? void 0 : _c.id;
152 | console.log(req.user);
153 | const query = 'select e.* from `Exam` as e,`Exam-Participants` as ep where ep.`examId`=e.`id` and ep.`participantId`=?';
154 | const [rows] = yield db.execute(query, [userId]);
155 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows, 'Exams given are : '));
156 | }));
157 |
--------------------------------------------------------------------------------
/src/utils/examFunctions.ts:
--------------------------------------------------------------------------------
1 | import scheduler from 'node-schedule';
2 | import db, { getDb } from '../database/dbConnection';
3 | import {
4 | participantDataInterface,
5 | answerInterface,
6 | answersObjInterface,
7 | participantRankingInterface,
8 | participantRankingData,
9 | } from './CustomInterfaces/ParticipantDataInterfaces';
10 | import {
11 | examInterface,
12 | RootObject,
13 | RootQuestionsObject,
14 | } from './CustomInterfaces/ExamInterface';
15 | import { evaluateQuestion } from './questionFunctions';
16 | import CustomError from '../errors/custom-error';
17 | import {
18 | fillInTheBlanksInterface,
19 | mcqInterface,
20 | multipleOptionsInterface,
21 | } from './CustomInterfaces/QuestionInterfaces';
22 |
23 | export const parseExam = (examObject: examInterface): examInterface => {
24 | examObject.tags = JSON.parse(examObject.tags);
25 | if (examObject.questions)
26 | examObject.questions = JSON.parse(examObject.questions);
27 | return examObject;
28 | };
29 |
30 | function shuffleArray(array: Array) {
31 | var currentIndex = array.length,
32 | temporaryValue,
33 | randomIndex;
34 | while (0 !== currentIndex) {
35 | randomIndex = Math.floor(Math.random() * currentIndex);
36 | currentIndex -= 1;
37 | temporaryValue = array[currentIndex];
38 | array[currentIndex] = array[randomIndex];
39 | array[randomIndex] = temporaryValue;
40 | if (
41 | array[randomIndex].type == 'mcq' ||
42 | array[randomIndex].type == 'multipleOptions'
43 | )
44 | array[randomIndex].options = shuffleArray(array[randomIndex].options);
45 | if (
46 | array[currentIndex].type == 'mcq' ||
47 | array[currentIndex].type == 'multipleOptions'
48 | )
49 | array[currentIndex].options = shuffleArray(array[currentIndex].options);
50 | }
51 | return array;
52 | }
53 |
54 | export const shuffleExam = (examObject: examInterface) => {
55 | examObject.questions = shuffleArray(examObject.questions);
56 | // console.log(examObject.questions);
57 | return examObject;
58 | };
59 |
60 | const getDefaultAns = (type: string) => {
61 | if (type == 'mcq' || type == 'multipleOptions') return [];
62 | return ''; //this means fillInTheBlanks type
63 | };
64 |
65 | export const getExamWithUserAns = (
66 | examQuestions: RootQuestionsObject,
67 | answers: answersObjInterface
68 | ) => {
69 | let questions = examQuestions;
70 | let questionsArray;
71 | questionsArray = Object.keys(questions).map((questionId: number | string) => {
72 | let answer = answers[questionId]?.answer;
73 | let question: RootObject = questions[questionId];
74 | if (!answer) answer = getDefaultAns(question.type); //this is triggered if the answer is not available then a default ans value is taken
75 | question.givenOption = answer;
76 | return question;
77 | });
78 | return questionsArray;
79 | };
80 |
81 | export const scheduleExam = (id: string, date: number, duration: number) => {
82 | //date = new Date(date);
83 | scheduler.scheduleJob(id + 'start', new Date(date), () => {
84 | startExam(id);
85 | destroyScheduler(id + 'start');
86 | });
87 |
88 | scheduler.scheduleJob(id + 'end', new Date(date + duration + 120000), () => {
89 | evaluateExam(id);
90 | console.log(id, ' Ended ');
91 | destroyScheduler(id + 'end');
92 | });
93 | };
94 |
95 | export const startExam = async (id: String) => {
96 | const db = getDb();
97 | let updateExamById = 'update `Exam` set `ongoing`=? where `id`=?';
98 | let result = await db.execute(updateExamById, [true, id]);
99 | if (result) console.log(id, ' Started !');
100 | else console.log(id, ' Error occured to start Exam');
101 | };
102 |
103 | export const scheduleOnServerRestart = async () => {
104 | const db = getDb();
105 | let query =
106 | 'select `id`,`startTime`,`duration` from `Exam` where `startTime`+`duration`>=?';
107 | let [rows] = await db.execute(query, [new Date().getTime()]);
108 | if (rows.length > 0) {
109 | console.log(rows);
110 | rows.map((exam: examInterface) =>
111 | scheduleExam(exam.id, exam.startTime, exam.duration)
112 | );
113 | } else console.log('No Exams to schedule!');
114 | };
115 | export const destroyScheduler = (id: any) => {
116 | let task = scheduler.scheduledJobs[id];
117 | if (task) task.cancel();
118 | };
119 |
120 | export const removeCorrectOptions = (examObject: examInterface) => {
121 | examObject.questions = [...examObject.questions].map((question) => {
122 | delete question['correctOption'];
123 | return question;
124 | });
125 | };
126 |
127 | export const evaluateParticipantData = (
128 | examData: examInterface,
129 | participantAnswers: answersObjInterface
130 | ): number => {
131 | // console.log(participantAnswers, ' ', examData.questions);
132 | let totalScore = 0;
133 | Object.keys(participantAnswers).map((key) => {
134 | let answer = participantAnswers[key];
135 | let question = examData.questions[key];
136 | if (!question || !answer || !(question.type === answer.type)) {
137 | console.log('error in question :', question, ' answer: ', answer);
138 | totalScore += 0;
139 | } else {
140 | totalScore += evaluateQuestion[question.type](question, answer);
141 | }
142 | });
143 | return totalScore;
144 | };
145 |
146 | const evaluateRanking = async (
147 | examId: string | undefined,
148 | totalRankingData: participantRankingData[]
149 | ) => {
150 | const db = getDb();
151 | totalRankingData.sort(
152 | (a: participantRankingInterface, b: participantRankingInterface) => {
153 | if (a.totalScore > b.totalScore) return -1;
154 | else if (b.totalScore > a.totalScore) return 1;
155 | else {
156 | let aFinishTime = a.finishTime || new Date().getTime() + 120000;
157 | let bFinishTime = b.finishTime || new Date().getTime() + 120000;
158 | return aFinishTime - bFinishTime;
159 | }
160 | }
161 | );
162 | console.log('rank sorted data is:', totalRankingData);
163 | let updateQuery = 'update `Exam-Participants` set `rank`= (case ';
164 | totalRankingData.map(
165 | (rankingData: participantRankingInterface, index: number) => {
166 | let rank = index + 1;
167 | updateQuery +=
168 | " when `participantId`='" + rankingData.id + "' then " + rank;
169 | }
170 | );
171 | updateQuery += " end) where `examId`='" + examId + "';";
172 | const [rows] = await db.execute(updateQuery);
173 | if (!rows.affectedRows)
174 | throw new CustomError('Error while rank update!', 500);
175 | };
176 |
177 | // Note : Evaluate exam modifies question data from arrays to hashmaps for algorithim purposes!
178 | export const evaluateExam = async (id: string | undefined) => {
179 | const db = getDb();
180 | let query;
181 | query = 'select * from `Exam` where id=? limit 1';
182 | const [examRows] = await db.execute(query, [id]);
183 | if (examRows.length != 1) throw new CustomError('Exam not found!', 500);
184 | let examData: examInterface = parseExam(examRows[0]);
185 | let questionsObj: any = {};
186 | examData.questions.map((question: any) => {
187 | questionsObj[question.id] = question;
188 | });
189 | examData.questions = questionsObj;
190 | query = 'select * from `Exam-Participants` where `examId`=? and `virtual`=?';
191 | const [participantRows] = await db.execute(query, [id, false]);
192 | if (participantRows.length > 0) {
193 | let totalRankingData: participantRankingInterface[] = [];
194 | let updateQuery = 'update `Exam-Participants` set `totalScore`= (case ';
195 | participantRows.map((data: participantDataInterface) => {
196 | let participantAnswers: answersObjInterface = JSON.parse(
197 | data.answers || '{}'
198 | );
199 | // console.log('xxxxxx ', participantAnswers);
200 | let totalScore: number = evaluateParticipantData(
201 | examData,
202 | participantAnswers
203 | );
204 | updateQuery +=
205 | " when `participantId`='" + data.participantId + "' then " + totalScore;
206 | // console.log(data.participantId, ' got : ', totalScore);
207 | totalRankingData.push(
208 | new participantRankingData(
209 | data.participantId,
210 | totalScore,
211 | data.finishTime
212 | )
213 | );
214 | });
215 | updateQuery += " end) where `examId`='" + id + "';";
216 | let [rows] = await db.execute(updateQuery);
217 | if (!rows.affectedRows) throw new CustomError('Score update error!', 500);
218 | await evaluateRanking(id, totalRankingData);
219 | }
220 | query = 'update `Exam` set `ongoing`=?,`finished`=? where `id`=?';
221 | await db.execute(query, [false, true, id]);
222 | };
223 |
--------------------------------------------------------------------------------
/dist/utils/examFunctions.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __importDefault = (this && this.__importDefault) || function (mod) {
12 | return (mod && mod.__esModule) ? mod : { "default": mod };
13 | };
14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | exports.evaluateExam = exports.evaluateParticipantData = exports.removeCorrectOptions = exports.destroyScheduler = exports.scheduleOnServerRestart = exports.startExam = exports.scheduleExam = exports.getExamWithUserAns = exports.shuffleExam = exports.parseExam = void 0;
16 | const node_schedule_1 = __importDefault(require("node-schedule"));
17 | const dbConnection_1 = require("../database/dbConnection");
18 | const ParticipantDataInterfaces_1 = require("./CustomInterfaces/ParticipantDataInterfaces");
19 | const questionFunctions_1 = require("./questionFunctions");
20 | const custom_error_1 = __importDefault(require("../errors/custom-error"));
21 | const parseExam = (examObject) => {
22 | examObject.tags = JSON.parse(examObject.tags);
23 | if (examObject.questions)
24 | examObject.questions = JSON.parse(examObject.questions);
25 | return examObject;
26 | };
27 | exports.parseExam = parseExam;
28 | function shuffleArray(array) {
29 | var currentIndex = array.length, temporaryValue, randomIndex;
30 | while (0 !== currentIndex) {
31 | randomIndex = Math.floor(Math.random() * currentIndex);
32 | currentIndex -= 1;
33 | temporaryValue = array[currentIndex];
34 | array[currentIndex] = array[randomIndex];
35 | array[randomIndex] = temporaryValue;
36 | if (array[randomIndex].type == 'mcq' ||
37 | array[randomIndex].type == 'multipleOptions')
38 | array[randomIndex].options = shuffleArray(array[randomIndex].options);
39 | if (array[currentIndex].type == 'mcq' ||
40 | array[currentIndex].type == 'multipleOptions')
41 | array[currentIndex].options = shuffleArray(array[currentIndex].options);
42 | }
43 | return array;
44 | }
45 | const shuffleExam = (examObject) => {
46 | examObject.questions = shuffleArray(examObject.questions);
47 | // console.log(examObject.questions);
48 | return examObject;
49 | };
50 | exports.shuffleExam = shuffleExam;
51 | const getDefaultAns = (type) => {
52 | if (type == 'mcq' || type == 'multipleOptions')
53 | return [];
54 | return ''; //this means fillInTheBlanks type
55 | };
56 | const getExamWithUserAns = (examQuestions, answers) => {
57 | let questions = examQuestions;
58 | let questionsArray;
59 | questionsArray = Object.keys(questions).map((questionId) => {
60 | var _a;
61 | let answer = (_a = answers[questionId]) === null || _a === void 0 ? void 0 : _a.answer;
62 | let question = questions[questionId];
63 | if (!answer)
64 | answer = getDefaultAns(question.type); //this is triggered if the answer is not available then a default ans value is taken
65 | question.givenOption = answer;
66 | return question;
67 | });
68 | return questionsArray;
69 | };
70 | exports.getExamWithUserAns = getExamWithUserAns;
71 | const scheduleExam = (id, date, duration) => {
72 | //date = new Date(date);
73 | node_schedule_1.default.scheduleJob(id + 'start', new Date(date), () => {
74 | (0, exports.startExam)(id);
75 | (0, exports.destroyScheduler)(id + 'start');
76 | });
77 | node_schedule_1.default.scheduleJob(id + 'end', new Date(date + duration + 120000), () => {
78 | (0, exports.evaluateExam)(id);
79 | console.log(id, ' Ended ');
80 | (0, exports.destroyScheduler)(id + 'end');
81 | });
82 | };
83 | exports.scheduleExam = scheduleExam;
84 | const startExam = (id) => __awaiter(void 0, void 0, void 0, function* () {
85 | const db = (0, dbConnection_1.getDb)();
86 | let updateExamById = 'update `Exam` set `ongoing`=? where `id`=?';
87 | let result = yield db.execute(updateExamById, [true, id]);
88 | if (result)
89 | console.log(id, ' Started !');
90 | else
91 | console.log(id, ' Error occured to start Exam');
92 | });
93 | exports.startExam = startExam;
94 | const scheduleOnServerRestart = () => __awaiter(void 0, void 0, void 0, function* () {
95 | const db = (0, dbConnection_1.getDb)();
96 | let query = 'select `id`,`startTime`,`duration` from `Exam` where `startTime`+`duration`>=?';
97 | let [rows] = yield db.execute(query, [new Date().getTime()]);
98 | if (rows.length > 0) {
99 | console.log(rows);
100 | rows.map((exam) => (0, exports.scheduleExam)(exam.id, exam.startTime, exam.duration));
101 | }
102 | else
103 | console.log('No Exams to schedule!');
104 | });
105 | exports.scheduleOnServerRestart = scheduleOnServerRestart;
106 | const destroyScheduler = (id) => {
107 | let task = node_schedule_1.default.scheduledJobs[id];
108 | if (task)
109 | task.cancel();
110 | };
111 | exports.destroyScheduler = destroyScheduler;
112 | const removeCorrectOptions = (examObject) => {
113 | examObject.questions = [...examObject.questions].map((question) => {
114 | delete question['correctOption'];
115 | return question;
116 | });
117 | };
118 | exports.removeCorrectOptions = removeCorrectOptions;
119 | const evaluateParticipantData = (examData, participantAnswers) => {
120 | // console.log(participantAnswers, ' ', examData.questions);
121 | let totalScore = 0;
122 | Object.keys(participantAnswers).map((key) => {
123 | let answer = participantAnswers[key];
124 | let question = examData.questions[key];
125 | if (!question || !answer || !(question.type === answer.type)) {
126 | console.log('error in question :', question, ' answer: ', answer);
127 | totalScore += 0;
128 | }
129 | else {
130 | totalScore += questionFunctions_1.evaluateQuestion[question.type](question, answer);
131 | }
132 | });
133 | return totalScore;
134 | };
135 | exports.evaluateParticipantData = evaluateParticipantData;
136 | const evaluateRanking = (examId, totalRankingData) => __awaiter(void 0, void 0, void 0, function* () {
137 | const db = (0, dbConnection_1.getDb)();
138 | totalRankingData.sort((a, b) => {
139 | if (a.totalScore > b.totalScore)
140 | return -1;
141 | else if (b.totalScore > a.totalScore)
142 | return 1;
143 | else {
144 | let aFinishTime = a.finishTime || new Date().getTime() + 120000;
145 | let bFinishTime = b.finishTime || new Date().getTime() + 120000;
146 | return aFinishTime - bFinishTime;
147 | }
148 | });
149 | console.log('rank sorted data is:', totalRankingData);
150 | let updateQuery = 'update `Exam-Participants` set `rank`= (case ';
151 | totalRankingData.map((rankingData, index) => {
152 | let rank = index + 1;
153 | updateQuery +=
154 | " when `participantId`='" + rankingData.id + "' then " + rank;
155 | });
156 | updateQuery += " end) where `examId`='" + examId + "';";
157 | const [rows] = yield db.execute(updateQuery);
158 | if (!rows.affectedRows)
159 | throw new custom_error_1.default('Error while rank update!', 500);
160 | });
161 | // Note : Evaluate exam modifies question data from arrays to hashmaps for algorithim purposes!
162 | const evaluateExam = (id) => __awaiter(void 0, void 0, void 0, function* () {
163 | const db = (0, dbConnection_1.getDb)();
164 | let query;
165 | query = 'select * from `Exam` where id=? limit 1';
166 | const [examRows] = yield db.execute(query, [id]);
167 | if (examRows.length != 1)
168 | throw new custom_error_1.default('Exam not found!', 500);
169 | let examData = (0, exports.parseExam)(examRows[0]);
170 | let questionsObj = {};
171 | examData.questions.map((question) => {
172 | questionsObj[question.id] = question;
173 | });
174 | examData.questions = questionsObj;
175 | query = 'select * from `Exam-Participants` where `examId`=? and `virtual`=?';
176 | const [participantRows] = yield db.execute(query, [id, false]);
177 | if (participantRows.length > 0) {
178 | let totalRankingData = [];
179 | let updateQuery = 'update `Exam-Participants` set `totalScore`= (case ';
180 | participantRows.map((data) => {
181 | let participantAnswers = JSON.parse(data.answers || '{}');
182 | // console.log('xxxxxx ', participantAnswers);
183 | let totalScore = (0, exports.evaluateParticipantData)(examData, participantAnswers);
184 | updateQuery +=
185 | " when `participantId`='" + data.participantId + "' then " + totalScore;
186 | // console.log(data.participantId, ' got : ', totalScore);
187 | totalRankingData.push(new ParticipantDataInterfaces_1.participantRankingData(data.participantId, totalScore, data.finishTime));
188 | });
189 | updateQuery += " end) where `examId`='" + id + "';";
190 | let [rows] = yield db.execute(updateQuery);
191 | if (!rows.affectedRows)
192 | throw new custom_error_1.default('Score update error!', 500);
193 | yield evaluateRanking(id, totalRankingData);
194 | }
195 | query = 'update `Exam` set `ongoing`=?,`finished`=? where `id`=?';
196 | yield db.execute(query, [false, true, id]);
197 | });
198 | exports.evaluateExam = evaluateExam;
199 |
--------------------------------------------------------------------------------
/prisma/migrations/20220406091007_init/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the `Exam` table. If the table is not empty, all the data it contains will be lost.
5 | - You are about to drop the `ExamParticipant` table. If the table is not empty, all the data it contains will be lost.
6 | - You are about to drop the `ExamQuestion` table. If the table is not empty, all the data it contains will be lost.
7 | - You are about to drop the `ExamTag` table. If the table is not empty, all the data it contains will be lost.
8 | - You are about to drop the `PrivateExamEmail` table. If the table is not empty, all the data it contains will be lost.
9 | - You are about to drop the `Question` table. If the table is not empty, all the data it contains will be lost.
10 | - You are about to drop the `QuestionUserAnswer` table. If the table is not empty, all the data it contains will be lost.
11 | - You are about to drop the `QuestionVariant` table. If the table is not empty, all the data it contains will be lost.
12 | - You are about to drop the `Tag` table. If the table is not empty, all the data it contains will be lost.
13 | - You are about to drop the `User` table. If the table is not empty, all the data it contains will be lost.
14 |
15 | */
16 | -- DropForeignKey
17 | ALTER TABLE `Exam` DROP FOREIGN KEY `Exam_creator_id_fkey`;
18 |
19 | -- DropForeignKey
20 | ALTER TABLE `ExamParticipant` DROP FOREIGN KEY `ExamParticipant_exam_id_fkey`;
21 |
22 | -- DropForeignKey
23 | ALTER TABLE `ExamParticipant` DROP FOREIGN KEY `ExamParticipant_participant_id_fkey`;
24 |
25 | -- DropForeignKey
26 | ALTER TABLE `ExamQuestion` DROP FOREIGN KEY `ExamQuestion_exam_id_fkey`;
27 |
28 | -- DropForeignKey
29 | ALTER TABLE `ExamQuestion` DROP FOREIGN KEY `ExamQuestion_question_id_fkey`;
30 |
31 | -- DropForeignKey
32 | ALTER TABLE `ExamTag` DROP FOREIGN KEY `ExamTag_exam_id_fkey`;
33 |
34 | -- DropForeignKey
35 | ALTER TABLE `ExamTag` DROP FOREIGN KEY `ExamTag_tag_id_fkey`;
36 |
37 | -- DropForeignKey
38 | ALTER TABLE `PrivateExamEmail` DROP FOREIGN KEY `PrivateExamEmail_examId_fkey`;
39 |
40 | -- DropForeignKey
41 | ALTER TABLE `Question` DROP FOREIGN KEY `Question_variant_id_fkey`;
42 |
43 | -- DropForeignKey
44 | ALTER TABLE `QuestionUserAnswer` DROP FOREIGN KEY `QuestionUserAnswer_exam_id_fkey`;
45 |
46 | -- DropForeignKey
47 | ALTER TABLE `QuestionUserAnswer` DROP FOREIGN KEY `QuestionUserAnswer_question_id_fkey`;
48 |
49 | -- DropForeignKey
50 | ALTER TABLE `QuestionUserAnswer` DROP FOREIGN KEY `QuestionUserAnswer_user_id_fkey`;
51 |
52 | -- DropTable
53 | DROP TABLE `Exam`;
54 |
55 | -- DropTable
56 | DROP TABLE `ExamParticipant`;
57 |
58 | -- DropTable
59 | DROP TABLE `ExamQuestion`;
60 |
61 | -- DropTable
62 | DROP TABLE `ExamTag`;
63 |
64 | -- DropTable
65 | DROP TABLE `PrivateExamEmail`;
66 |
67 | -- DropTable
68 | DROP TABLE `Question`;
69 |
70 | -- DropTable
71 | DROP TABLE `QuestionUserAnswer`;
72 |
73 | -- DropTable
74 | DROP TABLE `QuestionVariant`;
75 |
76 | -- DropTable
77 | DROP TABLE `Tag`;
78 |
79 | -- DropTable
80 | DROP TABLE `User`;
81 |
82 | -- CreateTable
83 | CREATE TABLE `users` (
84 | `id` INTEGER NOT NULL AUTO_INCREMENT,
85 | `name` VARCHAR(191) NOT NULL,
86 | `email` VARCHAR(191) NOT NULL,
87 | `bio` VARCHAR(191) NULL,
88 | `dob` DATETIME(3) NOT NULL,
89 | `address` VARCHAR(255) NULL,
90 | `country` VARCHAR(191) NULL,
91 | `profile_image` VARCHAR(191) NULL,
92 | `password` VARCHAR(191) NOT NULL,
93 | `latest_degree` VARCHAR(191) NULL,
94 | `Institution` VARCHAR(191) NULL,
95 | `gender` VARCHAR(191) NULL,
96 | `country_code` INTEGER NOT NULL,
97 | `phone` VARCHAR(191) NOT NULL,
98 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
99 | `updated_at` DATETIME(3) NOT NULL,
100 |
101 | UNIQUE INDEX `users_email_key`(`email`),
102 | PRIMARY KEY (`id`)
103 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
104 |
105 | -- CreateTable
106 | CREATE TABLE `exams` (
107 | `id` INTEGER NOT NULL AUTO_INCREMENT,
108 | `creator_id` INTEGER NOT NULL,
109 | `name` VARCHAR(191) NOT NULL,
110 | `description` VARCHAR(191) NULL,
111 | `banner` VARCHAR(191) NULL,
112 | `thumbnail` VARCHAR(191) NULL,
113 | `start_time` DATETIME(3) NOT NULL,
114 | `duration` BIGINT NOT NULL,
115 | `ongoing` BOOLEAN NOT NULL DEFAULT false,
116 | `is_private` BOOLEAN NOT NULL DEFAULT false,
117 | `participant_count` INTEGER NOT NULL DEFAULT 0,
118 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
119 | `updated_at` DATETIME(3) NOT NULL,
120 |
121 | PRIMARY KEY (`id`)
122 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
123 |
124 | -- CreateTable
125 | CREATE TABLE `exam_participants` (
126 | `exam_id` INTEGER NOT NULL,
127 | `participant_id` INTEGER NOT NULL,
128 | `score` INTEGER NULL,
129 | `finish_time` INTEGER NULL,
130 | `rank` INTEGER NULL,
131 |
132 | UNIQUE INDEX `exam_participants_exam_id_participant_id_key`(`exam_id`, `participant_id`)
133 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
134 |
135 | -- CreateTable
136 | CREATE TABLE `private_exam_emails` (
137 | `examId` INTEGER NOT NULL,
138 | `email` VARCHAR(191) NOT NULL,
139 |
140 | UNIQUE INDEX `private_exam_emails_examId_email_key`(`examId`, `email`)
141 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
142 |
143 | -- CreateTable
144 | CREATE TABLE `tags` (
145 | `id` INTEGER NOT NULL AUTO_INCREMENT,
146 | `tag` VARCHAR(191) NOT NULL,
147 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
148 | `updated_at` DATETIME(3) NOT NULL,
149 |
150 | UNIQUE INDEX `tags_tag_key`(`tag`),
151 | PRIMARY KEY (`id`)
152 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
153 |
154 | -- CreateTable
155 | CREATE TABLE `exam_tags` (
156 | `exam_id` INTEGER NOT NULL,
157 | `tag_id` INTEGER NOT NULL,
158 |
159 | UNIQUE INDEX `exam_tags_exam_id_tag_id_key`(`exam_id`, `tag_id`)
160 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
161 |
162 | -- CreateTable
163 | CREATE TABLE `questions` (
164 | `id` INTEGER NOT NULL AUTO_INCREMENT,
165 | `question` VARCHAR(191) NOT NULL,
166 | `variant_id` INTEGER NOT NULL,
167 | `solution` VARCHAR(191) NOT NULL,
168 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
169 | `updated_at` DATETIME(3) NOT NULL,
170 |
171 | PRIMARY KEY (`id`)
172 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
173 |
174 | -- CreateTable
175 | CREATE TABLE `exam_questions` (
176 | `exam_id` INTEGER NOT NULL,
177 | `question_id` INTEGER NOT NULL,
178 | `mark` INTEGER NOT NULL,
179 | `negative_mark` INTEGER NOT NULL,
180 |
181 | UNIQUE INDEX `exam_questions_exam_id_question_id_key`(`exam_id`, `question_id`)
182 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
183 |
184 | -- CreateTable
185 | CREATE TABLE `question_variants` (
186 | `id` INTEGER NOT NULL AUTO_INCREMENT,
187 | `variant` VARCHAR(191) NOT NULL,
188 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
189 | `updated_at` DATETIME(3) NOT NULL,
190 |
191 | UNIQUE INDEX `question_variants_variant_key`(`variant`),
192 | PRIMARY KEY (`id`)
193 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
194 |
195 | -- CreateTable
196 | CREATE TABLE `question_exam_submissions` (
197 | `exam_id` INTEGER NOT NULL,
198 | `question_id` INTEGER NOT NULL,
199 | `user_id` INTEGER NOT NULL,
200 | `submission` VARCHAR(191) NOT NULL,
201 | `score` INTEGER NOT NULL DEFAULT 0,
202 |
203 | UNIQUE INDEX `question_exam_submissions_exam_id_question_id_user_id_key`(`exam_id`, `question_id`, `user_id`)
204 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
205 |
206 | -- AddForeignKey
207 | ALTER TABLE `exams` ADD CONSTRAINT `exams_creator_id_fkey` FOREIGN KEY (`creator_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
208 |
209 | -- AddForeignKey
210 | ALTER TABLE `exam_participants` ADD CONSTRAINT `exam_participants_participant_id_fkey` FOREIGN KEY (`participant_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
211 |
212 | -- AddForeignKey
213 | ALTER TABLE `exam_participants` ADD CONSTRAINT `exam_participants_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
214 |
215 | -- AddForeignKey
216 | ALTER TABLE `private_exam_emails` ADD CONSTRAINT `private_exam_emails_examId_fkey` FOREIGN KEY (`examId`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
217 |
218 | -- AddForeignKey
219 | ALTER TABLE `exam_tags` ADD CONSTRAINT `exam_tags_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
220 |
221 | -- AddForeignKey
222 | ALTER TABLE `exam_tags` ADD CONSTRAINT `exam_tags_tag_id_fkey` FOREIGN KEY (`tag_id`) REFERENCES `tags`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
223 |
224 | -- AddForeignKey
225 | ALTER TABLE `questions` ADD CONSTRAINT `questions_variant_id_fkey` FOREIGN KEY (`variant_id`) REFERENCES `question_variants`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
226 |
227 | -- AddForeignKey
228 | ALTER TABLE `exam_questions` ADD CONSTRAINT `exam_questions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
229 |
230 | -- AddForeignKey
231 | ALTER TABLE `exam_questions` ADD CONSTRAINT `exam_questions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
232 |
233 | -- AddForeignKey
234 | ALTER TABLE `question_exam_submissions` ADD CONSTRAINT `question_exam_submissions_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
235 |
236 | -- AddForeignKey
237 | ALTER TABLE `question_exam_submissions` ADD CONSTRAINT `question_exam_submissions_exam_id_fkey` FOREIGN KEY (`exam_id`) REFERENCES `exams`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
238 |
239 | -- AddForeignKey
240 | ALTER TABLE `question_exam_submissions` ADD CONSTRAINT `question_exam_submissions_question_id_fkey` FOREIGN KEY (`question_id`) REFERENCES `questions`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
241 |
--------------------------------------------------------------------------------
/src/controllers/examController.ts:
--------------------------------------------------------------------------------
1 | import catchAsync from '../utils/catchAsync';
2 | import { SuccessResponse } from '../utils/response-handler';
3 | import CustomError from '../errors/custom-error';
4 | import { Response, Request, NextFunction } from 'express';
5 | import db, { getDb } from '../database/dbConnection';
6 | import { v4 as uuid } from 'uuid';
7 | import { CustomRequest } from '../utils/CustomInterfaces/CustomRequest';
8 | import {
9 | mcqInterface,
10 | fillInTheBlanksInterface,
11 | multipleOptionsInterface,
12 | } from '../utils/CustomInterfaces/QuestionInterfaces';
13 | import {
14 | scheduleExam,
15 | destroyScheduler,
16 | parseExam,
17 | shuffleExam,
18 | removeCorrectOptions,
19 | evaluateExam,
20 | getExamWithUserAns,
21 | } from '../utils/examFunctions';
22 | import { parse } from 'path';
23 |
24 | const defaultBannerImage =
25 | 'https://image.freepik.com/free-vector/online-exam-isometric-web-banner_33099-2305.jpg';
26 |
27 | export const testing = catchAsync(async (req: Request, res: Response) =>
28 | res.status(200).send('Server is up and running!')
29 | );
30 |
31 | export const createTables = catchAsync(async (req: Request, res: Response) => {
32 | const db = getDb();
33 | let query, result;
34 |
35 | //User table===================
36 | query =
37 | 'create table User ( id varchar(50) not null, name varchar(50) , email varchar(50) ,bio longtext, dob datetime , address longtext,image longtext, password longtext ,institution longtext ,gender varchar(50),phoneNumber varchar(50) ,constraint user_pk primary key(id) )';
38 | result = await db.execute(query);
39 | if (!result) throw new CustomError('User Table Not created !', 500);
40 | //=============================
41 |
42 | //Exam table==================
43 | query =
44 | 'create table `Exam` ( id varchar(50) , name varchar(50) , description longtext , image longtext ,userId varchar(50),tags json, questions json , startTime bigint , duration integer ,ongoing boolean default False,finished boolean default False ,isPrivate boolean default False ,numberOfParticipants integer, constraint exam_pk primary key(id) ,constraint fk1 foreign key (userId) references User(id) )';
45 | result = await db.execute(query);
46 | if (!result) throw new CustomError('Exam Table not created !', 500);
47 | //============================
48 |
49 | //Exam-Participants table========
50 | query =
51 | 'create table `Exam-Participants` (id integer auto_increment ,examId varchar(50) , participantId varchar(50) ,answers json ,totalScore integer , finishTime bigint,virtual boolean default False,rank boolean default 0,constraint pk primary key(id) ,constraint fep1 foreign key (examId) references Exam(id) , constraint fep2 foreign key (participantId) references User(id))';
52 | result = await db.execute(query);
53 | if (!result)
54 | throw new CustomError('Exam-Participants table not created!', 500);
55 | //================================
56 |
57 | //private exam emails table====
58 | query =
59 | 'create table `Private-Exam-Emails` (id integer auto_increment,examId varchar(50) , email varchar(50) , constraint pk primary key(id) , constraint fkx foreign key (examId) references Exam(id))';
60 | result = await db.execute(query);
61 | if (!result)
62 | throw new CustomError('Private-Exam Emails table not created !', 500);
63 | //=============================
64 | res.status(200).send('All tables created !');
65 | });
66 |
67 | export const getExams = catchAsync(async (req: Request, res: Response) => {
68 | const db = getDb();
69 | let query =
70 | 'select id,name,description,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants from `Exam`';
71 | let [rows] = await db.execute(query);
72 | rows.map((exam: any) => {
73 | exam = parseExam(exam);
74 | });
75 | res
76 | .status(200)
77 | .json(SuccessResponse(rows, 'These are upcoming and ongoing Exams !'));
78 | });
79 |
80 | export const getExamDetails = catchAsync(
81 | async (req: Request, res: Response) => {
82 | const db = getDb();
83 | const examId = req.params.id;
84 | let query =
85 | "select id,name,description,questions,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants, (SELECT JSON_OBJECT('id',u.id,'name',u.name,'image', u.image) FROM User AS u WHERE u.id = Exam.userId) AS user from Exam where id=? limit 1";
86 | let [rows] = await db.execute(query, [examId]);
87 | if (rows.length != 1) throw new CustomError('Exam not found', 500);
88 | parseExam(rows[0]);
89 | let totalMarks = 0;
90 | rows[0].questions?.map(
91 | (
92 | question:
93 | | mcqInterface
94 | | multipleOptionsInterface
95 | | fillInTheBlanksInterface
96 | ) => {
97 | totalMarks += question.marks;
98 | }
99 | );
100 | rows[0].user = JSON.parse(rows[0].user);
101 | rows[0].totalMarks = totalMarks;
102 | delete rows[0].questions;
103 | res.status(200).json(SuccessResponse(rows[0], 'Exam Found !'));
104 | }
105 | );
106 |
107 | export const createExam = catchAsync(
108 | async (req: CustomRequest, res: Response) => {
109 | const db = getDb();
110 | const userId = req.user?.id;
111 | let data = req.body || {};
112 | let query =
113 | 'insert into `Exam` (`id`,`name`,`description`,`image`,`userId`,`tags`,`questions`,`startTime`,`duration`,`isPrivate`) values(?,?,?,?,?,?,?,?,?,?)';
114 | data.id = uuid();
115 | const result1 = await db.execute(query, [
116 | data.id,
117 | data.name,
118 | data.description || ' ',
119 | data.image?.trim() || defaultBannerImage,
120 | userId,
121 | JSON.stringify(data.tags),
122 | JSON.stringify(data.questions),
123 | data.startTime,
124 | data.duration,
125 | data.isPrivate,
126 | ]);
127 | if (!result1) throw new CustomError('Exam not created !', 500);
128 | if (data.isPrivate && data.allowedUsers.length > 0) {
129 | query = 'insert into `Private-Exam-Emails` (`examId`,`email`) values ';
130 | let queryArray: any = [];
131 | data.allowedUsers.map((allowedUserEmail: String) => {
132 | queryArray.push(`('` + data.id + `','` + allowedUserEmail + `')`);
133 | });
134 | queryArray = queryArray.join(',');
135 | let result2 = await db.execute(query + queryArray);
136 | if (!result2)
137 | throw new CustomError(
138 | 'Failed to set Private User in Private-Exam-Email table',
139 | 500
140 | );
141 | }
142 | scheduleExam(data.id, data.startTime, data.duration);
143 | return res.status(200).json(SuccessResponse({}, 'Exam Created!'));
144 | }
145 | );
146 |
147 | export const editExam = catchAsync(
148 | async (req: CustomRequest, res: Response) => {
149 | const db = getDb();
150 | // if(!req.user) throw new CustomError("User Error",404);
151 | const examId = req.params.id;
152 | const userId = req.user?.id;
153 | const {
154 | name,
155 | description,
156 | image,
157 | tags,
158 | questions,
159 | startTime,
160 | duration,
161 | isPrivate,
162 | allowedUsers,
163 | } = req.body;
164 | let getExamByUserId =
165 | 'select count(*) as examExists from `Exam` where `userId`=? and `id`=? limit 1';
166 | let [rows] = await db.execute(getExamByUserId, [userId, examId]); //authorization to be added ! to e compared with req.user.id and req.body.userId
167 | console.log(rows);
168 | if (!rows[0].examExists) throw new CustomError('Exam not found !', 500);
169 | //console.log(rows[0]);
170 | let date = new Date();
171 | date.setSeconds(date.getSeconds() + 60);
172 | let updateExam =
173 | 'update `Exam` set `name`=? ,`description`=?,`image`=?,`tags` = ?, `questions`=? , `startTime`=? , `duration`=? , `isPrivate`=? where `id`=?';
174 | let result1 = await db.execute(updateExam, [
175 | name,
176 | description || '',
177 | image.trim() || defaultBannerImage,
178 | JSON.stringify(tags),
179 | JSON.stringify(questions),
180 | date,
181 | duration,
182 | isPrivate,
183 | examId,
184 | ]);
185 | if (!result1) throw new CustomError('Exam not updated !', 500);
186 |
187 | let deleteByEmail = 'delete from `Private-Exam-Emails` where `examId`=?';
188 | const result2 = await db.execute(deleteByEmail, [examId]);
189 | if (!result2)
190 | throw new CustomError('Delete from Private-Exam-Emails failed !', 500);
191 |
192 | if (isPrivate && allowedUsers.length > 0) {
193 | let query =
194 | 'insert into `Private-Exam-Emails` (`examId`,`email`) values ';
195 | let queryArray: any = [];
196 | allowedUsers.map((allowedUserEmail: String) => {
197 | queryArray.push(`('` + examId + `','` + allowedUserEmail + `')`);
198 | });
199 | queryArray = queryArray.join(',');
200 | let result3 = await db.execute(query + queryArray);
201 | if (!result3)
202 | throw new CustomError(
203 | 'Failed to update Private User in Private-Exam-Email table',
204 | 500
205 | );
206 | }
207 | // destroyScheduler(examId + 'start');
208 | // destroyScheduler(examId + 'end');
209 | // scheduleExam(examId, date, duration);
210 | return res.status(200).json(SuccessResponse({}, 'Exam Updated!'));
211 | }
212 | );
213 |
214 | export const registerInExam = catchAsync(
215 | async (req: CustomRequest, res: Response) => {
216 | const db = getDb();
217 | let query;
218 | const { examId } = req.body;
219 | const userId = req.user?.id;
220 | const email = req.user?.email;
221 | //check if the ids are valid then insert in exam-participants table
222 | query =
223 | 'select isPrivate,userId as creatorId,count(*) as examExists,finished,startTime,duration from `Exam` where `id`=? limit 1';
224 | let [rows] = await db.execute(query, [examId]);
225 | // console.log(rows);
226 | let { isPrivate, creatorId, examExists, finished, startTime, duration } =
227 | rows[0];
228 | if (!examExists) throw new CustomError('Exam not Found', 500);
229 | if (creatorId === userId)
230 | throw new CustomError('Creator cannot give Exam !', 500);
231 | const currTime = new Date().getTime();
232 | if (currTime >= startTime && currTime <= startTime + duration + 120000)
233 | throw new CustomError(
234 | 'Not a valid time to register! Let the evaluation finish!',
235 | 500
236 | );
237 | if (isPrivate) {
238 | query =
239 | 'select count(*) as userAllowed from `Private-Exam-Emails` where `email`=? limit 1';
240 | let [rows] = await db.execute(query, [email]);
241 | //console.log(rows);
242 | if (!rows[0].userAllowed)
243 | throw new CustomError('Not allowed to enter !', 404);
244 | }
245 |
246 | query =
247 | 'select count(*) as alreadyRegistered from `Exam-Participants` where `participantId`=? and `examId`=? limit 1';
248 | [rows] = await db.execute(query, [userId, examId]);
249 | if (rows[0].alreadyRegistered)
250 | throw new CustomError('User already Registered !', 500);
251 |
252 | query =
253 | 'insert into `Exam-Participants` (`examId`,`participantId`,`virtual`) values(?,?,?)';
254 | let result = await db.execute(query, [examId, userId, finished]);
255 | if (!result) throw new CustomError('Registering failed !', 500);
256 | res.status(200).send('Successfully Registered !');
257 | }
258 | );
259 |
260 | export const startExam = catchAsync(
261 | async (req: CustomRequest, res: Response) => {
262 | const db = getDb();
263 | const examId = req.params.id;
264 | const userId = req.user?.id;
265 | let query = 'select * from `Exam` where `id`=? limit 1';
266 | let [examRows] = await db.execute(query, [examId]);
267 | if (examRows.length != 1) throw new CustomError('Exam not found !', 500);
268 | examRows[0] = parseExam(examRows[0]);
269 | shuffleExam(examRows[0]);
270 | removeCorrectOptions(examRows[0]);
271 | query =
272 | 'select count(*) as userRegistered,virtual,answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1';
273 | let [rows] = await db.execute(query, [userId, examId]);
274 | console.log(rows);
275 | if (!rows[0].userRegistered)
276 | throw new CustomError('User not yet Registered !', 500);
277 | if (rows[0].answers)
278 | throw new CustomError('Answers already Submitted!', 500);
279 | if (rows[0].virtual)
280 | return res
281 | .status(200)
282 | .json(SuccessResponse(examRows[0], 'Virtual Exam Started !'));
283 | if (!examRows[0].ongoing)
284 | throw new CustomError('Exam has not started yet !', 500);
285 |
286 | //console.log(shuffleExam(examRows[0]));
287 | res.status(200).json(SuccessResponse(examRows[0], 'Exam Started !'));
288 | }
289 | );
290 |
291 | export const submitExam = catchAsync(
292 | async (req: CustomRequest, res: Response) => {
293 | const db = getDb();
294 | let query;
295 | const { answers, finishTime, examId } = req.body;
296 | const participantId = req.user?.id;
297 | if (!answers || !finishTime || !participantId || !examId)
298 | throw new CustomError('Fields are missing !', 500);
299 | query =
300 | 'select `startTime`+`duration` as `endTime` from `Exam` where `id`=? limit 1';
301 | let [rows] = await db.execute(query, [examId]);
302 | if (rows.length != 1) throw new CustomError('Exam not found!', 500);
303 | if (finishTime > rows[0].endTime + 15000)
304 | //extra 15secs
305 | throw new CustomError('Not a valid time to submit! Exam Finished!', 500);
306 |
307 | query =
308 | 'update `Exam-Participants` set `answers`=? , `finishTime`=? where `participantId`=? and `examId`=?';
309 | [rows] = await db.execute(query, [
310 | JSON.stringify(answers),
311 | finishTime,
312 | participantId,
313 | examId,
314 | ]);
315 | if (!rows.affectedRows)
316 | throw new CustomError('Error occured while submitting! :(', 500);
317 | res
318 | .status(200)
319 | .json(SuccessResponse(rows[0], 'Exam Submitted Successfully!'));
320 | }
321 | );
322 |
323 | export const forceEvaluateExam = catchAsync(
324 | async (req: Request, res: Response) => {
325 | const id: any = req.query.id;
326 | await evaluateExam(id);
327 | res.status(200).send('Evaluation Completed!');
328 | }
329 | );
330 |
331 | export const getTags = catchAsync(async (req: Request, res: Response) => {
332 | const tags: String[] = [
333 | 'Maths',
334 | 'Physics',
335 | 'Chemistry',
336 | 'Exam',
337 | 'Contest',
338 | 'Computer Science',
339 | 'Competitive Programming',
340 | 'Dynamic Programming',
341 | 'Graph Theory',
342 | 'Literature',
343 | ];
344 | res.status(200).json(SuccessResponse(tags, 'The tags are :'));
345 | });
346 |
347 | export const getQuestionTypes = catchAsync(
348 | async (req: Request, res: Response) => {
349 | const questionTypes = [
350 | { label: 'MCQ Single Option', value: 'mcq' },
351 | { label: 'Fill in the blanks', value: 'fillInTheBlanks' },
352 | { label: 'MCQ Multiple Options', value: 'multipleOptions' },
353 | ];
354 | res
355 | .status(200)
356 | .json(SuccessResponse(questionTypes, 'The question types are :'));
357 | }
358 | );
359 |
360 | export const examRegisterStatus = catchAsync(
361 | async (req: CustomRequest, res: Response) => {
362 | const db = getDb();
363 | const userId = req.user?.id;
364 | const examId = req.query.examId;
365 | let query =
366 | 'select count(*) registered from `Exam-Participants` where `examId`=? and `participantId`=?';
367 | const [rows] = await db.execute(query, [examId, userId]);
368 | res
369 | .status(200)
370 | .json(
371 | SuccessResponse(
372 | { registered: rows[0].registered },
373 | 'Register status of the user is:'
374 | )
375 | );
376 | }
377 | );
378 |
379 | //this route uses customized question object for algorithm purposes!
380 | export const getExamSolution = catchAsync(
381 | async (req: CustomRequest, res: Response) => {
382 | const db = getDb();
383 | const examId = req.query.examId;
384 | const userId = req.user?.id;
385 | let examQuestions;
386 | let query = 'select questions,finished from `Exam` where `id`=? limit 1';
387 | let [rows] = await db.execute(query, [examId]);
388 | if (rows.length != 1) throw new CustomError('Exam not found!', 500);
389 | if (!rows[0].finished)
390 | res.status(200).json(SuccessResponse(null, 'Exam not yet finished!'));
391 | examQuestions = JSON.parse(rows[0].questions);
392 | let questionsObj: any = {};
393 | examQuestions.map((question: any) => {
394 | questionsObj[question.id] = question;
395 | });
396 | examQuestions = questionsObj;
397 | query =
398 | 'select answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1';
399 | [rows] = await db.execute(query, [userId, examId]);
400 | if (rows.length != 1)
401 | res
402 | .status(200)
403 | .json(
404 | SuccessResponse(
405 | null,
406 | 'Error while finding answers!, maybe user not registered!'
407 | )
408 | );
409 | if (!rows[0].answers)
410 | res.status(200).json(SuccessResponse(null, 'Answers null!'));
411 | let answers = JSON.parse(rows[0].answers);
412 | let examWithUserAns = getExamWithUserAns(examQuestions, answers);
413 | res
414 | .status(200)
415 | .json(SuccessResponse(examWithUserAns, 'Your solution is :'));
416 | }
417 | );
418 |
419 | export const getExamScores = catchAsync(
420 | async (req: CustomRequest, res: Response) => {
421 | const db = getDb();
422 | let query, rows;
423 | const userId = req.user?.id;
424 | const examId = req.query.examId;
425 | query = 'select `finished` from `Exam` where `id`=? limit 1';
426 | [rows] = await db.execute(query, [examId]);
427 | if (rows.length != 1) throw new CustomError('Exam not found!', 500);
428 | if (!rows[0].finished) throw new CustomError('Exam not finished !', 500);
429 | query =
430 | "select `participantId`, `rank`, `finishTime`, `totalScore`, (SELECT JSON_ARRAYAGG(JSON_OBJECT('participantId', ep.participantId, 'totalScore', ep.totalScore, 'rank', ep.rank, 'finishTime', ep.finishTime, 'name', u.name)) FROM `Exam-Participants` AS ep, `User` AS u WHERE ep.participantId = u.id AND ep.examId = e.examId ORDER BY ep.rank LIMIT 5) AS `topPerformers` from `Exam-Participants` AS e where `participantId`=? and `examId`=? LIMIT 1";
431 | [rows] = await db.execute(query, [userId, examId]);
432 | if (rows.length != 1) throw new CustomError('Data not found!', 500);
433 | rows[0].topPerformers = JSON.parse(rows[0].topPerformers);
434 | res.status(200).json(SuccessResponse(rows[0], 'The data is : '));
435 | }
436 | );
437 |
--------------------------------------------------------------------------------
/dist/controllers/examController.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 | return new (P || (P = Promise))(function (resolve, reject) {
5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 | step((generator = generator.apply(thisArg, _arguments || [])).next());
9 | });
10 | };
11 | var __importDefault = (this && this.__importDefault) || function (mod) {
12 | return (mod && mod.__esModule) ? mod : { "default": mod };
13 | };
14 | Object.defineProperty(exports, "__esModule", { value: true });
15 | exports.getExamScores = exports.getExamSolution = exports.examRegisterStatus = exports.getQuestionTypes = exports.getTags = exports.forceEvaluateExam = exports.submitExam = exports.startExam = exports.registerInExam = exports.editExam = exports.createExam = exports.getExamDetails = exports.getExams = exports.createTables = exports.testing = void 0;
16 | const catchAsync_1 = __importDefault(require("../utils/catchAsync"));
17 | const response_handler_1 = require("../utils/response-handler");
18 | const custom_error_1 = __importDefault(require("../errors/custom-error"));
19 | const dbConnection_1 = require("../database/dbConnection");
20 | const uuid_1 = require("uuid");
21 | const examFunctions_1 = require("../utils/examFunctions");
22 | const defaultBannerImage = 'https://image.freepik.com/free-vector/online-exam-isometric-web-banner_33099-2305.jpg';
23 | exports.testing = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () { return res.status(200).send('Server is up and running!'); }));
24 | exports.createTables = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
25 | const db = (0, dbConnection_1.getDb)();
26 | let query, result;
27 | //User table===================
28 | query =
29 | 'create table User ( id varchar(50) not null, name varchar(50) , email varchar(50) ,bio longtext, dob datetime , address longtext,image longtext, password longtext ,institution longtext ,gender varchar(50),phoneNumber varchar(50) ,constraint user_pk primary key(id) )';
30 | result = yield db.execute(query);
31 | if (!result)
32 | throw new custom_error_1.default('User Table Not created !', 500);
33 | //=============================
34 | //Exam table==================
35 | query =
36 | 'create table `Exam` ( id varchar(50) , name varchar(50) , description longtext , image longtext ,userId varchar(50),tags json, questions json , startTime bigint , duration integer ,ongoing boolean default False,finished boolean default False ,isPrivate boolean default False ,numberOfParticipants integer, constraint exam_pk primary key(id) ,constraint fk1 foreign key (userId) references User(id) )';
37 | result = yield db.execute(query);
38 | if (!result)
39 | throw new custom_error_1.default('Exam Table not created !', 500);
40 | //============================
41 | //Exam-Participants table========
42 | query =
43 | 'create table `Exam-Participants` (id integer auto_increment ,examId varchar(50) , participantId varchar(50) ,answers json ,totalScore integer , finishTime bigint,virtual boolean default False,rank boolean default 0,constraint pk primary key(id) ,constraint fep1 foreign key (examId) references Exam(id) , constraint fep2 foreign key (participantId) references User(id))';
44 | result = yield db.execute(query);
45 | if (!result)
46 | throw new custom_error_1.default('Exam-Participants table not created!', 500);
47 | //================================
48 | //private exam emails table====
49 | query =
50 | 'create table `Private-Exam-Emails` (id integer auto_increment,examId varchar(50) , email varchar(50) , constraint pk primary key(id) , constraint fkx foreign key (examId) references Exam(id))';
51 | result = yield db.execute(query);
52 | if (!result)
53 | throw new custom_error_1.default('Private-Exam Emails table not created !', 500);
54 | //=============================
55 | res.status(200).send('All tables created !');
56 | }));
57 | exports.getExams = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
58 | const db = (0, dbConnection_1.getDb)();
59 | let query = 'select id,name,description,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants from `Exam`';
60 | let [rows] = yield db.execute(query);
61 | rows.map((exam) => {
62 | exam = (0, examFunctions_1.parseExam)(exam);
63 | });
64 | res
65 | .status(200)
66 | .json((0, response_handler_1.SuccessResponse)(rows, 'These are upcoming and ongoing Exams !'));
67 | }));
68 | exports.getExamDetails = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
69 | var _a;
70 | const db = (0, dbConnection_1.getDb)();
71 | const examId = req.params.id;
72 | let query = "select id,name,description,questions,image,tags,startTime,duration,ongoing,isPrivate,(SELECT COUNT(ep.id) FROM `Exam-Participants` AS ep WHERE ep.examId = Exam.id) AS numberOfParticipants, (SELECT JSON_OBJECT('id',u.id,'name',u.name,'image', u.image) FROM User AS u WHERE u.id = Exam.userId) AS user from Exam where id=? limit 1";
73 | let [rows] = yield db.execute(query, [examId]);
74 | if (rows.length != 1)
75 | throw new custom_error_1.default('Exam not found', 500);
76 | (0, examFunctions_1.parseExam)(rows[0]);
77 | let totalMarks = 0;
78 | (_a = rows[0].questions) === null || _a === void 0 ? void 0 : _a.map((question) => {
79 | totalMarks += question.marks;
80 | });
81 | rows[0].user = JSON.parse(rows[0].user);
82 | rows[0].totalMarks = totalMarks;
83 | delete rows[0].questions;
84 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows[0], 'Exam Found !'));
85 | }));
86 | exports.createExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
87 | var _b, _c;
88 | const db = (0, dbConnection_1.getDb)();
89 | const userId = (_b = req.user) === null || _b === void 0 ? void 0 : _b.id;
90 | let data = req.body || {};
91 | let query = 'insert into `Exam` (`id`,`name`,`description`,`image`,`userId`,`tags`,`questions`,`startTime`,`duration`,`isPrivate`) values(?,?,?,?,?,?,?,?,?,?)';
92 | data.id = (0, uuid_1.v4)();
93 | const result1 = yield db.execute(query, [
94 | data.id,
95 | data.name,
96 | data.description || ' ',
97 | ((_c = data.image) === null || _c === void 0 ? void 0 : _c.trim()) || defaultBannerImage,
98 | userId,
99 | JSON.stringify(data.tags),
100 | JSON.stringify(data.questions),
101 | data.startTime,
102 | data.duration,
103 | data.isPrivate,
104 | ]);
105 | if (!result1)
106 | throw new custom_error_1.default('Exam not created !', 500);
107 | if (data.isPrivate && data.allowedUsers.length > 0) {
108 | query = 'insert into `Private-Exam-Emails` (`examId`,`email`) values ';
109 | let queryArray = [];
110 | data.allowedUsers.map((allowedUserEmail) => {
111 | queryArray.push(`('` + data.id + `','` + allowedUserEmail + `')`);
112 | });
113 | queryArray = queryArray.join(',');
114 | let result2 = yield db.execute(query + queryArray);
115 | if (!result2)
116 | throw new custom_error_1.default('Failed to set Private User in Private-Exam-Email table', 500);
117 | }
118 | (0, examFunctions_1.scheduleExam)(data.id, data.startTime, data.duration);
119 | return res.status(200).json((0, response_handler_1.SuccessResponse)({}, 'Exam Created!'));
120 | }));
121 | exports.editExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
122 | var _d;
123 | const db = (0, dbConnection_1.getDb)();
124 | // if(!req.user) throw new CustomError("User Error",404);
125 | const examId = req.params.id;
126 | const userId = (_d = req.user) === null || _d === void 0 ? void 0 : _d.id;
127 | const { name, description, image, tags, questions, startTime, duration, isPrivate, allowedUsers, } = req.body;
128 | let getExamByUserId = 'select count(*) as examExists from `Exam` where `userId`=? and `id`=? limit 1';
129 | let [rows] = yield db.execute(getExamByUserId, [userId, examId]); //authorization to be added ! to e compared with req.user.id and req.body.userId
130 | console.log(rows);
131 | if (!rows[0].examExists)
132 | throw new custom_error_1.default('Exam not found !', 500);
133 | //console.log(rows[0]);
134 | let date = new Date();
135 | date.setSeconds(date.getSeconds() + 60);
136 | let updateExam = 'update `Exam` set `name`=? ,`description`=?,`image`=?,`tags` = ?, `questions`=? , `startTime`=? , `duration`=? , `isPrivate`=? where `id`=?';
137 | let result1 = yield db.execute(updateExam, [
138 | name,
139 | description || '',
140 | image.trim() || defaultBannerImage,
141 | JSON.stringify(tags),
142 | JSON.stringify(questions),
143 | date,
144 | duration,
145 | isPrivate,
146 | examId,
147 | ]);
148 | if (!result1)
149 | throw new custom_error_1.default('Exam not updated !', 500);
150 | let deleteByEmail = 'delete from `Private-Exam-Emails` where `examId`=?';
151 | const result2 = yield db.execute(deleteByEmail, [examId]);
152 | if (!result2)
153 | throw new custom_error_1.default('Delete from Private-Exam-Emails failed !', 500);
154 | if (isPrivate && allowedUsers.length > 0) {
155 | let query = 'insert into `Private-Exam-Emails` (`examId`,`email`) values ';
156 | let queryArray = [];
157 | allowedUsers.map((allowedUserEmail) => {
158 | queryArray.push(`('` + examId + `','` + allowedUserEmail + `')`);
159 | });
160 | queryArray = queryArray.join(',');
161 | let result3 = yield db.execute(query + queryArray);
162 | if (!result3)
163 | throw new custom_error_1.default('Failed to update Private User in Private-Exam-Email table', 500);
164 | }
165 | // destroyScheduler(examId + 'start');
166 | // destroyScheduler(examId + 'end');
167 | // scheduleExam(examId, date, duration);
168 | return res.status(200).json((0, response_handler_1.SuccessResponse)({}, 'Exam Updated!'));
169 | }));
170 | exports.registerInExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
171 | var _e, _f;
172 | const db = (0, dbConnection_1.getDb)();
173 | let query;
174 | const { examId } = req.body;
175 | const userId = (_e = req.user) === null || _e === void 0 ? void 0 : _e.id;
176 | const email = (_f = req.user) === null || _f === void 0 ? void 0 : _f.email;
177 | //check if the ids are valid then insert in exam-participants table
178 | query =
179 | 'select isPrivate,userId as creatorId,count(*) as examExists,finished,startTime,duration from `Exam` where `id`=? limit 1';
180 | let [rows] = yield db.execute(query, [examId]);
181 | // console.log(rows);
182 | let { isPrivate, creatorId, examExists, finished, startTime, duration } = rows[0];
183 | if (!examExists)
184 | throw new custom_error_1.default('Exam not Found', 500);
185 | if (creatorId === userId)
186 | throw new custom_error_1.default('Creator cannot give Exam !', 500);
187 | const currTime = new Date().getTime();
188 | if (currTime >= startTime && currTime <= startTime + duration + 120000)
189 | throw new custom_error_1.default('Not a valid time to register! Let the evaluation finish!', 500);
190 | if (isPrivate) {
191 | query =
192 | 'select count(*) as userAllowed from `Private-Exam-Emails` where `email`=? limit 1';
193 | let [rows] = yield db.execute(query, [email]);
194 | //console.log(rows);
195 | if (!rows[0].userAllowed)
196 | throw new custom_error_1.default('Not allowed to enter !', 404);
197 | }
198 | query =
199 | 'select count(*) as alreadyRegistered from `Exam-Participants` where `participantId`=? and `examId`=? limit 1';
200 | [rows] = yield db.execute(query, [userId, examId]);
201 | if (rows[0].alreadyRegistered)
202 | throw new custom_error_1.default('User already Registered !', 500);
203 | query =
204 | 'insert into `Exam-Participants` (`examId`,`participantId`,`virtual`) values(?,?,?)';
205 | let result = yield db.execute(query, [examId, userId, finished]);
206 | if (!result)
207 | throw new custom_error_1.default('Registering failed !', 500);
208 | res.status(200).send('Successfully Registered !');
209 | }));
210 | exports.startExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
211 | var _g;
212 | const db = (0, dbConnection_1.getDb)();
213 | const examId = req.params.id;
214 | const userId = (_g = req.user) === null || _g === void 0 ? void 0 : _g.id;
215 | let query = 'select * from `Exam` where `id`=? limit 1';
216 | let [examRows] = yield db.execute(query, [examId]);
217 | if (examRows.length != 1)
218 | throw new custom_error_1.default('Exam not found !', 500);
219 | examRows[0] = (0, examFunctions_1.parseExam)(examRows[0]);
220 | (0, examFunctions_1.shuffleExam)(examRows[0]);
221 | (0, examFunctions_1.removeCorrectOptions)(examRows[0]);
222 | query =
223 | 'select count(*) as userRegistered,virtual,answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1';
224 | let [rows] = yield db.execute(query, [userId, examId]);
225 | console.log(rows);
226 | if (!rows[0].userRegistered)
227 | throw new custom_error_1.default('User not yet Registered !', 500);
228 | if (rows[0].answers)
229 | throw new custom_error_1.default('Answers already Submitted!', 500);
230 | if (rows[0].virtual)
231 | return res
232 | .status(200)
233 | .json((0, response_handler_1.SuccessResponse)(examRows[0], 'Virtual Exam Started !'));
234 | if (!examRows[0].ongoing)
235 | throw new custom_error_1.default('Exam has not started yet !', 500);
236 | //console.log(shuffleExam(examRows[0]));
237 | res.status(200).json((0, response_handler_1.SuccessResponse)(examRows[0], 'Exam Started !'));
238 | }));
239 | exports.submitExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
240 | var _h;
241 | const db = (0, dbConnection_1.getDb)();
242 | let query;
243 | const { answers, finishTime, examId } = req.body;
244 | const participantId = (_h = req.user) === null || _h === void 0 ? void 0 : _h.id;
245 | if (!answers || !finishTime || !participantId || !examId)
246 | throw new custom_error_1.default('Fields are missing !', 500);
247 | query =
248 | 'select `startTime`+`duration` as `endTime` from `Exam` where `id`=? limit 1';
249 | let [rows] = yield db.execute(query, [examId]);
250 | if (rows.length != 1)
251 | throw new custom_error_1.default('Exam not found!', 500);
252 | if (finishTime > rows[0].endTime + 15000)
253 | //extra 15secs
254 | throw new custom_error_1.default('Not a valid time to submit! Exam Finished!', 500);
255 | query =
256 | 'update `Exam-Participants` set `answers`=? , `finishTime`=? where `participantId`=? and `examId`=?';
257 | [rows] = yield db.execute(query, [
258 | JSON.stringify(answers),
259 | finishTime,
260 | participantId,
261 | examId,
262 | ]);
263 | if (!rows.affectedRows)
264 | throw new custom_error_1.default('Error occured while submitting! :(', 500);
265 | res
266 | .status(200)
267 | .json((0, response_handler_1.SuccessResponse)(rows[0], 'Exam Submitted Successfully!'));
268 | }));
269 | exports.forceEvaluateExam = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
270 | const id = req.query.id;
271 | yield (0, examFunctions_1.evaluateExam)(id);
272 | res.status(200).send('Evaluation Completed!');
273 | }));
274 | exports.getTags = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
275 | const tags = [
276 | 'Maths',
277 | 'Physics',
278 | 'Chemistry',
279 | 'Exam',
280 | 'Contest',
281 | 'Computer Science',
282 | 'Competitive Programming',
283 | 'Dynamic Programming',
284 | 'Graph Theory',
285 | 'Literature',
286 | ];
287 | res.status(200).json((0, response_handler_1.SuccessResponse)(tags, 'The tags are :'));
288 | }));
289 | exports.getQuestionTypes = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
290 | const questionTypes = [
291 | { label: 'MCQ Single Option', value: 'mcq' },
292 | { label: 'Fill in the blanks', value: 'fillInTheBlanks' },
293 | { label: 'MCQ Multiple Options', value: 'multipleOptions' },
294 | ];
295 | res
296 | .status(200)
297 | .json((0, response_handler_1.SuccessResponse)(questionTypes, 'The question types are :'));
298 | }));
299 | exports.examRegisterStatus = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
300 | var _j;
301 | const db = (0, dbConnection_1.getDb)();
302 | const userId = (_j = req.user) === null || _j === void 0 ? void 0 : _j.id;
303 | const examId = req.query.examId;
304 | let query = 'select count(*) registered from `Exam-Participants` where `examId`=? and `participantId`=?';
305 | const [rows] = yield db.execute(query, [examId, userId]);
306 | res
307 | .status(200)
308 | .json((0, response_handler_1.SuccessResponse)({ registered: rows[0].registered }, 'Register status of the user is:'));
309 | }));
310 | //this route uses customized question object for algorithm purposes!
311 | exports.getExamSolution = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
312 | var _k;
313 | const db = (0, dbConnection_1.getDb)();
314 | const examId = req.query.examId;
315 | const userId = (_k = req.user) === null || _k === void 0 ? void 0 : _k.id;
316 | let examQuestions;
317 | let query = 'select questions,finished from `Exam` where `id`=? limit 1';
318 | let [rows] = yield db.execute(query, [examId]);
319 | if (rows.length != 1)
320 | throw new custom_error_1.default('Exam not found!', 500);
321 | if (!rows[0].finished)
322 | res.status(200).json((0, response_handler_1.SuccessResponse)(null, 'Exam not yet finished!'));
323 | examQuestions = JSON.parse(rows[0].questions);
324 | let questionsObj = {};
325 | examQuestions.map((question) => {
326 | questionsObj[question.id] = question;
327 | });
328 | examQuestions = questionsObj;
329 | query =
330 | 'select answers from `Exam-Participants` where `participantId`=? and `examId`=? limit 1';
331 | [rows] = yield db.execute(query, [userId, examId]);
332 | if (rows.length != 1)
333 | res
334 | .status(200)
335 | .json((0, response_handler_1.SuccessResponse)(null, 'Error while finding answers!, maybe user not registered!'));
336 | if (!rows[0].answers)
337 | res.status(200).json((0, response_handler_1.SuccessResponse)(null, 'Answers null!'));
338 | let answers = JSON.parse(rows[0].answers);
339 | let examWithUserAns = (0, examFunctions_1.getExamWithUserAns)(examQuestions, answers);
340 | res
341 | .status(200)
342 | .json((0, response_handler_1.SuccessResponse)(examWithUserAns, 'Your solution is :'));
343 | }));
344 | exports.getExamScores = (0, catchAsync_1.default)((req, res) => __awaiter(void 0, void 0, void 0, function* () {
345 | var _l;
346 | const db = (0, dbConnection_1.getDb)();
347 | let query, rows;
348 | const userId = (_l = req.user) === null || _l === void 0 ? void 0 : _l.id;
349 | const examId = req.query.examId;
350 | query = 'select `finished` from `Exam` where `id`=? limit 1';
351 | [rows] = yield db.execute(query, [examId]);
352 | if (rows.length != 1)
353 | throw new custom_error_1.default('Exam not found!', 500);
354 | if (!rows[0].finished)
355 | throw new custom_error_1.default('Exam not finished !', 500);
356 | query =
357 | "select `participantId`, `rank`, `finishTime`, `totalScore`, (SELECT JSON_ARRAYAGG(JSON_OBJECT('participantId', ep.participantId, 'totalScore', ep.totalScore, 'rank', ep.rank, 'finishTime', ep.finishTime, 'name', u.name)) FROM `Exam-Participants` AS ep, `User` AS u WHERE ep.participantId = u.id AND ep.examId = e.examId ORDER BY ep.rank LIMIT 5) AS `topPerformers` from `Exam-Participants` AS e where `participantId`=? and `examId`=? LIMIT 1";
358 | [rows] = yield db.execute(query, [userId, examId]);
359 | if (rows.length != 1)
360 | throw new custom_error_1.default('Data not found!', 500);
361 | rows[0].topPerformers = JSON.parse(rows[0].topPerformers);
362 | res.status(200).json((0, response_handler_1.SuccessResponse)(rows[0], 'The data is : '));
363 | }));
364 |
--------------------------------------------------------------------------------