consistent date parsing

This commit is contained in:
pandeymangg
2026-01-19 18:39:16 +05:30
parent 9cc27589a2
commit ae22194a3f
2 changed files with 123 additions and 111 deletions

View File

@@ -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)

View File

@@ -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);
}
};