Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add unit test cases and modify getSeasonRankings payload #147

Merged
merged 1 commit into from
Feb 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 28 additions & 84 deletions apps/challenges/src/controllers/season.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Request, Response } from "express";
import asyncHandler from "express-async-handler";
import { z } from "zod";

import { isValidObjectId, paramsSchema } from "../utils/db";
import { isValidObjectId } from "../utils/db";
import SeasonService from "../service/seasonService";
import isValidDate from "../utils/checkObjectProperties";
import { z } from "zod";
import mongoose from 'mongoose';
import { isNonNegativeInteger, isValidDate, zodIsValidObjectId } from "../utils/validator";
import { generatePaginationMetaData } from "../utils/pagination";

interface CreateSeasonRequest {
title: string;
Expand Down Expand Up @@ -105,6 +105,7 @@ const createSeason = asyncHandler(async (req: Request, res: Response) => {
}
});

/*
const getUserSeasonRanking = asyncHandler(async (req: Request, res: Response) => {
const { seasonID, userID } = req.params;

Expand Down Expand Up @@ -138,114 +139,59 @@ const getUserAllSeasonRankings = asyncHandler(async (req: Request, res: Response
res.status(500).json({ message: err.message });
}
});
*/

// @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 {
const seasonID = paramsSchema.parse(req.params.seasonID);
const querySchema = z.object({
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" }
);
const { page, limit } = querySchema.parse(req.query);
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){
if(!limit && !page){
const rankings = await SeasonService.getSeasonRankings(seasonID);
res.status(200).json({
seasonID: seasonID,
rankings: rankings,
});
return;
}
const { rankings, rankingsCount } = await SeasonService.getSeasonRankingsByPagination(seasonID, page!, limit!);
const metaData = {
page: page,
limit: limit,
pageCount: Math.ceil(rankingsCount / limit!) || 0,
itemCount: rankingsCount || 0,
links: getLinks(seasonID, page!, limit!, rankingsCount)
}
res.setHeader("access-control-expose-headers", "pagination");
res.setHeader("pagination", JSON.stringify(metaData));
res.status(200).json({
rankings: rankings,
_metaData: metaData
});
} catch (err) {
if(err instanceof z.ZodError){
res.status(400).json({ message: 'Invalid request' });
}else{
res.status(500).json({ message: 'Internal Server Error' });
}
}
});

const getLinks = (seasonID: string, page: number, limit: number, rankingsCount: number) => {
var links;
if(page < 0 || page > Math.ceil(rankingsCount / limit) - 1){
links = {
self: `/api/seasons/${seasonID}/rankings?page=${page}&limit=${limit}`,
first: `/api/seasons/${seasonID}/rankings?page=0&limit=${limit}`,
previous: null,
next: null,
last: `/api/seasons/${seasonID}/rankings?page=${Math.ceil(rankingsCount / limit) - 1}&limit=${limit}`
}
}else if(page == 0){
links = {
self: `/api/seasons/${seasonID}/rankings?page=${page}&limit=${limit}`,
first: `/api/seasons/${seasonID}/rankings?page=0&limit=${limit}`,
previous: null,
next: `/api/seasons/${seasonID}/rankings?page=${page + 1}&limit=${limit}`,
last: `/api/seasons/${seasonID}/rankings?page=${Math.ceil(rankingsCount / limit) - 1}&limit=${limit}`
}
}else if (page == Math.ceil(rankingsCount / limit) - 1){
links = {
self: `/api/seasons/${seasonID}/rankings?page=${page}&limit=${limit}`,
first: `/api/seasons/${seasonID}/rankings?page=0&limit=${limit}`,
previous: `/api/seasons/${seasonID}/rankings?page=${page - 1}&limit=${limit}`,
next: null,
last: `/api/seasons/${seasonID}/rankings?page=${Math.ceil(rankingsCount / limit) - 1}&limit=${limit}`
}
}else{
links = {
self: `/api/seasons/${seasonID}/rankings?page=${page}&limit=${limit}`,
first: `/api/seasons/${seasonID}/rankings?page=0&limit=${limit}`,
previous: `/api/seasons/${seasonID}/rankings?page=${page - 1}&limit=${limit}`,
next: `/api/seasons/${seasonID}/rankings?page=${page + 1}&limit=${limit}`,
last: `/api/seasons/${seasonID}/rankings?page=${Math.ceil(rankingsCount / limit) - 1}&limit=${limit}`
}
}

return links;
}

