diff --git a/controllers/progresses.js b/controllers/progresses.js index 5a0e8f84b..9c8474afc 100644 --- a/controllers/progresses.js +++ b/controllers/progresses.js @@ -220,6 +220,8 @@ const getProgressRangeData = async (req, res) => { * @property {string} date - The iso format date of the query. */ + + /** * @typedef {Object} ProgressDocument * @property {string} id - The id of the progress document. @@ -267,4 +269,51 @@ const getProgressBydDateController = async (req, res) => { } }; -module.exports = { createProgress, getProgress, getProgressRangeData, getProgressBydDateController }; +/** + * Creates multiple progress documents in bulk. + * @param {Object} req - The HTTP request object. + * @param {Object} req.body - The request body containing an array of progress records. + * @param {Array} req.body.records - Array of progress records to create. + * @param {Object} res - The HTTP response object. + * @returns {Promise} A Promise that resolves when the response is sent. + */ +const createBulkProgress = async (req, res) => { + if (req.userData.roles.archived) { + return res.boom.forbidden(UNAUTHORIZED_WRITE); + } + + const { records } = req.body; + + try { + // Add userId to each record + const recordsWithUserId = records.map(record => ({ + ...record, + userId: req.userData.id + })); + + const result = await progressesModel.createBulkProgressDocuments(recordsWithUserId); + + return res.status(201).json({ + message: `Successfully created ${result.successCount} progress records`, + data: { + successCount: result.successCount, + failureCount: result.failureCount, + successfulRecords: result.successfulRecords, + failedRecords: result.failedRecords + } + }); + } catch (error) { + logger.error(`Error in bulk progress creation: ${error.message}`); + return res.status(500).json({ + message: INTERNAL_SERVER_ERROR_MESSAGE, + }); + } +}; + +module.exports = { + createProgress, + getProgress, + getProgressRangeData, + getProgressBydDateController, + createBulkProgress +}; diff --git a/middlewares/validators/progresses.js b/middlewares/validators/progresses.js index 2b04befee..1e43fbd05 100644 --- a/middlewares/validators/progresses.js +++ b/middlewares/validators/progresses.js @@ -1,7 +1,8 @@ const joi = require("joi"); const { VALID_PROGRESS_TYPES, PROGRESS_VALID_SORT_FIELDS } = require("../../constants/progresses"); -const validateCreateProgressRecords = async (req, res, next) => { +// Create a reusable progress record schema +const createProgressRecordSchema = () => { const baseSchema = joi .object() .strict() @@ -30,12 +31,19 @@ const validateCreateProgressRecords = async (req, res, next) => { }) .messages({ "object.unknown": "Invalid field provided." }); + return baseSchema; +}; + +const validateCreateProgressRecords = async (req, res, next) => { + const baseSchema = createProgressRecordSchema(); + const taskSchema = joi.object().keys({ taskId: joi.string().trim().required().messages({ "any.required": "Required field 'taskId' is missing.", "string.trim": "taskId must not have leading or trailing whitespace", }), }); + const schema = req.body.type === "task" ? baseSchema.concat(taskSchema) : baseSchema; try { @@ -144,9 +152,78 @@ const validateGetDayProgressParams = async (req, res, next) => { res.boom.badRequest(error.details[0].message); } }; +/** + * Validates bulk creation of progress records + * Ensures the request contains an array of valid progress records + * with a minimum of 1 and maximum of 50 records + */ +const validateBulkCreateProgressRecords = async (req, res, next) => { + const baseProgressSchema = createProgressRecordSchema(); + + const bulkSchema = joi + .object() + .keys({ + records: joi + .array() + .min(1) + .max(50) + .items( + joi.object().keys({ + type: joi + .string() + .trim() + .valid(...VALID_PROGRESS_TYPES) + .required() + .messages({ + "any.required": "Required field 'type' is missing.", + "any.only": "Type field is restricted to either 'user' or 'task'.", + }), + completed: joi.string().trim().required().messages({ + "any.required": "Required field 'completed' is missing.", + "string.trim": "completed must not have leading or trailing whitespace", + }), + planned: joi.string().trim().required().messages({ + "any.required": "Required field 'planned' is missing.", + "string.trim": "planned must not have leading or trailing whitespace", + }), + blockers: joi.string().trim().allow("").required().messages({ + "any.required": "Required field 'blockers' is missing.", + "string.trim": "blockers must not have leading or trailing whitespace", + }), + taskId: joi.string().trim().when("type", { + is: "task", + then: joi.required().messages({ + "any.required": "Required field 'taskId' is missing for task type.", + "string.trim": "taskId must not have leading or trailing whitespace", + }), + otherwise: joi.forbidden().messages({ + "any.unknown": "taskId should not be provided for user type.", + }), + }), + }) + ) + .required() + .messages({ + "array.min": "At least one progress record is required.", + "array.max": "Maximum of 50 progress records can be created at once.", + "any.required": "Progress records array is required.", + }), + }) + .messages({ "object.unknown": "Invalid field provided." }); + + try { + await bulkSchema.validateAsync(req.body, { abortEarly: false }); + next(); + } catch (error) { + logger.error(`Error validating bulk payload: ${error}`); + res.boom.badRequest(error.details[0].message); + } +}; + module.exports = { validateCreateProgressRecords, validateGetProgressRecordsQuery, validateGetRangeProgressRecordsParams, validateGetDayProgressParams, + validateBulkCreateProgressRecords, }; diff --git a/models/progresses.js b/models/progresses.js index c0eb353d6..1a7beab20 100644 --- a/models/progresses.js +++ b/models/progresses.js @@ -146,6 +146,89 @@ const addUserDetailsToProgressDocs = async (progressDocs) => { } }; +/** + * Creates multiple progress documents in a batch operation. + * @param {Array} progressDataArray - Array of progress data objects to create. + * @returns {Promise} A Promise that resolves with the result of the batch operation, + * including counts of successful and failed operations and details of each. + */ +const createBulkProgressDocuments = async (progressDataArray) => { + const batch = fireStore.batch(); + const createdAtTimestamp = new Date().getTime(); + const progressDateTimestamp = getProgressDateTimestamp(); + + const result = { + successCount: 0, + failureCount: 0, + successfulRecords: [], + failedRecords: [] + }; + + // First, check for existing progress documents for the current day + const existingProgressChecks = await Promise.all( + progressDataArray.map(async (progressData) => { + try { + const { type, taskId, userId } = progressData; + + // Validate task exists if taskId is provided + if (taskId) { + await assertTaskExists(taskId); + } + + // Check if progress already exists for today + const query = buildQueryForPostingProgress(progressData); + const existingDocumentSnapshot = await query.where("date", "==", progressDateTimestamp).get(); + + return { + progressData, + exists: !existingDocumentSnapshot.empty, + error: existingDocumentSnapshot.empty ? null : `${type.charAt(0).toUpperCase() + type.slice(1)} ${PROGRESS_ALREADY_CREATED}` + }; + } catch (error) { + return { + progressData, + exists: false, + error: error.message + }; + } + }) + ); + + // Process records that don't have existing progress for today + existingProgressChecks.forEach((check) => { + if (check.error) { + result.failureCount++; + result.failedRecords.push({ + record: check.progressData, + error: check.error + }); + } else { + // Add to batch + const progressDocumentData = { + ...check.progressData, + createdAt: createdAtTimestamp, + date: progressDateTimestamp + }; + + const docRef = progressesCollection.doc(); + batch.set(docRef, progressDocumentData); + + result.successCount++; + result.successfulRecords.push({ + id: docRef.id, + ...progressDocumentData + }); + } + }); + + // Commit the batch if there are any successful records + if (result.successCount > 0) { + await batch.commit(); + } + + return result; +}; + module.exports = { createProgressDocument, getProgressDocument, @@ -153,4 +236,5 @@ module.exports = { getRangeProgressData, getProgressByDate, addUserDetailsToProgressDocs, + createBulkProgressDocuments, }; diff --git a/routes/progresses.ts b/routes/progresses.ts index 0c12f37e2..8683e0138 100644 --- a/routes/progresses.ts +++ b/routes/progresses.ts @@ -5,17 +5,29 @@ import { validateGetProgressRecordsQuery, validateGetRangeProgressRecordsParams, validateGetDayProgressParams, + validateBulkCreateProgressRecords, } from "../middlewares/validators/progresses"; import { createProgress, getProgress, getProgressRangeData, getProgressBydDateController, + createBulkProgress, } from "../controllers/progresses"; const router = express.Router(); +// Create a single progress record router.post("/", authenticate, validateCreateProgressRecords, createProgress); + +// Create multiple progress records in bulk +router.post("/bulk", authenticate, validateBulkCreateProgressRecords, createBulkProgress); + +// Get progress records with optional filtering router.get("/", validateGetProgressRecordsQuery, getProgress); + +// Get progress for a specific date router.get("/:type/:typeId/date/:date", validateGetDayProgressParams, getProgressBydDateController); + +// Get progress records for a date range router.get("/range", validateGetRangeProgressRecordsParams, getProgressRangeData); module.exports = router;