From ae22194a3f47f8c0a8a2c9503ec49e5dcfc6aa24 Mon Sep 17 00:00:00 2001 From: pandeymangg Date: Mon, 19 Jan 2026 18:39:16 +0530 Subject: [PATCH] consistent date parsing --- .../ee/contacts/lib/detect-attribute-type.ts | 13 +- .../contacts/lib/validate-attribute-type.ts | 221 ++++++++++-------- 2 files changed, 123 insertions(+), 111 deletions(-) diff --git a/apps/web/modules/ee/contacts/lib/detect-attribute-type.ts b/apps/web/modules/ee/contacts/lib/detect-attribute-type.ts index 5bed32ddac..446bdb0138 100644 --- a/apps/web/modules/ee/contacts/lib/detect-attribute-type.ts +++ b/apps/web/modules/ee/contacts/lib/detect-attribute-type.ts @@ -4,7 +4,7 @@ import { TContactAttributeDataType } from "@formbricks/types/contact-attribute-k * Parses a date string in DD-MM-YYYY or MM-DD-YYYY format. * Uses heuristics to disambiguate between formats. */ -const parseDateFromParts = (part1: number, part2: number, part3: number): Date | null => { +export const parseDateFromParts = (part1: number, part2: number, part3: number): Date | null => { // Heuristic: If first part > 12, it's likely DD-MM-YYYY if (part1 > 12) { return new Date(part3, part2 - 1, part1); @@ -25,10 +25,7 @@ const parseDateFromParts = (part1: number, part2: number, part3: number): Date | return new Date(part3, part1 - 1, part2); }; -/** - * Attempts to parse a string as a date in various formats. - */ -const tryParseDate = (stringValue: string): Date | null => { +export const tryParseDate = (stringValue: string): Date | null => { // Try ISO format first (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss) if (/^\d{4}[-/]\d{2}[-/]\d{2}/.test(stringValue)) { return new Date(stringValue); @@ -49,9 +46,9 @@ const tryParseDate = (stringValue: string): Date | null => { * Used for first-time attribute creation to infer the dataType. * * Supported date formats: - * - ISO 8601: YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ - * - European: DD-MM-YYYY or DD/MM/YYYY - * - American: MM-DD-YYYY or MM/DD/YYYY + * - YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ + * - DD-MM-YYYY or DD/MM/YYYY + * - MM-DD-YYYY or MM/DD/YYYY * * @param value - The attribute value to detect the type of (string, number, or Date) * @returns The detected data type (string, number, or date) diff --git a/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts b/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts index 0251746e12..32320e8e38 100644 --- a/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts +++ b/apps/web/modules/ee/contacts/lib/validate-attribute-type.ts @@ -1,4 +1,7 @@ import { TContactAttributeDataType } from "@formbricks/types/contact-attribute-key"; +import { tryParseDate } from "@/modules/ee/contacts/lib/detect-attribute-type"; + +type TRawValue = string | number | Date; /** * Result of attribute value validation @@ -17,17 +20,6 @@ export type TAttributeValidationResult = error: string; }; -/** - * Checks if a string value is a valid ISO 8601 date - */ -const isValidISODate = (value: string): boolean => { - if (!/^\d{4}-\d{2}-\d{2}/.test(value)) { - return false; - } - const date = new Date(value); - return !Number.isNaN(date.getTime()); -}; - /** * Checks if a string value is a valid number */ @@ -36,6 +28,112 @@ const isValidNumber = (value: string): boolean => { return trimmed !== "" && !Number.isNaN(Number(trimmed)); }; +/** + * Converts any value to a string representation + */ +const convertToString = (value: TRawValue): string => { + if (value instanceof Date) return value.toISOString(); + if (typeof value === "number") return String(value); + return value; +}; + +/** + * Gets a human-readable type name for error messages + */ +const getTypeName = (value: TRawValue): string => { + if (value instanceof Date) return "Date"; + return typeof value; +}; + +/** + * Validates and parses a string type attribute + */ +const validateStringType = (value: TRawValue): TAttributeValidationResult => ({ + valid: true, + parsedValue: { + value: convertToString(value), + valueNumber: null, + valueDate: null, + }, +}); + +/** + * Validates and parses a number type attribute + */ +const validateNumberType = (value: TRawValue, attributeKey: string): TAttributeValidationResult => { + if (typeof value === "number") { + return { + valid: true, + parsedValue: { + value: String(value), + valueNumber: value, + valueDate: null, + }, + }; + } + + if (typeof value === "string" && isValidNumber(value)) { + const numericValue = Number(value.trim()); + return { + valid: true, + parsedValue: { + value: String(numericValue), + valueNumber: numericValue, + valueDate: null, + }, + }; + } + + return { + valid: false, + error: `Attribute '${attributeKey}' expects a number. Received: ${getTypeName(value)} value '${String(value)}'`, + }; +}; + +/** + * Validates and parses a date type attribute + * Supports multiple formats: ISO 8601, DD-MM-YYYY, MM-DD-YYYY, etc. + */ +const validateDateType = (value: TRawValue, attributeKey: string): TAttributeValidationResult => { + // Handle Date objects + if (value instanceof Date) { + if (Number.isNaN(value.getTime())) { + return { + valid: false, + error: `Attribute '${attributeKey}' expects a valid date. Received: Invalid Date`, + }; + } + return { + valid: true, + parsedValue: { + value: value.toISOString(), + valueNumber: null, + valueDate: value, + }, + }; + } + + // Handle string values with flexible parsing + if (typeof value === "string") { + const dateValue = tryParseDate(value.trim()); + if (dateValue && !Number.isNaN(dateValue.getTime())) { + return { + valid: true, + parsedValue: { + value: dateValue.toISOString(), + valueNumber: null, + valueDate: dateValue, + }, + }; + } + } + + return { + valid: false, + error: `Attribute '${attributeKey}' expects a valid date. Received: ${getTypeName(value)} value '${String(value)}'`, + }; +}; + /** * Validates that a value matches the expected data type and parses it for storage. * Used for subsequent writes to an existing attribute key. @@ -46,101 +144,18 @@ const isValidNumber = (value: string): boolean => { * @returns Validation result with parsed values for storage or error message */ export const validateAndParseAttributeValue = ( - value: string | number | Date, + value: TRawValue, expectedDataType: TContactAttributeDataType, attributeKey: string ): TAttributeValidationResult => { switch (expectedDataType) { - case "string": { - // String type accepts any value - convert to string - let stringValue: string; - - if (value instanceof Date) { - stringValue = value.toISOString(); - } else if (typeof value === "number") { - stringValue = String(value); - } else { - stringValue = value; - } - - return { - valid: true, - parsedValue: { - value: stringValue, - valueNumber: null, - valueDate: null, - }, - }; - } - - case "number": { - // Number type expects a numeric value - let numericValue: number; - - if (typeof value === "number") { - numericValue = value; - } else if (typeof value === "string" && isValidNumber(value)) { - numericValue = Number(value.trim()); - } else { - const receivedType = value instanceof Date ? "Date" : typeof value; - return { - valid: false, - error: `Attribute '${attributeKey}' expects a number. Received: ${receivedType} value '${String(value)}'`, - }; - } - - return { - valid: true, - parsedValue: { - value: String(numericValue), // Keep string column for backwards compatibility - valueNumber: numericValue, - valueDate: null, - }, - }; - } - - case "date": { - // Date type expects a Date object or valid ISO date string - let dateValue: Date; - - if (value instanceof Date) { - if (Number.isNaN(value.getTime())) { - return { - valid: false, - error: `Attribute '${attributeKey}' expects a valid date. Received: Invalid Date`, - }; - } - dateValue = value; - } else if (typeof value === "string" && isValidISODate(value)) { - dateValue = new Date(value); - } else { - const receivedType = typeof value; - return { - valid: false, - error: `Attribute '${attributeKey}' expects a date (ISO 8601 string or Date object). Received: ${receivedType} value '${String(value)}'`, - }; - } - - return { - valid: true, - parsedValue: { - value: dateValue.toISOString(), // Keep string column for backwards compatibility - valueNumber: null, - valueDate: dateValue, - }, - }; - } - - default: { - // Unknown type - treat as string - return { - valid: true, - parsedValue: { - value: String(value), - valueNumber: null, - valueDate: null, - }, - }; - } + case "string": + return validateStringType(value); + case "number": + return validateNumberType(value, attributeKey); + case "date": + return validateDateType(value, attributeKey); + default: + return validateStringType(value); } };