const updateSeasonRankings = asyncHandler(async (req: Request, res: Response) => {
const { seasonID, userID } = req.params;
const { points } = req.body;

if (!isValidObjectId(seasonID) || !isValidObjectId(userID)) {
res.status(400).json({ message: 'Invalid request' });
return;
}
if(points == null || typeof points !== "number"){
res.status(400).json({ message: 'Invalid request' });
return;
}
try {
const season = await SeasonService.updateSeasonRankings(seasonID, userID, points);
res.status(200).json(season);
} catch (error) {
const err = error as Error;
res.status(500).json({ message: err.message });
try{
const { rankings, rankingsCount } = await SeasonService.getSeasonRankingsByPagination(seasonID, page!, limit!);
isNonNegativeInteger.parse(rankingsCount);
const metaData = generatePaginationMetaData(`/api/seasons/${seasonID}/rankings`, page!, limit!, Math.ceil(rankingsCount / limit) - 1);
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' });
}
});

Expand All @@ -255,10 +201,8 @@ const SeasonController = {
getSeasonByID,
createSeason,
getSeasonRankings,
getUserSeasonRanking,
getUserAllSeasonRankings,
updateSeasonRankings,
getLinks
// getUserSeasonRanking,
// getUserAllSeasonRankings,
};

export { SeasonController as default };
41 changes: 5 additions & 36 deletions apps/challenges/src/model/rankingScore.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,9 @@
import mongoose, {Schema} from 'mongoose';

export interface UserRanking {
userID: string;
name: string;
points: number;
}

export interface RankingModel {
_id: mongoose.Types.ObjectId;
userID: mongoose.Types.ObjectId;
seasonID: mongoose.Types.ObjectId;
points: number;
createdAt: Date;
updatedAt: Date;
}

const seasonSchema: Schema<RankingModel> = new Schema({
userID: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User'
},
seasonID: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'Season'
},
points: {
type: Number,
required: [true, 'Please add a points value']
user: {
userID: string;
name: string;
}
}, {
timestamps: true
});


const Ranking = mongoose.model<RankingModel>('Ranking', seasonSchema);

export { Ranking as default }
points: number;
}
2 changes: 1 addition & 1 deletion apps/challenges/src/model/season.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import mongoose, {Schema} from 'mongoose';
import mongoose, { Schema } from 'mongoose';

