136 lines
3.3 KiB
TypeScript
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 !== "billed" && value !== "non_billable") {
|
|
throw badRequest("billingStatus must be 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;
|
|
}
|