import { z } from "zod"; // --------------------------------------------------------------------------- // Role level // --------------------------------------------------------------------------- /** Valid role level values */ export const VALID_ROLE_LEVELS = new Set([10, 20, 30, 40, 50]); /** Role level — coerces string/number to valid RoleLevel (10|20|30|40|50) */ export const roleLevel = z.coerce .number() .int() .refine((n): n is 10 | 20 | 30 | 40 | 50 => VALID_ROLE_LEVELS.has(n), { message: "Invalid role level. Must be 10, 20, 30, 40, or 50", }); // --------------------------------------------------------------------------- // Pagination // --------------------------------------------------------------------------- /** Pagination query params — cursor-based */ export const cursorPaginationQuery = z .object({ cursor: z.string().max(2048).optional().meta({ description: "Opaque cursor for pagination" }), limit: z.coerce.number().int().min(1).max(100).optional().default(50).meta({ description: "Maximum number of items to return (1-100, default 50)", }), }) .meta({ id: "CursorPaginationQuery" }); /** Pagination query params — offset-based */ export const offsetPaginationQuery = z .object({ limit: z.coerce.number().int().min(1).max(100).optional().default(50), offset: z.coerce.number().int().min(0).optional().default(0), }) .meta({ id: "OffsetPaginationQuery" }); // --------------------------------------------------------------------------- // Shared primitives // --------------------------------------------------------------------------- /** Slug pattern: lowercase letters, digits, underscores; starts with letter */ export const slugPattern = /^[a-z][a-z0-9_]*$/; /** Matches http(s) scheme at start of URL */ const HTTP_SCHEME_RE = /^https?:\/\//i; /** Validates that a URL string uses http or https scheme. Rejects javascript:/data: URI XSS vectors. */ export const httpUrl = z .string() .url() .refine((url) => HTTP_SCHEME_RE.test(url), "URL must use http or https"); /** BCP 47 locale code — language with optional script/region subtags (e.g. en, en-US, pt-BR, es-419, zh-Hant) */ export const localeCode = z .string() .regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "Invalid locale code") .transform((v) => v.toLowerCase()); /** Shared `?locale=xx` query shape for endpoints that filter by locale. */ export const localeFilterQuery = z .object({ locale: z.string().min(1).optional(), }) .meta({ id: "LocaleFilterQuery" }); // --------------------------------------------------------------------------- // OpenAPI: Shared response schemas // --------------------------------------------------------------------------- /** Standard API error response */ export const apiErrorSchema = z .object({ error: z.object({ code: z.string().meta({ description: "Machine-readable error code", example: "NOT_FOUND" }), message: z.string().meta({ description: "Human-readable error message" }), }), }) .meta({ id: "ApiError" }); /** Wrap a data schema in the standard success envelope: { data: T } */ export function successEnvelope(dataSchema: T) { return z.object({ data: dataSchema }); } /** Standard delete response */ export const deleteResponseSchema = z.object({ deleted: z.literal(true) }).meta({ id: "DeleteResponse", }); /** Standard count response */ export const countResponseSchema = z .object({ count: z.number().int().min(0) }) .meta({ id: "CountResponse" });