Files
best-choice/server/src/validation/projects.ts
T
2025-10-13 14:04:22 +03:00

120 lines
3.6 KiB
TypeScript

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<ProjectCreateInput>;
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<string, unknown>;
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<string, unknown>;
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';
};