143 lines
5.2 KiB
TypeScript
143 lines
5.2 KiB
TypeScript
const VALID_PROFILES = ['auto', 'fast', 'quality'] as const;
|
|
const VALID_OUTPUT_FORMATS = ['text', 'markdown', 'json'] as const;
|
|
const VALID_ASK_POLICIES = ['low', 'high'] as const;
|
|
const VALID_PRIORITIES = ['low', 'medium', 'high'] as const;
|
|
|
|
const MAX_BODY_LENGTH = 100_000;
|
|
const MAX_TITLE_LENGTH = 200;
|
|
const MAX_COMMENT_LENGTH = 100_000;
|
|
|
|
export function parseTaskId(raw: string): number | null {
|
|
const n = Number(raw);
|
|
if (!Number.isInteger(n) || n <= 0) return null;
|
|
return n;
|
|
}
|
|
|
|
export interface ValidatedCreateTask {
|
|
body: string;
|
|
title?: string;
|
|
piece?: string;
|
|
profile?: typeof VALID_PROFILES[number];
|
|
outputFormat?: typeof VALID_OUTPUT_FORMATS[number];
|
|
askPolicy?: typeof VALID_ASK_POLICIES[number];
|
|
priority?: typeof VALID_PRIORITIES[number];
|
|
attachments?: Array<{ name: string; contentBase64: string }>;
|
|
}
|
|
|
|
type ValidationResult =
|
|
| { valid: true; data: ValidatedCreateTask }
|
|
| { valid: false; error: string };
|
|
|
|
export function validateCreateTaskBody(raw: unknown): ValidationResult {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return { valid: false, error: 'Request body must be an object' };
|
|
}
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
if (typeof obj.body !== 'string' || obj.body.trim().length === 0) {
|
|
return { valid: false, error: 'body is required' };
|
|
}
|
|
if (obj.body.length > MAX_BODY_LENGTH) {
|
|
return { valid: false, error: `body must be ${MAX_BODY_LENGTH} characters or less` };
|
|
}
|
|
|
|
if (obj.title !== undefined && obj.title !== null) {
|
|
if (typeof obj.title !== 'string') {
|
|
return { valid: false, error: 'title must be a string' };
|
|
}
|
|
if (obj.title.length > MAX_TITLE_LENGTH) {
|
|
return { valid: false, error: `title must be ${MAX_TITLE_LENGTH} characters or less` };
|
|
}
|
|
}
|
|
|
|
if (obj.profile !== undefined && obj.profile !== null) {
|
|
if (!(VALID_PROFILES as readonly string[]).includes(String(obj.profile))) {
|
|
return { valid: false, error: `profile must be one of: ${VALID_PROFILES.join(', ')}` };
|
|
}
|
|
}
|
|
if (obj.outputFormat !== undefined && obj.outputFormat !== null) {
|
|
if (!(VALID_OUTPUT_FORMATS as readonly string[]).includes(String(obj.outputFormat))) {
|
|
return { valid: false, error: `outputFormat must be one of: ${VALID_OUTPUT_FORMATS.join(', ')}` };
|
|
}
|
|
}
|
|
if (obj.askPolicy !== undefined && obj.askPolicy !== null) {
|
|
if (!(VALID_ASK_POLICIES as readonly string[]).includes(String(obj.askPolicy))) {
|
|
return { valid: false, error: `askPolicy must be one of: ${VALID_ASK_POLICIES.join(', ')}` };
|
|
}
|
|
}
|
|
if (obj.priority !== undefined && obj.priority !== null) {
|
|
if (!(VALID_PRIORITIES as readonly string[]).includes(String(obj.priority))) {
|
|
return { valid: false, error: `priority must be one of: ${VALID_PRIORITIES.join(', ')}` };
|
|
}
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
data: {
|
|
body: obj.body as string,
|
|
title: obj.title as string | undefined,
|
|
piece: obj.piece as string | undefined,
|
|
profile: obj.profile as ValidatedCreateTask['profile'],
|
|
outputFormat: obj.outputFormat as ValidatedCreateTask['outputFormat'],
|
|
askPolicy: obj.askPolicy as ValidatedCreateTask['askPolicy'],
|
|
priority: obj.priority as ValidatedCreateTask['priority'],
|
|
attachments: obj.attachments as ValidatedCreateTask['attachments'],
|
|
},
|
|
};
|
|
}
|
|
|
|
export function validateCommentBody(raw: unknown): { valid: true; body: string; author: string; attachments?: Array<{ name: string; contentBase64: string }> } | { valid: false; error: string } {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return { valid: false, error: 'Request body must be an object' };
|
|
}
|
|
const obj = raw as Record<string, unknown>;
|
|
const body = String(obj.body ?? '').trim();
|
|
if (!body) {
|
|
return { valid: false, error: 'body is required' };
|
|
}
|
|
if (body.length > MAX_COMMENT_LENGTH) {
|
|
return { valid: false, error: `body must be ${MAX_COMMENT_LENGTH} characters or less` };
|
|
}
|
|
const author = String(obj.author ?? 'user').trim() || 'user';
|
|
const attachments = Array.isArray(obj.attachments) ? obj.attachments as Array<{ name: string; contentBase64: string }> : undefined;
|
|
return { valid: true, body, author, attachments };
|
|
}
|
|
|
|
export type ValidatedFeedback = {
|
|
rating: 'good' | 'bad';
|
|
tags: string[];
|
|
comment: string | null;
|
|
};
|
|
|
|
type FeedbackValidationResult =
|
|
| { valid: true; data: ValidatedFeedback }
|
|
| { valid: false; error: string };
|
|
|
|
export function validateFeedbackBody(raw: unknown): FeedbackValidationResult {
|
|
if (!raw || typeof raw !== 'object') {
|
|
return { valid: false, error: 'Request body must be an object' };
|
|
}
|
|
const obj = raw as Record<string, unknown>;
|
|
|
|
if (obj.rating !== 'good' && obj.rating !== 'bad') {
|
|
return { valid: false, error: "rating must be 'good' or 'bad'" };
|
|
}
|
|
|
|
if (!Array.isArray(obj.tags) || obj.tags.some((t: unknown) => typeof t !== 'string')) {
|
|
return { valid: false, error: 'tags must be an array of strings' };
|
|
}
|
|
if (obj.tags.length > 10) {
|
|
return { valid: false, error: 'tags must have at most 10 items' };
|
|
}
|
|
|
|
const comment = obj.comment != null ? String(obj.comment) : null;
|
|
if (comment && comment.length > 1000) {
|
|
return { valid: false, error: 'comment must be at most 1000 characters' };
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
data: { rating: obj.rating, tags: obj.tags as string[], comment },
|
|
};
|
|
}
|