/** * TASK-0106 §2/§3 — question DTOs. * * `validation` is a free-form object at the DTO level ON PURPOSE: the * authoritative check is the SHARED per-type schema * (`@factory/shared/forms::QUESTION_VALIDATION_SCHEMAS` + the worker ReDoS * probe) re-run by the service on EVERY write (spec: "server re-validates * on every write — a mismatched propertyId is 422"). The DTO stays a thin * envelope validator (object shape only); per-type shape + cross-field * rules + the ReDoS budget live in exactly one place (shared/forms.ts). */ import { z } from "zod"; import { FORM_QUESTION_TYPES, type FormQuestionType } from "@factory/shared"; export const QuestionValidationDto = z.record(z.unknown()).optional(); const baseQuestion = { label: z.string().min(1).max(200), description: z.string().max(500).optional(), required: z.boolean().default(false), propertyId: z.string().min(1).nullable().optional(), validation: QuestionValidationDto, placeholder: z.string().max(200).optional() }; export const CreateQuestionDto = z.object({ type: z.enum(FORM_QUESTION_TYPES), ...baseQuestion }); export type CreateQuestionInput = z.infer; export const UpdateQuestionDto = z .object({ type: z.enum(FORM_QUESTION_TYPES).optional(), ...baseQuestion, position: z.string().min(1).max(512).optional() }) .refine((q) => Object.keys(q).length > 1, { message: "nothing to update" }); export type UpdateQuestionInput = z.infer; export const ReorderQuestionsDto = z .object({ /** The question being moved. */ fromId: z.string().min(1), /** * Drop `fromId` BEFORE the question with this id; null/omitted = append * to the end. The API's fractional positions engine (Phase-4 reuse) * computes the new position value. */ toId: z.string().min(1).nullable().optional() }) .refine((q) => q.toId === undefined || q.toId === null || q.fromId !== q.toId, { message: "fromId and toId must differ" }); export type ReorderQuestionsInput = z.infer; export { FORM_QUESTION_TYPES }; export type { FormQuestionType };