Skip to content

Commit b863ad3

Browse files
Merge pull request #1074 from Real-Dev-Squad/feat/progresses-api-v1.3
Add progress tracking API endpoints
2 parents 6b916f2 + 1ee0497 commit b863ad3

File tree

7 files changed

+565
-0
lines changed

7 files changed

+565
-0
lines changed

constants/progresses.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const PROGRESS_DOCUMENT_CREATED_SUCCEEDED = "Progress document created successfully.";
2+
const PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED = "Progress document retrieved successfully.";
3+
const PROGRESS_DOCUMENT_NOT_FOUND = "No progress records found.";
4+
const PROGRESS_ALREADY_CREATED = "Progress for the day has already been created.";
5+
const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
6+
7+
const RESPONSE_MESSAGES = {
8+
PROGRESS_DOCUMENT_CREATED_SUCCEEDED,
9+
PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
10+
PROGRESS_DOCUMENT_NOT_FOUND,
11+
PROGRESS_ALREADY_CREATED,
12+
};
13+
14+
module.exports = { RESPONSE_MESSAGES, MILLISECONDS_IN_DAY };

controllers/progresses.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
const { Conflict, NotFound } = require("http-errors");
2+
const { createProgressDocument, getProgressDocument, getRangeProgressData } = require("../models/progresses");
3+
const { RESPONSE_MESSAGES } = require("../constants/progresses");
4+
const { PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED, PROGRESS_DOCUMENT_CREATED_SUCCEEDED } = RESPONSE_MESSAGES;
5+
6+
/**
7+
* @typedef {Object} ProgressRequestBody
8+
* @property {string} type - The type of progress document.
9+
* @property {string} completed - The completed progress.
10+
* @property {string} planned - The planned progress.
11+
* @property {string} blockers - The blockers.
12+
* @property {string} [taskId] - The task ID (optional).
13+
*/
14+
15+
/**
16+
* @typedef {Object} ProgressDocument
17+
* @property {string} type - The type of progress document.
18+
* @property {string} completed - The completed progress.
19+
* @property {string} planned - The planned progress.
20+
* @property {string} blockers - The blockers.
21+
* @property {string} userId - The User ID
22+
* @property {string} [taskId] - The task ID (optional).
23+
* @property {number} createdAt - The timestamp when the progress document was created.
24+
* @property {number} date - The timestamp for the day the progress document was created.
25+
*/
26+
27+
/**
28+
* @typedef {Object} ProgressResponse
29+
* @property {ProgressDocument} data - The progress document data.
30+
* @property {string} message - The success message.
31+
*/
32+
33+
/**
34+
* Creates a new progress document.
35+
* @param {Object} req - The HTTP request object.
36+
* @param {ProgressRequestBody} req.body - The progress document data.
37+
* @param {Object} res - The HTTP response object.
38+
* @returns {Promise<void>} A Promise that resolves when the response is sent.
39+
*/
40+
41+
const createProgress = async (req, res) => {
42+
const {
43+
body: { type },
44+
} = req;
45+
try {
46+
const data = await createProgressDocument({ ...req.body, userId: req.userData.id });
47+
return res.status(201).json({
48+
data,
49+
message: `${type.charAt(0).toUpperCase() + type.slice(1)} ${PROGRESS_DOCUMENT_CREATED_SUCCEEDED}`,
50+
});
51+
} catch (error) {
52+
if (error instanceof Conflict) {
53+
return res.status(409).json({
54+
message: error.message,
55+
});
56+
} else if (error instanceof NotFound) {
57+
return res.status(404).json({
58+
message: error.message,
59+
});
60+
}
61+
return res.status(400).json({
62+
message: error.message,
63+
});
64+
}
65+
};
66+
67+
/**
68+
* @typedef {Object} ProgressQueryParams
69+
* @property {string} [type] - The type of progress document.
70+
* @property {string} [taskId] - The task ID (optional).
71+
* @property {string} [userId] - The user ID (optional).
72+
*/
73+
74+
/**
75+
* @typedef {Object} ProgressDocument
76+
* @property {string} type - The type of progress document.
77+
* @property {string} completed - The completed progress.
78+
* @property {string} planned - The planned progress.
79+
* @property {string} blockers - The blockers.
80+
* @property {string} userId - The User ID
81+
* @property {string} [taskId] - The task ID (optional).
82+
* @property {number} createdAt - The timestamp when the progress document was created.
83+
* @property {number} date - The timestamp for the day the progress document was created.
84+
*/
85+
86+
/**
87+
* @typedef {Object} GetProgressResponse
88+
* @property {string} message - The success message.
89+
* @property {number} count - The no of progress documents retrieved
90+
* @property {[ProgressDocument]} data - An array of progress documents
91+
*/
92+
93+
/**
94+
* Retrieves the progress documents based on provided query parameters.
95+
* @param {Object} req - The HTTP request object.
96+
* @param {ProgressQueryParams} req.query - The query parameters
97+
* @param {Object} res - The HTTP response object.
98+
* @returns {Promise<void>} A Promise that resolves when the response is sent.
99+
*/
100+
101+
const getProgress = async (req, res) => {
102+
try {
103+
const data = await getProgressDocument(req.query);
104+
return res.json({
105+
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
106+
count: data.length,
107+
data,
108+
});
109+
} catch (error) {
110+
if (error instanceof NotFound) {
111+
return res.status(404).json({
112+
message: error.message,
113+
});
114+
}
115+
return res.status(400).json({
116+
message: error.message,
117+
});
118+
}
119+
};
120+
121+
/**
122+
* @typedef {Object} ProgressQueryParams
123+
* @property {string} [taskId] - The task ID (optional).
124+
* @property {string} [userId] - The user ID (optional).
125+
* @property {string} startDate - The start date of the date range to retrieve progress records for.
126+
* @property {string} endDate - The end date of the date range to retrieve progress records for.
127+
*/
128+
129+
/**
130+
* @typedef {Object} progressRecord
131+
* @property {boolean} date - the boolean indicating whether the progress was recorded or not for that date
132+
/**
133+
134+
/**
135+
* @typedef {Object} ProgressRangeData
136+
* @property {string} startDate - the start date for the progress records
137+
* @property {string} endDate - the end date for the progress records
138+
* @property {Object.<string, progressRecord>} progressRecords - An object where the keys are dates and the values are progress records.
139+
/**
140+
141+
/**
142+
* @typedef {Object} GetProgressRangeDataResponse
143+
* @property {string} message - The success message.
144+
* @property {ProgressRangeData} data - The progress range data.
145+
*/
146+
147+
/**
148+
* Retrieves the progress documents based on provided query parameters.
149+
* @param {Object} req - The HTTP request object.
150+
* @param {ProgressQueryParams} req.query - The query parameters
151+
* @param {Object} res - The HTTP response object.
152+
* @returns {Promise<void>} A Promise that resolves when the response is sent.
153+
*/
154+
155+
const getProgressRangeData = async (req, res) => {
156+
try {
157+
const data = await getRangeProgressData(req.query);
158+
return res.json({
159+
message: PROGRESS_DOCUMENT_RETRIEVAL_SUCCEEDED,
160+
data,
161+
});
162+
} catch (error) {
163+
if (error instanceof NotFound) {
164+
return res.status(404).json({
165+
message: error.message,
166+
});
167+
}
168+
return res.status(400).json({
169+
message: error.message,
170+
});
171+
}
172+
};
173+
174+
module.exports = { createProgress, getProgress, getProgressRangeData };
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const joi = require("joi");
2+
3+
const validateCreateProgressRecords = async (req, res, next) => {
4+
const baseSchema = joi
5+
.object()
6+
.strict()
7+
.keys({
8+
type: joi.string().trim().valid("user", "task").required().messages({
9+
"any.required": "Required field 'type' is missing.",
10+
"any.only": "Type field is restricted to either 'user' or 'task'.",
11+
}),
12+
completed: joi.string().trim().required().messages({
13+
"any.required": "Required field 'completed' is missing.",
14+
"string.trim": "completed must not have leading or trailing whitespace",
15+
}),
16+
planned: joi.string().trim().required().messages({
17+
"any.required": "Required field 'planned' is missing.",
18+
"string.trim": "planned must not have leading or trailing whitespace",
19+
}),
20+
blockers: joi.string().trim().allow("").required().messages({
21+
"any.required": "Required field 'blockers' is missing.",
22+
"string.trim": "blockers must not have leading or trailing whitespace",
23+
}),
24+
})
25+
.messages({ "object.unknown": "Invalid field provided." });
26+
27+
const taskSchema = joi.object().keys({
28+
taskId: joi.string().trim().required().messages({
29+
"any.required": "Required field 'taskId' is missing.",
30+
"string.trim": "taskId must not have leading or trailing whitespace",
31+
}),
32+
});
33+
const schema = req.body.type === "task" ? baseSchema.concat(taskSchema) : baseSchema;
34+
35+
try {
36+
await schema.validateAsync(req.body, { abortEarly: false });
37+
next();
38+
} catch (error) {
39+
logger.error(`Error validating payload: ${error}`);
40+
res.boom.badRequest(error.details[0].message);
41+
}
42+
};
43+
44+
const validateGetProgressRecordsQuery = async (req, res, next) => {
45+
const schema = joi
46+
.object({
47+
type: joi.string().valid("user", "task").optional().messages({
48+
"any.only": "Type field is restricted to either 'user' or 'task'.",
49+
}),
50+
userId: joi.string().optional().allow("").messages({
51+
"string.base": "userId must be a string",
52+
}),
53+
taskId: joi.string().optional().allow("").messages({
54+
"string.base": "taskId must be a string",
55+
}),
56+
})
57+
.xor("type", "userId", "taskId")
58+
.messages({
59+
"object.unknown": "Invalid field provided.",
60+
"object.xor": "Only one of type, userId, or taskId should be present",
61+
});
62+
try {
63+
await schema.validateAsync(req.query, { abortEarly: false });
64+
next();
65+
} catch (error) {
66+
logger.error(`Error validating payload: ${error}`);
67+
res.boom.badRequest(error.details[0].message);
68+
}
69+
};
70+
71+
const validateGetRangeProgressRecordsParams = async (req, res, next) => {
72+
const schema = joi
73+
.object({
74+
userId: joi.string().optional(),
75+
taskId: joi.string().optional(),
76+
startDate: joi.date().iso().required(),
77+
endDate: joi.date().iso().min(joi.ref("startDate")).required(),
78+
})
79+
.xor("userId", "taskId")
80+
.messages({
81+
"object.unknown": "Invalid field provided.",
82+
"object.missing": "Either userId or taskId is required.",
83+
"object.xor": "Only one of userId or taskId should be present",
84+
"any.required": "Start date and End date is mandatory.",
85+
"date.min": "EndDate must be on or after startDate",
86+
});
87+
try {
88+
await schema.validateAsync(req.query, { abortEarly: false });
89+
next();
90+
} catch (error) {
91+
logger.error(`Error validating payload: ${error}`);
92+
res.boom.badRequest(error.details[0].message);
93+
}
94+
};
95+
module.exports = {
96+
validateCreateProgressRecords,
97+
validateGetProgressRecordsQuery,
98+
validateGetRangeProgressRecordsParams,
99+
};

