From 897d1e2e129d980b5b2283ae70c3d044c870cbf0 Mon Sep 17 00:00:00 2001 From: Joey Lim Date: Sat, 25 May 2024 22:15:42 +0800 Subject: [PATCH 1/4] fetched problems data from api and displayed based on season --- apps/web/pages/challenges/problems/index.tsx | 93 ++++++++------------ 1 file changed, 39 insertions(+), 54 deletions(-) diff --git a/apps/web/pages/challenges/problems/index.tsx b/apps/web/pages/challenges/problems/index.tsx index 05f98fea..e3216fa9 100644 --- a/apps/web/pages/challenges/problems/index.tsx +++ b/apps/web/pages/challenges/problems/index.tsx @@ -6,6 +6,13 @@ import Link from "next/link"; import { useState, useEffect } from "react"; import { useRouter } from "next/router"; +interface Season { + _id: string, + title: string, + start_date: Date, + end_date: Date +} + type ProblemListData = { uuid: number; problem: string; @@ -14,50 +21,6 @@ type ProblemListData = { solved: number; }; -// dummy data for multiple season -const seasonsData: Record = { - season1: [ - { - uuid: 1001, - problem: "A", - title: "longest substring", - point: 500, - solved: 10, - }, - { - uuid: 1002, - problem: "B", - title: "3 sum", - point: 300, - solved: 15, - }, - { - uuid: 1003, - problem: "C", - title: "minimum spanning tree", - point: 400, - solved: 30, - }, - ], - - season2: [ - { - uuid: 1004, - problem: "A", - title: "binary tree", - point: 200, - solved: 100, - }, - { - uuid: 1005, - problem: "B", - title: "panlindrome", - point: 100, - solved: 1500, - }, - ], -}; - const columnHelper = createColumnHelper(); const columns = [ @@ -86,19 +49,42 @@ const columns = [ const Problems = () => { const [option, setOption] = useState(""); const router = useRouter(); + const [data, setData] = useState([]) + const [seasons, setSeasons] = useState([]) + + function updateSeasonProblems(seasonID: string) { + const url = `http://localhost:3000/api/seasons/${seasonID}/questions` + fetch(url) + .then((res: Response) => { + return res.json()}) + .then((res: any) => { + let probs: ProblemListData[] = res.map((ele: any) => { + return {uuid: ele._id, problem: ele.question_no, title: ele.question_title, point: ele.points, solved: ele.correct_submissions_count } as ProblemListData} + ) + setData(probs) + }) + + } useEffect(() => { - const { season } = router.query; - if (season) setOption(season as string); - }, [router.query]); + fetch("http://localhost:3000/api/seasons/") + .then((res: Response) => { + return res.json() + }) + .then((res: any) => { + let seasons: Season[] = res.seasons + setSeasons(seasons) + updateSeasonProblems(seasons[0]._id) + }) + + }, []); const handleOptionChange = (event: { target: { value: string } }) => { const selectedOption = event.target.value; setOption(selectedOption); - router.push(`/challenges/problems?season=${String(selectedOption)}`); + updateSeasonProblems(selectedOption) }; - const selectedSeasonData = seasonsData[option] || []; return ( { alignItems="center" > @@ -131,7 +116,7 @@ const Problems = () => { minHeight="70vh" borderRadius="8" > - + ); From 2c94511be88a67dc9a62488acbc8b9da2af42f2d Mon Sep 17 00:00:00 2001 From: Joey Lim Date: Tue, 28 May 2024 14:38:40 +0800 Subject: [PATCH 2/4] passed id to submission page --- apps/web/pages/challenges/problems/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/pages/challenges/problems/index.tsx b/apps/web/pages/challenges/problems/index.tsx index e3216fa9..9e17dca6 100644 --- a/apps/web/pages/challenges/problems/index.tsx +++ b/apps/web/pages/challenges/problems/index.tsx @@ -14,7 +14,7 @@ interface Season { } type ProblemListData = { - uuid: number; + uuid: string; problem: string; title: string; point: number; @@ -31,7 +31,7 @@ const columns = [ columnHelper.accessor("title", { cell: (prop) => ( - {prop.getValue()} + {prop.getValue()} ), header: "Title", From 2c20253f15ab22555fb40f8b0b3a27277dc02afd Mon Sep 17 00:00:00 2001 From: Joey Lim Date: Tue, 28 May 2024 14:56:16 +0800 Subject: [PATCH 3/4] passed data into submission page, ans validation (without submitting to backend) --- .../challenges/problems/submission/index.tsx | 84 +++++++------------ 1 file changed, 32 insertions(+), 52 deletions(-) diff --git a/apps/web/pages/challenges/problems/submission/index.tsx b/apps/web/pages/challenges/problems/submission/index.tsx index eabbbc23..2a7ee0c6 100644 --- a/apps/web/pages/challenges/problems/submission/index.tsx +++ b/apps/web/pages/challenges/problems/submission/index.tsx @@ -18,12 +18,21 @@ import { ModalFooter, useToast, } from "@chakra-ui/react"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; + type InputData = { input: string; }; +interface Problem { + uuid: string; + title: string; + desc: string; + answer: string +} + const inputData: InputData = { input: "2\n5\n30 40 20 20 100\n6\n1 2 3 4 5 6", }; @@ -33,9 +42,10 @@ const Profile = () => { const [errorMessage, setErrorMessage] = useState(""); const [isCorrect, setIsCorrect] = useState(false); const [isInvalid, setIsInvalid] = useState(false); - const { isOpen, onOpen, onClose } = useDisclosure(); - + const [problem, setProblem] = useState(); + + const router = useRouter() const toast = useToast(); const handleUserInputChange = (event: { target: { value: string } }) => { @@ -50,7 +60,7 @@ const Profile = () => { } else if (isNaN(Number(userInput))) { setErrorMessage("numbers only"); setIsInvalid(true); - } else if (userInput != 5) { + } else if (userInput != problem?.answer) { // just a dummy hardcoded answer here setErrorMessage("wrong answer, please try again"); setIsInvalid(true); @@ -79,6 +89,22 @@ const Profile = () => { }); }; + useEffect(() => { + fetch(`http://localhost:3000/api/question/${router.query.id}`) + .then((res: Response) => { + return res.json() + }) + .then((res: any) => { + const resProbem: Problem = { + uuid: res._uid, + title: res.question_title, + desc: res.question_desc, + answer: res.answer + } + setProblem(resProbem) + }) + }, []) + return ( { mx={20} flexDirection="column" > - -- Cube Conundrum -- + -- {problem?.title} -- @@ -117,53 +143,7 @@ const Profile = () => { - You're launched high into the atmosphere! The apex of your - trajectory just barely reaches the surface of a large island floating - in the sky. You gently land in a fluffy pile of leaves. It's - quite cold, but you don't see much snow. An Elf runs over to - greet you.

- The Elf explains that you've arrived at Snow Island and - apologizes for the lack of snow. He'll be happy to explain the - situation, but it's a bit of a walk, so you have some time. They - don't get many visitors up here; would you like to play a game in - the meantime? -

- As you walk, the Elf shows you a small bag and some cubes which are - either red, green, or blue. Each time you play this game, he will hide - a secret number of cubes of each color in the bag, and your goal is to - figure out information about the number of cubes.

- To get information, once a bag has been loaded with cubes, the Elf - will reach into the bag, grab a handful of random cubes, show them to - you, and then put them back in the bag. He'll do this a few times - per game.

- You play several games and record the information from each game (your - puzzle input). Each game is listed with its ID number (like the 11 in - Game 11: ...) followed by a semicolon-separated list of subsets of - cubes that were revealed from the bag (like 3 red, 5 green, 4 blue). -

- For example, the record of a few games might look like this:
- Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green Game 2: 1 blue, - 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue Game 3: 8 green, 6 - blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red Game 4: 1 green, - 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red Game 5: 6 red, - 1 blue, 3 green; 2 blue, 1 red, 2 green

- In game 1, three sets of cubes are revealed from the bag (and then put - back again). The first set is 3 blue cubes and 4 red cubes; the second - set is 1 red cube, 2 green cubes, and 6 blue cubes; the third set is - only 2 green cubes.

- The Elf would first like to know which games would have been possible - if the bag contained only 12 red cubes, 13 green cubes, and 14 blue - cubes?

- In the example above, games 1, 2, and 5 would have been possible if - the bag had been loaded with that configuration. However, game 3 would - have been impossible because at one point the Elf showed you 20 red - cubes at once; similarly, game 4 would also have been impossible - because the Elf showed you 15 blue cubes at once. If you add up the - IDs of the games that would have been possible, you get 8.
-
- Determine which games would have been possible if the bag had been - loaded with only 12 red cubes, 13 green cubes, and 14 blue cubes. What - is the sum of the IDs of those games?

+ {problem?.desc}
From c0cb95313ca96d60ad96e7fbf9883c4d02244289 Mon Sep 17 00:00:00 2001 From: Boon Hian <101855951+BoonHianLim@users.noreply.github.com> Date: Sun, 2 Jun 2024 17:33:23 +0800 Subject: [PATCH 4/4] Challenges merge login (#160) * fix: remove signin endpoint * fix: add eslint and deployment stuff * fix: temp fix for limit and page undefined in getSeasonRankings * chore: format document with prettier * fix: add nodelogger * feat: use node logger * chore: add more loggers --- apps/challenges/.env.example | 8 +- apps/challenges/.eslintrc.js | 4 + apps/challenges/Dockerfile | 51 ++ apps/challenges/README.md | 12 + apps/challenges/docker-compose-local.yaml | 2 +- apps/challenges/mock/mongo-init.js | 2 + apps/challenges/package.json | 15 +- apps/challenges/src/config/db.ts | 52 +- apps/challenges/src/config/jest.config.ts | 10 +- apps/challenges/src/controllers/auth.ts | 74 +-- .../src/controllers/questionaire.ts | 287 ++++---- apps/challenges/src/controllers/season.ts | 312 ++++----- apps/challenges/src/controllers/submission.ts | 97 +-- apps/challenges/src/controllers/user.ts | 37 +- apps/challenges/src/index.ts | 58 +- .../challenges/src/middleware/errorHandler.ts | 48 ++ .../src/middleware/jwtMiddleware.ts | 44 +- .../src/middleware/jwtRefreshMiddleware.ts | 87 ++- apps/challenges/src/model/constants.ts | 2 +- apps/challenges/src/model/question.ts | 139 ++-- apps/challenges/src/model/questionInput.ts | 48 +- apps/challenges/src/model/ranking.ts | 50 +- apps/challenges/src/model/rankingScore.ts | 14 +- apps/challenges/src/model/response.ts | 8 +- apps/challenges/src/model/season.ts | 56 +- apps/challenges/src/model/submission.ts | 62 +- apps/challenges/src/model/token.ts | 39 +- apps/challenges/src/model/user.ts | 41 +- apps/challenges/src/repo/questionRepo.ts | 166 +++-- apps/challenges/src/repo/seasonRepo.ts | 238 +++---- apps/challenges/src/repo/submissionRepo.ts | 14 +- apps/challenges/src/repo/tokenRepo.ts | 76 ++- apps/challenges/src/repo/userRepo.ts | 55 +- apps/challenges/src/routes/auth.ts | 3 +- apps/challenges/src/routes/questionaire.ts | 23 +- apps/challenges/src/routes/seasons.ts | 2 +- apps/challenges/src/routes/submission.ts | 14 +- apps/challenges/src/routes/user.ts | 3 +- apps/challenges/src/service/authService.ts | 183 ++--- .../challenges/src/service/questionService.ts | 246 +++---- apps/challenges/src/service/seasonService.ts | 162 ++--- .../src/service/submissionService.ts | 132 ++-- apps/challenges/src/service/tokenService.ts | 57 +- apps/challenges/src/service/userService.ts | 84 ++- .../src/service/validationService.ts | 150 +++-- .../src/tasks/rankingCalculation.ts | 65 +- apps/challenges/src/test/leaderboard.test.ts | 189 ------ apps/challenges/src/test/pagination.test.ts | 625 ++++++++++-------- apps/challenges/src/test/questionaire.test.ts | 155 ----- .../src/test/rankingCalculation.test.ts | 397 +++++------ apps/challenges/src/test/season.test.ts | 4 +- apps/challenges/src/test/seasonRepo.test.ts | 442 +++++++------ apps/challenges/src/test/submission.test.ts | 316 --------- apps/challenges/src/types/types.ts | 24 +- apps/challenges/src/utils/db.ts | 7 +- .../challenges/src/utils/fixtures/fixtures.ts | 68 +- .../challenges/src/utils/fixtures/question.ts | 34 +- apps/challenges/src/utils/fixtures/season.ts | 18 +- .../src/utils/fixtures/submission.ts | 22 +- apps/challenges/src/utils/pagination.ts | 109 +-- apps/challenges/src/utils/supabase.ts | 37 +- apps/challenges/src/utils/validator.ts | 70 +- deployment/docker-compose.yml | 17 + turbo.json | 32 +- yarn.lock | 80 ++- 65 files changed, 2867 insertions(+), 3111 deletions(-) create mode 100644 apps/challenges/.eslintrc.js create mode 100644 apps/challenges/Dockerfile create mode 100644 apps/challenges/README.md create mode 100644 apps/challenges/src/middleware/errorHandler.ts delete mode 100644 apps/challenges/src/test/leaderboard.test.ts delete mode 100644 apps/challenges/src/test/questionaire.test.ts delete mode 100644 apps/challenges/src/test/submission.test.ts diff --git a/apps/challenges/.env.example b/apps/challenges/.env.example index d338fa38..6f716b90 100644 --- a/apps/challenges/.env.example +++ b/apps/challenges/.env.example @@ -1,6 +1,8 @@ -MONGO_URI=mongodb://localhost:27017/ +NODE_ENV=production + +MONGODB_URI=mongodb://localhost:27017/ MONGO_PORT=27017 -MONGO_DATABSE_NAME=challenges +CHALLENGES_MONGO_DATABSE_NAME=challenges BASE_URL=http://localhost:3001 PORT=3001 @@ -11,4 +13,4 @@ SUPABASE_URL= SUPABASE_ANON_KEY= // set the secret to something (hey dont peek these are secrets! ba..baka!) -JWT_SECRET=abc +CHALLENGES_JWT_SECRET=abc diff --git a/apps/challenges/.eslintrc.js b/apps/challenges/.eslintrc.js new file mode 100644 index 00000000..c8df6075 --- /dev/null +++ b/apps/challenges/.eslintrc.js @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ["custom"], +}; diff --git a/apps/challenges/Dockerfile b/apps/challenges/Dockerfile new file mode 100644 index 00000000..39340fc3 --- /dev/null +++ b/apps/challenges/Dockerfile @@ -0,0 +1,51 @@ +FROM node:18-alpine AS builder + +RUN apk add --no-cache libc6-compat +RUN apk update + +# Set working directory +WORKDIR /app +RUN yarn global add turbo +COPY . . +RUN rm -rf **/node_modules **/build **/dist **/out **/.turbo +RUN rm -rf **/.env +RUN turbo prune --scope=merch --docker + +FROM node:18-alpine AS installer +# Add lockfile and package.json's of isolated subworkspace +RUN apk add --no-cache libc6-compat +RUN apk update +WORKDIR /app + +# First install dependencies (as they change less often) +#COPY .gitignore .gitignore +COPY --from=builder /app/out/json/ . +COPY --from=builder /app/out/yarn.lock ./yarn.lock +RUN yarn install + +# Build the project and its dependencies +COPY --from=builder /app/out/full/ . +COPY turbo.json turbo.json +RUN yarn turbo run build --filter=challenges... --color + +FROM node:18-alpine AS runner + +WORKDIR /app + +# Don't run production as root +#RUN addgroup --system --gid 1001 expressjs +#RUN adduser --system --uid 1001 expressjs +#USER expressjs + +COPY --from=installer /app . +RUN rm -rf /app/apps/challenges/src + +WORKDIR /app/apps/challenges + +#ENV NODE_ENV=production + +#EXPOSE 80 +EXPOSE 3002 + +CMD ["node", "dist/index.js"] + diff --git a/apps/challenges/README.md b/apps/challenges/README.md new file mode 100644 index 00000000..b498ae35 --- /dev/null +++ b/apps/challenges/README.md @@ -0,0 +1,12 @@ +# Challenges Backend Service + +This is the backend service for the Challenges feature for the SCSE Club. + +## Before you start... + +1. Make sure you have prettier and eslint enable in your IDE. + > For visual studio code user, please install and enable [eslint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) and [prettier extension](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) +2. The codebase is designed around controller-service-repository architecutre. Please strictly follow the architecture when writing your code. + > You may refer to this [blog](https://tom-collings.medium.com/controller-service-repository-16e29a4684e5) for more info. + +## Scripts \ No newline at end of file diff --git a/apps/challenges/docker-compose-local.yaml b/apps/challenges/docker-compose-local.yaml index c0bc33f3..485f6a2d 100644 --- a/apps/challenges/docker-compose-local.yaml +++ b/apps/challenges/docker-compose-local.yaml @@ -11,6 +11,6 @@ services: env_file: - ./.env environment: - MONGO_INITDB_DATABASE: ${MONGO_DATABSE_NAME} + MONGO_INITDB_DATABASE: ${CHALLENGES_MONGO_DATABSE_NAME} volumes: - ./mock/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro \ No newline at end of file diff --git a/apps/challenges/mock/mongo-init.js b/apps/challenges/mock/mongo-init.js index 2fa899d7..19f19861 100644 --- a/apps/challenges/mock/mongo-init.js +++ b/apps/challenges/mock/mongo-init.js @@ -1,3 +1,5 @@ +// This file is to init local mongodb. Check docker-compose-local.yaml file for more details +/* eslint-disable */ db.createCollection("seasons"); db.createCollection("rankings"); db.createCollection("questions"); diff --git a/apps/challenges/package.json b/apps/challenges/package.json index a64de685..60c6c59a 100644 --- a/apps/challenges/package.json +++ b/apps/challenges/package.json @@ -8,21 +8,14 @@ "@babel/preset-typescript": "^7.23.3", "@supabase/supabase-js": "^2.39.7", "@types/express": "^4.17.21", - "babel-jest": "^29.7.0", "cookie-parser": "^1.4.6", - "cron": "^3.1.6", "cross-env": "^7.0.3", "dotenv": "^16.3.1", "express": "^4.18.2", "express-async-handler": "^1.1.4", - "jest": "^29.7.0", "jsonwebtoken": "^9.0.2", - "jwt-decode": "^4.0.0", - "prettier": "^2.8.1", - "supertest": "^6.3.4", - "ts-jest": "^29.1.1", - "ts-node": "^10.9.2", - "turbo": "^1.10.1", + "node-cron": "^3.0.3", + "nodelogger": "*", "zod": "^3.22.4" }, "devDependencies": { @@ -30,11 +23,15 @@ "@types/jest": "^29.5.11", "@types/jsonwebtoken": "^9.0.6", "@types/node": "^20.10.6", + "@types/node-cron": "^3.0.11", "@types/supertest": "^6.0.2", "jest": "^29.7.0", + "morgan": "^1.10.0", "nodemon": "^3.0.2", + "prettier": "^2.8.1", "supertest": "^6.3.4", "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", "typescript": "^5.3.3" }, "scripts": { diff --git a/apps/challenges/src/config/db.ts b/apps/challenges/src/config/db.ts index b282e176..bab159a2 100644 --- a/apps/challenges/src/config/db.ts +++ b/apps/challenges/src/config/db.ts @@ -1,32 +1,38 @@ import moongose from "mongoose"; import * as dotenv from "dotenv"; import { ConnectionOptions } from "tls"; +import { Logger } from "nodelogger"; dotenv.config(); export const connectDB = async () => { - try { - const mongoURL = process.env.MONGO_URI || 'mongodb://localhost:27017'; - const conn = await moongose.connect(mongoURL, { - useNewUrlParser: true, - dbName: process.env.MONGO_DATABSE_NAME || 'challenges', - } as ConnectionOptions); - console.log(`MongoDB Connected: ${mongoURL}`); - } catch (error) { - console.log(error); - process.exit(1) + try { + const mongoURL = process.env.MONGODB_URI || "mongodb://localhost:27017"; + await moongose.connect(mongoURL, { + useNewUrlParser: true, + dbName: process.env.CHALLENGES_MONGO_DATABSE_NAME || "challenges", + } as ConnectionOptions); + Logger.info(`[server]: MongoDB Connected: ${mongoURL}`); + } catch (error) { + let errorReason = "unknown error"; + if (error instanceof Error) { + errorReason = error.message; } -} + Logger.error(`[server]: MongoDB connected failed due to ${errorReason}`); + process.exit(1); + } +}; export const connectTestDB = async () => { - try { - const mongoURL = process.env.MONGO_URI || 'mongodb://localhost:27017'; - const conn = await moongose.connect(mongoURL, { - useNewUrlParser: true, - dbName: process.env.MONGO_TEST_DATABSE_NAME || 'test', - } as ConnectionOptions); - console.log(`MongoDB Connected: ${mongoURL}`); - } catch (error) { - console.log(error); - process.exit(1) - } -} \ No newline at end of file + try { + const mongoURL = process.env.MONGODB_URI || "mongodb://localhost:27017"; + await moongose.connect(mongoURL, { + useNewUrlParser: true, + // eslint-disable-next-line turbo/no-undeclared-env-vars + dbName: process.env.MONGO_TEST_DATABSE_NAME || "test", + } as ConnectionOptions); + Logger.info(`MongoDB Connected: ${mongoURL}`); + } catch (error) { + Logger.error(error); + process.exit(1); + } +}; diff --git a/apps/challenges/src/config/jest.config.ts b/apps/challenges/src/config/jest.config.ts index f9ec7448..412082b2 100644 --- a/apps/challenges/src/config/jest.config.ts +++ b/apps/challenges/src/config/jest.config.ts @@ -1,8 +1,8 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', + preset: "ts-jest", + testEnvironment: "node", transform: { - '^.+\\.ts?$': 'ts-jest', + "^.+\\.ts?$": "ts-jest", }, - transformIgnorePatterns: ['/node_modules/'], -}; \ No newline at end of file + transformIgnorePatterns: ["/node_modules/"], +}; diff --git a/apps/challenges/src/controllers/auth.ts b/apps/challenges/src/controllers/auth.ts index f4edae34..bf14e813 100644 --- a/apps/challenges/src/controllers/auth.ts +++ b/apps/challenges/src/controllers/auth.ts @@ -1,58 +1,44 @@ import asyncHandler from "express-async-handler"; -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import AuthService from "../service/authService"; -import TokenService from "../service/tokenService"; +import { Logger } from "nodelogger"; -// TODO: remove this when deployed, only for local use now -const signIn = asyncHandler(async (req: Request, res: Response) => { - const url = process.env.BASE_URL || null; - if (!url?.includes("localhost")) { - res.status(404); - return; - } - try { - const accessToken = await TokenService.generateAccessToken("65d0479b3f6a9f5986e68ce3", "test@gmail.com"); - const refreshToken = await TokenService.generateRefreshToken("65d0479b3f6a9f5986e68ce3", "test@gmail.com"); - res.status(200).json({ - "access_token": accessToken, - "refresh_token": refreshToken - }) - } catch (error) { - console.log("AuthService.oauthSignIn", error); - res.status(500).json({ message: 'Internal Server Error' }); - } -}); +interface OauthSignInReq { + access_token: string; +} -const oauthSignIn = asyncHandler(async (req: Request, res: Response) => { - const { access_token } = req.body; +const oauthSignIn = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const { access_token } = req.body as OauthSignInReq; - try { - const { accessToken, refreshToken, createNewUser } = await AuthService.oauthSignIn(access_token); - res.status(createNewUser ? 200 : 201).json({ - "access_token": accessToken, - "refresh_token": refreshToken - }) - } catch (error) { - console.log("AuthService.oauthSignIn", error); - res.status(500).json({ message: 'Internal Server Error' }); - } + try { + const { accessToken, refreshToken, createNewUser } = + await AuthService.oauthSignIn(access_token); + res.status(createNewUser ? 200 : 201).json({ + access_token: accessToken, + refresh_token: refreshToken, + }); + } catch (error) { + Logger.error("AuthController.oauthSignIn error", error); + next(error) + } }); -const refreshToken = asyncHandler(async (req: Request, res: Response) => { +const refreshToken = asyncHandler( + async (req: Request, res: Response, next: NextFunction) => { try { - const userID = req.params.userID; - const token = await AuthService.refreshToken(userID); - res.status(200).json(token); + const userID = req.params.userID; + const token = await AuthService.refreshToken(userID); + res.status(200).json(token); } catch (err) { - console.error(err); - res.status(500).json({ message: "Internal Server Error" }) + Logger.error("AuthController.refreshToken error", err); + next(err); } -}) + } +); const AuthController = { - signIn, - oauthSignIn, - refreshToken, -} + oauthSignIn, + refreshToken, +}; export { AuthController as default }; diff --git a/apps/challenges/src/controllers/questionaire.ts b/apps/challenges/src/controllers/questionaire.ts index 2ce0ded3..31883ab7 100644 --- a/apps/challenges/src/controllers/questionaire.ts +++ b/apps/challenges/src/controllers/questionaire.ts @@ -1,229 +1,186 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import asyncHandler from "express-async-handler"; -import Question, { CreateQuestionReq } from '../model/question'; +import Question, { QuestionReq } from "../model/question"; import { isValidObjectId } from "../utils/db"; import QuestionService from "../service/questionService"; -import { isValidCreateQuestionRequest } from "../utils/validator"; +import { isValidQuestionRequest } from "../utils/validator"; import { z } from "zod"; +import { Logger } from "nodelogger"; // @desc Get questions // @route GET /api/question // @access Public -const getQuestions = asyncHandler(async (req: Request, res: Response) => { - const questions = await Question.find({}) - - res.status(200).json(questions) -}) +const getQuestions = asyncHandler( + async (req: Request, res: Response, next: NextFunction) => { + // We pass in empty object, meaning that we GetQuestions without setting any filters. + // This means we will return all questions in our db. + try { + const questions = await QuestionService.GetQuestions({}); + res.status(200).json(questions); + } catch (err) { + Logger.error("QuestionnaireController.GetQuestions error", err); + next(err); + } + } +); // @desc Get active questions // @route GET /api/activity/active // @access Public const getActiveQuestions = asyncHandler(async (req: Request, res: Response) => { - const questions = await Question.find({ "active": true }) + const questions = await QuestionService.GetQuestions({ isActive: true }); - res.status(200).json(questions) -}) + res.status(200).json(questions); +}); // @desc Get question // @route GET /api/question/:id // @access Public -const getQuestion = asyncHandler(async (req: Request, res: Response) => { +const getQuestion = asyncHandler( + async (req: Request, res: Response, next: NextFunction) => { const questionId = req.params.id; - if (!isValidObjectId(questionId)) { - res.status(400).json({ message: 'Invalid question ID' }); - return; - } - try { - const question = await QuestionService.getQuestionByID(questionId); - - if (!question) { - res.status(404).json({ message: 'Question not found' }); - return; - } + const question = await QuestionService.getQuestionByID(questionId); - res.status(200).json(question); + res.status(200).json(question); } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal Server Error' }); + Logger.error("QuestionnaireController.GetQuestion error", error); + next(error); } -}); + } +); -const getUserSpecificQuestion = asyncHandler(async (req: Request, res: Response) => { +const getUserSpecificQuestion = asyncHandler( + async (req: Request, res: Response) => { const { userID } = req.params; const questionId = req.params.id; try { - const question = await QuestionService.getUserSpecificQuestion(userID, questionId); + const question = await QuestionService.getUserSpecificQuestion( + userID, + questionId + ); - if (!question) { - res.status(404).json({ message: 'Question not found' }); - return; - } + if (!question) { + res.status(404).json({ message: "Question not found" }); + return; + } - res.status(200).json(question); + res.status(200).json(question); } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal Server Error' }); + Logger.error( + "QuestionnaireController.GetUserSpecificQuestion error", + error + ); + res.status(500).json({ message: "Internal Server Error" }); } -}) - + } +); // @desc Set question // @route POST /api/question // @access Private const setQuestion = asyncHandler(async (req: Request, res: Response) => { - try { - const question = isValidCreateQuestionRequest.parse(req.body); - - const createQuestionReq: CreateQuestionReq = { - ...question, - question_date: new Date(question.question_date), - expiry: new Date(question.expiry), - } - - const resp = await QuestionService.createQuestion(createQuestionReq); - - res.status(resp.status).json(resp); - } catch (err) { - console.error(err); - if (err instanceof z.ZodError) { - const message = err.issues.map((issue) => issue.message).join(", "); - res.status(400).json({ message }); - return; - } - - res.status(500).json({ message: 'Internal Server Error' }); + try { + const question = isValidQuestionRequest.parse(req.body); + + const createQuestionReq: QuestionReq = { + ...question, + question_date: new Date(question.question_date), + expiry: new Date(question.expiry), + }; + + const resp = await QuestionService.createQuestion(createQuestionReq); + + res.status(resp.status).json(resp); + } catch (err) { + if (err instanceof z.ZodError) { + const message = err.issues.map((issue) => issue.message).join(", "); + res.status(400).json({ message }); + return; } + + Logger.error("QuestionnaireController.SetQuestion error", err); + res.status(500).json({ message: "Internal Server Error" }); + } }); // @desc Update question // @route PUT /api/question/:id // @access Private const updateQuestion = asyncHandler(async (req: Request, res: Response) => { - const questionId = req.params.id; - if (!isValidObjectId(questionId)) { - res.status(400).json({ message: 'Invalid question ID' }); - return; + const questionId = req.params.id; + if (!isValidObjectId(questionId)) { + res.status(400).json({ message: "Invalid question ID" }); + return; + } + try { + const question = await Question.findById(questionId); + + if (!question) { + res.status(404).json({ message: "Question not found" }); + return; } - try { - const question = await Question.findById(questionId); - - if (!question) { - res.status(404).json({ message: 'Question not found' }); - return; - } - - const updatedQuestion = await Question.findByIdAndUpdate(questionId, req.body, { new: true }); - res.status(200).json(updatedQuestion); - } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal Server Error' }); - } + const toBeUpdateQuestion = isValidQuestionRequest.parse(req.body); + + const updateQuestionReq: QuestionReq = { + ...toBeUpdateQuestion, + question_date: new Date(question.question_date), + expiry: new Date(question.expiry), + }; + + const updatedQuestion = await Question.findByIdAndUpdate( + questionId, + updateQuestionReq, + { new: true } + ); + + res.status(200).json(updatedQuestion); + } catch (error) { + Logger.error("QuestionnaireController.UpdateQuestion error", error); + res.status(500).json({ message: "Internal Server Error" }); + } }); // @desc Delete question // @route DELETE /api/question/:id // @access Private const deleteQuestion = asyncHandler(async (req: Request, res: Response) => { - const questionId = req.params.id; - - if (!isValidObjectId(questionId)) { - res.status(400).json({ message: 'Invalid question ID' }); - return; - } - - try { - const question = await Question.findById(questionId); - - if (!question) { - res.status(404).json({ message: 'Question not found' }); - return; - } - - await question.remove() - - res.status(200).json({ message: 'Question deleted' }); - } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal Server Error' }); - } -}); + const questionId = req.params.id; -// @desc Submit answer -// @route POST /api/question/submit/:id -// @access Private + if (!isValidObjectId(questionId)) { + res.status(400).json({ message: "Invalid question ID" }); + return; + } -// TODO: fix this -/* -const submitAnswer = asyncHandler(async (req: Request, res: Response) => { - const questionId = req.params.id; + try { + const question = await Question.findById(questionId); - if (!isValidObjectId(questionId)) { - return res.status(400).json({ message: 'Invalid question ID' }); + if (!question) { + res.status(404).json({ message: "Question not found" }); + return; } - try { - const question = await Question.findById(questionId); - - if (!question) { - return res.status(404).json({ message: 'Question not found' }); - } - - if (!question.active) { - return res.status(400).json({ message: 'Question is not active' }); - } - - if (new Date(question.expiry) < new Date()) { - return res.status(400).json({ message: 'Question has expired' }); - } - - const submission = await Submission.create({ - user: req.body.user, - leaderboard: req.body.leaderboard, - answer: req.body.answer, - correct: req.body.answer === question.answer, - points_awarded: req.body.answer === question.answer ? question.points : 0, - question: questionId - }); - - // Update question submissions array using $push - await Question.findByIdAndUpdate(questionId, { $push: { submissions: submission._id } }, { new: true }); - - // Retrieve user and update points of the entry in the leaderboard - const season = await Season.findOne({ _id: req.body.leaderboard }); - const ranking = season?.rankings.find((ranking: any) => ranking.user == req.body.user); - if (!ranking) { - await Season.findByIdAndUpdate(req.body.leaderboard, { $push: { rankings: { user: req.body.user, points: submission.points_awarded } } }, { new: true }); - } else {Season - // Update points - await Season.findOneAndUpdate({ _id: req.body.leaderboard, 'rankings.user': req.body.user }, { $set: { 'rankings.$.points': ranking.points + submission.points_awarded } }, { new: true }); - } - - res.status(201).json({ message: 'Answer submitted' }); + await question.remove(); - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - } -}) -*/ - -async function updateUserPoints() { - -} + res.status(200).json({ message: "Question deleted" }); + } catch (error) { + Logger.error("QuestionnaireController.DeleteQuestion error", error); + res.status(500).json({ message: "Internal Server Error" }); + } +}); const QuestionController = { - getQuestion, - getUserSpecificQuestion, - getQuestions, - getActiveQuestions, - setQuestion, - updateQuestion, - // submitAnswer - deleteQuestion + getQuestion, + getUserSpecificQuestion, + getQuestions, + getActiveQuestions, + setQuestion, + updateQuestion, + deleteQuestion, }; -export { QuestionController as default }; \ No newline at end of file +export { QuestionController as default }; diff --git a/apps/challenges/src/controllers/season.ts b/apps/challenges/src/controllers/season.ts index 517bea11..444e2b3e 100644 --- a/apps/challenges/src/controllers/season.ts +++ b/apps/challenges/src/controllers/season.ts @@ -1,197 +1,213 @@ -import { Request, Response } from "express"; +import { NextFunction, Request, Response } from "express"; import asyncHandler from "express-async-handler"; import { z } from "zod"; import { isValidObjectId } from "../utils/db"; import SeasonService from "../service/seasonService"; -import { isNonNegativeInteger, isValidDate, zodIsValidObjectId } from "../utils/validator"; +import { + isNonNegativeInteger, + isValidDate, + zodIsValidObjectId, +} from "../utils/validator"; import { generatePaginationMetaData } from "../utils/pagination"; - +import { Logger } from "nodelogger"; interface CreateSeasonRequest { - title: string; - startDate: number; - endDate: number; + title: string; + startDate: number; + endDate: number; } // @desc Get season // @route GET /api/seasons // @access Public -const getSeasons = asyncHandler(async (req: Request, res: Response) => { - const { start, end } = req.query; - var _startDate; - var _endDate; - if (start != null && typeof start !== "string"){ - res.status(400).json({ message: 'Invalid request' }); - return; - } - if (end != null && typeof end !== "string") { - res.status(400).json({ message: 'Invalid request' }); - return; - } - - _startDate = start != null ? new Date(parseInt((start) as string)) : null; - _endDate = end != null ? new Date(parseInt((end) as string)) : null; - - try { - const seasons = await SeasonService.getSeasonsByDate(_startDate, _endDate); - - res.status(200).json({ - seasons: seasons, - }); - } catch { - res.status(500).json({ message: 'Internal Server Error' }); - } +const getSeasons = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const { start, end } = req.query; + + try { + const seasons = await SeasonService.GetSeasons(start, end); + + res.status(200).json({ + seasons: seasons, + }); + } catch (error) { + Logger.error("SeasonController.GetSeasons error", error); + next(error); + } }); // @desc Getc active season // @route GET /api/seasons/active // @access Public const getActiveSeasons = asyncHandler(async (req: Request, res: Response) => { - try { - const seasons = await SeasonService.getActiveSeasons(); - res.status(200).json(seasons); - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - } + try { + const seasons = await SeasonService.getActiveSeasons(); + res.status(200).json(seasons); + } catch (error) { + Logger.error("SeasonController.GetActiveSeasons error", error); + res.status(500).json({ message: "Internal Server Error" }); + } }); // @desc Get season // @route GET /api/seasons/:seasonID // @access Public const getSeasonByID = asyncHandler(async (req: Request, res: Response) => { - const { seasonID } = req.params; - - if (!isValidObjectId(seasonID)) { - res.status(400).json({ message: 'Invalid season ID' }); - return; - } - try { - const season = await SeasonService.getSeasonByID(seasonID); - res.status(200).json(season); - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - } + const { seasonID } = req.params; + + if (!isValidObjectId(seasonID)) { + res.status(400).json({ message: "Invalid season ID" }); + return; + } + try { + const season = await SeasonService.getSeasonByID(seasonID); + res.status(200).json(season); + } catch (error) { + Logger.error("SeasonController.GetSeasonById error", error); + res.status(500).json({ message: "Internal Server Error" }); + } }); // @desc Set season // @route POST /api/seasons // @access Private const createSeason = asyncHandler(async (req: Request, res: Response) => { - const body: CreateSeasonRequest = req.body; - if(!body.title || !body.startDate || !body.endDate) { - res.status(400).json({ message: 'Invalid request' }); - return; - } - var _startDate; - var _endDate; - try{ - _startDate = new Date(body.startDate); - _endDate = new Date(body.endDate); - if(!isValidDate(_startDate) || !isValidDate(_endDate)){ - throw new Error("Invalid Date"); - } - }catch{ - res.status(400).json({ message: 'Invalid request: Date invalid' }); - return; - } - - try { - const season = await SeasonService.createSeason( - body.title, - _startDate, - _endDate - ); - res.status(201).json(season); - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); + const body: CreateSeasonRequest = req.body as CreateSeasonRequest; + if (!body.title || !body.startDate || !body.endDate) { + res.status(400).json({ message: "Invalid request" }); + return; + } + let _startDate: Date; + let _endDate: Date; + try { + _startDate = new Date(body.startDate); + _endDate = new Date(body.endDate); + if (!isValidDate(_startDate) || !isValidDate(_endDate)) { + throw new Error("Invalid Date"); } + } catch { + res.status(400).json({ message: "Invalid request: Date invalid" }); + return; + } + + try { + const season = await SeasonService.createSeason( + body.title, + _startDate, + _endDate + ); + res.status(201).json(season); + } catch (error) { + res.status(500).json({ message: "Internal Server Error" }); + } }); // @desc Get season rankings // @route GET /api/seasons/:seasonID/rankings // @access Public const getSeasonRankings = asyncHandler(async (req: Request, res: Response) => { - let seasonID, page, limit; - try { - seasonID = zodIsValidObjectId.parse(req.params.seasonID); - const queryIsValid = z.object({ - page: z.coerce.number().int().min(0).optional(), - limit: z.coerce.number().int().min(1).optional() - }).refine( - data => ((data.page || data.page === 0) && data.limit) || ((!data.page && data.page !== 0)&& !data.limit), - { message: "Invalid request" } - ); - page = queryIsValid.parse(req.query).page; - limit = queryIsValid.parse(req.query).limit; - const season = await SeasonService.getSeasonByID(seasonID); - if(!season){ - res.status(404).json({ message: 'Season not found' }); - return; - } - - if(!limit && !page){ - const rankings = await SeasonService.getSeasonRankings(seasonID); - res.status(200).json({ - seasonID: seasonID, - rankings: rankings, - }); - return; - } - } catch (err) { - if(err instanceof z.ZodError){ - res.status(400).json({ message: 'Invalid request' }); - }else{ - res.status(500).json({ message: 'Internal Server Error' }); - } + let seasonID = ""; + let page: number | undefined, limit: number | undefined; + try { + seasonID = zodIsValidObjectId.parse(req.params.seasonID); + const queryIsValid = z + .object({ + page: z.coerce.number().int().min(0).optional(), + limit: z.coerce.number().int().min(1).optional(), + }) + .refine( + // page and limit must either both exist or both not exist + (data) => + ((data.page || data.page === 0) && data.limit) || + (!data.page && data.page !== 0 && !data.limit), + { message: "Invalid request" } + ); + page = queryIsValid.parse(req.query).page; + limit = queryIsValid.parse(req.query).limit; + const season = await SeasonService.getSeasonByID(seasonID); + if (!season) { + res.status(404).json({ message: "Season not found" }); + return; } - try{ - const { rankings, rankingsCount } = await SeasonService.getSeasonRankingsByPagination(seasonID, page!, limit!); - - isNonNegativeInteger.parse(rankingsCount); - const maxPageIndex = rankingsCount == 0 ? 0 : Math.ceil(rankingsCount / limit) - 1 - - const metaData = generatePaginationMetaData(`/api/seasons/${seasonID}/rankings`, page!, limit!, maxPageIndex, rankingsCount); - res.setHeader("access-control-expose-headers", "pagination"); - res.setHeader("pagination", JSON.stringify(metaData)); - res.status(200).json({ - seasonID: seasonID, - rankings: rankings, - _metaData: metaData - }); - }catch(e){ - res.status(500).json({ message: 'Internal Server Error' }); + if (!limit && !page) { + const rankings = SeasonService.getSeasonRankings(seasonID); + res.status(200).json({ + seasonID: seasonID, + rankings: rankings, + }); + return; + } + } catch (err) { + if (err instanceof z.ZodError) { + res.status(400).json({ message: "Invalid request" }); + } else { + res.status(500).json({ message: "Internal Server Error" }); + } + } + + try { + // Limit and page must either both exist or both not exist, since both not exist is checked above, now it must be both exist + // This is just a redundant check added for eslint + if (limit === undefined || page === undefined) { + console.log(limit, page); + res.status(500).json({ message: "Internal Server Error" }); + return; } + + const { rankings, rankingsCount } = + SeasonService.getSeasonRankingsByPagination(seasonID, page, limit); + + isNonNegativeInteger.parse(rankingsCount); + const maxPageIndex = + rankingsCount == 0 ? 0 : Math.ceil(rankingsCount / limit) - 1; + + const metaData = generatePaginationMetaData( + `/api/seasons/${seasonID}/rankings`, + page, + limit, + maxPageIndex, + rankingsCount + ); + res.setHeader("access-control-expose-headers", "pagination"); + res.setHeader("pagination", JSON.stringify(metaData)); + res.status(200).json({ + seasonID: seasonID, + rankings: rankings, + _metaData: metaData, + }); + } catch (e) { + res.status(500).json({ message: "Internal Server Error" }); + } }); const getSeasonQuestions = asyncHandler(async (req: Request, res: Response) => { - try { - const seasonID = zodIsValidObjectId.parse(req.params.seasonID); - - const season = await SeasonService.getSeasonByID(seasonID); - if (!season) { - res.status(404).json({ message: 'Season not found' }); - return; - } - - const questions = await SeasonService.getSeasonQuestions(seasonID); - - res.status(200).json(questions); - } catch (err) { - if (err instanceof z.ZodError) { - res.status(400).json({ message: 'Invalid request' }); - } else { - res.status(500).json({ message: 'Internal Server Error' }); - } + try { + const seasonID = zodIsValidObjectId.parse(req.params.seasonID); + + const season = await SeasonService.getSeasonByID(seasonID); + if (!season) { + res.status(404).json({ message: "Season not found" }); + return; + } + + const questions = await SeasonService.getSeasonQuestions(seasonID); + + res.status(200).json(questions); + } catch (err) { + if (err instanceof z.ZodError) { + res.status(400).json({ message: "Invalid request" }); + } else { + res.status(500).json({ message: "Internal Server Error" }); } + } }); const SeasonController = { - getSeasons, - getActiveSeasons, - getSeasonByID, - createSeason, - getSeasonRankings, - getSeasonQuestions, + getSeasons, + getActiveSeasons, + getSeasonByID, + createSeason, + getSeasonRankings, + getSeasonQuestions, }; export { SeasonController as default }; diff --git a/apps/challenges/src/controllers/submission.ts b/apps/challenges/src/controllers/submission.ts index 469af500..5cf25782 100644 --- a/apps/challenges/src/controllers/submission.ts +++ b/apps/challenges/src/controllers/submission.ts @@ -1,78 +1,81 @@ import { Request, Response } from "express"; -const asyncHandler = require('express-async-handler'); -import Question from '../model/question'; -import Submission from '../model/submission'; -import Season from "../model/season"; +import asyncHandler from "express-async-handler"; +import Submission from "../model/submission"; import { isValidObjectId } from "../utils/db"; -import QuestionService from "../service/questionService"; import SubmissionService from "../service/submissionService"; import { isValidCreateSubmissionRequest } from "../utils/validator"; -import mongoose from 'mongoose'; - +import mongoose from "mongoose"; +import { Logger } from "nodelogger"; // @desc Get submissions // @route GET /api/submission // @access Public const getSubmissions = asyncHandler(async (req: Request, res: Response) => { - const submissions = await Submission.find({}) - res.status(200).json(submissions) -}) + const submissions = await Submission.find({}); + res.status(200).json(submissions); +}); // @desc Get submission // @route GET /api/submission/:id // @access Public const getSubmission = asyncHandler(async (req: Request, res: Response) => { - const submissionId = req.params.id; - - if (!isValidObjectId(submissionId)) { - return res.status(400).json({ message: 'Invalid submission ID' }); - } + const submissionId = req.params.id; - try { - const submission = await Submission.findById(submissionId); + if (!isValidObjectId(submissionId)) { + res.status(400).json({ message: "Invalid submission ID" }); + return; + } - if (!submission) { - return res.status(404).json({ message: 'Submission not found' }); - } + try { + const submission = await Submission.findById(submissionId); - res.status(200).json(submission); - } catch (error) { - console.error(error); - res.status(500).json({ message: 'Internal Server Error' }); + if (!submission) { + res.status(404).json({ message: "Submission not found" }); + return; } -}) + res.status(200).json(submission); + } catch (error) { + Logger.error("SubmissionController.GetSubmission error", error); + res.status(500).json({ message: "Internal Server Error" }); + } +}); + +interface SetSubmissionReq { + questionID: string; +} // @desc Set submission // @route POST /api/submission/ // @access Private const setSubmission = asyncHandler(async (req: Request, res: Response) => { - const questionID = req.body.question; + const { questionID } = req.body as SetSubmissionReq; - if (!isValidObjectId(questionID)) { - return res.status(400).json({ message: 'Invalid question ID' }); - } - - try { - const submission = isValidCreateSubmissionRequest.parse(req.body); - const createSubmissionReq = { - user: new mongoose.Types.ObjectId(submission.user), - question: new mongoose.Types.ObjectId(submission.question), - answer: submission.answer - }; + if (!isValidObjectId(questionID)) { + res.status(400).json({ message: "Invalid question ID" }); + return; + } - const resp = await SubmissionService.createSubmission(createSubmissionReq); + try { + const submission = isValidCreateSubmissionRequest.parse(req.body); + const createSubmissionReq = { + user: new mongoose.Types.ObjectId(submission.user), + question: new mongoose.Types.ObjectId(submission.question), + answer: submission.answer, + }; - res.status(resp.status).json(resp); + const resp = await SubmissionService.createSubmission(createSubmissionReq); - } catch (error) { - res.status(500).json({ message: 'Internal Server Error' }); - } -}) + res.status(resp.status).json(resp); + } catch (error) { + Logger.error("SubmissionController.SetSubmission error", error); + res.status(500).json({ message: "Internal Server Error" }); + } +}); const SubmissionController = { - getSubmissions, - getSubmission, - setSubmission, + getSubmissions, + getSubmission, + setSubmission, }; -export { SubmissionController as default }; \ No newline at end of file +export { SubmissionController as default }; diff --git a/apps/challenges/src/controllers/user.ts b/apps/challenges/src/controllers/user.ts index dc38fc53..31421c98 100644 --- a/apps/challenges/src/controllers/user.ts +++ b/apps/challenges/src/controllers/user.ts @@ -1,33 +1,22 @@ import UserService from "../service/userService"; import asyncHandler from "express-async-handler"; -import { Request, Response } from "express"; -import { StatusCodeError } from "../types/types"; +import { NextFunction, Request, Response } from "express"; +import { Logger } from "nodelogger"; -const getUser = asyncHandler(async (req: Request, res: Response) => { - const { userID } = req.params; +const getUser = asyncHandler(async (req: Request, res: Response, next: NextFunction) => { + const { userID } = req.params; - try { - const user = await UserService.getUserByID(userID); - res.status(200).json(user); - } catch (err) { - if (err instanceof StatusCodeError) { - res.status(err.status).json({ message: err.message }); - } else { - res.status(500).json({ message: "Internal Server Error" }) - } - } + try { + const user = await UserService.getUserByID(userID); + res.status(200).json(user); + } catch (err) { + Logger.error("UserController.GetUser error", err); + next(err); + } }); -const checkTokens = asyncHandler(async (req: Request, res: Response) => { - const token = req.signedCookies; - - console.log(token); - - res.status(200); -}); const UserController = { - getUser, - checkTokens, -} + getUser, +}; export { UserController as default }; diff --git a/apps/challenges/src/index.ts b/apps/challenges/src/index.ts index 627f9aee..57ea1901 100644 --- a/apps/challenges/src/index.ts +++ b/apps/challenges/src/index.ts @@ -1,57 +1,61 @@ import express, { Express, Request, Response } from "express"; import dotenv from "dotenv"; +import cron from "node-cron"; import SeasonRouter from "./routes/seasons"; import QuestionaireRouter from "./routes/questionaire"; import SubmissionRouter from "./routes/submission"; import UserRouter from "./routes/user"; import AuthRouter from "./routes/auth"; import { connectDB } from "./config/db"; -import { CronJob } from "cron"; + import { rankingCalculation } from "./tasks/rankingCalculation"; import { SupabaseService } from "./utils/supabase"; -dotenv.config({ path: "../.env"}); +import { Logger, nodeloggerMiddleware } from "nodelogger"; +import { ExpressErrorHandler } from "./middleware/errorHandler"; +dotenv.config({ path: "../.env" }); // Database -connectDB(); +void connectDB(); SupabaseService.initClient(); const app: Express = express(); +// eslint-disable-next-line turbo/no-undeclared-env-vars const port = process.env.PORT || 3000; // Middleware +app.use(nodeloggerMiddleware); app.use(express.json()); -app.use(function(req, res, next) { - // TODO: change when deploying to production - res.header("Access-Control-Allow-Origin", "*"); // TODO: configure properly before deployment - res.header("Access-Control-Allow-Headers", "*"); - next(); - }); +app.use(function (req, res, next) { + // TODO: change when deploying to production + res.header("Access-Control-Allow-Origin", "*"); // TODO: configure properly before deployment + res.header("Access-Control-Allow-Headers", "*"); + next(); +}); // Routes app.get("/ping", (req: Request, res: Response) => { - res.status(200).json({ message: "pong" }); + res.status(200).json({ message: "pong" }); }); app.use("/api/seasons", SeasonRouter); -app.use('/api/question', QuestionaireRouter); -app.use('/api/submission', SubmissionRouter); -app.use('/api/user', UserRouter); -app.use('/api/auth', AuthRouter); +app.use("/api/question", QuestionaireRouter); +app.use("/api/submission", SubmissionRouter); +app.use("/api/user", UserRouter); +app.use("/api/auth", AuthRouter); +app.use(ExpressErrorHandler); // the check is needed for testing // refer to https://stackoverflow.com/a/63299022 -if (process.env.NODE_ENV !== 'test') { - app.listen(port, () => { - console.log(`[server]: Server is running at http://localhost:${port}`); - }); +if (process.env.NODE_ENV !== "test") { + app.listen(port, () => { + Logger.info(`[server]: Server is running at http://localhost:${port}`); + }); } -if (process.env.NODE_ENV !== 'test' && process.env.DO_RANKING_CALCULATION){ - const job = new CronJob( - '*/15 * * * * *', - (async () => await rankingCalculation()), - null, - true, - null - ) +if (process.env.NODE_ENV !== "test" && process.env.DO_RANKING_CALCULATION) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-misused-promises + cron.schedule("*/15 * * * * *", async () => { + Logger.info("Calculating rankings..."); + await rankingCalculation(); + }); } -export default app; \ No newline at end of file +export default app; diff --git a/apps/challenges/src/middleware/errorHandler.ts b/apps/challenges/src/middleware/errorHandler.ts new file mode 100644 index 00000000..84d6dd33 --- /dev/null +++ b/apps/challenges/src/middleware/errorHandler.ts @@ -0,0 +1,48 @@ +import { NextFunction, Request, Response } from "express"; +import { StatusCodeError } from "../types/types"; +import { z } from "zod"; + +// ExpressErrorHandler define a default error handling middleware +// which return error responses based on the error type forwarded from controllers. +// All controllers should pass the error to this middleware instead of returning the error response themselves. +export const ExpressErrorHandler = ( + err: unknown, + req: Request, + res: Response, + next: NextFunction +) => { + // If the headers have already sent to the client but there is error when writing the response + // We must pass the error to express default error handler to close the connection and fail the request + // Refer to https://expressjs.com/en/guide/error-handling.html#:~:text=If%20you%20call,to%20the%20client%3A + if (res.headersSent) { + return next(err); + } + // We check the error status type, and return a default error response based on it + // Refers to https://stackoverflow.com/a/54286277 + switch (true) { + case err instanceof StatusCodeError: + res + .status((err as StatusCodeError).status) + .json({ message: (err as StatusCodeError).message }); + break; + case err instanceof z.ZodError: + // We assume all zod error is validation error, hence we return 400 status code + + // Block-scope is needed since lexical declaration in switch case will leak to other case + // Refer to https://docs.datadoghq.com/code_analysis/static_analysis_rules/javascript-best-practices/no-case-declarations/ + // Refer to https://gist.github.com/BoonHianLim/608462a5a83231391b6c3d2a4ef0d8c5 for short example + { + const message = (err as z.ZodError).issues + .map((issue) => issue.message) + .join(", "); + res.status(400).json({ message: message }); + } + + break; + case err instanceof Error: + res.status(500).json({ message: (err as Error).message }); + break; + default: + res.status(500).json({ message: "Internal Server Error" }); + } +}; diff --git a/apps/challenges/src/middleware/jwtMiddleware.ts b/apps/challenges/src/middleware/jwtMiddleware.ts index 019901be..a1fc1486 100644 --- a/apps/challenges/src/middleware/jwtMiddleware.ts +++ b/apps/challenges/src/middleware/jwtMiddleware.ts @@ -1,25 +1,39 @@ // jwt middleware for express import jwt from "jsonwebtoken"; import { Request, Response, NextFunction } from "express"; - +import { Logger } from "nodelogger"; +interface JWTAcessTokenContent { + id: string; + email: string; +} const jwtMiddleware = (req: Request, res: Response, next: NextFunction) => { - const token = req.headers.authorization; + const token = req.headers.authorization; - if (token == null) { - return res.sendStatus(401); - } + if (token == null) { + Logger.debug("jwtMiddleware receive null token"); + return res.sendStatus(401); + } - jwt.verify(token, process.env.JWT_SECRET || "", (err, tokenContent: any) => { - if (err) { - return res.sendStatus(401); - } + jwt.verify( + token, + process.env.CHALLENGES_JWT_SECRET || "", + (err, tokenContent) => { + if (err) { + Logger.debug( + "jwtMiddleware error when receiving this tokenContent", + err, + tokenContent + ); + return res.sendStatus(401); + } + const jwtAccessToken = tokenContent as JWTAcessTokenContent; - req.params.userID = tokenContent.id; - req.params.email = tokenContent.email; + req.params.userID = jwtAccessToken.id; + req.params.email = jwtAccessToken.email; - - next(); - }); -} + next(); + } + ); +}; export default jwtMiddleware; diff --git a/apps/challenges/src/middleware/jwtRefreshMiddleware.ts b/apps/challenges/src/middleware/jwtRefreshMiddleware.ts index 0badb6de..01edcff4 100644 --- a/apps/challenges/src/middleware/jwtRefreshMiddleware.ts +++ b/apps/challenges/src/middleware/jwtRefreshMiddleware.ts @@ -1,38 +1,61 @@ // jwt middleware for express import jwt from "jsonwebtoken"; import { Request, Response, NextFunction } from "express"; -import TokenRepo from "../repo/tokenRepo"; -import { refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants"; - -const jwtRefreshMiddleware = (req: Request, res: Response, next: NextFunction) => { - const token = req.headers.authorization; - - if (token == null) { - return res.sendStatus(401); - } - - jwt.verify(token, process.env.JWT_SECRET || "", (err, tokenContent: any) => { - if (err) { - return res.sendStatus(401); - } - - const token = TokenRepo.extendRefreshToken(tokenContent.id) - if (token == null) { - res.status(401).json({message: "Invalid refresh token"}) - return; - } - - res.cookie('refresh_token', token, { - httpOnly: true, - maxAge: refreshCookieMaxAgeSeconds * secondInMilliseconds, - signed: true, - }); - - req.params.userID = tokenContent.id; - req.params.email = tokenContent.email; - - next(); - }); +import { + refreshCookieMaxAgeSeconds, + secondInMilliseconds, +} from "../model/constants"; +import TokenService from "../service/tokenService"; +import { z } from "zod"; +import { TokenModel } from "../model/token"; + +interface JWTRefreshTokenContent { + id: string; + email: string; } +const jwtRefreshMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + const token = req.headers.authorization; + + if (token == null) { + return res.sendStatus(401); + } + + let jwtRefreshToken: JWTRefreshTokenContent; + try { + const decoded = jwt.verify(token, process.env.CHALLENGES_JWT_SECRET || ""); + jwtRefreshToken = decoded as JWTRefreshTokenContent; + } catch (err) { + return res.sendStatus(401); + } + + TokenService.extendRefreshToken(jwtRefreshToken.id) + .then((tokenModel: TokenModel | null) => { + if (tokenModel == null) { + res.status(401).json({ message: "Invalid refresh token" }); + return; + } + + res.cookie("refresh_token", tokenModel, { + httpOnly: true, + maxAge: refreshCookieMaxAgeSeconds * secondInMilliseconds, + signed: true, + }); + + req.params.userID = jwtRefreshToken.id; + req.params.email = jwtRefreshToken.email; + + next(); + }) + .catch((err) => { + if (err instanceof z.ZodError) { + res.status(400).json({ message: "Invalid request" }); + } + return; + }); +}; export default jwtRefreshMiddleware; diff --git a/apps/challenges/src/model/constants.ts b/apps/challenges/src/model/constants.ts index 642bbff9..e816e40c 100644 --- a/apps/challenges/src/model/constants.ts +++ b/apps/challenges/src/model/constants.ts @@ -3,4 +3,4 @@ const minuteInSeconds = 60; const dayInSeconds = 86400; export const accessTokenMaxAgeSeconds = 10 * minuteInSeconds; -export const refreshCookieMaxAgeSeconds = 7 * dayInSeconds; \ No newline at end of file +export const refreshCookieMaxAgeSeconds = 7 * dayInSeconds; diff --git a/apps/challenges/src/model/question.ts b/apps/challenges/src/model/question.ts index 2ec614a6..8416aa2b 100644 --- a/apps/challenges/src/model/question.ts +++ b/apps/challenges/src/model/question.ts @@ -1,104 +1,107 @@ -import mongoose, { Schema, Document } from 'mongoose'; +import mongoose, { Schema } from "mongoose"; -export interface CreateQuestionReq { - question_no: string; - question_title: string; - question_desc: string; - question_date: Date; - season_id: string; - expiry: Date; - points: number; - validation_function: string; - generate_input_function: string; +export interface QuestionReq { + question_no: string; + question_title: string; + question_desc: string; + question_date: Date; + season_id: string; + expiry: Date; + points: number; + validation_function: string; + generate_input_function: string; } export interface GetUserSpecificQuestionResp { - id: string; - question_no: string; - question_title: string; - question_desc: string; - question_date: Date; - seasonID: string; - question_input: string[]; - expiry: Date; - points: number; + id: string; + question_no: string; + question_title: string; + question_desc: string; + question_date: Date; + seasonID: string; + question_input: string[]; + expiry: Date; + points: number; } export interface QuestionModel { - _id: mongoose.Types.ObjectId; - question_no: string; - question_title: string; - question_desc: string; - question_date: Date; - seasonID: mongoose.Types.ObjectId; - expiry: Date; - points: number; - submissions: Array; - submissions_count: number; - correct_submissions_count: number; - active: boolean; - validation_function: string; - generate_input_function: string; + _id: mongoose.Types.ObjectId; + question_no: string; + question_title: string; + question_desc: string; + question_date: Date; + seasonID: mongoose.Types.ObjectId; + expiry: Date; + points: number; + submissions: Array; + submissions_count: number; + correct_submissions_count: number; + active: boolean; + validation_function: string; + generate_input_function: string; } -const questionSchema: Schema = new Schema({ +const questionSchema: Schema = new Schema( + { question_no: { - type: String, - required: [true, 'Please add a question number'] + type: String, + required: [true, "Please add a question number"], }, question_title: { - type: String, - required: [true, 'Please add a question title'] + type: String, + required: [true, "Please add a question title"], }, question_desc: { - type: String, - required: [true, 'Please add a question desc'] + type: String, + required: [true, "Please add a question desc"], }, question_date: { - type: Date, - required: [true, 'Please add a question date'] + type: Date, + required: [true, "Please add a question date"], }, seasonID: { - type: mongoose.Schema.Types.ObjectId, - ref: 'Season', - required: [true, 'Please add a season ID'] + type: mongoose.Schema.Types.ObjectId, + ref: "Season", + required: [true, "Please add a season ID"], }, expiry: { - type: Date, - required: [true, 'Please add an expiry date'] + type: Date, + required: [true, "Please add an expiry date"], }, points: { - type: Number, - required: [true, 'Please add a points value'] + type: Number, + required: [true, "Please add a points value"], }, submissions: { - type: [mongoose.Types.ObjectId], - ref: 'Submission' + type: [mongoose.Types.ObjectId], + ref: "Submission", }, submissions_count: { - type: Number, - default: 0 + type: Number, + default: 0, }, correct_submissions_count: { - type: Number, - default: 0 + type: Number, + default: 0, }, active: { - type: Boolean, - default: true + type: Boolean, + default: true, }, validation_function: { - type: String, - required: [true, 'Please add a validation function'] + type: String, + required: [true, "Please add a validation function"], }, generate_input_function: { - type: String, - required: [true, 'Please add a generate input function'] - } -}, { - timestamps: true -}); + type: String, + required: [true, "Please add a generate input function"], + }, + }, + { + timestamps: true, + } +); -const Question = mongoose.model('Question', questionSchema); +const Question = mongoose.model("Question", questionSchema); -export { Question as default }; \ No newline at end of file +export { Question as default }; diff --git a/apps/challenges/src/model/questionInput.ts b/apps/challenges/src/model/questionInput.ts index cde2b763..a8697e04 100644 --- a/apps/challenges/src/model/questionInput.ts +++ b/apps/challenges/src/model/questionInput.ts @@ -1,34 +1,40 @@ -import mongoose, { Schema, model } from 'mongoose'; +import mongoose, { Schema } from "mongoose"; export interface QuestionInputModel { - userID: mongoose.Types.ObjectId; - seasonID: mongoose.Types.ObjectId; - questionID: mongoose.Types.ObjectId; - input: string[]; + userID: mongoose.Types.ObjectId; + seasonID: mongoose.Types.ObjectId; + questionID: mongoose.Types.ObjectId; + input: string[]; } -const questionInputSchema: Schema = new Schema({ +const questionInputSchema: Schema = new Schema( + { userID: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'User', + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "User", }, seasonID: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Season', + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "Season", }, questionID: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Question', + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "Question", }, input: { - type: [String], - } -}, { + type: [String], + }, + }, + { timestamps: true, -}); + } +); -const QuestionInput = mongoose.model('QuestionInput', questionInputSchema); +const QuestionInput = mongoose.model( + "QuestionInput", + questionInputSchema +); -export { QuestionInput as default } \ No newline at end of file +export { QuestionInput as default }; diff --git a/apps/challenges/src/model/ranking.ts b/apps/challenges/src/model/ranking.ts index 71c83cca..2ab474f9 100644 --- a/apps/challenges/src/model/ranking.ts +++ b/apps/challenges/src/model/ranking.ts @@ -1,35 +1,37 @@ -import mongoose, {Schema} from 'mongoose'; +import mongoose, { Schema } from "mongoose"; export interface RankingModel { - _id: mongoose.Types.ObjectId; - userID: mongoose.Types.ObjectId; - seasonID: mongoose.Types.ObjectId; - username: string; - points: number; - createdAt: Date; - updatedAt: Date; + _id: mongoose.Types.ObjectId; + userID: mongoose.Types.ObjectId; + seasonID: mongoose.Types.ObjectId; + username: string; + points: number; + createdAt: Date; + updatedAt: Date; } -const seasonSchema: Schema = new Schema({ +const seasonSchema: Schema = new Schema( + { userID: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'User' + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "User", }, seasonID: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Season' + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "Season", }, points: { - type: Number, - required: [true, 'Please add a points value'] - } -}, { - timestamps: true -}); - + type: Number, + required: [true, "Please add a points value"], + }, + }, + { + timestamps: true, + } +); -const Ranking = mongoose.model('Ranking', seasonSchema); +const Ranking = mongoose.model("Ranking", seasonSchema); -export { Ranking as default } +export { Ranking as default }; diff --git a/apps/challenges/src/model/rankingScore.ts b/apps/challenges/src/model/rankingScore.ts index 88cec8a6..14446e3d 100644 --- a/apps/challenges/src/model/rankingScore.ts +++ b/apps/challenges/src/model/rankingScore.ts @@ -1,9 +1,7 @@ -import mongoose, {Schema} from 'mongoose'; - export interface UserRanking { - user: { - userID: string; - name: string; - } - points: number; -} \ No newline at end of file + user: { + userID: string; + name: string; + }; + points: number; +} diff --git a/apps/challenges/src/model/response.ts b/apps/challenges/src/model/response.ts index 04ad531b..1265661a 100644 --- a/apps/challenges/src/model/response.ts +++ b/apps/challenges/src/model/response.ts @@ -1,5 +1,5 @@ export interface GeneralResp { - status: number; - message: string; - data?: any; -} \ No newline at end of file + status: number; + message: string; + data?: any; +} diff --git a/apps/challenges/src/model/season.ts b/apps/challenges/src/model/season.ts index 3a5ae5c2..5276f764 100644 --- a/apps/challenges/src/model/season.ts +++ b/apps/challenges/src/model/season.ts @@ -1,42 +1,44 @@ -import mongoose, { Schema } from 'mongoose'; -import { QuestionModel } from './question'; +import mongoose, { Schema } from "mongoose"; +import { QuestionModel } from "./question"; export interface GetSeasonResp { - id: mongoose.Types.ObjectId; - title: string; - startDate: Date; - endDate: Date; - questions: QuestionModel[]; + id: mongoose.Types.ObjectId; + title: string; + startDate: Date; + endDate: Date; + questions: QuestionModel[]; } // TODO: season should store simple question model for faster retrieval export interface SeasonModel { - _id: mongoose.Types.ObjectId; - title: string; - startDate: Date; - endDate: Date; - createdAt: Date; - updatedAt: Date; + _id: mongoose.Types.ObjectId; + title: string; + startDate: Date; + endDate: Date; + createdAt: Date; + updatedAt: Date; } -const seasonSchema: Schema = new Schema({ +const seasonSchema: Schema = new Schema( + { title: { - type: String, - required: [true, 'Please add a title'] + type: String, + required: [true, "Please add a title"], }, startDate: { - type: Date, - required: [true, 'Please add a start date'] + type: Date, + required: [true, "Please add a start date"], }, endDate: { - type: Date, - required: [true, 'Please add an end date'] - } -}, { - timestamps: true -}); - + type: Date, + required: [true, "Please add an end date"], + }, + }, + { + timestamps: true, + } +); -const Season = mongoose.model('Season', seasonSchema); +const Season = mongoose.model("Season", seasonSchema); -export { Season as default } +export { Season as default }; diff --git a/apps/challenges/src/model/submission.ts b/apps/challenges/src/model/submission.ts index d117636f..021de8bf 100644 --- a/apps/challenges/src/model/submission.ts +++ b/apps/challenges/src/model/submission.ts @@ -1,50 +1,56 @@ -import mongoose, { Schema, model } from 'mongoose'; +import mongoose, { Schema } from "mongoose"; export interface CreateSubmissionReq { - user: mongoose.Types.ObjectId; - question: mongoose.Types.ObjectId; - answer: string; + user: mongoose.Types.ObjectId; + question: mongoose.Types.ObjectId; + answer: string; } export interface SubmissionModel { - user: mongoose.Types.ObjectId; - seasonID: mongoose.Types.ObjectId; - question: mongoose.Types.ObjectId; - answer: string; - correct?: boolean; - points_awarded?: number; + user: mongoose.Types.ObjectId; + seasonID: mongoose.Types.ObjectId; + question: mongoose.Types.ObjectId; + answer: string; + correct?: boolean; + points_awarded?: number; } -const submissionSchema: Schema = new Schema({ +const submissionSchema: Schema = new Schema( + { user: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'User', + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "User", }, seasonID: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Season', + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "Season", }, question: { - type: mongoose.Schema.Types.ObjectId, - required: true, - ref: 'Question', + type: mongoose.Schema.Types.ObjectId, + required: true, + ref: "Question", }, answer: { - type: String, - required: [true, 'Please add an answer'], + type: String, + required: [true, "Please add an answer"], }, correct: { - type: Boolean, + type: Boolean, }, points_awarded: { - type: Number, + type: Number, }, -}, { + }, + { timestamps: true, -}); + } +); -const Submission = mongoose.model('Submission', submissionSchema); +const Submission = mongoose.model( + "Submission", + submissionSchema +); -export { Submission as default } +export { Submission as default }; diff --git a/apps/challenges/src/model/token.ts b/apps/challenges/src/model/token.ts index 8a5ee162..80d210c7 100644 --- a/apps/challenges/src/model/token.ts +++ b/apps/challenges/src/model/token.ts @@ -1,30 +1,33 @@ -import mongoose, { Schema } from 'mongoose'; +import mongoose, { Schema } from "mongoose"; export interface TokenModel { - _id: mongoose.Types.ObjectId; - jwt: string; - userID: mongoose.Types.ObjectId; - expiry: Date; + _id: mongoose.Types.ObjectId; + jwt: string; + userID: mongoose.Types.ObjectId; + expiry: Date; } -const tokenSchema: Schema = new Schema({ +const tokenSchema: Schema = new Schema( + { jwt: { - type: String, - unique: true, + type: String, + unique: true, }, expiry: { - type: Date, - required: [true, 'Please add a expiry date'] + type: Date, + required: [true, "Please add a expiry date"], }, userID: { - type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: [true, 'Please add a user ID'] + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: [true, "Please add a user ID"], }, -}, { - timestamps: true -}) + }, + { + timestamps: true, + } +); -const Token = mongoose.model('Token', tokenSchema); +const Token = mongoose.model("Token", tokenSchema); -export { Token as default } \ No newline at end of file +export { Token as default }; diff --git a/apps/challenges/src/model/user.ts b/apps/challenges/src/model/user.ts index c84250e2..b07010cf 100644 --- a/apps/challenges/src/model/user.ts +++ b/apps/challenges/src/model/user.ts @@ -1,31 +1,34 @@ -import mongoose, { Schema } from 'mongoose'; +import mongoose, { Schema } from "mongoose"; export interface UserModel { - _id: mongoose.Types.ObjectId; - name: string; - email: string; - active: boolean; - createdAt: Date; - updatedAt: Date; + _id: mongoose.Types.ObjectId; + name: string; + email: string; + active: boolean; + createdAt: Date; + updatedAt: Date; } -const questionSchema: Schema = new Schema({ +const questionSchema: Schema = new Schema( + { name: { - type: String, - required: [true, 'Please add a name'] + type: String, + required: [true, "Please add a name"], }, email: { - type: String, - required: [true, 'Please add an email'] + type: String, + required: [true, "Please add an email"], }, active: { - type: Boolean, - default: true + type: Boolean, + default: true, }, -}, { - timestamps: true -}); + }, + { + timestamps: true, + } +); -const User = mongoose.model('User', questionSchema); +const User = mongoose.model("User", questionSchema); -export { User as default }; \ No newline at end of file +export { User as default }; diff --git a/apps/challenges/src/repo/questionRepo.ts b/apps/challenges/src/repo/questionRepo.ts index c179d2bd..bb5a0178 100644 --- a/apps/challenges/src/repo/questionRepo.ts +++ b/apps/challenges/src/repo/questionRepo.ts @@ -1,98 +1,118 @@ -import Question, { CreateQuestionReq, QuestionModel } from "../model/question" -import mongoose from 'mongoose'; +import Question, { QuestionReq, QuestionModel } from "../model/question"; +import mongoose from "mongoose"; import QuestionInput, { QuestionInputModel } from "../model/questionInput"; +import { GetQuestionsFilter } from "../types/types"; + +const GetQuestions = async ( + filter: GetQuestionsFilter +): Promise => { + const queryFilter: mongoose.FilterQuery = {}; + if (filter.isActive) { + queryFilter.active = filter.isActive; + } + return await Question.find(queryFilter); +}; const getQuestionByID = async ( - questionID: mongoose.Types.ObjectId, + questionID: mongoose.Types.ObjectId ): Promise => { - const question = await Question.findOne({ - _id: questionID - }); - return question; -} + const question = await Question.findOne({ + _id: questionID, + }); + return question; +}; const createQuestionByReq = async ( - req: CreateQuestionReq + req: QuestionReq ): Promise => { - const questionModel = { - _id: new mongoose.Types.ObjectId(), - question_no: req.question_no, - question_title: req.question_title, - question_desc: req.question_desc, - question_date: req.question_date, - seasonID: new mongoose.Types.ObjectId(req.season_id), - expiry: req.expiry, - points: req.points, - submissions: [], - submissions_count: 0, - correct_submissions_count: 0, - active: true, - validation_function: req.validation_function, - generate_input_function: req.generate_input_function, - } + const questionModel = { + _id: new mongoose.Types.ObjectId(), + question_no: req.question_no, + question_title: req.question_title, + question_desc: req.question_desc, + question_date: req.question_date, + seasonID: new mongoose.Types.ObjectId(req.season_id), + expiry: req.expiry, + points: req.points, + submissions: [], + submissions_count: 0, + correct_submissions_count: 0, + active: true, + validation_function: req.validation_function, + generate_input_function: req.generate_input_function, + }; - const question = await Question.create(questionModel); + const question = await Question.create(questionModel); - await question.save(); + await question.save(); - return question; -} + return question; +}; const updateQuestionByID = async ( - questionID: mongoose.Types.ObjectId, - questionModel: QuestionModel + questionID: mongoose.Types.ObjectId, + questionModel: QuestionModel ): Promise => { - const question = await Question.findOneAndUpdate({ - _id: questionID - }, questionModel, { new: true }); - return question; -} + const question = await Question.findOneAndUpdate( + { + _id: questionID, + }, + questionModel, + { new: true } + ); + return question; +}; const updateQuestionSubmissions = async ( - questionID: mongoose.Types.ObjectId, - submissionID: mongoose.Types.ObjectId, - isCorrect: boolean + questionID: mongoose.Types.ObjectId, + submissionID: mongoose.Types.ObjectId, + isCorrect: boolean ): Promise => { - const question = await Question.findOneAndUpdate({ - _id: questionID - }, { - $push: { submissions: submissionID }, - $inc: { - submissions_count: 1, - correct_submissions_count: isCorrect ? 1 : 0 - } - }, { new: true }); - return question; -} + const question = await Question.findOneAndUpdate( + { + _id: questionID, + }, + { + $push: { submissions: submissionID }, + $inc: { + submissions_count: 1, + correct_submissions_count: isCorrect ? 1 : 0, + }, + }, + { new: true } + ); + return question; +}; const getQuestionInput = async ( - userID: mongoose.Types.ObjectId, - seasonID: mongoose.Types.ObjectId, - questionID: mongoose.Types.ObjectId, + userID: mongoose.Types.ObjectId, + seasonID: mongoose.Types.ObjectId, + questionID: mongoose.Types.ObjectId ): Promise => { - const question = await QuestionInput.findOne({ - userID: userID, - seasonID: seasonID, - questionID: questionID, - }); - return question; -} + const question = await QuestionInput.findOne({ + userID: userID, + seasonID: seasonID, + questionID: questionID, + }); + return question; +}; const saveQuestionInput = async ( - questionInput: QuestionInputModel + questionInput: QuestionInputModel ): Promise => { - const dbQuestionInput = new QuestionInput(questionInput); - dbQuestionInput.save(); - return dbQuestionInput; -} + const dbQuestionInput = new QuestionInput(questionInput); + await dbQuestionInput.save(); + return dbQuestionInput; +}; const QuestionRepo = { - getQuestionByID, - createQuestionByReq, - updateQuestionByID, - updateQuestionSubmissions, - getQuestionInput, - saveQuestionInput, -} + GetQuestions, + getQuestionByID, + createQuestionByReq, + updateQuestionByID, + updateQuestionSubmissions, + getQuestionInput, + saveQuestionInput, +}; -export { QuestionRepo as default } \ No newline at end of file +export { QuestionRepo as default }; diff --git a/apps/challenges/src/repo/seasonRepo.ts b/apps/challenges/src/repo/seasonRepo.ts index 6772d168..f43b213e 100644 --- a/apps/challenges/src/repo/seasonRepo.ts +++ b/apps/challenges/src/repo/seasonRepo.ts @@ -1,5 +1,5 @@ import Season, { SeasonModel } from "../model/season"; -import mongoose from 'mongoose'; +import mongoose from "mongoose"; import { UserRanking } from "../model/rankingScore"; import Submission from "../model/submission"; @@ -8,39 +8,40 @@ import { paginateArray } from "../utils/pagination"; import Question, { QuestionModel } from "../model/question"; const getSeasonsByDate = async ( - startDate: Date | null, - endDate: Date | null, + startDate: Date | null, + endDate: Date | null ): Promise => { - console.log(startDate, endDate) - const seasons = await Season.find({ - $and: [ - startDate != null ? { endDate: { $gte: startDate } } : {}, - endDate != null ? { startDate: { $lte: endDate } } : {} - ] - }); - return seasons; -} + const seasons = await Season.find({ + $and: [ + startDate != null ? { endDate: { $gte: startDate } } : {}, + endDate != null ? { startDate: { $lte: endDate } } : {}, + ], + }); + return seasons; +}; -const getSeasonByID = async (id: mongoose.Types.ObjectId): Promise => { - const season = await Season.findOne({ - _id: id - }); - return season; -} +const getSeasonByID = async ( + id: mongoose.Types.ObjectId +): Promise => { + const season = await Season.findOne({ + _id: id, + }); + return season; +}; const createSeason = async ( - title: string, - startDate: Date, - endDate: Date, + title: string, + startDate: Date, + endDate: Date ): Promise => { - const season = await Season.create({ - title, - startDate: startDate, - endDate: endDate - }); - await season.save(); - return season; -} + const season = await Season.create({ + title, + startDate: startDate, + endDate: endDate, + }); + await season.save(); + return season; +}; /* const chatgptCalculateSeasonRankings = async ( @@ -67,104 +68,103 @@ const chatgptCalculateSeasonRankings = async ( } */ -const calculateSeasonRankings = async ( - seasonID: mongoose.Types.ObjectId, -) => { - const rankings = await Submission.aggregate([ - { - $match: { - seasonID: seasonID - } - }, - { - $group: { - _id: { - "user": "$user", - "question": "$question", - }, - points_awarded: { $max: "$points_awarded" } - } - }, - { $match: { points_awarded: { $exists: true, $ne: null } } }, - { - $group: { - _id: { - "user": "$_id.user", - }, - points: { $sum: "$points_awarded" } - } - }, - { - $sort: { - points: -1 - } +const calculateSeasonRankings = async (seasonID: mongoose.Types.ObjectId) => { + const rankings = await Submission.aggregate([ + { + $match: { + seasonID: seasonID, + }, + }, + { + $group: { + _id: { + user: "$user", + question: "$question", }, - { - $lookup: { - from: "users", - localField: "_id.user", - foreignField: "_id", - as: "user" - } + points_awarded: { $max: "$points_awarded" }, + }, + }, + { $match: { points_awarded: { $exists: true, $ne: null } } }, + { + $group: { + _id: { + user: "$_id.user", }, - { - $unwind: "$user" + points: { $sum: "$points_awarded" }, + }, + }, + { + $sort: { + points: -1, + }, + }, + { + $lookup: { + from: "users", + localField: "_id.user", + foreignField: "_id", + as: "user", + }, + }, + { + $unwind: "$user", + }, + { + $project: { + _id: 0, + user: { + userID: "$user._id", + name: "$user.name", }, - { - $project: { - _id: 0, - user: { - userID: "$user._id", - name: "$user.name", - }, - points: 1 - } - } - ]); - console.log(rankings) - return rankings; -} + points: 1, + }, + }, + ]); + return rankings as UserRanking[]; +}; -const getSeasonRankings = async ( - seasonID: mongoose.Types.ObjectId, -): Promise => { - return rankingsMap[seasonID.toString()]; -} +const getSeasonRankings = ( + seasonID: mongoose.Types.ObjectId +): UserRanking[] | null => { + return rankingsMap[seasonID.toString()]; +}; - -const getSeasonRankingsByPagination = async ( - seasonID: mongoose.Types.ObjectId, - page: number, - limit: number, -): Promise<{ rankings: UserRanking[], rankingsCount: number }> => { - const rankings = rankingsMap[seasonID.toString()]; - if (!rankings) { - return { rankings: [], rankingsCount: 0 }; - } - return { rankings: paginateArray(rankings, limit, page), rankingsCount: rankings.length }; -} +const getSeasonRankingsByPagination = ( + seasonID: mongoose.Types.ObjectId, + page: number, + limit: number +): { rankings: UserRanking[]; rankingsCount: number } => { + const rankings = rankingsMap[seasonID.toString()]; + if (!rankings) { + return { rankings: [], rankingsCount: 0 }; + } + return { + rankings: paginateArray(rankings, limit, page) as UserRanking[], + rankingsCount: rankings.length, + }; +}; const getSeasonQuestions = async ( - seasonID: mongoose.Types.ObjectId, + seasonID: mongoose.Types.ObjectId ): Promise => { - const questions = await Question.aggregate([ - { - $match: { - seasonID: seasonID - } - }, - ]); - return questions; -} + const questions = await Question.aggregate([ + { + $match: { + seasonID: seasonID, + }, + }, + ]); + return questions as QuestionModel[]; +}; const SeasonRepo = { - getSeasonsByDate, - getSeasonByID, - createSeason, - getSeasonRankings, - getSeasonRankingsByPagination, - calculateSeasonRankings, - getSeasonQuestions -} + getSeasonsByDate, + getSeasonByID, + createSeason, + getSeasonRankings, + getSeasonRankingsByPagination, + calculateSeasonRankings, + getSeasonQuestions, +}; -export { SeasonRepo as default } \ No newline at end of file +export { SeasonRepo as default }; diff --git a/apps/challenges/src/repo/submissionRepo.ts b/apps/challenges/src/repo/submissionRepo.ts index ed5005a4..7d0a32b1 100644 --- a/apps/challenges/src/repo/submissionRepo.ts +++ b/apps/challenges/src/repo/submissionRepo.ts @@ -1,13 +1,11 @@ import Submission, { SubmissionModel } from "../model/submission"; -const createSubmission = ( - submission: SubmissionModel -) => { - return Submission.create(submission); -} +const createSubmission = (submission: SubmissionModel) => { + return Submission.create(submission); +}; const SubmissionRepo = { - createSubmission, -} + createSubmission, +}; -export default SubmissionRepo; \ No newline at end of file +export default SubmissionRepo; diff --git a/apps/challenges/src/repo/tokenRepo.ts b/apps/challenges/src/repo/tokenRepo.ts index 612a0ba0..a8b399c6 100644 --- a/apps/challenges/src/repo/tokenRepo.ts +++ b/apps/challenges/src/repo/tokenRepo.ts @@ -1,41 +1,53 @@ -import { refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants"; -import Token, { TokenModel } from "../model/token" -import mongoose from 'mongoose'; +import { + refreshCookieMaxAgeSeconds, + secondInMilliseconds, +} from "../model/constants"; +import Token, { TokenModel } from "../model/token"; +import mongoose from "mongoose"; -const getRefreshToken = async (userID: mongoose.Types.ObjectId): Promise => { - const token = await Token.findOne({ - userID: userID, - expiry: { $gt: new Date() } - }); - return token; -} +const getRefreshToken = async ( + userID: mongoose.Types.ObjectId +): Promise => { + const token = await Token.findOne({ + userID: userID, + expiry: { $gt: new Date() }, + }); + return token; +}; const saveRefreshToken = async ( - token: TokenModel + token: TokenModel ): Promise => { - const dbToken = new Token(token); - dbToken.save(); - return dbToken; -} - + const dbToken = new Token(token); + await dbToken.save(); + return dbToken; +}; -const extendRefreshToken = async (userID: mongoose.Types.ObjectId): Promise => { - const now = new Date(); - const newExpiry = new Date(now.getTime() + refreshCookieMaxAgeSeconds * secondInMilliseconds); +const extendRefreshToken = async ( + userID: mongoose.Types.ObjectId +): Promise => { + const now = new Date(); + const newExpiry = new Date( + now.getTime() + refreshCookieMaxAgeSeconds * secondInMilliseconds + ); - const token = await Token.findOneAndUpdate({ - userID: userID, - expiry : { $gt: new Date() } - }, { - $set: { "expiry": newExpiry } - }, { new: true }) - return token -} + const token = await Token.findOneAndUpdate( + { + userID: userID, + expiry: { $gt: new Date() }, + }, + { + $set: { expiry: newExpiry }, + }, + { new: true } + ); + return token; +}; const TokenRepo = { - getRefreshToken, - saveRefreshToken, - extendRefreshToken, -} + getRefreshToken, + saveRefreshToken, + extendRefreshToken, +}; -export { TokenRepo as default } \ No newline at end of file +export { TokenRepo as default }; diff --git a/apps/challenges/src/repo/userRepo.ts b/apps/challenges/src/repo/userRepo.ts index 2e687fb1..7fb34400 100644 --- a/apps/challenges/src/repo/userRepo.ts +++ b/apps/challenges/src/repo/userRepo.ts @@ -1,37 +1,34 @@ -import mongoose from 'mongoose'; -import User from '../model/user'; +import mongoose from "mongoose"; +import User from "../model/user"; const getUserByID = async (id: mongoose.Types.ObjectId) => { - // get user by id from mongo - const user = await User.findOne({ - _id: id - }); - return user; -} + // get user by id from mongo + const user = await User.findOne({ + _id: id, + }); + return user; +}; const getUserByEmail = async (email: string) => { - const user = await User.findOne({ - email: email - }); - return user; -} + const user = await User.findOne({ + email: email, + }); + return user; +}; -const createUser = async ( - name: string, - email: string, -) => { - const user = await User.create({ - name: name, - email: email, - active: true, - }); - return user; -} +const createUser = async (name: string, email: string) => { + const user = await User.create({ + name: name, + email: email, + active: true, + }); + return user; +}; const UserRepo = { - getUserByID, - getUserByEmail, - createUser, -} + getUserByID, + getUserByEmail, + createUser, +}; -export { UserRepo as default }; \ No newline at end of file +export { UserRepo as default }; diff --git a/apps/challenges/src/routes/auth.ts b/apps/challenges/src/routes/auth.ts index 9e7523c4..bfeafec6 100644 --- a/apps/challenges/src/routes/auth.ts +++ b/apps/challenges/src/routes/auth.ts @@ -7,6 +7,5 @@ const router = Express.Router(); router.post("/refresh", jwtRefreshMiddleware, AuthController.refreshToken); router.post("/oauth/signin", AuthController.oauthSignIn); -router.post("/signin", AuthController.signIn); -export { router as default }; \ No newline at end of file +export { router as default }; diff --git a/apps/challenges/src/routes/questionaire.ts b/apps/challenges/src/routes/questionaire.ts index 74cd3f68..fc72bd4e 100644 --- a/apps/challenges/src/routes/questionaire.ts +++ b/apps/challenges/src/routes/questionaire.ts @@ -1,10 +1,21 @@ -const express = require('express'); -const router = express.Router(); +import Express from "express"; +const router = Express.Router(); import QuestionController from "../controllers/questionaire"; import jwtMiddleware from "../middleware/jwtMiddleware"; -router.route('/').get(QuestionController.getQuestions).post(QuestionController.setQuestion); -router.get('/:id/user', jwtMiddleware, QuestionController.getUserSpecificQuestion) -router.route('/:id').get(QuestionController.getQuestion).delete(QuestionController.deleteQuestion).put(QuestionController.updateQuestion); +router + .route("/") + .get(QuestionController.getQuestions) + .post(QuestionController.setQuestion); +router.get( + "/:id/user", + jwtMiddleware, + QuestionController.getUserSpecificQuestion +); +router + .route("/:id") + .get(QuestionController.getQuestion) + .delete(QuestionController.deleteQuestion) + .put(QuestionController.updateQuestion); -export { router as default }; \ No newline at end of file +export { router as default }; diff --git a/apps/challenges/src/routes/seasons.ts b/apps/challenges/src/routes/seasons.ts index d8d1a38e..dfb84f0d 100644 --- a/apps/challenges/src/routes/seasons.ts +++ b/apps/challenges/src/routes/seasons.ts @@ -14,4 +14,4 @@ router.get("/:seasonID/questions", SeasonController.getSeasonQuestions); // router.get("/:seasonID/rankings/:userID", SeasonController.getUserSeasonRanking); // router.put("/:seasonID/rankings/:userID", SeasonController.updateSeasonRankings); -export { router as default }; \ No newline at end of file +export { router as default }; diff --git a/apps/challenges/src/routes/submission.ts b/apps/challenges/src/routes/submission.ts index 3726980f..16bdd878 100644 --- a/apps/challenges/src/routes/submission.ts +++ b/apps/challenges/src/routes/submission.ts @@ -1,8 +1,12 @@ -const express = require('express'); -const router = express.Router(); +import Express from "express"; +const router = Express.Router(); import SubmissionController from "../controllers/submission"; +import jwtMiddleware from "../middleware/jwtMiddleware"; -router.route('/').get(SubmissionController.getSubmissions).post(SubmissionController.setSubmission); -router.route('/:id').get(SubmissionController.getSubmission); +router + .route("/") + .get(SubmissionController.getSubmissions) + .post(jwtMiddleware, SubmissionController.setSubmission); +router.route("/:id").get(SubmissionController.getSubmission); -export { router as default }; \ No newline at end of file +export { router as default }; diff --git a/apps/challenges/src/routes/user.ts b/apps/challenges/src/routes/user.ts index fb678086..7596c3e6 100644 --- a/apps/challenges/src/routes/user.ts +++ b/apps/challenges/src/routes/user.ts @@ -5,6 +5,5 @@ import jwtMiddleware from "../middleware/jwtMiddleware"; const router = Express.Router(); router.get("/", jwtMiddleware, UserController.getUser); -router.get("/token", UserController.checkTokens); -export { router as default }; \ No newline at end of file +export { router as default }; diff --git a/apps/challenges/src/service/authService.ts b/apps/challenges/src/service/authService.ts index 9ddf08ec..8574171d 100644 --- a/apps/challenges/src/service/authService.ts +++ b/apps/challenges/src/service/authService.ts @@ -1,99 +1,108 @@ -import { SupabaseService } from '../utils/supabase'; -import mongoose from 'mongoose'; -import { jwtDecode } from "jwt-decode"; +import mongoose from "mongoose"; import UserService from "../service/userService"; -import { isValidEmail, getEmailPrefix, zodIsValidObjectId } from "../utils/validator"; +import { + isValidEmail, + getEmailPrefix, + zodIsValidObjectId, +} from "../utils/validator"; import supabase from "../utils/supabase"; -import { z } from 'zod'; -import TokenService from './tokenService'; -import { GeneralResp, OauthcallbackResp } from '../types/types'; -import TokenRepo from '../repo/tokenRepo'; -import { StatusCodeError } from "../types/types" +import TokenService from "./tokenService"; +import TokenRepo from "../repo/tokenRepo"; +import { StatusCodeError } from "../types/types"; import jwt from "jsonwebtoken"; -import UserRepo from '../repo/userRepo'; -import { refreshCookieMaxAgeSeconds, secondInMilliseconds } from "../model/constants"; - -const oauthSignIn = async ( - supabaseAccessToken: string -) => { - jwt.verify(supabaseAccessToken, process.env.SUPABASE_JWT_SECRET || ""); - - const resp = await supabase.auth.getUser(supabaseAccessToken); - - if (resp.error) { - throw new Error("Failed to exchange supabaseAccessToken for session."); - } - - // const decodedJWTToken = jwtDecode(code); - // const decodedJWTObj = decodedJWTToken as { - // aud: string; - // exp: number; - // iat: number; - // iss: string; - // sub: string; - // email: string; - // role: string; - // session_id: string; - // } - // console.log(decodedJWTObj); - - const email = isValidEmail.parse(resp.data.user.email); - - var createNewUser: boolean = false; - let user = await UserService.getUserByEmail(email); - - if (!user) { - const userName = getEmailPrefix(email); - user = await UserService.createUser(userName, email); - createNewUser = true; - } - - const accessToken = await TokenService.generateAccessToken(user._id.toString(), user.email); - const refreshToken = await TokenService.generateRefreshToken(user._id.toString(), user.email); - const now = new Date(); - const newExpiry = new Date(now.getTime() + refreshCookieMaxAgeSeconds * secondInMilliseconds); - - const tokenModel = await TokenRepo.saveRefreshToken({ - _id: new mongoose.Types.ObjectId(), - jwt: refreshToken, - userID: user._id, - expiry: newExpiry - }) - - if (!tokenModel) { - throw new Error("token not saved") - } - - return { - accessToken: accessToken, - refreshToken: refreshToken, - createNewUser: createNewUser - } -} +import UserRepo from "../repo/userRepo"; +import { + refreshCookieMaxAgeSeconds, + secondInMilliseconds, +} from "../model/constants"; + +const oauthSignIn = async (supabaseAccessToken: string) => { + jwt.verify(supabaseAccessToken, process.env.SUPABASE_JWT_SECRET || ""); + + const resp = await supabase.auth.getUser(supabaseAccessToken); + + if (resp.error) { + throw new Error("Failed to exchange supabaseAccessToken for session."); + } + + // const decodedJWTToken = jwtDecode(code); + // const decodedJWTObj = decodedJWTToken as { + // aud: string; + // exp: number; + // iat: number; + // iss: string; + // sub: string; + // email: string; + // role: string; + // session_id: string; + // } + // console.log(decodedJWTObj); + + const email = isValidEmail.parse(resp.data.user.email); + + let createNewUser = false; + let user = await UserService.getUserByEmail(email); + + if (!user) { + const userName = getEmailPrefix(email); + user = await UserService.createUser(userName, email); + createNewUser = true; + } + + const accessToken = TokenService.generateAccessToken( + user._id.toString(), + user.email + ); + const refreshToken = TokenService.generateRefreshToken( + user._id.toString(), + user.email + ); + const now = new Date(); + const newExpiry = new Date( + now.getTime() + refreshCookieMaxAgeSeconds * secondInMilliseconds + ); + + const tokenModel = await TokenRepo.saveRefreshToken({ + _id: new mongoose.Types.ObjectId(), + jwt: refreshToken, + userID: user._id, + expiry: newExpiry, + }); + + if (!tokenModel) { + throw new Error("token not saved"); + } + + return { + accessToken: accessToken, + refreshToken: refreshToken, + createNewUser: createNewUser, + }; +}; const refreshToken = async (userID: string) => { - const _id = zodIsValidObjectId.parse(userID); - const mongoUserID = new mongoose.Types.ObjectId(_id) - const token = await TokenRepo.getRefreshToken(mongoUserID); + const _id = zodIsValidObjectId.parse(userID); + const mongoUserID = new mongoose.Types.ObjectId(_id); + const token = await TokenRepo.getRefreshToken(mongoUserID); - if (token == null) { - throw new StatusCodeError(500, "Token not found") - } + if (token == null) { + throw new StatusCodeError(500, "Token not found"); + } - // verify jwt token - const decoded = jwt.verify(token.jwt, process.env.JWT_SECRET || "") - const user = await UserRepo.getUserByID(mongoUserID); + // verify jwt token, throw error if verify failed + jwt.verify(token.jwt, process.env.CHALLENGES_JWT_SECRET || ""); + const user = await UserRepo.getUserByID(mongoUserID); - if (!user) { - throw new StatusCodeError(500, "User not found") - } + if (!user) { + throw new StatusCodeError(500, "User not found"); + } - return TokenService.generateAccessToken(user._id.toString(), user.email) -} + return TokenService.generateAccessToken(user._id.toString(), user.email); +}; const AuthService = { - oauthSignIn, - refreshToken, -} + oauthSignIn, + refreshToken, +}; -export { AuthService as default }; \ No newline at end of file +export { AuthService as default }; diff --git a/apps/challenges/src/service/questionService.ts b/apps/challenges/src/service/questionService.ts index 3d68a7ac..d821d96a 100644 --- a/apps/challenges/src/service/questionService.ts +++ b/apps/challenges/src/service/questionService.ts @@ -1,125 +1,149 @@ -import mongoose from 'mongoose'; -import QuestionRepo from '../repo/questionRepo'; -import { CreateQuestionReq, GetUserSpecificQuestionResp } from '../model/question'; -import ValidationService from './validationService'; -import { GeneralResp, StatusCodeError } from '../types/types'; -import { QuestionInputModel } from '../model/questionInput'; - -const getQuestionByID = async ( - questionID: string, -) => { - if (!mongoose.isValidObjectId(questionID)) { - throw new Error('Invalid question ID'); - } - const _id = new mongoose.Types.ObjectId(questionID); - const question = await QuestionRepo.getQuestionByID(_id); - return question; -} - -const createQuestion = async ( - req: CreateQuestionReq, -): Promise => { - if (!ValidationService.getValidationFunction(req.validation_function)) { - console.log('Invalid validation function'); - return { - status: 400, - message: 'Invalid validation function', - data: null, - }; - } - - if (!ValidationService.getGenerateInputFunction(req.generate_input_function)) { - console.log('Invalid generate input function'); - return { - status: 400, - message: 'Invalid generate input function', - data: null, - }; - } - - const question = await QuestionRepo.createQuestionByReq(req); +import mongoose from "mongoose"; +import QuestionRepo from "../repo/questionRepo"; +import { QuestionReq, GetUserSpecificQuestionResp } from "../model/question"; +import ValidationService from "./validationService"; +import { GeneralResp, GetQuestionsFilter, StatusCodeError } from "../types/types"; +import { QuestionInputModel } from "../model/questionInput"; + +const GetQuestions = async (filter: GetQuestionsFilter) => { + return await QuestionRepo.GetQuestions(filter); +}; + +const getQuestionByID = async (questionID: string) => { + if (!mongoose.isValidObjectId(questionID)) { + throw new StatusCodeError(400, "Invalid question ID"); + } + const _id = new mongoose.Types.ObjectId(questionID); + const question = await QuestionRepo.getQuestionByID(_id); + + if (!question) { + throw new StatusCodeError(404, "Question not found") + } + return question; +}; + +const createQuestion = async (req: QuestionReq): Promise => { + if (!ValidationService.getValidationFunction(req.validation_function)) { + console.log("Invalid validation function"); + return { + status: 400, + message: "Invalid validation function", + data: null, + }; + } + if ( + !ValidationService.getGenerateInputFunction(req.generate_input_function) + ) { + console.log("Invalid generate input function"); return { - status: 201, - message: 'Question created', - data: question, + status: 400, + message: "Invalid generate input function", + data: null, }; -} + } + + const question = await QuestionRepo.createQuestionByReq(req); + + return { + status: 201, + message: "Question created", + data: question, + }; +}; const updateQuestionSubmissions = async ( - questionID: string, - submissionID: string, - isCorrect: boolean + questionID: string, + submissionID: string, + isCorrect: boolean ) => { - if (!mongoose.isValidObjectId(questionID) || !mongoose.isValidObjectId(submissionID)) { - throw new Error('Invalid question or submission ID'); - } - const _questionID = new mongoose.Types.ObjectId(questionID); - const _submissionID = new mongoose.Types.ObjectId(submissionID); - return await QuestionRepo.updateQuestionSubmissions(_questionID, _submissionID, isCorrect); -} + if ( + !mongoose.isValidObjectId(questionID) || + !mongoose.isValidObjectId(submissionID) + ) { + throw new Error("Invalid question or submission ID"); + } + const _questionID = new mongoose.Types.ObjectId(questionID); + const _submissionID = new mongoose.Types.ObjectId(submissionID); + return await QuestionRepo.updateQuestionSubmissions( + _questionID, + _submissionID, + isCorrect + ); +}; const saveQuestionInput = async ( - userID: string, - seasonID: string, - questionID: string, - input: string[] + userID: string, + seasonID: string, + questionID: string, + input: string[] ) => { - const _userID = new mongoose.Types.ObjectId(userID); - const _seasonID = new mongoose.Types.ObjectId(seasonID); - const _questionID = new mongoose.Types.ObjectId(questionID); - - const questionInput = { - userID: _userID, - seasonID: _seasonID, - questionID: _questionID, - input: input, - } as QuestionInputModel - return await QuestionRepo.saveQuestionInput(questionInput); -} + const _userID = new mongoose.Types.ObjectId(userID); + const _seasonID = new mongoose.Types.ObjectId(seasonID); + const _questionID = new mongoose.Types.ObjectId(questionID); + + const questionInput = { + userID: _userID, + seasonID: _seasonID, + questionID: _questionID, + input: input, + } as QuestionInputModel; + return await QuestionRepo.saveQuestionInput(questionInput); +}; const getUserSpecificQuestion = async ( - userID: string, - questionID: string, + userID: string, + questionID: string ): Promise => { - if (!mongoose.isValidObjectId(userID) || !mongoose.isValidObjectId(questionID)) { - throw new Error('Invalid user, question or season ID'); - } - - const question = await QuestionService.getQuestionByID(questionID); - if (!question) { - throw new StatusCodeError(404, 'Question not found'); - } - - const seasonID = question.seasonID.toString(); - const _userID = new mongoose.Types.ObjectId(userID); - const _seasonID = new mongoose.Types.ObjectId(seasonID); - const _questionID = new mongoose.Types.ObjectId(questionID); - - const questionInput = await QuestionRepo.getQuestionInput(_userID, _seasonID, _questionID) - let input = questionInput?.input - if (!input) { - input = await ValidationService.generateInput(questionID) - await saveQuestionInput(userID, seasonID, questionID, input); - } - const resp: GetUserSpecificQuestionResp = { - id: question._id.toString(), - question_no: question.question_no, - question_title: question.question_title, - question_desc: question.question_desc, - question_date: question.question_date, - seasonID: questionInput!.seasonID.toString(), - question_input: input, - expiry: question.expiry, - points: question.points - } - return resp -} + if ( + !mongoose.isValidObjectId(userID) || + !mongoose.isValidObjectId(questionID) + ) { + throw new Error("Invalid user, question or season ID"); + } + + const question = await QuestionService.getQuestionByID(questionID); + if (!question) { + throw new StatusCodeError(404, "Question not found"); + } + + const seasonID = question.seasonID.toString(); + const _userID = new mongoose.Types.ObjectId(userID); + const _seasonID = new mongoose.Types.ObjectId(seasonID); + const _questionID = new mongoose.Types.ObjectId(questionID); + let input: string[] = []; + const questionInput = await QuestionRepo.getQuestionInput( + _userID, + _seasonID, + _questionID + ); + + if (!questionInput) { + input = await ValidationService.generateInput(questionID); + await saveQuestionInput(userID, seasonID, questionID, input); + } else { + input = questionInput.input; + } + + const resp: GetUserSpecificQuestionResp = { + id: question._id.toString(), + question_no: question.question_no, + question_title: question.question_title, + question_desc: question.question_desc, + question_date: question.question_date, + seasonID: seasonID, + question_input: input, + expiry: question.expiry, + points: question.points, + }; + return resp; +}; const QuestionService = { - getQuestionByID, - updateQuestionSubmissions, - createQuestion, - getUserSpecificQuestion -} + GetQuestions, + getQuestionByID, + updateQuestionSubmissions, + createQuestion, + getUserSpecificQuestion, +}; -export { QuestionService as default } \ No newline at end of file +export { QuestionService as default }; diff --git a/apps/challenges/src/service/seasonService.ts b/apps/challenges/src/service/seasonService.ts index 1262bf2f..3b806da5 100644 --- a/apps/challenges/src/service/seasonService.ts +++ b/apps/challenges/src/service/seasonService.ts @@ -1,61 +1,66 @@ -import SeasonRepo from "../repo/seasonRepo" -import mongoose from 'mongoose'; -import UserService from "./userService"; -import { get } from "http"; +import SeasonRepo from "../repo/seasonRepo"; +import mongoose from "mongoose"; +import { StatusCodeError } from "../types/types"; +import { zodGetValidObjectId } from "../utils/validator"; -const getSeasonsByDate = async( - startDate: Date | null, - endDate: Date | null -) => { - const seasons = await SeasonRepo.getSeasonsByDate(startDate, endDate); - return seasons; -} +const GetSeasons = async (start: unknown, end: unknown) => { + if (start != null && typeof start !== "string") { + throw new StatusCodeError(400, "invalid start date"); + } + if (end != null && typeof end !== "string") { + throw new StatusCodeError(400, "invalid end date"); + } -const getActiveSeasons = async() => { - const seasons = await SeasonRepo.getSeasonsByDate(new Date(), null); - return seasons; -} + const _startDate = start != null ? new Date(parseInt(start)) : null; + const _endDate = end != null ? new Date(parseInt(end)) : null; -const getSeasonByID = async(id: string) => { - if (!mongoose.isValidObjectId(id)) { - throw new Error('Invalid season ID'); - } - const _id = new mongoose.Types.ObjectId(id); - const season = await SeasonRepo.getSeasonByID(_id); - return season; -} + return await getSeasonsByDate(_startDate, _endDate); +}; -const createSeason = async( - title: string, - startDate: Date, - endDate: Date, +const getSeasonsByDate = async ( + startDate: Date | null, + endDate: Date | null ) => { - const season = await SeasonRepo.createSeason(title, startDate, endDate); - return season; -} + const seasons = await SeasonRepo.getSeasonsByDate(startDate, endDate); + return seasons; +}; -const getSeasonRankings = async( - seasonID: string, -) => { - if (!mongoose.isValidObjectId(seasonID)) { - throw new Error('Invalid season ID'); - } - const _id = new mongoose.Types.ObjectId(seasonID); - const rankings = await SeasonRepo.getSeasonRankings(_id); - return rankings; -} +const getActiveSeasons = async () => { + const seasons = await SeasonRepo.getSeasonsByDate(new Date(), null); + return seasons; +}; -const getSeasonRankingsByPagination = async( - seasonID: string, - page: number, - limit: number, +const getSeasonByID = async (id: string) => { + const _id = zodGetValidObjectId.parse(id); + const season = await SeasonRepo.getSeasonByID(_id); + return season; +}; + +const createSeason = async (title: string, startDate: Date, endDate: Date) => { + const season = await SeasonRepo.createSeason(title, startDate, endDate); + return season; +}; + +const getSeasonRankings = (seasonID: string) => { + if (!mongoose.isValidObjectId(seasonID)) { + throw new Error("Invalid season ID"); + } + const _id = new mongoose.Types.ObjectId(seasonID); + const rankings = SeasonRepo.getSeasonRankings(_id); + return rankings; +}; + +const getSeasonRankingsByPagination = ( + seasonID: string, + page: number, + limit: number ) => { - if (!mongoose.isValidObjectId(seasonID)) { - throw new Error('Invalid season ID'); - } - const _id = new mongoose.Types.ObjectId(seasonID); - return await SeasonRepo.getSeasonRankingsByPagination(_id, page, limit); -} + if (!mongoose.isValidObjectId(seasonID)) { + throw new Error("Invalid season ID"); + } + const _id = new mongoose.Types.ObjectId(seasonID); + return SeasonRepo.getSeasonRankingsByPagination(_id, page, limit); +}; /* const getUserSeasonRanking = async( @@ -83,37 +88,34 @@ const getUserAllSeasonRankings = async( } */ -const calculateSeasonRankings = async( - seasonID: string -) => { - if (!mongoose.isValidObjectId(seasonID)) { - throw new Error('Invalid user ID'); - } - const _seasonID = new mongoose.Types.ObjectId(seasonID); - return await SeasonRepo.calculateSeasonRankings(_seasonID); -} +const calculateSeasonRankings = async (seasonID: string) => { + if (!mongoose.isValidObjectId(seasonID)) { + throw new Error("Invalid user ID"); + } + const _seasonID = new mongoose.Types.ObjectId(seasonID); + return await SeasonRepo.calculateSeasonRankings(_seasonID); +}; -const getSeasonQuestions = async( - seasonID: string -) => { - if (!mongoose.isValidObjectId(seasonID)) { - throw new Error('Invalid season ID'); - } - const _seasonID = new mongoose.Types.ObjectId(seasonID); - return await SeasonRepo.getSeasonQuestions(_seasonID); -} +const getSeasonQuestions = async (seasonID: string) => { + if (!mongoose.isValidObjectId(seasonID)) { + throw new Error("Invalid season ID"); + } + const _seasonID = new mongoose.Types.ObjectId(seasonID); + return await SeasonRepo.getSeasonQuestions(_seasonID); +}; const SeasonService = { - getSeasonsByDate, - getActiveSeasons, - getSeasonByID, - createSeason, - getSeasonRankings, - getSeasonRankingsByPagination, - // getUserSeasonRanking, - // getUserAllSeasonRankings, - calculateSeasonRankings, - getSeasonQuestions, -} + GetSeasons, + getSeasonsByDate, + getActiveSeasons, + getSeasonByID, + createSeason, + getSeasonRankings, + getSeasonRankingsByPagination, + // getUserSeasonRanking, + // getUserAllSeasonRankings, + calculateSeasonRankings, + getSeasonQuestions, +}; -export { SeasonService as default } \ No newline at end of file +export { SeasonService as default }; diff --git a/apps/challenges/src/service/submissionService.ts b/apps/challenges/src/service/submissionService.ts index cec4478d..cbb3b88d 100644 --- a/apps/challenges/src/service/submissionService.ts +++ b/apps/challenges/src/service/submissionService.ts @@ -1,80 +1,92 @@ -import { CreateSubmissionReq, SubmissionModel } from "../model/submission"; +import { QuestionModel } from "../model/question"; +import { CreateSubmissionReq } from "../model/submission"; import SubmissionRepo from "../repo/submissionRepo"; import { GeneralResp } from "../types/types"; import QuestionService from "./questionService"; import ValidationService from "./validationService"; +import { Logger } from "nodelogger"; const createSubmission = async ( - submission: CreateSubmissionReq + submission: CreateSubmissionReq ): Promise => { - var question; - try { - question = await QuestionService.getQuestionByID(submission.question.toString()); + let question: QuestionModel | null; + try { + question = await QuestionService.getQuestionByID( + submission.question.toString() + ); - if (!question) { - throw new Error('Question not found'); - } - - if (!question.active) { - throw new Error('Question is not active'); - } + if (!question) { + throw new Error("Question not found"); + } - if (new Date(question.expiry) < new Date()) { - throw new Error('Question has expired'); - } - } catch (err) { - return { - status: 400, - message: (err as Error).message - } + if (!question.active) { + throw new Error("Question is not active"); } - let isCorrect = false; - try { - isCorrect = await ValidationService.validateAnswer(submission.question.toString(), submission.answer); - } catch (err) { - console.log("submissionService createSubmission fail to validate answer: ", err); + if (new Date(question.expiry) < new Date()) { + throw new Error("Question has expired"); } + } catch (err) { + Logger.error("SubmissionService.CreateSubmission validation error", err); + return { + status: 400, + message: (err as Error).message, + }; + } - try { - const dbSubmission = { - user: submission.user, - seasonID: question.seasonID, - question: submission.question, - answer: submission.answer, - correct: isCorrect, - points_awarded: isCorrect ? question.points : 0 - } - const result = await SubmissionRepo.createSubmission(dbSubmission); + let isCorrect = false; + try { + isCorrect = await ValidationService.validateAnswer( + submission.question.toString(), + submission.answer + ); + } catch (err) { + Logger.error( + "SubmissionService.CreateSubmission validate answer error", + err + ); + } - // Update question submissions array using $push and $inc submission counts - question = await QuestionService.updateQuestionSubmissions( - submission.question.toString(), - result._id.toString(), - isCorrect - ); + try { + const dbSubmission = { + user: submission.user, + seasonID: question.seasonID, + question: submission.question, + answer: submission.answer, + correct: isCorrect, + points_awarded: isCorrect ? question.points : 0, + }; + const result = await SubmissionRepo.createSubmission(dbSubmission); - if (!question) { - throw new Error('Failed to update question submissions'); - } + // Update question submissions array using $push and $inc submission counts + question = await QuestionService.updateQuestionSubmissions( + submission.question.toString(), + result._id.toString(), + isCorrect + ); - return { - status: 201, - message: 'Answer submitted', - data: result - }; - } catch (err) { - return { - status: 500, - message: (err as Error).message - } + if (!question) { + throw new Error("Failed to update question submissions"); } -} - - + Logger.info( + `SubmissionService.CreateSubmission user ${submission.user.toString()} successfully created submission at season ${question.seasonID.toString()} and question ${submission.question.toString()}` + ); + return { + status: 201, + message: "Answer submitted", + data: result, + }; + } catch (err) { + Logger.error("SubmissionService.CreateSubmission create submission error", err); + return { + status: 500, + message: (err as Error).message, + }; + } +}; const SubmissionService = { - createSubmission, -} + createSubmission, +}; -export default SubmissionService; \ No newline at end of file +export default SubmissionService; diff --git a/apps/challenges/src/service/tokenService.ts b/apps/challenges/src/service/tokenService.ts index f5ef7513..098834e9 100644 --- a/apps/challenges/src/service/tokenService.ts +++ b/apps/challenges/src/service/tokenService.ts @@ -1,33 +1,36 @@ import jwt from "jsonwebtoken"; -import { accessTokenMaxAgeSeconds, refreshCookieMaxAgeSeconds } from "../model/constants"; -import { isValidObjectId } from "../utils/db"; +import { + accessTokenMaxAgeSeconds, + refreshCookieMaxAgeSeconds, +} from "../model/constants"; +import { zodGetValidObjectId } from "../utils/validator"; +import TokenRepo from "../repo/tokenRepo"; +import { TokenModel } from "../model/token"; -const generateAccessToken = async ( - id: string, - email: string -) => { - const secret = process.env.JWT_SECRET || ""; - const token = jwt.sign({ id, email }, secret, { - expiresIn: accessTokenMaxAgeSeconds - }); - return token; -} - -const generateRefreshToken = async ( - id: string, - email: string -) => { - const secret = process.env.JWT_SECRET || ""; - const token = jwt.sign({ id, email }, secret, { - expiresIn: refreshCookieMaxAgeSeconds, - }); - return token; -} +const generateAccessToken = (id: string, email: string) => { + const secret = process.env.CHALLENGES_JWT_SECRET || ""; + const token = jwt.sign({ id, email }, secret, { + expiresIn: accessTokenMaxAgeSeconds, + }); + return token; +}; +const generateRefreshToken = (id: string, email: string) => { + const secret = process.env.CHALLENGES_JWT_SECRET || ""; + const token = jwt.sign({ id, email }, secret, { + expiresIn: refreshCookieMaxAgeSeconds, + }); + return token; +}; +const extendRefreshToken = (userID: string): Promise => { + const mongoUserID = zodGetValidObjectId.parse(userID); + return TokenRepo.extendRefreshToken(mongoUserID); +}; const TokenService = { - generateAccessToken, - generateRefreshToken, -} + generateAccessToken, + generateRefreshToken, + extendRefreshToken, +}; -export { TokenService as default }; \ No newline at end of file +export { TokenService as default }; diff --git a/apps/challenges/src/service/userService.ts b/apps/challenges/src/service/userService.ts index 61ccbe72..7739e4c1 100644 --- a/apps/challenges/src/service/userService.ts +++ b/apps/challenges/src/service/userService.ts @@ -1,54 +1,50 @@ -import mongoose from 'mongoose'; -import UserRepo from '../repo/userRepo'; -import { z } from 'zod'; -import { isValidEmail, zodIsValidObjectId } from '../utils/validator'; -import { StatusCodeError } from '../types/types'; -import { UserModel } from '../model/user'; +import mongoose from "mongoose"; +import UserRepo from "../repo/userRepo"; +import { isValidEmail, zodIsValidObjectId } from "../utils/validator"; +import { StatusCodeError } from "../types/types"; +import { UserModel } from "../model/user"; const getUserByID = async (id: string) => { - let _id: string - let user: UserModel | null - try { - _id = zodIsValidObjectId.parse(id); - } catch (err) { - throw new StatusCodeError(400, "Invalid id"); - } - - const mongoUserID = new mongoose.Types.ObjectId(_id); - - try { - user = await UserRepo.getUserByID(mongoUserID); - } catch (err) { - throw new StatusCodeError(500, "Internal Server Error") - } - - if (!user) { - throw new StatusCodeError(404, "User not found") - } - - return user; -} + let _id: string; + let user: UserModel | null; + try { + _id = zodIsValidObjectId.parse(id); + } catch (err) { + throw new StatusCodeError(400, "Invalid id"); + } + + const mongoUserID = new mongoose.Types.ObjectId(_id); + + try { + user = await UserRepo.getUserByID(mongoUserID); + } catch (err) { + throw new StatusCodeError(500, "Internal Server Error"); + } + + if (!user) { + throw new StatusCodeError(404, "User not found"); + } + + return user; +}; const getUserByEmail = async (email: string) => { - const _email = isValidEmail.parse(email); + const _email = isValidEmail.parse(email); - const user = await UserRepo.getUserByEmail(_email); + const user = await UserRepo.getUserByEmail(_email); - return user; -} + return user; +}; -const createUser = async ( - name: string, - email: string, -) => { - const user = await UserRepo.createUser(name, email); - return user; -} +const createUser = async (name: string, email: string) => { + const user = await UserRepo.createUser(name, email); + return user; +}; const UserService = { - getUserByID, - getUserByEmail, - createUser, -} + getUserByID, + getUserByEmail, + createUser, +}; -export { UserService as default }; \ No newline at end of file +export { UserService as default }; diff --git a/apps/challenges/src/service/validationService.ts b/apps/challenges/src/service/validationService.ts index 48043f6c..8ee98d0d 100644 --- a/apps/challenges/src/service/validationService.ts +++ b/apps/challenges/src/service/validationService.ts @@ -2,93 +2,97 @@ import { z } from "zod"; import QuestionService from "./questionService"; const isHello = (input: any) => { - // use zod to confirm input is string - z.string().parse(input); + // use zod to confirm input is string + z.string().parse(input); - return input === 'hello'; -} + return input === "hello"; +}; // DO NOT EXPOSE this map. Add new validation function by directly adding to the map -const validationFunctionMap: Map = new Map([ - ['isHello', isHello], -]); +const validationFunctionMap: Map boolean> = new Map< + string, + (input: any) => boolean +>([["isHello", isHello]]); const getValidationFunction = (functionName: string) => { - if (validationFunctionMap.has(functionName)) { - return validationFunctionMap.get(functionName); - } - return null; -} + if (validationFunctionMap.has(functionName)) { + return validationFunctionMap.get(functionName); + } + return null; +}; const validateAnswer = async ( - questionID: string, - submission: any, + questionID: string, + submission: any ): Promise => { - - const question = await QuestionService.getQuestionByID(questionID); - if (!question) { - throw new Error('Question not found'); - } - - //check if validation function exists as keys - const invokeFunction = getValidationFunction(question.validation_function); - if (!invokeFunction) { - throw new Error('Validation function not found'); - } - - try { - const isCorrect = invokeFunction(submission); - return isCorrect; - } catch (e) { - console.log(`validationService validateAnswer fail to validate answer: ${e}`) - return false; - } -} + const question = await QuestionService.getQuestionByID(questionID); + if (!question) { + throw new Error("Question not found"); + } + + //check if validation function exists as keys + const invokeFunction = getValidationFunction(question.validation_function); + if (!invokeFunction) { + throw new Error("Validation function not found"); + } + + try { + const isCorrect = invokeFunction(submission); + return isCorrect; + } catch (err) { + console.log( + `validationService validateAnswer fail to validate answer`, + err + ); + return false; + } +}; const generateOneTwo = (): string[] => { - return ["1", "2", "1, 2"]; -} + return ["1", "2", "1, 2"]; +}; // DO NOT EXPOSE this map. Add new validation function by directly adding to the map -const generateInputFunctionMap: Map = new Map([ - ['generateOneTwo', generateOneTwo], -]); +const generateInputFunctionMap: Map string[]> = new Map< + string, + () => string[] +>([["generateOneTwo", generateOneTwo]]); const getGenerateInputFunction = (functionName: string) => { - if (generateInputFunctionMap.has(functionName)) { - return generateInputFunctionMap.get(functionName); - } - return null; -} - -const generateInput = async ( - questionID: string, -): Promise => { - const question = await QuestionService.getQuestionByID(questionID); - if (!question) { - throw new Error('Question not found'); - } - - //check if validation function exists as keys - const invokeFunction = getGenerateInputFunction(question.generate_input_function); - if (!invokeFunction) { - throw new Error('Validation function not found'); - } - - try { - const input = invokeFunction(); - return input; - } catch (e) { - console.log(`validationService validateAnswer fail to validate answer: ${e}`) - throw new Error('fail to generate input'); - } -} + if (generateInputFunctionMap.has(functionName)) { + return generateInputFunctionMap.get(functionName); + } + return null; +}; + +const generateInput = async (questionID: string): Promise => { + const question = await QuestionService.getQuestionByID(questionID); + if (!question) { + throw new Error("Question not found"); + } + + //check if validation function exists as keys + const invokeFunction = getGenerateInputFunction( + question.generate_input_function + ); + if (!invokeFunction) { + throw new Error("Validation function not found"); + } + + try { + const input = invokeFunction(); + return input; + } catch (e) { + console.log(`validationService validateAnswer fail to validate answer`, e); + throw new Error("fail to generate input"); + } +}; const ValidationService = { - validateAnswer, - getValidationFunction, - generateInput, - getGenerateInputFunction, -} + validateAnswer, + getValidationFunction, + generateInput, + getGenerateInputFunction, +}; -export { ValidationService as default } \ No newline at end of file +export { ValidationService as default }; diff --git a/apps/challenges/src/tasks/rankingCalculation.ts b/apps/challenges/src/tasks/rankingCalculation.ts index eda541d1..299fe864 100644 --- a/apps/challenges/src/tasks/rankingCalculation.ts +++ b/apps/challenges/src/tasks/rankingCalculation.ts @@ -1,39 +1,52 @@ +import { Logger } from "nodelogger"; import { UserRanking } from "../model/rankingScore"; +import { SeasonModel } from "../model/season"; import SeasonService from "../service/seasonService"; export const rankingsMap: { [id: string]: UserRanking[] } = {}; export const clearRankingsMap = () => { - for (var key in rankingsMap) { - delete rankingsMap[key]; - } -} + for (const key in rankingsMap) { + delete rankingsMap[key]; + } +}; export const rankingCalculation = async () => { - console.log("Calculating rankings..."); - let activeSeasons; - try{ - activeSeasons = await SeasonService.getActiveSeasons(); - }catch(err){ - console.log(err); - return; + let activeSeasons: SeasonModel[] | null; + try { + activeSeasons = await SeasonService.getActiveSeasons(); + } catch (err) { + let errorReason = "unknown error"; + if (err instanceof Error) { + errorReason = err.message; } + Logger.error( + `rankingCalculation cronjob: getActiveSeasons error ${errorReason}` + ); + return; + } - if (!activeSeasons) return; + if (!activeSeasons) { + Logger.info("rankingCalculation cronjob: no season found"); + return; + } - var activeSeasonIDs = activeSeasons?.map((value) => { - return value._id.toString(); - }); + const activeSeasonIDs: string[] = activeSeasons.map((a) => a._id.toString()); - try{ - for (var seasonID of activeSeasonIDs) { - const rankings = await SeasonService.calculateSeasonRankings(seasonID); - if(rankings && rankings.length > 0){ - rankingsMap[seasonID] = rankings; - } - } - }catch(err){ - console.log(err); + try { + for (const seasonID of activeSeasonIDs) { + const rankings = await SeasonService.calculateSeasonRankings(seasonID); + if (rankings && rankings.length > 0) { + rankingsMap[seasonID] = rankings; + } } -} - + } catch (err) { + let errorReason = "unknown error"; + if (err instanceof Error) { + errorReason = err.message; + } + Logger.error( + `rankingCalculation cronjob: calculateSeasonRankings error ${errorReason}` + ); + } +}; diff --git a/apps/challenges/src/test/leaderboard.test.ts b/apps/challenges/src/test/leaderboard.test.ts deleted file mode 100644 index 6233a537..00000000 --- a/apps/challenges/src/test/leaderboard.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -import request from 'supertest'; -import mongoose from 'mongoose'; -import app from '../index'; -const Question = require('../model/question'); -const Submission = require('../model/submission'); -const User = require('../model/user'); -const Leaderboard = require('../model/leaderboard'); -import { userFixture, questionFixture, answerFixture, leaderboardFixture } from '../utils/fixtures/fixtures'; - -beforeAll(async () => { - await Leaderboard.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) -}) - -afterAll(async () => { - await Leaderboard.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) - await mongoose.connection.close() -}) - -describe('List Leaderboards: GET /api/leaderboard', () => { - beforeAll(async () => { - await Leaderboard.deleteMany({}); - await Leaderboard.create(leaderboardFixture()); - await Leaderboard.create(leaderboardFixture()); - await Leaderboard.create(leaderboardFixture({ active: false})); - }); - - it('should return all leaderboards', async () => { - const response = await request(app).get('/api/leaderboard'); - expect(response.status).toBe(200); - expect(response.body.length).toBe(3); - }); - - it('should return only active leaderboards', async () => { - const response = await request(app).get('/api/leaderboard/active'); - expect(response.status).toBe(200); - expect(response.body.length).toBe(2); - }); -}) - -describe('Get Leaderboard: GET /api/leaderboard/:id', () => { - it('should not get a leaderboard with invalid leaderboard id', async () => { - const response = await request(app).get('/api/leaderboard/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid leaderboard ID"); - }) - - it('should not get a leaderboard with non existing leaderboard id', async () => { - const response = await request(app).get(`/api/leaderboard/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Leaderboard not found"); - }) - - it('should get a leaderboard', async () => { - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app).get(`/api/leaderboard/${leaderboard._id}`); - expect(response.status).toBe(200); - expect(response.body.title).toBe(leaderboard.title); - expect(new Date(response.body.start_date)).toEqual(new Date(leaderboard.start_date)); - expect(new Date(response.body.end_date)).toEqual(new Date(leaderboard.end_date)); - expect(response.body.rankings.length).toBe(0); - }) -}); - -describe('Set Leaderboard: POST /api/leaderboard', () => { - it('should not set a leaderboard with missing fields', async () => { - const response = await request(app) - .post('/api/leaderboard') - .send(leaderboardFixture({ title: null })); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Leaderboard validation failed: title: Please add a title"); - }) - - it('should set a leaderboard', async () => { - const leaderboard = leaderboardFixture(); - const response = await request(app) - .post('/api/leaderboard') - .send(leaderboard); - expect(response.status).toBe(201); - expect(response.body.title).toBe(leaderboard.title); - expect(new Date(response.body.start_date)).toEqual(new Date(leaderboard.start_date)); - expect(new Date(response.body.end_date)).toEqual(new Date(leaderboard.end_date)); - expect(response.body.rankings.length).toBe(0); - }) -}) - -describe('Update Leaderboard: PUT /api/leaderboard/:id', () => { - it('should not update a leaderboard with invalid leaderboard id', async () => { - const response = await request(app) - .put('/api/leaderboard/123456789012') - .send(leaderboardFixture()); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid leaderboard ID"); - }) - - it('should not update a leaderboard with non existing leaderboard id', async () => { - const response = await request(app) - .put(`/api/leaderboard/65a8ba0b8c8139544b9955ac`) - .send(leaderboardFixture()); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Leaderboard not found"); - }) - - it('should update a leaderboard', async () => { - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app) - .put(`/api/leaderboard/${leaderboard._id}`) - .send(leaderboardFixture({ title: "Updated Title", active: false, start_date: "2024-05-01T00:00:00.000Z", end_date: "2041-06-01T00:00:00.000Z" })); - expect(response.status).toBe(200); - expect(response.body.title).toBe("Updated Title"); - expect(new Date(response.body.start_date)).toEqual(new Date("2024-05-01T00:00:00.000Z")); - expect(new Date(response.body.end_date)).toEqual(new Date("2041-06-01T00:00:00.000Z")); - expect(response.body.active).toBe(false); - }) -}) - -describe('Delete Leaderboard: DELETE /api/leaderboard/:id', () => { - it('should not delete a leaderboard with invalid leaderboard id', async () => { - const response = await request(app).delete('/api/leaderboard/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid leaderboard ID"); - }) - - it('should not delete a leaderboard with non existing leaderboard id', async () => { - const response = await request(app).delete(`/api/leaderboard/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Leaderboard not found"); - }) - - it('should delete a leaderboard', async () => { - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app).delete(`/api/leaderboard/${leaderboard._id}`); - expect(response.status).toBe(200); - expect(response.body.message).toBe("Leaderboard deleted"); - }) -}) - -describe('Get Leaderboard Rankings: GET /api/leaderboard/rankings/:id/:top', () => { - it('should not get leaderboard rankings with invalid leaderboard id', async () => { - const response = await request(app).get('/api/leaderboard/rankings/123456789012/10'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid leaderboard ID"); - }) - - it('should not get leaderboard rankings with non existing leaderboard id', async () => { - const response = await request(app).get(`/api/leaderboard/rankings/65a8ba0b8c8139544b9955ac/10`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Leaderboard not found"); - }) - - it('should not get leaderboard rankings with invalid top', async () => { - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app).get(`/api/leaderboard/rankings/${leaderboard._id}/abc`); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid top"); - }) - - it('should not get leaderboard rankings with negative top', async () => { - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app).get(`/api/leaderboard/rankings/${leaderboard._id}/-1`); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid top"); - }) - - // it('should get leaderboard rankings', async () => { - // const leaderboard = await Leaderboard.create(leaderboardFixture({ title: "Winter 2023", start_date: "2023-12-01T00:00:00.000Z", end_date: "2024-01-01T00:00:00.000Z", active: true})); - // const user1 = await User.create(userFixture()); - // const user2 = await User.create(userFixture()); - // const user3 = await User.create(userFixture()); - // const question1 = await Question.create(questionFixture()); - // const question2 = await Question.create(questionFixture()); - // const question3 = await Question.create(questionFixture()); - // const submission1 = await Submission.create({ user: user1._id, leaderboard: leaderboard._id, question: question1._id, answer: "Answer 1", points: 10 }); - // const submission2 = await Submission.create({ user: user2._id, leaderboard: leaderboard._id, question: question2._id, answer: "Answer 2", points: 20 }); - // const submission3 = await Submission.create({ user: user3._id, leaderboard: leaderboard._id, question: question3._id, answer: "Answer 3", points: 30 }); - // const response = await request(app).get(`/api/leaderboard/rankings/${leaderboard._id}/2`); - // expect(response.status).toBe(200); - // expect(response.body.length).toBe(2); - // expect(response.body[0].user.name).toBe(user3.name); - // expect(response.body[0].points).toBe(30); - // expect(response.body[1].user.name).toBe(user2.name); - // expect(response.body[1].points).toBe(20); - // }) -}) diff --git a/apps/challenges/src/test/pagination.test.ts b/apps/challenges/src/test/pagination.test.ts index bcb2aa8b..ce883e25 100644 --- a/apps/challenges/src/test/pagination.test.ts +++ b/apps/challenges/src/test/pagination.test.ts @@ -1,286 +1,345 @@ import { z } from "zod"; import { generatePaginationMetaData, paginateArray } from "../utils/pagination"; -describe('generatePaginationMetaData', () => { - it('should return error if pageIndex < 0', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = -1; - const limit = 10; - const maxPageIndex = 0; - const itemCount = 2; - - expect( - () => {generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount)} - ) - .toThrow(z.ZodError) - }); - - it('should return error if maxPageIndex < 0', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 0; - const limit = 10; - const maxPageIndex = -1; - const itemCount = 2; - - expect( - () => { generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount) } - ) - .toThrow(z.ZodError) - }); - - it('should return error if limit < 1', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 0; - const limit = 0; - const maxPageIndex = 0; - const itemCount = 2; - - expect( - () => { generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount) } - ) - .toThrow(z.ZodError) - }); - - it('should return error if itemCount < 0', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 0; - const limit = 1; - const maxPageIndex = 0; - const itemCount = -1; - - expect( - () => { generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount) } - ) - .toThrow(z.ZodError) - }); - - it('should return correct links even if itemCount is 0', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 0; - const limit = 1; - const maxPageIndex = 0; - const itemCount = 0; - - const links = { - self: `${baseUrl}?page=0&limit=${limit}`, - first: `${baseUrl}?page=0&limit=${limit}`, - previous: null, - next: null, - last: `${baseUrl}?page=0&limit=${limit}` - } - - const metaData = { - itemCount: 0, - limit: 1, - pageCount: 0, - page: 0, - links: links - } - expect(generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount)).toEqual(metaData); - }); - - it('should return correct links (boundary value check -> all input is exactly one step away from being invalid input)', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 0; - const limit = 1; - const maxPageIndex = 0; - const itemCount = 1; - - const links = { - self: `${baseUrl}?page=0&limit=${limit}`, - first: `${baseUrl}?page=0&limit=${limit}`, - previous: null, - next: null, - last: `${baseUrl}?page=0&limit=${limit}` - } - - const metaData = { - itemCount: 1, - limit: 1, - pageCount: 1, - page: 0, - links: links - } - - expect(generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount)).toEqual(metaData); - }); - - it('should return correct links when the page is in the middle', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 1; - const limit = 1; - const maxPageIndex = 2; - const itemCount = 3; - - const links = { - self: `${baseUrl}?page=1&limit=${limit}`, - first: `${baseUrl}?page=0&limit=${limit}`, - previous: `${baseUrl}?page=0&limit=${limit}`, - next: `${baseUrl}?page=2&limit=${limit}`, - last: `${baseUrl}?page=2&limit=${limit}` - } - - const metaData = { - itemCount: 3, - limit: 1, - pageCount: 3, - page: 1, - links: links - } - - expect(generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount)).toEqual(metaData); - }); - - it('should all links should be unique when they should be', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 2; - const limit = 1; - const maxPageIndex = 4; - const itemCount = 5; - - const links = { - self: `${baseUrl}?page=2&limit=${limit}`, - first: `${baseUrl}?page=0&limit=${limit}`, - previous: `${baseUrl}?page=1&limit=${limit}`, - next: `${baseUrl}?page=3&limit=${limit}`, - last: `${baseUrl}?page=4&limit=${limit}` - } - - const metaData = { - itemCount: 5, - limit: 1, - pageCount: 5, - page: 2, - links: links - } - - expect(generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount)).toEqual(metaData); - }); - - it('should return correct links even if pageIndex > maxPageIndex', () => { - const baseUrl = '/api/seasons/123/rankings'; - const pageIndex = 6; - const limit = 1; - const maxPageIndex = 5; - const itemCount = 6; - - const links = { - self: `${baseUrl}?page=${pageIndex}&limit=${limit}`, - first: `${baseUrl}?page=0&limit=${limit}`, - previous: null, - next: null, - last: `${baseUrl}?page=${maxPageIndex}&limit=${limit}` - } - - const metaData = { - itemCount: 6, - limit: limit, - pageCount: 6, - page: 6, - links: links - } - expect(generatePaginationMetaData(baseUrl, pageIndex, limit, maxPageIndex, itemCount)).toEqual(metaData); - }); -}) - -describe('paginateArray', () => { - it('should return empty array if the array is empty', () => { - const array = []; - const pageSize = 0 - const pageNumber = 0 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - it('should return empty array if the array is empty with invalid pageSize', () => { - const array = []; - const pageSize = -1 - const pageNumber = 0 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - it('should return empty array if the array is empty with invalid pageNumber', () => { - const array = []; - const pageSize = 0 - const pageNumber = -123 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - it('should return empty array if the array is empty with invalid pageSize and pageNumber', () => { - const array = []; - const pageSize = 132242 - const pageNumber = 123 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - it('should return empty array if the array is empty with invalid pageSize and pageNumber', () => { - const array = []; - const pageSize = 132242 - const pageNumber = 123 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - - it('should return empty array if the array is exist but filled with invalid pageSize and pageNumber', () => { - const array = [1, 2, 3]; - const pageSize = 132242 - const pageNumber = 123 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - it('should return empty array if the array is exist but filled with invalid pageSize and pageNumber', () => { - const array = [1, 2, 3]; - const pageSize = -1 - const pageNumber = 2 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - it('should return empty array if the array is exist but filled with invalid pageSize and pageNumber', () => { - const array = [1, 2, 3]; - const pageSize = 0 - const pageNumber = 2 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([]) - }); - - // do note that while page number being negative do work, - // in Session getSeasonRankingsByPagination, - // we do not accept negative page number - it('should return correct paginated data even if pageNumber is negative', () => { - const array = [1, 2, 3, 4, 5]; - const pageSize = 1 - const pageNumber = -2 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([4]) - }); - - it('should return correct paginated data', () => { - const array = [1, 2, 3]; - const pageSize = 1 - const pageNumber = 2 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([3]) - }); - - it('should return correct paginated data', () => { - const array = [1, 2, 3]; - const pageSize = 2 - const pageNumber = 0 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([1, 2]) - }); - - it('should return correct paginated data', () => { - const array = [1, 2, 3, 4, 5]; - const pageSize = 2 - const pageNumber = 1 - - expect(paginateArray(array, pageSize, pageNumber)).toEqual([3,4]) - }); -}) \ No newline at end of file +describe("generatePaginationMetaData", () => { + it("should return error if pageIndex < 0", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = -1; + const limit = 10; + const maxPageIndex = 0; + const itemCount = 2; + + expect(() => { + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ); + }).toThrow(z.ZodError); + }); + + it("should return error if maxPageIndex < 0", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 0; + const limit = 10; + const maxPageIndex = -1; + const itemCount = 2; + + expect(() => { + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ); + }).toThrow(z.ZodError); + }); + + it("should return error if limit < 1", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 0; + const limit = 0; + const maxPageIndex = 0; + const itemCount = 2; + + expect(() => { + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ); + }).toThrow(z.ZodError); + }); + + it("should return error if itemCount < 0", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 0; + const limit = 1; + const maxPageIndex = 0; + const itemCount = -1; + + expect(() => { + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ); + }).toThrow(z.ZodError); + }); + + it("should return correct links even if itemCount is 0", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 0; + const limit = 1; + const maxPageIndex = 0; + const itemCount = 0; + + const links = { + self: `${baseUrl}?page=0&limit=${limit}`, + first: `${baseUrl}?page=0&limit=${limit}`, + previous: null, + next: null, + last: `${baseUrl}?page=0&limit=${limit}`, + }; + + const metaData = { + itemCount: 0, + limit: 1, + pageCount: 0, + page: 0, + links: links, + }; + expect( + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ) + ).toEqual(metaData); + }); + + it("should return correct links (boundary value check -> all input is exactly one step away from being invalid input)", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 0; + const limit = 1; + const maxPageIndex = 0; + const itemCount = 1; + + const links = { + self: `${baseUrl}?page=0&limit=${limit}`, + first: `${baseUrl}?page=0&limit=${limit}`, + previous: null, + next: null, + last: `${baseUrl}?page=0&limit=${limit}`, + }; + + const metaData = { + itemCount: 1, + limit: 1, + pageCount: 1, + page: 0, + links: links, + }; + + expect( + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ) + ).toEqual(metaData); + }); + + it("should return correct links when the page is in the middle", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 1; + const limit = 1; + const maxPageIndex = 2; + const itemCount = 3; + + const links = { + self: `${baseUrl}?page=1&limit=${limit}`, + first: `${baseUrl}?page=0&limit=${limit}`, + previous: `${baseUrl}?page=0&limit=${limit}`, + next: `${baseUrl}?page=2&limit=${limit}`, + last: `${baseUrl}?page=2&limit=${limit}`, + }; + + const metaData = { + itemCount: 3, + limit: 1, + pageCount: 3, + page: 1, + links: links, + }; + + expect( + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ) + ).toEqual(metaData); + }); + + it("should all links should be unique when they should be", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 2; + const limit = 1; + const maxPageIndex = 4; + const itemCount = 5; + + const links = { + self: `${baseUrl}?page=2&limit=${limit}`, + first: `${baseUrl}?page=0&limit=${limit}`, + previous: `${baseUrl}?page=1&limit=${limit}`, + next: `${baseUrl}?page=3&limit=${limit}`, + last: `${baseUrl}?page=4&limit=${limit}`, + }; + + const metaData = { + itemCount: 5, + limit: 1, + pageCount: 5, + page: 2, + links: links, + }; + + expect( + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ) + ).toEqual(metaData); + }); + + it("should return correct links even if pageIndex > maxPageIndex", () => { + const baseUrl = "/api/seasons/123/rankings"; + const pageIndex = 6; + const limit = 1; + const maxPageIndex = 5; + const itemCount = 6; + + const links = { + self: `${baseUrl}?page=${pageIndex}&limit=${limit}`, + first: `${baseUrl}?page=0&limit=${limit}`, + previous: null, + next: null, + last: `${baseUrl}?page=${maxPageIndex}&limit=${limit}`, + }; + + const metaData = { + itemCount: 6, + limit: limit, + pageCount: 6, + page: 6, + links: links, + }; + expect( + generatePaginationMetaData( + baseUrl, + pageIndex, + limit, + maxPageIndex, + itemCount + ) + ).toEqual(metaData); + }); +}); + +describe("paginateArray", () => { + it("should return empty array if the array is empty", () => { + const array = []; + const pageSize = 0; + const pageNumber = 0; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is empty with invalid pageSize", () => { + const array = []; + const pageSize = -1; + const pageNumber = 0; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is empty with invalid pageNumber", () => { + const array = []; + const pageSize = 0; + const pageNumber = -123; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is empty with invalid pageSize and pageNumber", () => { + const array = []; + const pageSize = 132242; + const pageNumber = 123; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is empty with invalid pageSize and pageNumber", () => { + const array = []; + const pageSize = 132242; + const pageNumber = 123; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is exist but filled with invalid pageSize and pageNumber", () => { + const array = [1, 2, 3]; + const pageSize = 132242; + const pageNumber = 123; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is exist but filled with invalid pageSize and pageNumber", () => { + const array = [1, 2, 3]; + const pageSize = -1; + const pageNumber = 2; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + it("should return empty array if the array is exist but filled with invalid pageSize and pageNumber", () => { + const array = [1, 2, 3]; + const pageSize = 0; + const pageNumber = 2; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([]); + }); + + // do note that while page number being negative do work, + // in Session getSeasonRankingsByPagination, + // we do not accept negative page number + it("should return correct paginated data even if pageNumber is negative", () => { + const array = [1, 2, 3, 4, 5]; + const pageSize = 1; + const pageNumber = -2; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([4]); + }); + + it("should return correct paginated data", () => { + const array = [1, 2, 3]; + const pageSize = 1; + const pageNumber = 2; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([3]); + }); + + it("should return correct paginated data", () => { + const array = [1, 2, 3]; + const pageSize = 2; + const pageNumber = 0; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([1, 2]); + }); + + it("should return correct paginated data", () => { + const array = [1, 2, 3, 4, 5]; + const pageSize = 2; + const pageNumber = 1; + + expect(paginateArray(array, pageSize, pageNumber)).toEqual([3, 4]); + }); +}); diff --git a/apps/challenges/src/test/questionaire.test.ts b/apps/challenges/src/test/questionaire.test.ts deleted file mode 100644 index da55ad03..00000000 --- a/apps/challenges/src/test/questionaire.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -import request from 'supertest'; -import mongoose from 'mongoose'; -import app from '../index'; -const Question = require('../model/question'); -const Submission = require('../model/submission'); -const User = require('../model/user'); -const Leaderboard = require('../model/leaderboard'); -import { userFixture, questionFixture, answerFixture, leaderboardFixture } from '../utils/fixtures/fixtures'; -import { isValidObjectId } from '../utils/db'; - -beforeAll(async () => { - await Leaderboard.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) -}) - -afterAll(async () => { - await Leaderboard.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) - await mongoose.connection.close() -}) - -describe('List Questions: GET /api/question', () => { - beforeAll(async () => { - await Question.deleteMany({}); - await Question.create(questionFixture()); - await Question.create(questionFixture()); - await Question.create(questionFixture({ active: false })); - }); - - it('should return all questions', async () => { - const response = await request(app).get('/api/question'); - expect(response.status).toBe(200); - expect(response.body.length).toBe(3); - }); - - it('should return only active questions', async () => { - const response = await request(app).get('/api/question/active'); - expect(response.status).toBe(200); - expect(response.body.length).toBe(2); - }); -}) - -describe('Get Questions: GET /api/question/:id', () => { - it('should not get a question with invalid question id', async () => { - const response = await request(app).get('/api/question/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid question ID"); - }) - - it('should not get a question with non existing question id', async () => { - const response = await request(app).get(`/api/question/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Question not found"); - }) - - it('should get a question', async () => { - const question = await Question.create(questionFixture()); - const response = await request(app).get(`/api/question/${question._id}`); - expect(response.status).toBe(200); - expect(response.body.question_no).toBe(question.question_no); - expect(response.body.question_title).toBe(question.question_title); - expect(response.body.question_desc).toBe(question.question_desc); - expect(new Date(response.body.question_date)).toEqual(new Date(question.question_date)); - expect(new Date(response.body.expiry)).toEqual(new Date(question.expiry)); - expect(response.body.points).toBe(question.points); - expect(response.body.answer).toBe(question.answer); - }) -}); - -describe('Create Questions: POST /api/question', () => { - it('should not create a question with missing fields', async () => { - const response = await request(app) - .post('/api/question') - .send(questionFixture({ question_no: null })); - expect(response.status).toBe(400); - }); - - it('should create a question', async () => { - const question = questionFixture(); - const response = await request(app) - .post('/api/question') - .send(question); - expect(response.status).toBe(201); - expect(response.body.question_no).toBe(question.question_no); - expect(response.body.question_title).toBe(question.question_title); - expect(response.body.question_desc).toBe(question.question_desc); - expect(response.body.question_date).toBe(question.question_date); - expect(response.body.expiry).toBe(question.expiry); - expect(response.body.points).toBe(question.points); - expect(response.body.answer).toBe(question.answer); - expect(response.body.active).toBe(question.active); - }); -}) - -describe('Update Questions: PUT /api/question/:id', () => { - it('should not update a question with invalid question id', async () => { - const response = await request(app) - .put('/api/question/123456789012') - .send({ question_no: "updated question_no", question_title: "updated question_title", question_desc: "updated question_desc", question_date: "2024-05-01T00:00:00.000Z", expiry: "2024-06-01T00:00:00.000Z", points: 11, answer: "updated answer" }); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid question ID"); - }); - - it('should not update a question with non existing question id', async () => { - const response = await request(app) - .put(`/api/question/65a8ba0b8c8139544b9955ac`) - .send({ question_no: "updated question_no", question_title: "updated question_title", question_desc: "updated question_desc", question_date: "2024-05-01T00:00:00.000Z", expiry: "2024-06-01T00:00:00.000Z", points: 11, answer: "updated answer" }); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Question not found"); - }) - - it('should update a question', async () => { - const question = await Question.create(questionFixture()); - const response = await request(app) - .put(`/api/question/${question._id}`) - .send({ question_no: "updated question_no", question_title: "updated question_title", question_desc: "updated question_desc", question_date: "2024-05-01T00:00:00.000Z", expiry: "2024-06-01T00:00:00.000Z", points: 11, answer: "updated answer" }); - expect(response.status).toBe(200); - expect(response.body.question_no).toBe("updated question_no"); - expect(response.body.question_title).toBe("updated question_title"); - expect(response.body.question_desc).toBe("updated question_desc"); - expect(response.body.question_date).toBe("2024-05-01T00:00:00.000Z"); - expect(response.body.expiry).toBe("2024-06-01T00:00:00.000Z"); - expect(response.body.points).toBe(11); - expect(response.body.answer).toBe("updated answer"); - }) -}) - -describe('Delete Questions: DELETE /api/question/:id', () => { - - it('should not delete a question with invalid question id', async () => { - const response = await request(app) - .delete('/api/question/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid question ID"); - }); - - it('should not delete a question with non existing question id', async () => { - const response = await request(app) - .delete(`/api/question/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Question not found"); - }); - - it('should delete a question', async () => { - const question = await Question.create(questionFixture()); - const response = await request(app) - .delete(`/api/question/${question._id}`); - expect(response.status).toBe(200); - expect(response.body.message).toBe("Question deleted"); - }) -}) diff --git a/apps/challenges/src/test/rankingCalculation.test.ts b/apps/challenges/src/test/rankingCalculation.test.ts index 05449aaf..2063b954 100644 --- a/apps/challenges/src/test/rankingCalculation.test.ts +++ b/apps/challenges/src/test/rankingCalculation.test.ts @@ -1,207 +1,214 @@ import { SeasonModel } from "../model/season"; -import mongoose from 'mongoose'; +import mongoose from "mongoose"; import SeasonService from "../service/seasonService"; -import { clearRankingsMap, rankingCalculation, rankingsMap } from "../tasks/rankingCalculation"; +import { + clearRankingsMap, + rankingCalculation, + rankingsMap, +} from "../tasks/rankingCalculation"; -describe('rankingCalculation', () => { - const mockGetActiveSeasons = jest.spyOn(SeasonService, 'getActiveSeasons'); - const mockCalculateSeasonRankings = jest.spyOn(SeasonService, 'calculateSeasonRankings'); +describe("rankingCalculation", () => { + const mockGetActiveSeasons = jest.spyOn(SeasonService, "getActiveSeasons"); + const mockCalculateSeasonRankings = jest.spyOn( + SeasonService, + "calculateSeasonRankings" + ); - beforeEach(() => { - jest.resetAllMocks(); - clearRankingsMap(); - }); + beforeEach(() => { + jest.resetAllMocks(); + clearRankingsMap(); + }); - it('should return if getActiveSeasons throw error', async () => { - mockGetActiveSeasons.mockRejectedValue(new Error('Error')); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).not.toHaveBeenCalled(); - expect(rankingsMap).toEqual({}); - }); + it("should return if getActiveSeasons throw error", async () => { + mockGetActiveSeasons.mockRejectedValue(new Error("Error")); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).not.toHaveBeenCalled(); + expect(rankingsMap).toEqual({}); + }); - it('should return if calculateSeasonRankings throw error', async () => { - const seasonID = new mongoose.Types.ObjectId(); - mockGetActiveSeasons.mockResolvedValue([( - { - _id: seasonID, - title: 'Season 1', - } as SeasonModel - )]); - mockCalculateSeasonRankings.mockRejectedValue(new Error('Error')); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).toHaveBeenCalled(); - expect(rankingsMap).toEqual({}); - expect(rankingsMap[seasonID.toString()]).toBeUndefined(); - }); + it("should return if calculateSeasonRankings throw error", async () => { + const seasonID = new mongoose.Types.ObjectId(); + mockGetActiveSeasons.mockResolvedValue([ + { + _id: seasonID, + title: "Season 1", + } as SeasonModel, + ]); + mockCalculateSeasonRankings.mockRejectedValue(new Error("Error")); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).toHaveBeenCalled(); + expect(rankingsMap).toEqual({}); + expect(rankingsMap[seasonID.toString()]).toBeUndefined(); + }); - it('should return if activeSeasons is empty', async () => { - mockGetActiveSeasons.mockResolvedValue([]); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).not.toHaveBeenCalled(); - }); + it("should return if activeSeasons is empty", async () => { + mockGetActiveSeasons.mockResolvedValue([]); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).not.toHaveBeenCalled(); + }); - it('should return if activeSeasons is null', async () => { - mockGetActiveSeasons.mockResolvedValue(null); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).not.toHaveBeenCalled(); - }); + it("should return if activeSeasons is null", async () => { + mockGetActiveSeasons.mockResolvedValue(null); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).not.toHaveBeenCalled(); + }); - it('should return if rankings is null', async () => { - const seasonID = new mongoose.Types.ObjectId(); - mockGetActiveSeasons.mockResolvedValue([( - { - _id: seasonID, - title: 'Season 1', - } as SeasonModel - )]); - mockCalculateSeasonRankings.mockResolvedValue([]); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).toHaveBeenCalled(); - expect(rankingsMap).toEqual({}); - expect(rankingsMap[seasonID.toString()]).toBeUndefined(); - }); + it("should return if rankings is null", async () => { + const seasonID = new mongoose.Types.ObjectId(); + mockGetActiveSeasons.mockResolvedValue([ + { + _id: seasonID, + title: "Season 1", + } as SeasonModel, + ]); + mockCalculateSeasonRankings.mockResolvedValue([]); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).toHaveBeenCalled(); + expect(rankingsMap).toEqual({}); + expect(rankingsMap[seasonID.toString()]).toBeUndefined(); + }); - it('should return if rankings is not null', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const userID = new mongoose.Types.ObjectId() - mockGetActiveSeasons.mockResolvedValue([( - { - _id: seasonID, - title: 'Season 1', - } as SeasonModel - )]); - mockCalculateSeasonRankings.mockResolvedValue([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - } - ]); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).toHaveBeenCalled(); - expect(rankingsMap[seasonID.toString()]).toEqual([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - } - ]); - }); + it("should return if rankings is not null", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const userID = new mongoose.Types.ObjectId().toString(); + mockGetActiveSeasons.mockResolvedValue([ + { + _id: seasonID, + title: "Season 1", + } as SeasonModel, + ]); + mockCalculateSeasonRankings.mockResolvedValue([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + ]); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).toHaveBeenCalled(); + expect(rankingsMap[seasonID.toString()]).toEqual([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + ]); + }); - it('should return if rankings is not null and there is multiple users', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const userID = new mongoose.Types.ObjectId(); - const userID2 = new mongoose.Types.ObjectId(); - mockGetActiveSeasons.mockResolvedValue([( - { - _id: seasonID, - title: 'Season 1', - } as SeasonModel - )]); - mockCalculateSeasonRankings.mockResolvedValue([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - }, - { - points: 20, - user: { - userID: userID2, - name: 'Hello2' - } - } - ]); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).toHaveBeenCalled(); - expect(rankingsMap[seasonID.toString()]).toEqual([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - }, - { - points: 20, - user: { - userID: userID2, - name: 'Hello2' - } - } - ]); - }); + it("should return if rankings is not null and there is multiple users", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const userID = new mongoose.Types.ObjectId().toString(); + const userID2 = new mongoose.Types.ObjectId().toString(); + mockGetActiveSeasons.mockResolvedValue([ + { + _id: seasonID, + title: "Season 1", + } as SeasonModel, + ]); + mockCalculateSeasonRankings.mockResolvedValue([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + { + points: 20, + user: { + userID: userID2, + name: "Hello2", + }, + }, + ]); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).toHaveBeenCalled(); + expect(rankingsMap[seasonID.toString()]).toEqual([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + { + points: 20, + user: { + userID: userID2, + name: "Hello2", + }, + }, + ]); + }); - it('should return if rankings is not null and there is multiple seasons', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const seasonID2 = new mongoose.Types.ObjectId(); - const userID = new mongoose.Types.ObjectId(); - const userID2 = new mongoose.Types.ObjectId(); - mockGetActiveSeasons.mockResolvedValue([ - { - _id: seasonID, - title: 'Season 1', - } as SeasonModel, - { - _id: seasonID2, - title: 'Season 2', - } as SeasonModel - ]); - mockCalculateSeasonRankings.mockResolvedValue([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - }, - { - points: 20, - user: { - userID: userID2, - name: 'Hello2' - } - } - ]); - await rankingCalculation(); - expect(mockCalculateSeasonRankings).toHaveBeenCalledTimes(2); - expect(rankingsMap[seasonID.toString()]).toEqual([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - }, - { - points: 20, - user: { - userID: userID2, - name: 'Hello2' - } - } - ]); - expect(rankingsMap[seasonID2.toString()]).toEqual([ - { - points: 10, - user: { - userID: userID, - name: 'Hello' - } - }, - { - points: 20, - user: { - userID: userID2, - name: 'Hello2' - } - } - ]); - }); -}); \ No newline at end of file + it("should return if rankings is not null and there is multiple seasons", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const seasonID2 = new mongoose.Types.ObjectId(); + const userID = new mongoose.Types.ObjectId().toString(); + const userID2 = new mongoose.Types.ObjectId().toString(); + mockGetActiveSeasons.mockResolvedValue([ + { + _id: seasonID, + title: "Season 1", + } as SeasonModel, + { + _id: seasonID2, + title: "Season 2", + } as SeasonModel, + ]); + mockCalculateSeasonRankings.mockResolvedValue([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + { + points: 20, + user: { + userID: userID2, + name: "Hello2", + }, + }, + ]); + await rankingCalculation(); + expect(mockCalculateSeasonRankings).toHaveBeenCalledTimes(2); + expect(rankingsMap[seasonID.toString()]).toEqual([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + { + points: 20, + user: { + userID: userID2, + name: "Hello2", + }, + }, + ]); + expect(rankingsMap[seasonID2.toString()]).toEqual([ + { + points: 10, + user: { + userID: userID, + name: "Hello", + }, + }, + { + points: 20, + user: { + userID: userID2, + name: "Hello2", + }, + }, + ]); + }); +}); diff --git a/apps/challenges/src/test/season.test.ts b/apps/challenges/src/test/season.test.ts index 0e439eaf..574f2de0 100644 --- a/apps/challenges/src/test/season.test.ts +++ b/apps/challenges/src/test/season.test.ts @@ -98,7 +98,7 @@ describe('getSeasonRankings', () => { createdAt: new Date(), updatedAt: new Date() }); - mockGetSeasonRankings.mockResolvedValueOnce(mockRankings); + mockGetSeasonRankings.mockResolvedValueOnce(mockRankings as never); const res = await request(app).get(`/api/seasons/${seasonID}/rankings`); expect(mockGetSeasonByID).toHaveBeenCalledTimes(1); expect(mockGetSeasonByID.mock.calls[0][0]).toBe(seasonID); @@ -146,7 +146,7 @@ describe('getSeasonRankings', () => { mockGetSeasonRankingsByPagination.mockResolvedValueOnce({ rankings: mockRankings, rankingsCount: mockRankingsCount - }); + } as never); const res = await request(app).get(`/api/seasons/${seasonID}/rankings?page=${page}&limit=${limit}`); expect(mockGetSeasonByID).toHaveBeenCalledTimes(1); expect(mockGetSeasonByID.mock.calls[0][0]).toBe(seasonID); diff --git a/apps/challenges/src/test/seasonRepo.test.ts b/apps/challenges/src/test/seasonRepo.test.ts index c9bdcaae..3c232544 100644 --- a/apps/challenges/src/test/seasonRepo.test.ts +++ b/apps/challenges/src/test/seasonRepo.test.ts @@ -10,216 +10,238 @@ import { questionFixture } from "../utils/fixtures/question"; import { connectTestDB } from "../config/db"; beforeAll(async () => { - await connectTestDB(); - await Season.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) -}) + await connectTestDB(); + await Season.deleteMany({}); + await Question.deleteMany({}); + await Submission.deleteMany({}); + await User.deleteMany({}); +}); afterAll(async () => { - await Season.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) - await mongoose.connection.close() -}) - -describe('calculateSeasonRankings', () => { - afterEach(async () => { - await Season.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) - }) - - it('should return all rankings', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const questionID = new mongoose.Types.ObjectId(); - const userID = new mongoose.Types.ObjectId(); - - const name = "Hello"; - await Submission.create(submissionFixture({ - seasonID: seasonID, - user: userID, - question: questionID, - })); - await User.create(userFixture({ - _id: userID, - name: name - })); - const rankings = await SeasonRepo.calculateSeasonRankings(seasonID); - expect(rankings).toHaveLength(1); - expect(rankings).toContainEqual({ - points: 10, - user: { - userID: userID, - name: name - } - }); - }) - - it('should return multiple users if there is multiple users', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const userID = new mongoose.Types.ObjectId(); - const userID2 = new mongoose.Types.ObjectId(); - const name = "Hello"; - const name2 = "Hello2"; - for (var i = 0; i < 6; i++) { - const questionID = new mongoose.Types.ObjectId(); - await Submission.create(submissionFixture({ - seasonID: seasonID, - user: userID, - question: questionID, - })); - - await Submission.create(submissionFixture({ - seasonID: seasonID, - user: userID2, - question: questionID, - })); - } - for (var i = 0; i < 2; i++) { - const questionID = new mongoose.Types.ObjectId(); - await Submission.create(submissionFixture({ - seasonID: seasonID, - user: userID2, - question: questionID, - })); - } - await User.create(userFixture({ - _id: userID, - name: name - })); - await User.create(userFixture({ - _id: userID2, - name: name2 - })); - const rankings = await SeasonRepo.calculateSeasonRankings(seasonID); - expect(rankings).toHaveLength(2); - expect(rankings).toContainEqual({ - points: 60, - user: { - userID: userID, - name: name - } - }); - expect(rankings).toContainEqual({ - points: 80, - user: { - userID: userID2, - name: name2 - } - }); - }) - - it('should return only the users that have submission (user with no submission should not be in leaderboard)', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const userID = new mongoose.Types.ObjectId(); - const userID2 = new mongoose.Types.ObjectId(); - const name = "Hello"; - const name2 = "Hello2"; - for (var i = 0; i < 8; i++){ - const questionID = new mongoose.Types.ObjectId(); - - await Submission.create(submissionFixture({ - seasonID: seasonID, - user: userID, - question: questionID, - })); - } - await User.create(userFixture({ - _id: userID, - name: name - })); - await User.create(userFixture({ - _id: userID2, - name: name2 - })); - const rankings = await SeasonRepo.calculateSeasonRankings(seasonID); - expect(rankings).toHaveLength(1); - expect(rankings).toContainEqual({ - points: 80, - user: { - userID: userID, - name: name - } - }); - }) -}) - -describe('getSeasonQuestions', () => { - afterEach(async () => { - await Season.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) - }) - - it('should return all questions', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const questionID = new mongoose.Types.ObjectId(); - const questionID2 = new mongoose.Types.ObjectId(); - - const fixture1 = questionFixture({ - _id: questionID, - seasonID: seasonID - }); - const fixture2 = questionFixture({ - _id: questionID2, - seasonID: seasonID - }) - await Question.create(fixture1); - await Question.create(fixture2); - const questions = await SeasonRepo.getSeasonQuestions(seasonID); - - for (var question of questions!) { - delete question['__v']; - delete question['createdAt']; - delete question['updatedAt']; - } - expect(questions).toHaveLength(2); - expect(questions).toContainEqual(fixture1); - expect(questions).toContainEqual(fixture2); - }) - - it('should return only the questions that belong to the season', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const questionID = new mongoose.Types.ObjectId(); - const questionID2 = new mongoose.Types.ObjectId(); - const questionID3 = new mongoose.Types.ObjectId(); - - const fixture1 = questionFixture({ - _id: questionID, - seasonID: seasonID - }); - const fixture2 = questionFixture({ - _id: questionID2, - seasonID: seasonID - }) - const fixture3 = questionFixture({ - _id: questionID3, - seasonID: new mongoose.Types.ObjectId() - }) - await Question.create(fixture1); - await Question.create(fixture2); - await Question.create(fixture3); - const questions = await SeasonRepo.getSeasonQuestions(seasonID); - - for (var question of questions!) { - delete question['__v']; - delete question['createdAt']; - delete question['updatedAt']; - } - expect(questions).toHaveLength(2); - expect(questions).toContainEqual(fixture1); - expect(questions).toContainEqual(fixture2); - expect(questions).not.toContainEqual(fixture3); + await Season.deleteMany({}); + await Question.deleteMany({}); + await Submission.deleteMany({}); + await User.deleteMany({}); + await mongoose.connection.close(); +}); + +describe("calculateSeasonRankings", () => { + afterEach(async () => { + await Season.deleteMany({}); + await Question.deleteMany({}); + await Submission.deleteMany({}); + await User.deleteMany({}); + }); + + it("should return all rankings", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const questionID = new mongoose.Types.ObjectId(); + const userID = new mongoose.Types.ObjectId(); + + const name = "Hello"; + await Submission.create( + submissionFixture({ + seasonID: seasonID, + user: userID, + question: questionID, + }) + ); + await User.create( + userFixture({ + _id: userID, + name: name, + }) + ); + const rankings = await SeasonRepo.calculateSeasonRankings(seasonID); + expect(rankings).toHaveLength(1); + expect(rankings).toContainEqual({ + points: 10, + user: { + userID: userID, + name: name, + }, }); - - it('should return empty array if there is no questions', async () => { - const seasonID = new mongoose.Types.ObjectId(); - const questions = await SeasonRepo.getSeasonQuestions(seasonID); - expect(questions).toHaveLength(0); - }) -}) \ No newline at end of file + }); + + it("should return multiple users if there is multiple users", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const userID = new mongoose.Types.ObjectId(); + const userID2 = new mongoose.Types.ObjectId(); + const name = "Hello"; + const name2 = "Hello2"; + for (let i = 0; i < 6; i++) { + const questionID = new mongoose.Types.ObjectId(); + await Submission.create( + submissionFixture({ + seasonID: seasonID, + user: userID, + question: questionID, + }) + ); + + await Submission.create( + submissionFixture({ + seasonID: seasonID, + user: userID2, + question: questionID, + }) + ); + } + for (let i = 0; i < 2; i++) { + const questionID = new mongoose.Types.ObjectId(); + await Submission.create( + submissionFixture({ + seasonID: seasonID, + user: userID2, + question: questionID, + }) + ); + } + await User.create( + userFixture({ + _id: userID, + name: name, + }) + ); + await User.create( + userFixture({ + _id: userID2, + name: name2, + }) + ); + const rankings = await SeasonRepo.calculateSeasonRankings(seasonID); + expect(rankings).toHaveLength(2); + expect(rankings).toContainEqual({ + points: 60, + user: { + userID: userID, + name: name, + }, + }); + expect(rankings).toContainEqual({ + points: 80, + user: { + userID: userID2, + name: name2, + }, + }); + }); + + it("should return only the users that have submission (user with no submission should not be in leaderboard)", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const userID = new mongoose.Types.ObjectId(); + const userID2 = new mongoose.Types.ObjectId(); + const name = "Hello"; + const name2 = "Hello2"; + for (let i = 0; i < 8; i++) { + const questionID = new mongoose.Types.ObjectId(); + + await Submission.create( + submissionFixture({ + seasonID: seasonID, + user: userID, + question: questionID, + }) + ); + } + await User.create( + userFixture({ + _id: userID, + name: name, + }) + ); + await User.create( + userFixture({ + _id: userID2, + name: name2, + }) + ); + const rankings = await SeasonRepo.calculateSeasonRankings(seasonID); + expect(rankings).toHaveLength(1); + expect(rankings).toContainEqual({ + points: 80, + user: { + userID: userID, + name: name, + }, + }); + }); +}); + +describe("getSeasonQuestions", () => { + afterEach(async () => { + await Season.deleteMany({}); + await Question.deleteMany({}); + await Submission.deleteMany({}); + await User.deleteMany({}); + }); + + it("should return all questions", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const questionID = new mongoose.Types.ObjectId(); + const questionID2 = new mongoose.Types.ObjectId(); + + const fixture1 = questionFixture({ + _id: questionID, + seasonID: seasonID, + }); + const fixture2 = questionFixture({ + _id: questionID2, + seasonID: seasonID, + }); + await Question.create(fixture1); + await Question.create(fixture2); + const questions = await SeasonRepo.getSeasonQuestions(seasonID); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for (const question of questions!) { + delete question["__v"]; + delete question["createdAt"]; + delete question["updatedAt"]; + } + expect(questions).toHaveLength(2); + expect(questions).toContainEqual(fixture1); + expect(questions).toContainEqual(fixture2); + }); + + it("should return only the questions that belong to the season", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const questionID = new mongoose.Types.ObjectId(); + const questionID2 = new mongoose.Types.ObjectId(); + const questionID3 = new mongoose.Types.ObjectId(); + + const fixture1 = questionFixture({ + _id: questionID, + seasonID: seasonID, + }); + const fixture2 = questionFixture({ + _id: questionID2, + seasonID: seasonID, + }); + const fixture3 = questionFixture({ + _id: questionID3, + seasonID: new mongoose.Types.ObjectId(), + }); + await Question.create(fixture1); + await Question.create(fixture2); + await Question.create(fixture3); + const questions = await SeasonRepo.getSeasonQuestions(seasonID); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + for (const question of questions!) { + delete question["__v"]; + delete question["createdAt"]; + delete question["updatedAt"]; + } + expect(questions).toHaveLength(2); + expect(questions).toContainEqual(fixture1); + expect(questions).toContainEqual(fixture2); + expect(questions).not.toContainEqual(fixture3); + }); + + it("should return empty array if there is no questions", async () => { + const seasonID = new mongoose.Types.ObjectId(); + const questions = await SeasonRepo.getSeasonQuestions(seasonID); + expect(questions).toHaveLength(0); + }); +}); diff --git a/apps/challenges/src/test/submission.test.ts b/apps/challenges/src/test/submission.test.ts deleted file mode 100644 index ea596bc1..00000000 --- a/apps/challenges/src/test/submission.test.ts +++ /dev/null @@ -1,316 +0,0 @@ -import request from 'supertest'; -import mongoose from 'mongoose'; -import app from '../index'; -const Question = require('../model/question'); -const Submission = require('../model/submission'); -const User = require('../model/user'); -const Leaderboard = require('../model/leaderboard'); -import { userFixture, questionFixture, answerFixture, leaderboardFixture } from '../utils/fixtures/fixtures'; -import { isValidObjectId } from '../utils/db'; - -beforeAll(async () => { - await Leaderboard.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) -}) - -afterAll(async () => { - await Leaderboard.deleteMany({}) - await Question.deleteMany({}) - await Submission.deleteMany({}) - await User.deleteMany({}) - await mongoose.connection.close() -}) - -describe('List Submissions: GET /api/submission', () => { - it('should not get submissions with invalid submission id', async () => { - const response = await request(app).get('/api/submission/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid submission ID"); - }) - - it('should not get submissions with non existing submission id', async () => { - const response = await request(app).get(`/api/submission/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Submission not found"); - }) - - it('should return all submissions', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture()); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - await Submission.create(answerFixture({user: user._id, question: question._id, leaderboard: leaderboard._id})); - await Submission.create(answerFixture({user: user._id, question: question._id, leaderboard: leaderboard._id})); - const response = await request(app).get('/api/submission'); - expect(response.status).toBe(200); - expect(response.body.length).toBe(2); - }) -}) - -describe('Get Submissions: GET /api/submissions/:id', () => { - it('should not get a submission with invalid submission id', async () => { - const response = await request(app).get('/api/submission/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid submission ID"); - }) - - it('should not get a submission with non existing submission id', async () => { - const response = await request(app).get(`/api/submission/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Submission not found"); - }) - - it('should return a submission', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture()); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const submission = await Submission.create(answerFixture({user: user._id, question: question._id, leaderboard: leaderboard._id})); - const response = await request(app).get(`/api/submission/${submission._id}`); - expect(response.status).toBe(200); - expect(response.body._id).toBe(submission._id.toString()); - expect(response.body.user).toBe(submission.user.toString()); - }) -}) - -describe('Set Submission: POST /api/submission/:id', () => { - it('should not submit a question with invalid question id', async () => { - const response = await request(app) - .post('/api/submission') - .send(answerFixture({ question: "123456789012" })); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid question ID"); - }); - - it('should not submit a question with non existing question id', async () => { - const response = await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: "65a8ba0b8c8139544b9955ac"})); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Question not found"); - }) - - it('should not submit a question with inactive question', async () => { - const question = await Question.create(questionFixture({ active: false })); - const response = await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id })); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Question is not active"); - }) - - it('should not submit a question with expired question', async () => { - const question = await Question.create(questionFixture({ expiry: "2021-06-01T00:00:00.000Z" })); - const response = await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id})); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Question has expired"); - }) - - it('should submit an incorrect answer', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture({ answer: "answer", points: 12})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "incorrect answer", user: user._id, leaderboard: leaderboard._id})); - expect(response.status).toBe(201); - expect(response.body.message).toBe("Answer submitted"); - - // Submission should be created - const submission = await Submission.findOne({ question: question._id, user: user._id }).populate('user'); - expect(submission.answer).toBe("incorrect answer"); - expect(submission.correct).toBe(false); - expect(submission.points_awarded).toBe(0); - expect(submission.user.name).toBe(user.name); - - // Question should have the submission in submissions array - const updatedQuestion = await Question.findOne({ _id: question._id }); - expect(updatedQuestion.submissions.length).toBe(1); - expect(updatedQuestion.submissions[0]).toEqual(submission._id); - - // Question should have the submission count incremented - expect(updatedQuestion.submissions_count).toBe(1); - expect(updatedQuestion.correct_submissions_count).toBe(0); - - // Leaderboard should have the user in rankings array - const updatedLeaderboard = await Leaderboard.findOne({ _id: leaderboard._id }); - expect(updatedLeaderboard.rankings.length).toBe(1); - expect(updatedLeaderboard.rankings[0].user).toEqual(user._id); - expect(updatedLeaderboard.rankings[0].points).toEqual(0); - }) - - it('should submit an answer', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture({ answer: "answer", points: 11})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - expect(response.status).toBe(201); - expect(response.body.message).toBe("Answer submitted"); - - // Submission should be created - const submission = await Submission.findOne({ question: question._id }).populate('user'); - expect(submission.answer).toBe("answer"); - expect(submission.correct).toBe(true); - expect(submission.points_awarded).toBe(11); - expect(submission.user.name).toBe(user.name); - - // Question should have the submission in submissions array - const updatedQuestion = await Question.findOne({ _id: question._id }); - expect(updatedQuestion.submissions.length).toBe(1); - expect(updatedQuestion.submissions[0]).toEqual(submission._id); - - // Question should have the submission count incremented - expect(updatedQuestion.submissions_count).toBe(1); - expect(updatedQuestion.correct_submissions_count).toBe(1); - }) - - it('should submit an correct answer and update leaderboard', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture({ answer: "answer", points: 11})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const response = await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - expect(response.status).toBe(201); - expect(response.body.message).toBe("Answer submitted"); - - // Leaderboard should have the user in rankings array - const updatedLeaderboard = await Leaderboard.findOne({ _id: leaderboard._id }); - expect(updatedLeaderboard.rankings.length).toBe(1); - expect(updatedLeaderboard.rankings[0].user).toEqual(user._id); - expect(updatedLeaderboard.rankings[0].points).toEqual(11); - }) - - it('should submit an answer and ranking points should not stack even with multiple correct submissions', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture({ answer: "answer", points: 11})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - - const updatedLeaderboard = await Leaderboard.findOne({ _id: leaderboard._id }); - expect(updatedLeaderboard.rankings.length).toBe(1); - expect(updatedLeaderboard.rankings[0].user).toEqual(user._id); - expect(updatedLeaderboard.rankings[0].points).toEqual(11); - }) - - it('should submit an answer and ranking points should stack with multiple correct submissions for different questions', async () => { - const user = await User.create(userFixture()); - const question1 = await Question.create(questionFixture({ answer: "answer", points: 34})); - const question2 = await Question.create(questionFixture({ answer: "answer", points: 35})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question1._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question2._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - - const updatedLeaderboard = await Leaderboard.findOne({ _id: leaderboard._id }); - expect(updatedLeaderboard.rankings.length).toBe(1); - expect(updatedLeaderboard.rankings[0].user).toEqual(user._id); - expect(updatedLeaderboard.rankings[0].points).toEqual(69); - }) - - it('should submit an answer and get ranking points if the previous submission was incorrect', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture({ answer: "answer", points: 11})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "incorrect answer", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "incorrect answer2", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - - const updatedLeaderboard = await Leaderboard.findOne({ _id: leaderboard._id }); - expect(updatedLeaderboard.rankings.length).toBe(1); - expect(updatedLeaderboard.rankings[0].user).toEqual(user._id); - expect(updatedLeaderboard.rankings[0].points).toEqual(11); - }) - - it('should submit an answer and get ranking points if the previous submission was incorrect and the new submission is correct', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture({ answer: "answer", points: 11})); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "incorrect answer", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "incorrect answer2", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - await request(app) - .post(`/api/submission`) - .send(answerFixture({ question: question._id, answer: "answer", user: user._id, leaderboard: leaderboard._id })); - - const updatedLeaderboard = await Leaderboard.findOne({ _id: leaderboard._id }); - expect(updatedLeaderboard.rankings.length).toBe(1); - expect(updatedLeaderboard.rankings[0].user).toEqual(user._id); - expect(updatedLeaderboard.rankings[0].points).toEqual(11); - }) -}) - - -describe('Update Submissions: PUT /api/submissions/:id', () => { - it('should not update a submission with invalid submission id', async () => { - const response = await request(app).put('/api/submission/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid submission ID"); - }) - - it('should not update a submission with non existing submission id', async () => { - const response = await request(app).put(`/api/submission/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Submission not found"); - }) - - it('should update a submission', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture()); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const submission = await Submission.create(answerFixture({user: user._id, question: question._id, leaderboard: leaderboard._id})); - const response = await request(app).put(`/api/submission/${submission._id}`).send({answer: "new answer"}); - expect(response.status).toBe(200); - expect(response.body._id).toBe(submission._id.toString()); - expect(response.body.answer).toBe("new answer"); - }) -}) - -describe('Delete Submissions: DELETE /api/submissions/:id', () => { - it('should not delete a submission with invalid submission id', async () => { - const response = await request(app).delete('/api/submission/123456789012'); - expect(response.status).toBe(400); - expect(response.body.message).toBe("Invalid submission ID"); - }) - - it('should not delete a submission with non existing submission id', async () => { - const response = await request(app).delete(`/api/submission/65a8ba0b8c8139544b9955ac`); - expect(response.status).toBe(404); - expect(response.body.message).toBe("Submission not found"); - }) - - it('should delete a submission', async () => { - const user = await User.create(userFixture()); - const question = await Question.create(questionFixture()); - const leaderboard = await Leaderboard.create(leaderboardFixture()); - const submission = await Submission.create(answerFixture({user: user._id, question: question._id, leaderboard: leaderboard._id})); - const response = await request(app).delete(`/api/submission/${submission._id}`); - expect(response.status).toBe(200); - expect(response.body.message).toBe("Submission deleted"); - expect(await Submission.findById(submission._id)).toBe(null); - }) -}) \ No newline at end of file diff --git a/apps/challenges/src/types/types.ts b/apps/challenges/src/types/types.ts index 20563b1b..451499a0 100644 --- a/apps/challenges/src/types/types.ts +++ b/apps/challenges/src/types/types.ts @@ -1,18 +1,22 @@ export interface GeneralResp { - status: number; - message: string; - data?: any; + status: number; + message: string; + data?: any; } export class StatusCodeError extends Error { - status: number; - constructor(status: number, message: string) { - super(message); - this.status = status; - } + status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } } export interface OauthcallbackResp { - accessToken: string; - refreshToken: string; + accessToken: string; + refreshToken: string; +} + +export interface GetQuestionsFilter { + isActive?: boolean; } \ No newline at end of file diff --git a/apps/challenges/src/utils/db.ts b/apps/challenges/src/utils/db.ts index bc711ec6..efa44498 100644 --- a/apps/challenges/src/utils/db.ts +++ b/apps/challenges/src/utils/db.ts @@ -1,9 +1,8 @@ -import mongoose from 'mongoose'; -import { z } from 'zod'; +import mongoose from "mongoose"; // Helper function to validate ObjectId function isValidObjectId(id: string): boolean { - return mongoose.Types.ObjectId.isValid(id); + return mongoose.Types.ObjectId.isValid(id); } -export { isValidObjectId }; \ No newline at end of file +export { isValidObjectId }; diff --git a/apps/challenges/src/utils/fixtures/fixtures.ts b/apps/challenges/src/utils/fixtures/fixtures.ts index 3e7efaae..e01417b4 100644 --- a/apps/challenges/src/utils/fixtures/fixtures.ts +++ b/apps/challenges/src/utils/fixtures/fixtures.ts @@ -1,47 +1,47 @@ function userFixture(overrides = {}) { - var defaultValues = { - name: (Math.random() + 1).toString(36).substring(2), - email: (Math.random() + 1).toString(36).substring(2), - active: true, - }; + const defaultValues = { + name: (Math.random() + 1).toString(36).substring(2), + email: (Math.random() + 1).toString(36).substring(2), + active: true, + }; - return { ...defaultValues, ...overrides }; + return { ...defaultValues, ...overrides }; } function questionFixture(overrides = {}) { - var defaultValues = { - question_no: (Math.random() + 1).toString(36).substring(2), - question_title: (Math.random() + 1).toString(36).substring(2), - question_desc: (Math.random() + 1).toString(36).substring(2), - question_date: "2022-05-01T00:00:00.000Z", - expiry: "2040-06-01T00:00:00.000Z", - points: 10, - answer: (Math.random() + 1).toString(36).substring(2), - submissions: [], - active: true, - }; - - return { ...defaultValues, ...overrides }; + const defaultValues = { + question_no: (Math.random() + 1).toString(36).substring(2), + question_title: (Math.random() + 1).toString(36).substring(2), + question_desc: (Math.random() + 1).toString(36).substring(2), + question_date: "2022-05-01T00:00:00.000Z", + expiry: "2040-06-01T00:00:00.000Z", + points: 10, + answer: (Math.random() + 1).toString(36).substring(2), + submissions: [], + active: true, + }; + + return { ...defaultValues, ...overrides }; } function answerFixture(overrides = {}) { - var defaultValues = { - name: (Math.random() + 1).toString(36).substring(2), - answer: (Math.random() + 1).toString(36).substring(2), - }; + const defaultValues = { + name: (Math.random() + 1).toString(36).substring(2), + answer: (Math.random() + 1).toString(36).substring(2), + }; - return { ...defaultValues, ...overrides }; -}; + return { ...defaultValues, ...overrides }; +} function leaderboardFixture(overrides = {}) { - var defaultValues = { - title: (Math.random() + 1).toString(36).substring(2), - start_date: "2023-05-01T00:00:00.000Z", - end_date: "2040-06-01T00:00:00.000Z", - rankings: [], - }; - - return { ...defaultValues, ...overrides }; + const defaultValues = { + title: (Math.random() + 1).toString(36).substring(2), + start_date: "2023-05-01T00:00:00.000Z", + end_date: "2040-06-01T00:00:00.000Z", + rankings: [], + }; + + return { ...defaultValues, ...overrides }; } -export { userFixture, questionFixture, answerFixture, leaderboardFixture }; \ No newline at end of file +export { userFixture, questionFixture, answerFixture, leaderboardFixture }; diff --git a/apps/challenges/src/utils/fixtures/question.ts b/apps/challenges/src/utils/fixtures/question.ts index 07edbe52..a7f8d0ef 100644 --- a/apps/challenges/src/utils/fixtures/question.ts +++ b/apps/challenges/src/utils/fixtures/question.ts @@ -1,20 +1,20 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; export const questionFixture = (overrides = {}) => { - var defaultValues = { - question_no: Math.floor(Math.random() * 100).toString(), - question_title: (Math.random() + 1).toString(36).substring(2), - question_desc: (Math.random() + 1).toString(36).substring(2), - question_date: new Date(), - seasonID: new mongoose.Types.ObjectId(), - expiry: new Date(), - points: 10, - answer: "hello", - submissions: [], - submissions_count: 0, - correct_submissions_count: 0, - active: true - }; + const defaultValues = { + question_no: Math.floor(Math.random() * 100).toString(), + question_title: (Math.random() + 1).toString(36).substring(2), + question_desc: (Math.random() + 1).toString(36).substring(2), + question_date: new Date(), + seasonID: new mongoose.Types.ObjectId(), + expiry: new Date(), + points: 10, + answer: "hello", + submissions: [], + submissions_count: 0, + correct_submissions_count: 0, + active: true, + }; - return { ...defaultValues, ...overrides }; -} \ No newline at end of file + return { ...defaultValues, ...overrides }; +}; diff --git a/apps/challenges/src/utils/fixtures/season.ts b/apps/challenges/src/utils/fixtures/season.ts index deacda45..eb253077 100644 --- a/apps/challenges/src/utils/fixtures/season.ts +++ b/apps/challenges/src/utils/fixtures/season.ts @@ -1,12 +1,12 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; export const seasonFixtures = (overrides = {}) => { - var defaultValues = { - _id: new mongoose.Types.ObjectId(), - title: (Math.random() + 1).toString(36).substring(2), - startDate: "2023-05-01T00:00:00.000Z", - endDate: "2040-06-01T00:00:00.000Z", - }; + const defaultValues = { + _id: new mongoose.Types.ObjectId(), + title: (Math.random() + 1).toString(36).substring(2), + startDate: "2023-05-01T00:00:00.000Z", + endDate: "2040-06-01T00:00:00.000Z", + }; - return { ...defaultValues, ...overrides }; -} \ No newline at end of file + return { ...defaultValues, ...overrides }; +}; diff --git a/apps/challenges/src/utils/fixtures/submission.ts b/apps/challenges/src/utils/fixtures/submission.ts index 2b795247..9fcb28cf 100644 --- a/apps/challenges/src/utils/fixtures/submission.ts +++ b/apps/challenges/src/utils/fixtures/submission.ts @@ -1,15 +1,15 @@ -import mongoose from 'mongoose'; +import mongoose from "mongoose"; export const submissionFixture = (overrides = {}) => { - var defaultValues = { - user: new mongoose.Types.ObjectId(), - seasonID: new mongoose.Types.ObjectId(), - answer: (Math.random() + 1).toString(36).substring(2), - question: new mongoose.Types.ObjectId(), - correct: true, - points_awarded: 10, - attempt: 1, - }; + const defaultValues = { + user: new mongoose.Types.ObjectId(), + seasonID: new mongoose.Types.ObjectId(), + answer: (Math.random() + 1).toString(36).substring(2), + question: new mongoose.Types.ObjectId(), + correct: true, + points_awarded: 10, + attempt: 1, + }; - return { ...defaultValues, ...overrides }; + return { ...defaultValues, ...overrides }; }; diff --git a/apps/challenges/src/utils/pagination.ts b/apps/challenges/src/utils/pagination.ts index e81e7d7f..8c83eec4 100644 --- a/apps/challenges/src/utils/pagination.ts +++ b/apps/challenges/src/utils/pagination.ts @@ -1,51 +1,60 @@ -import { z } from 'zod'; -import { isNonNegativeInteger, isPositiveInteger } from './validator'; - -export const generatePaginationMetaData = (baseUrl: string, pageIndex: number, limit: number, maxPageIndex: number, itemCount: number) => { - isPositiveInteger.parse(limit); - isNonNegativeInteger.parse(pageIndex); - isNonNegativeInteger.parse(maxPageIndex); - isNonNegativeInteger.parse(itemCount); - - let self = `${baseUrl}?page=${pageIndex}&limit=${limit}` - let first = `${baseUrl}?page=0&limit=${limit}` - let last = `${baseUrl}?page=${maxPageIndex}&limit=${limit}` - let previous, next; - - if (pageIndex <= 0 || pageIndex > maxPageIndex){ - previous = null; - }else { - previous = `${baseUrl}?page=${pageIndex -1}&limit=${limit}` - } - - if (pageIndex >= maxPageIndex) { - next = null; - }else{ - next = `${baseUrl}?page=${pageIndex +1}&limit=${limit}` - } - - let links = { - self: self, - first: first, - previous: previous, - next: next, - last: last - } - - let metaData = { - itemCount: itemCount, - limit: limit, - pageCount: Math.ceil(itemCount / limit), - page: pageIndex, - links: links - } - - return metaData; -} - -// do note that while page number being negative do work, -// in Session getSeasonRankingsByPagination, +import { isNonNegativeInteger, isPositiveInteger } from "./validator"; + +export const generatePaginationMetaData = ( + baseUrl: string, + pageIndex: number, + limit: number, + maxPageIndex: number, + itemCount: number +) => { + isPositiveInteger.parse(limit); + isNonNegativeInteger.parse(pageIndex); + isNonNegativeInteger.parse(maxPageIndex); + isNonNegativeInteger.parse(itemCount); + + const self = `${baseUrl}?page=${pageIndex}&limit=${limit}`; + const first = `${baseUrl}?page=0&limit=${limit}`; + const last = `${baseUrl}?page=${maxPageIndex}&limit=${limit}`; + let previous: string | null, next: string | null; + + if (pageIndex <= 0 || pageIndex > maxPageIndex) { + previous = null; + } else { + previous = `${baseUrl}?page=${pageIndex - 1}&limit=${limit}`; + } + + if (pageIndex >= maxPageIndex) { + next = null; + } else { + next = `${baseUrl}?page=${pageIndex + 1}&limit=${limit}`; + } + + const links = { + self: self, + first: first, + previous: previous, + next: next, + last: last, + }; + + const metaData = { + itemCount: itemCount, + limit: limit, + pageCount: Math.ceil(itemCount / limit), + page: pageIndex, + links: links, + }; + + return metaData; +}; + +// do note that while page number being negative do work, +// in Session getSeasonRankingsByPagination, // we do not accept negative page number -export const paginateArray = (array: any[], pageSize: number, pageIndex: number) => { - return array.slice((pageIndex) * pageSize, (pageIndex + 1) * pageSize); -} +export const paginateArray = ( + array: unknown[], + pageSize: number, + pageIndex: number +) => { + return array.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); +}; diff --git a/apps/challenges/src/utils/supabase.ts b/apps/challenges/src/utils/supabase.ts index 14b81452..b13a1fb1 100644 --- a/apps/challenges/src/utils/supabase.ts +++ b/apps/challenges/src/utils/supabase.ts @@ -1,28 +1,27 @@ -import { SupabaseClient, createClient } from '@supabase/supabase-js' +import { SupabaseClient, createClient } from "@supabase/supabase-js"; import dotenv from "dotenv"; dotenv.config({ path: "../.env" }); - let supabase: SupabaseClient; const initClient = () => { - supabase = createClient( - process.env.SUPABASE_URL || "", - process.env.SUPABASE_ANON_KEY || "", - { - auth: { - autoRefreshToken: false, - persistSession: false, - detectSessionInUrl: false, - flowType: "pkce" - }, - } - ) - console.log("Supabase client initialized"); -} + supabase = createClient( + process.env.SUPABASE_URL || "", + process.env.SUPABASE_ANON_KEY || "", + { + auth: { + autoRefreshToken: false, + persistSession: false, + detectSessionInUrl: false, + flowType: "pkce", + }, + } + ); + console.log("Supabase client initialized"); +}; const SupabaseService = { - initClient, -} + initClient, +}; -export { SupabaseService, supabase as default } \ No newline at end of file +export { SupabaseService, supabase as default }; diff --git a/apps/challenges/src/utils/validator.ts b/apps/challenges/src/utils/validator.ts index 40139aa6..de3b099b 100644 --- a/apps/challenges/src/utils/validator.ts +++ b/apps/challenges/src/utils/validator.ts @@ -1,51 +1,55 @@ -import mongoose from 'mongoose'; -import { z } from 'zod'; +import mongoose from "mongoose"; +import { z } from "zod"; export const getEmailPrefix = (email: string) => { - return email.replace(/@.*$/, ""); -} + return email.replace(/@.*$/, ""); +}; export const isValidDate = (d: Date) => { - return d instanceof Date && !isNaN(d.valueOf()) -} + return d instanceof Date && !isNaN(d.valueOf()); +}; // Helper zod function to validate ObjectId -export const zodIsValidObjectId = z.string().refine( - (val) => mongoose.Types.ObjectId.isValid(val), - { message: 'Invalid ObjectId' } -); +export const zodIsValidObjectId = z + .string() + .refine((val) => mongoose.Types.ObjectId.isValid(val), { + message: "Invalid ObjectId", + }); // Helper zod function to validate ObjectId -export const zodGetValidObjectId = z.string().refine( - (val) => mongoose.Types.ObjectId.isValid(val), - { message: 'Invalid ObjectId' } -).transform((val) => new mongoose.Types.ObjectId(val)); - -export const zodIsValidRFC3339 = z.string().refine( - (val) => new Date(val).toISOString() === val, - { message: 'Invalid RFC3339 date' } -); +export const zodGetValidObjectId = z + .string() + .refine((val) => mongoose.Types.ObjectId.isValid(val), { + message: "Invalid ObjectId", + }) + .transform((val) => new mongoose.Types.ObjectId(val)); + +export const zodIsValidRFC3339 = z + .string() + .refine((val) => new Date(val).toISOString() === val, { + message: "Invalid RFC3339 date", + }); export const isValidCreateSubmissionRequest = z.object({ - user: zodIsValidObjectId, - question: zodIsValidObjectId, - answer: z.string(), + user: zodIsValidObjectId, + question: zodIsValidObjectId, + answer: z.string(), }); export const isValidEmail = z.string().min(1).email(); -export const isValidCreateQuestionRequest = z.object({ - question_no: z.string(), - question_title: z.string(), - question_desc: z.string(), - question_date: zodIsValidRFC3339, - season_id: zodIsValidObjectId, - expiry: zodIsValidRFC3339, - points: z.number().int(), - validation_function: z.string(), - generate_input_function: z.string(), +export const isValidQuestionRequest = z.object({ + question_no: z.string(), + question_title: z.string(), + question_desc: z.string(), + question_date: zodIsValidRFC3339, + season_id: zodIsValidObjectId, + expiry: zodIsValidRFC3339, + points: z.number().int(), + validation_function: z.string(), + generate_input_function: z.string(), }); export const isPositiveInteger = z.number().int().min(1); -export const isNonNegativeInteger = z.number().int().min(0); \ No newline at end of file +export const isNonNegativeInteger = z.number().int().min(0); diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 51c8de35..a516c922 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -94,6 +94,23 @@ services: - "traefik.http.routers.uptime-kuma.entrypoints=websecure" - "traefik.http.routers.uptime-kuma.tls.certresolver=myresolver" # - "traefik.http.services.uptime-kuma.loadbalancer.server.port=3001" + + challenges: + container_name: challenges + image: ghcr.io/ntuscse/website/challenges:latest + networks: + cms_network: + ipv4_address: 10.5.0.5 + env_file: + - challenges.env + depends_on: + - mongo + restart: always + labels: + - "traefik.enable=true" + - "traefik.http.routers.merch.rule=Host(`$TRAEFIK_HTTP_CHALLENGES_HOST`)" + - "traefik.http.routers.merch.entrypoints=websecure" + - "traefik.http.routers.merch.tls.certresolver=myresolver" networks: default: diff --git a/turbo.json b/turbo.json index a0a937ae..e1cff265 100644 --- a/turbo.json +++ b/turbo.json @@ -22,7 +22,13 @@ "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN", "NEXT_PUBLIC_FRONTEND_URL", - "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", + "SUPABASE_JWT_SECRET", + "DO_RANKING_CALCULATION", + "SUPABASE_URL", + "SUPABASE_ANON_KEY", + "CHALLENGES_JWT_SECRET", + "CHALLENGES_MONGO_DATABSE_NAME" ] }, "build": { @@ -46,7 +52,13 @@ "ORDER_HOLD_TABLE_NAME", "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN", - "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", + "SUPABASE_JWT_SECRET", + "DO_RANKING_CALCULATION", + "SUPABASE_URL", + "SUPABASE_ANON_KEY", + "CHALLENGES_JWT_SECRET", + "CHALLENGES_MONGO_DATABSE_NAME" ], "outputs": ["dist/**", "build/**", "out/**", ".next/**"] }, @@ -88,7 +100,13 @@ "STRIPE_SECRET_KEY", "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN", - "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", + "SUPABASE_JWT_SECRET", + "DO_RANKING_CALCULATION", + "SUPABASE_URL", + "SUPABASE_ANON_KEY", + "CHALLENGES_JWT_SECRET", + "CHALLENGES_MONGO_DATABSE_NAME" ] }, "start:ci": { @@ -119,7 +137,13 @@ "STRIPE_SECRET_KEY", "ORDER_EXPIRY_TIME", "NEXT_PUBLIC_MERCH_API_ORIGIN", - "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY" + "NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY", + "SUPABASE_JWT_SECRET", + "DO_RANKING_CALCULATION", + "SUPABASE_URL", + "SUPABASE_ANON_KEY", + "CHALLENGES_JWT_SECRET", + "CHALLENGES_MONGO_DATABSE_NAME" ] }, "lint": { diff --git a/yarn.lock b/yarn.lock index 0d2122f0..90a51626 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7105,6 +7105,13 @@ resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.0.tgz#f92f6b646533776a41bc62b650d2a03f1e282af8" integrity sha512-nElTXkS+nMfDNMkWfLmyeqHQfMGJ1JjrjAVMibV61Oc/rYdUv0cKRYCi1l4ivZ5SySB3vQLcLolxbKBkbNznZA== +"@supabase/auth-js@2.64.2": + version "2.64.2" + resolved "https://registry.yarnpkg.com/@supabase/auth-js/-/auth-js-2.64.2.tgz#fe6828ed2c9844bf2e71b27f88ddfb635f24d1c1" + integrity sha512-s+lkHEdGiczDrzXJ1YWt2y3bxRi+qIUnXcgkpLSrId7yjBeaXBFygNjTaoZLG02KNcYwbuZ9qkEIqmj2hF7svw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@supabase/functions-js@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.1.5.tgz#ed1b85f499dfda21d40fe39b86ab923117cb572b" @@ -7112,6 +7119,13 @@ dependencies: "@supabase/node-fetch" "^2.6.14" +"@supabase/functions-js@2.3.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.3.1.tgz#bddc12a97872f3978a733b66bddac53370721589" + integrity sha512-QyzNle/rVzlOi4BbVqxLSH828VdGY1RElqGFAj+XeVypj6+PVtMlD21G8SDnsPQDtlqqTtoGRgdMlQZih5hTuw== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@supabase/gotrue-js@2.62.2": version "2.62.2" resolved "https://registry.yarnpkg.com/@supabase/gotrue-js/-/gotrue-js-2.62.2.tgz#9f15a451559d71475c953aa0027e1248b0210196" @@ -7126,6 +7140,13 @@ dependencies: whatwg-url "^5.0.0" +"@supabase/postgrest-js@1.15.2": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.15.2.tgz#c0a725706e3d534570d014d7b713cea12553ab98" + integrity sha512-9/7pUmXExvGuEK1yZhVYXPZnLEkDTwxgMQHXLrN5BwPZZm4iUCL1YEyep/Z2lIZah8d8M433mVAUEGsihUj5KQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@supabase/postgrest-js@1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.9.2.tgz#39c839022ce73f4eb5da6fb7724fb6a6392150c7" @@ -7143,6 +7164,16 @@ "@types/ws" "^8.5.10" ws "^8.14.2" +"@supabase/realtime-js@2.9.5": + version "2.9.5" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.9.5.tgz#22b7de952a7f37868ffc25d32d19f03f27bfcb40" + integrity sha512-TEHlGwNGGmKPdeMtca1lFTYCedrhTAv3nZVoSjrKQ+wkMmaERuCe57zkC5KSWFzLYkb5FVHW8Hrr+PX1DDwplQ== + dependencies: + "@supabase/node-fetch" "^2.6.14" + "@types/phoenix" "^1.5.4" + "@types/ws" "^8.5.10" + ws "^8.14.2" + "@supabase/storage-js@2.5.5": version "2.5.5" resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.5.5.tgz#2958e2a2cec8440e605bb53bd36649288c4dfa01" @@ -7162,6 +7193,18 @@ "@supabase/realtime-js" "2.9.3" "@supabase/storage-js" "2.5.5" +"@supabase/supabase-js@^2.39.8": + version "2.43.2" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.43.2.tgz#86ee9379d4441c2773b1a84919770ac704c5cfb4" + integrity sha512-F9CljeJBo5aPucNhrLoMnpEHi5yqNZ0vH0/CL4mGy+/Ggr7FUrYErVJisa1NptViqyhs1HGNzzwjOYG6626h8g== + dependencies: + "@supabase/auth-js" "2.64.2" + "@supabase/functions-js" "2.3.1" + "@supabase/node-fetch" "2.6.15" + "@supabase/postgrest-js" "1.15.2" + "@supabase/realtime-js" "2.9.5" + "@supabase/storage-js" "2.5.5" + "@swc/core-darwin-arm64@1.3.37": version "1.3.37" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.37.tgz#a92e075ae35f18a64aaf3823ea175f03564f8da1" @@ -7758,11 +7801,6 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== -"@types/luxon@~3.3.0": - version "3.3.8" - resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.3.8.tgz#84dbf2d020a9209a272058725e168f21d331a67e" - integrity sha512-jYvz8UMLDgy3a5SkGJne8H7VA7zPV2Lwohjx0V8V31+SqAjNmurWMkk9cQhfvlcnXWudBpK9xPM1n4rljOcHYQ== - "@types/mdx@^2.0.0": version "2.0.5" resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.5.tgz#9a85a8f70c7c4d9e695a21d5ae5c93645eda64b1" @@ -7795,6 +7833,11 @@ dependencies: "@types/node" "*" +"@types/node-cron@^3.0.11": + version "3.0.11" + resolved "https://registry.yarnpkg.com/@types/node-cron/-/node-cron-3.0.11.tgz#70b7131f65038ae63cfe841354c8aba363632344" + integrity sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg== + "@types/node-fetch@^2.5.7": version "2.6.4" resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.4.tgz#1bc3a26de814f6bf466b25aeb1473fa1afe6a660" @@ -10137,14 +10180,6 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== -cron@^3.1.6: - version "3.1.6" - resolved "https://registry.yarnpkg.com/cron/-/cron-3.1.6.tgz#e7e1798a468e017c8d31459ecd7c2d088f97346c" - integrity sha512-cvFiQCeVzsA+QPM6fhjBtlKGij7tLLISnTSvFxVdnFGLdz+ZdXN37kNe0i2gefmdD17XuZA6n2uPVwzl4FxW/w== - dependencies: - "@types/luxon" "~3.3.0" - luxon "~3.4.0" - cross-env@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -14485,11 +14520,6 @@ jwt-decode@^3.1.2: resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59" integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A== -jwt-decode@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-4.0.0.tgz#2270352425fd413785b2faf11f6e755c5151bd4b" - integrity sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA== - kareem@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.4.1.tgz#7d81ec518204a48c1cb16554af126806c3cd82b0" @@ -14787,11 +14817,6 @@ lru-queue@^0.1.0: dependencies: es5-ext "~0.10.2" -luxon@~3.4.0: - version "3.4.4" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.4.tgz#cf20dc27dc532ba41a169c43fdcc0063601577af" - integrity sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA== - lz-string@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" @@ -15317,6 +15342,13 @@ node-addon-api@^5.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.0.0.tgz#7d7e6f9ef89043befdb20c1989c905ebde18c501" integrity sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA== +node-cron@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/node-cron/-/node-cron-3.0.3.tgz#c4bc7173dd96d96c50bdb51122c64415458caff2" + integrity sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A== + dependencies: + uuid "8.3.2" + node-dir@^0.1.10, node-dir@^0.1.17: version "0.1.17" resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5" @@ -19651,7 +19683,7 @@ utils-merge@1.0.1, utils-merge@^1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -uuid@^8.3.2: +uuid@8.3.2, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==