Merge pull request #23 from internship-humic/feat/project-api
feat/multi-status-project
This commit is contained in:
@@ -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 = {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -125,7 +125,7 @@ const router = express.Router();
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: Senior Program Manager at Internify.
|
||||
* ProjectListItem:
|
||||
* ProjectBaseItem:
|
||||
* type: object
|
||||
* properties:
|
||||
* id:
|
||||
@@ -136,6 +136,7 @@ const router = express.Router();
|
||||
* example: internify-platform-dev
|
||||
* project_icon:
|
||||
* type: string
|
||||
* enum: [code, chart, cloud, mobile, gear, users, clipboard, speedometer, lightbulb, shield]
|
||||
* example: code
|
||||
* background_color:
|
||||
* type: string
|
||||
@@ -146,6 +147,10 @@ const router = express.Router();
|
||||
* description:
|
||||
* type: string
|
||||
* example: Upgrading the core Internify platform backend and web client.
|
||||
* certificate_template:
|
||||
* type: string
|
||||
* nullable: true
|
||||
* example: /uploads/certificate-template.png
|
||||
* start_date:
|
||||
* type: string
|
||||
* format: date-time
|
||||
@@ -156,6 +161,7 @@ const router = express.Router();
|
||||
* example: 2026-10-15T00:00:00.000Z
|
||||
* status:
|
||||
* type: string
|
||||
* enum: [active, completed, archived]
|
||||
* example: active
|
||||
* admin:
|
||||
* $ref: '#/components/schemas/ProjectAdminSummary'
|
||||
@@ -165,10 +171,6 @@ const router = express.Router();
|
||||
* total_tasks:
|
||||
* type: integer
|
||||
* example: 3
|
||||
* task_done:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* description: Number of completed tasks. For interns, this counts tasks submitted by the logged-in intern. For admin/mentor, this counts tasks submitted by all active project members.
|
||||
* created_at:
|
||||
* type: string
|
||||
* format: date-time
|
||||
@@ -177,6 +179,15 @@ const router = express.Router();
|
||||
* type: string
|
||||
* format: date-time
|
||||
* example: 2026-06-10T10:00:00.000Z
|
||||
* ProjectListItem:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/ProjectBaseItem'
|
||||
* - type: object
|
||||
* properties:
|
||||
* task_done:
|
||||
* type: integer
|
||||
* example: 2
|
||||
* description: Number of completed tasks. For interns, this counts tasks submitted by the logged-in intern. For admin/mentor, this counts tasks submitted by all active project members.
|
||||
* ProjectMemberUser:
|
||||
* type: object
|
||||
* properties:
|
||||
@@ -256,7 +267,7 @@ const router = express.Router();
|
||||
* example: 2026-06-10T10:00:00.000Z
|
||||
* ProjectDetail:
|
||||
* allOf:
|
||||
* - $ref: '#/components/schemas/ProjectListItem'
|
||||
* - $ref: '#/components/schemas/ProjectBaseItem'
|
||||
* - type: object
|
||||
* properties:
|
||||
* members:
|
||||
@@ -523,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
|
||||
@@ -534,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:
|
||||
|
||||
Reference in New Issue
Block a user