import "server-only"; import { validateBlockResponses } from "@formbricks/surveys/validation"; import { TResponseData } from "@formbricks/types/responses"; import { TSurveyBlock } from "@formbricks/types/surveys/blocks"; import { TSurveyQuestion } from "@formbricks/types/surveys/types"; import { TValidationErrorMap } from "@formbricks/types/surveys/validation-rules"; import { transformQuestionsToBlocks } from "@/app/lib/api/survey-transformation"; import { getElementsFromBlocks } from "@/lib/survey/utils"; import { ApiErrorDetails } from "@/modules/api/v2/types/api-error"; /** * Validates response data against survey validation rules. * Only validates elements that have data in responseData - never validates * all survey elements regardless of completion status. * * @param blocks - Survey blocks containing elements with validation rules (preferred) * @param responseData - Response data to validate (keyed by element ID) * @param languageCode - Language code for error messages (defaults to "en") * @param questions - Survey questions (legacy format, used as fallback if blocks are empty) * @returns Validation error map keyed by element ID, or null if validation passes */ export const validateResponseData = ( blocks: TSurveyBlock[] | undefined | null, responseData: TResponseData, languageCode: string = "en", questions?: TSurveyQuestion[] | undefined | null ): TValidationErrorMap | null => { // Use blocks if available, otherwise transform questions to blocks let blocksToUse: TSurveyBlock[] = []; if (blocks && blocks.length > 0) { blocksToUse = blocks; } else if (questions && questions.length > 0) { // Transform legacy questions format to blocks for validation blocksToUse = transformQuestionsToBlocks(questions, []); } else { // No blocks or questions to validate against return null; } // Extract elements from blocks const allElements = getElementsFromBlocks(blocksToUse); // Always validate only elements that are present in responseData const elementsToValidate = allElements.filter((element) => Object.keys(responseData).includes(element.id)); // Validate selected elements const errorMap = validateBlockResponses(elementsToValidate, responseData, languageCode); // Return null if no errors (validation passed), otherwise return error map return Object.keys(errorMap).length === 0 ? null : errorMap; }; /** * Converts validation error map to V2 API error response format * * @param errorMap - Validation error map from validateResponseData * @returns V2 API error response details */ export const formatValidationErrorsForV2Api = (errorMap: TValidationErrorMap) => { const details: ApiErrorDetails = []; for (const [elementId, errors] of Object.entries(errorMap)) { // Include all error messages for each element for (const error of errors) { details.push({ field: `response.data.${elementId}`, issue: error.message, meta: { elementId, ruleId: error.ruleId, ruleType: error.ruleType, }, }); } } return details; }; /** * Converts validation error map to V1 API error response format * * @param errorMap - Validation error map from validateResponseData * @returns V1 API error details as Record */ export const formatValidationErrorsForV1Api = (errorMap: TValidationErrorMap): Record => { const details: Record = {}; for (const [elementId, errors] of Object.entries(errorMap)) { // Combine all error messages for each element const errorMessages = errors.map((error) => error.message).join("; "); details[`response.data.${elementId}`] = errorMessages; } return details; };