import { HttpError } from '../errors/http-error.js'; const projectStatuses = new Set(['active', 'archived']); const isStringArray = (value: unknown): value is string[] => { return ( Array.isArray(value) && value.every((item) => typeof item === 'string' && item.trim().length > 0) ); }; export interface ProjectCreateInput { name: string; description: string | null; status: 'active' | 'archived'; projectAttributes: string[]; projectPriorityRules: unknown; } export type ProjectUpdateInput = Partial; export const parseProjectCreatePayload = (payload: unknown): ProjectCreateInput => { if (payload === null || typeof payload !== 'object') { throw new HttpError(400, 'Body must be an object'); } const { name, description = null, status = 'active', attributes, priorityRules } = payload as Record; if (typeof name !== 'string' || name.trim().length === 0 || name.length > 150) { throw new HttpError(400, 'name must be a non-empty string up to 150 characters'); } if (description !== null && typeof description !== 'string') { throw new HttpError(400, 'description must be a string or null'); } if (typeof status !== 'string' || !projectStatuses.has(status)) { throw new HttpError(400, 'status must be one of active, archived'); } const projectAttributes = attributes ?? []; if (!isStringArray(projectAttributes)) { throw new HttpError(400, 'attributes must be an array of non-empty strings'); } return { name: name.trim(), description: description === null ? null : description, status: status as 'active' | 'archived', projectAttributes, projectPriorityRules: priorityRules ?? [] }; }; export const parseProjectUpdatePayload = ( payload: unknown ): ProjectUpdateInput => { if (payload === null || typeof payload !== 'object') { throw new HttpError(400, 'Body must be an object'); } const result: ProjectUpdateInput = {}; const { name, description, status, attributes, priorityRules } = payload as Record; if (name !== undefined) { if (typeof name !== 'string' || name.trim().length === 0 || name.length > 150) { throw new HttpError(400, 'name must be a non-empty string up to 150 characters'); } result.name = name.trim(); } if (description !== undefined) { if (description !== null && typeof description !== 'string') { throw new HttpError(400, 'description must be a string or null'); } result.description = description === null ? null : (description as string); } if (status !== undefined) { if (typeof status !== 'string' || !projectStatuses.has(status)) { throw new HttpError(400, 'status must be one of active, archived'); } result.status = status as 'active' | 'archived'; } if (attributes !== undefined) { if (!isStringArray(attributes)) { throw new HttpError(400, 'attributes must be an array of non-empty strings'); } result.projectAttributes = attributes; } if (priorityRules !== undefined) { result.projectPriorityRules = priorityRules; } if (Object.keys(result).length === 0) { throw new HttpError(400, 'At least one field must be provided for update'); } return result; }; export const parseProjectStatusFilter = (value: unknown): 'active' | 'archived' | undefined => { if (value === undefined) { return undefined; } if (typeof value !== 'string' || !projectStatuses.has(value)) { throw new HttpError(400, 'status filter must be one of active, archived'); } return value as 'active' | 'archived'; };