From b82174a1aeb905af286189cc18e6eda234fa492f Mon Sep 17 00:00:00 2001 From: Muhammad Zhafran Ilham Date: Sun, 28 Jun 2026 01:10:17 +0700 Subject: [PATCH] feat(project): implement filter multi status pada get all projects --- src/modules/project/models/project.model.js | 31 ++++++++++++-- .../repositories/project.repository.js | 8 ++-- .../project/services/project.service.js | 41 ++++++++++++++++++- src/routes/project.routes.js | 8 ++-- 4 files changed, 77 insertions(+), 11 deletions(-) diff --git a/src/modules/project/models/project.model.js b/src/modules/project/models/project.model.js index 8eb9c40..b4f7410 100644 --- a/src/modules/project/models/project.model.js +++ b/src/modules/project/models/project.model.js @@ -88,10 +88,33 @@ const updateProjectModel = joi.object().keys({ }); const getAllProjectsModel = joi.object().keys({ - status: joi.string().valid(...PROJECT_STATUS_VALUES).optional().messages({ - 'string.base': 'Project status must be a string.', - 'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`, - }), + status: joi + .alternatives() + .try( + joi.string(), + joi.array().items(joi.string()) + ) + .optional() + .custom((value, helpers) => { + const statuses = Array.isArray(value) + ? value.flatMap((status) => String(status).split(',')) + : String(value).split(','); + + const invalidStatuses = statuses + .map((status) => status.trim().toLowerCase()) + .filter(Boolean) + .filter((status) => !PROJECT_STATUS_VALUES.includes(status)); + + if (invalidStatuses.length > 0) { + return helpers.error('any.only'); + } + + return value; + }) + .messages({ + 'alternatives.types': 'Project status must be a string or an array of strings.', + 'any.only': `Project status must be one of: ${PROJECT_STATUS_VALUES.join(', ')}.`, + }), }); module.exports = { diff --git a/src/modules/project/repositories/project.repository.js b/src/modules/project/repositories/project.repository.js index c493fa7..43ebc2a 100644 --- a/src/modules/project/repositories/project.repository.js +++ b/src/modules/project/repositories/project.repository.js @@ -58,11 +58,13 @@ class ProjectRepository { }); } - async findAll({ status, actor }) { + async findAll({ statuses, actor }) { const where = {}; - if (status) { - where.status = status; + if (statuses && statuses.length > 0) { + where.status = { + in: statuses, + }; } if (actor?.role === "intern") { diff --git a/src/modules/project/services/project.service.js b/src/modules/project/services/project.service.js index 5758e40..1e21a5f 100644 --- a/src/modules/project/services/project.service.js +++ b/src/modules/project/services/project.service.js @@ -109,8 +109,13 @@ class ProjectService { async getAllProjects(query = {}, actor = {}) { try { + const statusFilter = this.parseProjectStatusFilter(query.status); + if (statusFilter.err) { + return statusFilter; + } + const projects = await projectRepository.findAll({ - status: query.status, + statuses: statusFilter.data, actor, }); @@ -293,6 +298,40 @@ class ProjectService { return new ForbiddenError("Access denied: invalid role"); } + parseProjectStatusFilter(rawStatus) { + if (!rawStatus) { + return data(null); + } + + const allowedStatuses = ["active", "completed", "archived"]; + + const statuses = Array.isArray(rawStatus) + ? rawStatus.flatMap((status) => String(status).split(",")) + : String(rawStatus).split(","); + + const cleanedStatuses = [ + ...new Set( + statuses + .map((status) => status.trim().toLowerCase()) + .filter(Boolean) + ), + ]; + + const invalidStatuses = cleanedStatuses.filter( + (status) => !allowedStatuses.includes(status) + ); + + if (invalidStatuses.length > 0) { + return error( + new BadRequestError( + `Invalid project status: ${invalidStatuses.join(", ")}` + ) + ); + } + + return data(cleanedStatuses); + } + normalizeEmails(emails = []) { return [ ...new Set( diff --git a/src/routes/project.routes.js b/src/routes/project.routes.js index f102036..f5ac657 100644 --- a/src/routes/project.routes.js +++ b/src/routes/project.routes.js @@ -534,10 +534,10 @@ router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject) * name: status * schema: * type: string - * enum: [active, completed, archived] + * example: 'active,completed' * required: false - * description: Filter projects by status - * example: active + * description: Filter projects by one or more statuses. Use comma-separated values, for example active,completed. Values are **active, completed, archived** + * example: active,completed * responses: * 200: * description: Projects retrieved successfully @@ -545,6 +545,8 @@ router.post('/add', verifyJWT, isMentorOrAdmin, projectController.createProject) * application/json: * schema: * $ref: '#/components/schemas/ProjectListResponse' + * 400: + * description: Invalid project status * 401: * description: Unauthorized * 417: