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

Dev to Main Sync #2336

Merged
merged 6 commits into from
Jan 11, 2025
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
4 changes: 4 additions & 0 deletions constants/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const REQUEST_TYPE = {
EXTENSION: "EXTENSION",
TASK: "TASK",
ALL: "ALL",
ONBOARDING: "ONBOARDING",
};

export const REQUEST_LOG_TYPE = {
Expand Down Expand Up @@ -53,3 +54,6 @@ export const TASK_REQUEST_MESSAGES = {
ERROR_CREATING_TASK_REQUEST: "Error while creating task request",
TASK_REQUEST_UPDATED_SUCCESS: "Task request updated successfully",
};

export const ONBOARDING_REQUEST_CREATED_SUCCESSFULLY = "Onboarding extension request created successfully"
export const UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST = "Only super user and onboarding user are authorized to create an onboarding extension request"
124 changes: 124 additions & 0 deletions controllers/onboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
ERROR_WHILE_CREATING_REQUEST,
LOG_ACTION,
ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
REQUEST_ALREADY_PENDING,
REQUEST_LOG_TYPE,
REQUEST_STATE,
REQUEST_TYPE,
UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST,
} from "../constants/requests";
import { userState } from "../constants/userStatus";
import { addLog } from "../services/logService";
import { createRequest, getRequestByKeyValues } from "../models/requests";
import { fetchUser } from "../models/users";
import { getUserStatus } from "../models/userStatus";
import { User } from "../typeDefinitions/users";
import {
CreateOnboardingExtensionBody,
OnboardingExtension,
OnboardingExtensionCreateRequest,
OnboardingExtensionResponse
} from "../types/onboardingExtension";
import { convertDateStringToMilliseconds, getNewDeadline } from "../utils/requests";
import { convertDaysToMilliseconds } from "../utils/time";

