mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-04 10:30:00 -06:00
consistent date parsing
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user