Files
tickettracker/backend/src/validation.ts
T
2026-09-23 11:09:10 +00:00

136 lines
3.3 KiB
TypeScript

export const ticketPattern = /^Ticket#\d{6}$/;
export const trackableTicketPattern = /^(Ticket#\d{6}|Fix#\d+)$/;
export const monthPattern = /^\d{4}-\d{2}$/;
export const dayPattern = /^\d{4}-\d{2}-\d{2}$/;
export const usernamePattern = /^\S{3,40}$/u;
export function requireString(value: unknown, field: string) {
if (typeof value !== "string" || value.trim().length === 0) {
throw badRequest(`${field} is required`);
}
return value.trim();
}
export function parseTicketNumber(value: unknown) {
const ticketNumber = requireString(value, "ticketNumber");
if (!ticketPattern.test(ticketNumber)) {
throw badRequest("ticketNumber must match Ticket#XXXXXX");
}
return ticketNumber;
}
export function parseTrackableTicketNumber(value: unknown) {
const ticketNumber = requireString(value, "ticketNumber");
if (!trackableTicketPattern.test(ticketNumber)) {
throw badRequest("ticketNumber must match Ticket#XXXXXX or Fix#ID");
}
return ticketNumber;
}
export function parseUsername(value: unknown) {
const username = requireString(value, "username");
if (!usernamePattern.test(username)) {
throw badRequest("username must be 3-40 characters without spaces");
}
return username;
}
export function parseMonth(value: string) {
if (!monthPattern.test(value)) {
throw badRequest("month must use YYYY-MM");
}
const monthStart = `${value}-01`;
const date = new Date(`${monthStart}T00:00:00.000Z`);
if (Number.isNaN(date.getTime()) || date.getUTCMonth() + 1 !== Number(value.slice(5, 7))) {
throw badRequest("month is invalid");
}
const next = new Date(date);
next.setUTCMonth(next.getUTCMonth() + 1);
return {
label: value,
start: monthStart,
startIso: date.toISOString(),
endIso: next.toISOString()
};
}
export function parseDay(value: string) {
if (!dayPattern.test(value)) {
throw badRequest("day must use YYYY-MM-DD");
}
const date = new Date(`${value}T00:00:00.000Z`);
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
throw badRequest("day is invalid");
}
const next = new Date(date);
next.setUTCDate(next.getUTCDate() + 1);
return {
label: value,
start: value,
startIso: date.toISOString(),
endIso: next.toISOString()
};
}
export function parsePositiveInteger(value: unknown, field: string) {
const number = Number(value);
if (!Number.isInteger(number) || number < 0) {
throw badRequest(`${field} must be a positive integer`);
}
return number;
}
export function parseWorkType(value: unknown) {
if (value !== "support" && value !== "consulting") {
throw badRequest("workType must be support or consulting");
}
return value;
}
export function parseBillingStatus(value: unknown) {
if (value === null) {
return null;
}
if (value !== "billable" && value !== "billed" && value !== "non_billable") {
throw badRequest("billingStatus must be billable, billed, non_billable or null");
}
return value;
}
export function parseIsoDate(value: unknown, field: string) {
const raw = requireString(value, field);
const date = new Date(raw);
if (Number.isNaN(date.getTime())) {
throw badRequest(`${field} must be an ISO date`);
}
return date;
}
export function badRequest(message: string) {
const error = new Error(message) as Error & { status?: number };
error.status = 400;
return error;
}