SPB Git

Repository Public

schemas.ts 5.3 KB 86 lines typescript

1import { z } from "zod";23export const isoInstant = z.string().refine((s) => { try { Temporal.Instant.from(s); return true; } catch { return false; } }, "invalid instant");4export const toMs = (s: string) => Temporal.Instant.from(s).epochMilliseconds;5export const localDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/).refine((s) => { try { Temporal.PlainDate.from(s); return true; } catch { return false; } }, "invalid date");6export const localDateTime = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/).refine((s) => { try { Temporal.PlainDateTime.from(s); return true; } catch { return false; } }, "invalid date-time");7export const timezone = z.string().max(64).refine((tz) => { try { new Intl.DateTimeFormat("en", { timeZone: tz }); return true; } catch { return false; } }, "unknown time zone");8export const locale = z.enum(["fr", "en"]);9export const email = z.string().trim().toLowerCase().max(254).pipe(z.email());10export const color = z.string().regex(/^#[0-9a-fA-F]{6}$/);11const short = (n: number) => z.string().trim().max(n);1213export const question = z.object({ key: z.string().regex(/^[a-z0-9_]{1,32}$/), label: short(120).min(1), label_en: short(120).optional().default(""), kind: z.enum(["text", "textarea"]), required: z.boolean() });1415export const eventTypeInput = z.object({16  slug: z.string().regex(/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/),17  title: short(120).min(1), title_en: short(120).default(""),18  description: short(2000).default(""), description_en: short(2000).default(""),19  duration_min: z.number().int().min(5).max(720),20  color: color.default("#2257e0"),21  location_kind: z.enum(["video", "phone", "in_person"]),22  location_detail: short(500).default(""),23  instructions: short(2000).default(""), instructions_en: short(2000).default(""),24  languages: z.array(locale).min(1).max(2).default(["fr"]),25  approval: z.enum(["manual", "auto"]).default("manual"),26  hold_policy: z.enum(["hold", "none"]).default("hold"),27  hold_ttl_hours: z.number().int().min(1).max(720).default(48),28  buffer_before_min: z.number().int().min(0).max(240).default(0),29  buffer_after_min: z.number().int().min(0).max(240).default(0),30  slot_step_min: z.number().int().min(5).max(240).default(30),31  min_notice_min: z.number().int().min(0).max(43200).default(240),32  horizon_days: z.number().int().min(1).max(365).default(60),33  daily_limit: z.number().int().min(1).max(100).nullable().default(null),34  weekly_limit: z.number().int().min(1).max(500).nullable().default(null),35  questions: z.array(question).max(10).default([]).refine((q) => new Set(q.map((x) => x.key)).size === q.length, "duplicate question key"),36  ask_organization: z.boolean().default(true),37  require_reason: z.boolean().default(true),38  allow_visitor_cancel: z.boolean().default(true),39  allow_visitor_reschedule: z.boolean().default(true),40  reminder_hours: z.number().int().min(1).max(336).nullable().default(null),41  custom_availability: z.boolean().default(false),42  active: z.boolean().default(true),43  position: z.number().int().min(0).max(1000).default(0),44});4546export const rulesInput = z.object({47  eventTypeId: z.string().uuid().nullable().default(null),48  rules: z.array(z.object({ weekday: z.number().int().min(1).max(7), startMinute: z.number().int().min(0).max(1439), endMinute: z.number().int().min(1).max(1440) })49    .refine((r) => r.endMinute > r.startMinute, "end must be after start")).max(70),50});5152export const exceptionInput = z.discriminatedUnion("mode", [53  z.object({ mode: z.literal("range"), kind: z.enum(["open", "closed"]), eventTypeId: z.string().uuid().nullable().default(null), startsLocal: localDateTime, endsLocal: localDateTime, allDay: z.boolean().default(false), note: short(200).default("") })54    .refine((e) => e.endsLocal > e.startsLocal, "end must be after start"),55  z.object({ mode: z.literal("weekly"), kind: z.enum(["open", "closed"]), eventTypeId: z.string().uuid().nullable().default(null), weekdays: z.array(z.number().int().min(1).max(7)).min(1).max(7), startMinute: z.number().int().min(0).max(1439), durationMin: z.number().int().min(1).max(1440), validFrom: localDate, validUntil: localDate.nullable().default(null), note: short(200).default("") }),56]);5758export const settingsInput = z.object({59  timezone, locale,60  public_name: short(80).min(1), public_headline: short(160).default(""), public_headline_en: short(160).default(""),61  public_bio: short(1500).default(""), public_bio_en: short(1500).default(""),62  brand_color: color,63  notify_email: email.nullable(), reply_to: email.nullable(),64  retention_days: z.number().int().min(30).max(3650),65  allowed_email_domains: z.array(z.string().trim().toLowerCase().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/)).max(50),66  blocked_email_domains: z.array(z.string().trim().toLowerCase().regex(/^[a-z0-9.-]+\.[a-z]{2,}$/)).max(200),67  external_stale_minutes: z.number().int().min(5).max(10080),68  external_stale_policy: z.enum(["conservative", "ignore"]),69});7071export const submitInput = z.object({72  slug: z.string().max(64),73  start: isoInstant,74  name: short(120).min(2),75  email,76  organization: short(160).default(""),77  reason: short(2000).default(""),78  answers: z.record(z.string().regex(/^[a-z0-9_]{1,32}$/), short(2000)).default({}),79  visitorTz: timezone,80  locale,81  idempotencyKey: z.string().uuid(),82  consent: z.literal(true),83  formToken: z.string().max(200),84  website: z.string().max(200).default(""),   // honeypot: must stay empty85});86
Files main
Menu

Theme