models/progresses.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const { Conflict } = require("http-errors");
2+
const fireStore = require("../utils/firestore");
3+
const progressesCollection = fireStore.collection("progresses");
4+
const { RESPONSE_MESSAGES } = require("../constants/progresses");
5+
const {
6+
buildQueryToFetchDocs,
7+
getProgressDocs,
8+
buildRangeProgressQuery,
9+
getProgressRecords,
10+
assertUserOrTaskExists,
11+
buildQueryForPostingProgress,
12+
assertTaskExists,
13+
getProgressDateTimestamp,
14+
} = require("../utils/progresses");
15+
const { PROGRESS_ALREADY_CREATED } = RESPONSE_MESSAGES;
16+
17+
/**
18+
* Adds a new progress document for the given user or task, with a limit of one progress document per day.
19+
* @param progressData {object} The data to be added. It should be an object containing key-value pairs of the fields to be added, including a "type" field set to either "user" or "task".
20+
* @returns {Promise<object>} A Promise that resolves with the added progress document object, or rejects with an error object if the add operation fails.
21+
* @throws {Error} If a progress document has already been created for the given user or task on the current day.
22+
**/
23+
const createProgressDocument = async (progressData) => {
24+
const { type, taskId } = progressData;
25+
const createdAtTimestamp = new Date().getTime();
26+
const progressDateTimestamp = getProgressDateTimestamp();
27+
if (taskId) {
28+
await assertTaskExists(taskId);
29+
}
30+
const query = buildQueryForPostingProgress(progressData);
31+
const existingDocumentSnapshot = await query.where("date", "==", progressDateTimestamp).get();
32+
if (!existingDocumentSnapshot.empty) {
33+
throw new Conflict(`${type.charAt(0).toUpperCase() + type.slice(1)} ${PROGRESS_ALREADY_CREATED}`);
34+
}
35+
const progressDocumentData = { ...progressData, createdAt: createdAtTimestamp, date: progressDateTimestamp };
36+
const { id } = await progressesCollection.add(progressDocumentData);
37+
return { id, ...progressDocumentData };
38+
};
39+
40+
/**
41+
* This function retrieves the progress document for a specific user or task, or for all users or all tasks if no specific user or task is provided.
42+
* @param queryParams {object} This is the data that will be used for querying. It should be an object that includes key-value pairs for the fields - type, userId, taskId.
43+
* @returns {Promise<object>} A Promise that resolves with the progress document objects.
44+
* @throws {Error} If the userId or taskId is invalid or does not exist.
45+
**/
46+
const getProgressDocument = async (queryParams) => {
47+
await assertUserOrTaskExists(queryParams);
48+
const query = buildQueryToFetchDocs(queryParams);
49+
const progressDocs = await getProgressDocs(query);
50+
return progressDocs;
51+
};
52+
53+
/**
54+
* This function fetches the progress records for a particular user or task within the specified date range, from start to end date.
55+
* @param queryParams {object} This is the data that will be used for querying. It should be an object that includes key-value pairs for the fields - userId, taskId, startDate, and endDate.
56+
* @returns {Promise<object>} A Promise that resolves with the progress records of the queried user or task.
57+
* @throws {Error} If the userId or taskId is invalid or does not exist.
58+
**/
59+
const getRangeProgressData = async (queryParams) => {
60+
const { startDate, endDate } = queryParams;
61+
await assertUserOrTaskExists(queryParams);
62+
const query = buildRangeProgressQuery(queryParams);
63+
const progressRecords = await getProgressRecords(query, queryParams);
64+
return {
65+
startDate,
66+
endDate,
67+
progressRecords,
68+
};
69+
};
70+
71+
module.exports = { createProgressDocument, getProgressDocument, getRangeProgressData };

routes/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ app.use("/cache", require("./cloudflareCache.js"));
2626
app.use("/external-accounts", require("./external-accounts.js"));
2727
app.use("/discord-actions", require("./discordactions.js"));
2828
app.use("/issues", require("./issues.js"));
29+
app.use("/progresses", require("./progresses.js"));
2930

3031
module.exports = app;

routes/progresses.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const express = require("express");
2+
const router = express.Router();
3+
const authenticate = require("../middlewares/authenticate");
4+
const {
5+
validateCreateProgressRecords,
6+
validateGetProgressRecordsQuery,
7+
validateGetRangeProgressRecordsParams,
8+
} = require("../middlewares/validators/progresses");
9+
const { createProgress, getProgress, getProgressRangeData } = require("../controllers/progresses");
10+
11+
router.post("/", authenticate, validateCreateProgressRecords, createProgress);
12+
router.get("/", validateGetProgressRecordsQuery, getProgress);
13+
router.get("/range", validateGetRangeProgressRecordsParams, getProgressRangeData);
14+
15+
module.exports = router;

0 commit comments

Comments
 (0)