/**
* Controller to handle the creation of onboarding extension requests.
*
* This function processes the request to create an extension for the onboarding period,
* validates the user status, checks existing requests, calculates new deadlines,
* and stores the new request in the database with logging.
*
* @param {OnboardingExtensionCreateRequest} req - The Express request object containing the body with extension details.
* @param {OnboardingExtensionResponse} res - The Express response object used to send back the response.
* @returns {Promise<OnboardingExtensionResponse>} Resolves to a response with the status and data or an error message.
*/
export const createOnboardingExtensionRequestController = async (req: OnboardingExtensionCreateRequest, res: OnboardingExtensionResponse): Promise<OnboardingExtensionResponse> => {
try {

const data = req.body as CreateOnboardingExtensionBody;
const {user, userExists} = await fetchUser({discordId: data.userId});

if(!userExists) {
return res.boom.notFound("User not found");
}

const { id: userId, discordJoinedAt, username} = user as User;
const { data: userStatus } = await getUserStatus(userId);

if(!userStatus || userStatus.currentStatus.state != userState.ONBOARDING){
return res.boom.forbidden(UNAUTHORIZED_TO_CREATE_ONBOARDING_EXTENSION_REQUEST);
}

const latestExtensionRequest: OnboardingExtension = await getRequestByKeyValues({
userId: userId,
type: REQUEST_TYPE.ONBOARDING
});

if(latestExtensionRequest && latestExtensionRequest.state === REQUEST_STATE.PENDING){
return res.boom.conflict(REQUEST_ALREADY_PENDING);
}

const millisecondsInThirtyOneDays = convertDaysToMilliseconds(31);
const numberOfDaysInMillisecond = convertDaysToMilliseconds(data.numberOfDays);
const { isDate, milliseconds: discordJoinedDateInMillisecond } = convertDateStringToMilliseconds(discordJoinedAt);

if(!isDate){
logger.error(ERROR_WHILE_CREATING_REQUEST, "Invalid date");
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
}

let requestNumber: number;
let oldEndsOn: number;
const currentDate = Date.now();

if(!latestExtensionRequest){
requestNumber = 1;
oldEndsOn = discordJoinedDateInMillisecond + millisecondsInThirtyOneDays;
}else if(latestExtensionRequest.state === REQUEST_STATE.REJECTED) {
requestNumber = latestExtensionRequest.requestNumber + 1;
oldEndsOn = latestExtensionRequest.oldEndsOn;
}else{
requestNumber = latestExtensionRequest.requestNumber + 1;
oldEndsOn = latestExtensionRequest.newEndsOn;
}

const newEndsOn = getNewDeadline(currentDate, oldEndsOn, numberOfDaysInMillisecond);

const onboardingExtension = await createRequest({
type: REQUEST_TYPE.ONBOARDING,
state: REQUEST_STATE.PENDING,
userId: userId,
requestedBy: username,
oldEndsOn: oldEndsOn,
newEndsOn: newEndsOn,
reason: data.reason,
requestNumber: requestNumber,
});

const onboardingExtensionLog = {
type: REQUEST_LOG_TYPE.REQUEST_CREATED,
meta: {
requestId: onboardingExtension.id,
action: LOG_ACTION.CREATE,
userId: userId,
createdAt: Date.now(),
},
body: onboardingExtension,
};

await addLog(onboardingExtensionLog.type, onboardingExtensionLog.meta, onboardingExtensionLog.body);

return res.status(201).json({
message: ONBOARDING_REQUEST_CREATED_SUCCESSFULLY,
data: {
id: onboardingExtension.id,
...onboardingExtension,
}
});
}catch (err) {
logger.error(ERROR_WHILE_CREATING_REQUEST, err);
return res.boom.badImplementation(ERROR_WHILE_CREATING_REQUEST);
}
};
2 changes: 1 addition & 1 deletion controllers/progresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ const getProgressRangeData = async (req, res) => {

const getProgressBydDateController = async (req, res) => {
try {
const data = await getProgressByDate(req.params);
const data = await getProgressByDate(req.params, req.query);
return res.json({
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
data,
Expand Down
6 changes: 5 additions & 1 deletion controllers/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import { createTaskExtensionRequest, updateTaskExtensionRequest } from "./extens
import { UpdateRequest } from "../types/requests";
import { TaskRequestRequest } from "../types/taskRequests";
import { createTaskRequestController } from "./taskRequestsv2";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../types/onboardingExtension";
import { createOnboardingExtensionRequestController } from "./onboardingExtension";

export const createRequestController = async (
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest,
req: OooRequestCreateRequest | ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
res: CustomResponse
) => {
const type = req.body.type;
Expand All @@ -26,6 +28,8 @@ export const createRequestController = async (
return await createTaskExtensionRequest(req as ExtensionRequestRequest, res as ExtensionRequestResponse);
case REQUEST_TYPE.TASK:
return await createTaskRequestController(req as TaskRequestRequest, res as CustomResponse);
case REQUEST_TYPE.ONBOARDING:
return await createOnboardingExtensionRequestController(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse);
default:
return res.boom.badRequest("Invalid request type");
}
Expand Down
30 changes: 30 additions & 0 deletions middlewares/skipAuthenticateForOnboardingExtension.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NextFunction, Request, Response } from "express"
import { REQUEST_TYPE } from "../constants/requests";
/**
* Middleware to selectively authenticate or verify Discord bot based on the request type.
* Specifically handles requests for onboarding extensions by skipping authentication.
*
* @param {Function} authenticate - The authentication middleware to apply for general requests.
* @param {Function} verifyDiscordBot - The middleware to verify requests from a Discord bot.
* @returns {Function} A middleware function that processes the request based on its type.
*
* @example
* app.use(skipAuthenticateForOnboardingExtensionRequest(authenticate, verifyDiscordBot));
*/
export const skipAuthenticateForOnboardingExtensionRequest = (authenticate, verifyDiscordBot) => {
return async (req: Request, res: Response, next: NextFunction) => {
const type = req.body.type;
const dev = req.query.dev;

if(type === REQUEST_TYPE.ONBOARDING){
if (dev != "true"){
return res.status(501).json({
message: "Feature not implemented"
})
}
return await verifyDiscordBot(req, res, next);
}

return await authenticate(req, res, next)
}
}
42 changes: 42 additions & 0 deletions middlewares/validators/onboardingExtensionRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import joi from "joi";
import { NextFunction } from "express";
import { REQUEST_TYPE } from "../../constants/requests";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension";

export const createOnboardingExtensionRequestValidator = async (
req: OnboardingExtensionCreateRequest,
_res: OnboardingExtensionResponse,
_next: NextFunction
) => {

const schema = joi
.object()
.strict()
.keys({
numberOfDays: joi.number().required().positive().integer().min(1).messages({
"number.base": "numberOfDays must be a number",
"any.required": "numberOfDays is required",
"number.positive": "numberOfDays must be positive",
"number.min": "numberOfDays must be greater than zero",
"number.integer": "numberOfDays must be a integer"
}),
reason: joi.string().required().messages({
"string.empty": "reason cannot be empty",
"any.required": "reason is required",
}),
type: joi.string().valid(REQUEST_TYPE.ONBOARDING).required().messages({
"string.empty": "type cannot be empty",
"any.required": "type is required",
}),
userId: joi.string().required().messages({
"string.empty": "userId cannot be empty",
"any.required": "userId is required"
})
});
try{
await schema.validateAsync(req.body, { abortEarly: false });
}catch(error){
logger.error(`Error while validating request payload`, error);
throw error;
}
};
5 changes: 5 additions & 0 deletions middlewares/validators/progresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ const validateGetProgressRecordsQuery = async (req, res, next) => {
taskId: joi.string().optional().allow("").messages({
"string.base": "taskId must be a string",
}),
dev: joi.boolean().optional().messages({
"boolean.base": "dev must be a boolean value (true or false).",
}),
orderBy: joi
.string()
.optional()
Expand Down Expand Up @@ -92,6 +95,7 @@ const validateGetRangeProgressRecordsParams = async (req, res, next) => {
taskId: joi.string().optional(),
startDate: joi.date().iso().required(),
endDate: joi.date().iso().min(joi.ref("startDate")).required(),
dev: joi.boolean().optional(),
})
.xor("userId", "taskId")
.messages({
Expand Down Expand Up @@ -121,6 +125,7 @@ const validateGetDayProgressParams = async (req, res, next) => {
}),
typeId: joi.string().required(),
date: joi.date().iso().required(),
dev: joi.boolean().optional(),
});
try {
await schema.validateAsync(req.params, { abortEarly: false });
Expand Down
9 changes: 7 additions & 2 deletions middlewares/validators/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import { ExtensionRequestRequest, ExtensionRequestResponse } from "../../types/e
import { CustomResponse } from "../../typeDefinitions/global";
import { UpdateRequest } from "../../types/requests";
import { TaskRequestRequest, TaskRequestResponse } from "../../types/taskRequests";
import { createOnboardingExtensionRequestValidator } from "./onboardingExtensionRequest";
import { OnboardingExtensionCreateRequest, OnboardingExtensionResponse } from "../../types/onboardingExtension";

export const createRequestsMiddleware = async (
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest,
req: OooRequestCreateRequest|ExtensionRequestRequest | TaskRequestRequest | OnboardingExtensionCreateRequest,
res: CustomResponse,
next: NextFunction
) => {
Expand All @@ -28,6 +30,9 @@ export const createRequestsMiddleware = async (
case REQUEST_TYPE.TASK:
await createTaskRequestValidator(req as TaskRequestRequest, res as TaskRequestResponse, next);
break;
case REQUEST_TYPE.ONBOARDING:
await createOnboardingExtensionRequestValidator(req as OnboardingExtensionCreateRequest, res as OnboardingExtensionResponse, next);
break;
default:
res.boom.badRequest(`Invalid request type: ${type}`);
}
Expand All @@ -36,7 +41,7 @@ export const createRequestsMiddleware = async (
} catch (error) {
const errorMessages = error.details.map((detail:any) => detail.message);
logger.error(`Error while validating request payload : ${errorMessages}`);
res.boom.badRequest(errorMessages);
return res.boom.badRequest(errorMessages);
}
};

Expand Down
55 changes: 52 additions & 3 deletions models/progresses.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
getProgressDateTimestamp,
buildQueryToSearchProgressByDay,
} = require("../utils/progresses");
const { retrieveUsers } = require("../services/dataAccessLayer");
const { PROGRESS_ALREADY_CREATED, PROGRESS_DOCUMENT_NOT_FOUND } = PROGRESSES_RESPONSE_MESSAGES;

/**
Expand Down Expand Up @@ -47,9 +48,14 @@
* @throws {Error} If the userId or taskId is invalid or does not exist.
**/
const getProgressDocument = async (queryParams) => {
const { dev } = queryParams;
await assertUserOrTaskExists(queryParams);
const query = buildQueryToFetchDocs(queryParams);
const progressDocs = await getProgressDocs(query);

if (dev === "true") {
return await addUserDetailsToProgressDocs(progressDocs);
}
return progressDocs;
};

Expand Down Expand Up @@ -77,16 +83,59 @@
* @returns {Promise<object>} A Promise that resolves with the progress records of the queried user or task.
* @throws {Error} If the userId or taskId is invalid or does not exist.
**/
async function getProgressByDate(pathParams) {
async function getProgressByDate(pathParams, queryParams) {
const { type, typeId, date } = pathParams;
const { dev } = queryParams;
await assertUserOrTaskExists({ [TYPE_MAP[type]]: typeId });

Check warning on line 89 in models/progresses.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Generic Object Injection Sink

Check warning on line 89 in models/progresses.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Generic Object Injection Sink
const query = buildQueryToSearchProgressByDay({ [TYPE_MAP[type]]: typeId, date });

Check warning on line 90 in models/progresses.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Generic Object Injection Sink

Check warning on line 90 in models/progresses.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Generic Object Injection Sink
const result = await query.get();
if (!result.size) {
throw new NotFound(PROGRESS_DOCUMENT_NOT_FOUND);
}
const doc = result.docs[0];
return { id: doc.id, ...doc.data() };
const docData = doc.data();
if (dev === "true") {
const { user: userData } = await retrieveUsers({ id: docData.userId });
return { id: doc.id, ...docData, userData };
}

return { id: doc.id, ...docData };
}

module.exports = { createProgressDocument, getProgressDocument, getRangeProgressData, getProgressByDate };
/**
* Adds user details to progress documents by fetching unique users.
* This function retrieves user details for each user ID in the progress documents and attaches the user data to each document.
*
* @param {Array<object>} progressDocs - An array of progress documents. Each document should include a `userId` property.
* @returns {Promise<Array<object>>} A Promise that resolves to an array of progress documents with the `userData` field populated.
* If an error occurs while fetching the user details, the `userData` field will be set to `null` for each document.
*/
const addUserDetailsToProgressDocs = async (progressDocs) => {
try {
const uniqueUserIds = [...new Set(progressDocs.map((doc) => doc.userId))];

const uniqueUsersData = await retrieveUsers({
userIds: uniqueUserIds,
});
const allUsers = uniqueUsersData.flat();
const userByIdMap = allUsers.reduce((lookup, user) => {
if (user) lookup[user.id] = user;
return lookup;
}, {});

return progressDocs.map((doc) => {
const userDetails = userByIdMap[doc.userId] || null;
return { ...doc, userData: userDetails };
});
} catch (err) {
return progressDocs.map((doc) => ({ ...doc, userData: null }));
}
};

module.exports = {
createProgressDocument,
getProgressDocument,
getRangeProgressData,
getProgressByDate,
addUserDetailsToProgressDocs,
};
Loading
Loading