export interface SeasonModel {
_id: mongoose.Types.ObjectId;
Expand Down
59 changes: 8 additions & 51 deletions apps/challenges/src/repo/seasonRepo.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Season, { SeasonModel } from "../model/season";
import mongoose from 'mongoose';
import Ranking, { RankingModel, UserRanking } from "../model/rankingScore";

import { UserRanking } from "../model/rankingScore";
import Submission from "../model/submission";
import { rankingsMap } from "../tasks/rankingCalculation";
import { array } from "zod";
import { paginateArray } from "../utils/pagination";

const getSeasonsByDate = async(
startDate: Date | null,
Expand Down Expand Up @@ -74,8 +75,10 @@ const calculateSeasonRankings = async(
{
$project: {
_id: 0,
userID: "$user._id",
name: "$user.name",
user: {
userID: "$user._id",
name: "$user.name",
},
points: 1
}
}
Expand All @@ -99,61 +102,15 @@ const getSeasonRankingsByPagination = async (
if(!rankings){
return { rankings: [] , rankingsCount: 0 };
}
return { rankings: paginate(rankings, limit, page), rankingsCount: rankings.length };
}

const paginate = (array: any[], page_size: number, page_number: number) => {
// human-readable page numbers usually start with 1, so we reduce 1 in the first argument
return array.slice((page_number - 1) * page_size, page_number * page_size);
}

const getUserSeasonRanking = async (
seasonID: mongoose.Types.ObjectId,
userID: mongoose.Types.ObjectId
): Promise<RankingModel | null> => {
const ranking = await Ranking.findOne({
seasonID: seasonID,
userID: userID
});
return ranking;
}

const getUserAllSeasonRankings = async (
userID: mongoose.Types.ObjectId
): Promise<RankingModel[] | null> => {
const rankings = await Ranking.find({
userID: userID
});
return rankings;
}

const updateSeasonRankings = async (
seasonID: mongoose.Types.ObjectId,
userID: mongoose.Types.ObjectId,
points: number
): Promise<RankingModel | null> => {
const ranking = await Ranking.findOneAndUpdate({
seasonID: seasonID,
userID: userID
}, {
points: points
}, {
new: true,
upsert: true
});
return ranking;
return { rankings: paginateArray(rankings, limit, page), rankingsCount: rankings.length };
}


const SeasonRepo = {
getSeasonsByDate,
getSeasonByID,
createSeason,
getSeasonRankings,
getSeasonRankingsByPagination,
getUserSeasonRanking,
getUserAllSeasonRankings,
updateSeasonRankings,
calculateSeasonRankings
}

Expand Down
4 changes: 2 additions & 2 deletions apps/challenges/src/routes/seasons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ router.get("/active", SeasonController.getActiveSeasons);
router.get("/:seasonID", SeasonController.getSeasonByID);

router.get("/:seasonID/rankings", SeasonController.getSeasonRankings);
router.get("/:seasonID/rankings/:userID", SeasonController.getUserSeasonRanking);
router.put("/:seasonID/rankings/:userID", SeasonController.updateSeasonRankings);
// router.get("/:seasonID/rankings/:userID", SeasonController.getUserSeasonRanking);
// router.put("/:seasonID/rankings/:userID", SeasonController.updateSeasonRankings);

export { router as default };
2 changes: 1 addition & 1 deletion apps/challenges/src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import SeasonController from "../controllers/season";
const router = Express.Router();

router.post("/", UserController.createUser);
router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings);
// router.get("/:userID/rankings", SeasonController.getUserAllSeasonRankings);

export { router as default };
24 changes: 4 additions & 20 deletions apps/challenges/src/service/seasonService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const getSeasonRankingsByPagination = async(
return await SeasonRepo.getSeasonRankingsByPagination(_id, page, limit);
}

/*
const getUserSeasonRanking = async(
seasonID: string,
userID: string
Expand All @@ -80,23 +81,7 @@ const getUserAllSeasonRankings = async(
const rankings = await SeasonRepo.getUserAllSeasonRankings(_userID);
return rankings;
}

const updateSeasonRankings = async(
seasonID: string,
userID: string,
points: number
) => {
const user = await UserService.getUserByID(userID);
if (user == null) {
throw new Error('Invalid user ID');
}
const season = await getSeasonByID(seasonID);
if(season == null){
throw new Error('Invalid season ID');
}
const ranking = await SeasonRepo.updateSeasonRankings(season._id, user._id, points);
return ranking;
}
*/

const calculateSeasonRankings = async(
seasonID: string
Expand All @@ -115,9 +100,8 @@ const SeasonService = {
createSeason,
getSeasonRankings,
getSeasonRankingsByPagination,
getUserSeasonRanking,
getUserAllSeasonRankings,
updateSeasonRankings,
// getUserSeasonRanking,
// getUserAllSeasonRankings,
calculateSeasonRankings
}

Expand Down
Loading
Loading