common
9 months ago
components
8 months ago
fields
8 months ago
hooks
8 months ago
ajv.ts
9 months ago
types.ts
8 months ago
utils.ts
9 months ago
ajv.ts
542 lines
| 1 | import {__, sprintf} from '@wordpress/i18n'; |
| 2 | import {JSONSchemaType} from 'ajv'; |
| 3 | import addErrors from 'ajv-errors'; |
| 4 | import addFormats from 'ajv-formats'; |
| 5 | |
| 6 | /** |
| 7 | * Create an AJV resolver for react-hook-form with WordPress REST API schema |
| 8 | * |
| 9 | * This function creates a custom resolver that works with WordPress REST API schemas (Draft 03/04) |
| 10 | * and provides enhanced frontend validation using AJV (Draft 7/2019-09). It handles: |
| 11 | * - Transforming WordPress Draft 03/04 schema to Draft 7/2019-09 for AJV compatibility |
| 12 | * - Data transformation before validation (string numbers, enum handling, etc.) |
| 13 | * - Error handling and fallback behavior |
| 14 | * |
| 15 | * Key advantage: WordPress REST API supports most JSON Schema Draft 4 features but lacks |
| 16 | * some advanced features (if/then/else, allOf, not) that AJV can provide for enhanced frontend validation. |
| 17 | * |
| 18 | * @since 4.10.0 Refactor transformWordPressSchemaToDraft7 to handle readonly/readOnly fields and conditionally remove enum from nullable fields when value is null to prevent AJV conflicts |
| 19 | * @since 4.9.0 |
| 20 | * |
| 21 | * @param schema - The JSON Schema from WordPress REST API |
| 22 | * @returns A resolver function compatible with react-hook-form |
| 23 | */ |
| 24 | export function ajvResolver(schema: JSONSchemaType<any>) { |
| 25 | return (data: any) => { |
| 26 | try { |
| 27 | // Ensure we have valid data to validate |
| 28 | if (!data || typeof data !== 'object') { |
| 29 | return {values: data || {}, errors: {}}; |
| 30 | } |
| 31 | |
| 32 | const transformedData = transformFormDataForValidation(data, schema); |
| 33 | const ajv = configureAjvForWordPress(); |
| 34 | const transformedSchema = transformWordPressSchemaToDraft7(schema, data); |
| 35 | const validate = ajv.compile(transformedSchema); |
| 36 | const valid = validate(transformedData); |
| 37 | |
| 38 | if (valid) { |
| 39 | return {values: transformedData, errors: {}}; |
| 40 | } else { |
| 41 | console.error('🔴 Validation failed, errors:', validate.errors); |
| 42 | const errors: any = {}; |
| 43 | if (validate.errors) { |
| 44 | validate.errors.forEach((error) => { |
| 45 | const path = error.instancePath || error.schemaPath; |
| 46 | if (path) { |
| 47 | const fieldName = path.replace('/', ''); |
| 48 | // Use the error message from ajv-errors |
| 49 | // ajv-errors should provide the custom message in error.message |
| 50 | const errorMessage = error.message || sprintf(__('%s is invalid.', 'give'), fieldName); |
| 51 | |
| 52 | errors[fieldName] = { |
| 53 | type: 'validation', |
| 54 | message: errorMessage, |
| 55 | }; |
| 56 | } |
| 57 | }); |
| 58 | } |
| 59 | return {values: {}, errors}; |
| 60 | } |
| 61 | } catch (error) { |
| 62 | console.error('AJV validation error:', error); |
| 63 | return {values: data, errors: {}}; |
| 64 | } |
| 65 | }; |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Configure standard AJV (Draft 7/2019-09) for WordPress REST API compatibility |
| 70 | * |
| 71 | * WordPress REST API Schema Characteristics (Draft 03/04): |
| 72 | * - Uses 'required: true' on individual properties (Draft 03 syntax) |
| 73 | * - Uses 'readonly' property (lowercase, WordPress REST API standard) |
| 74 | * - Supports most JSON Schema Draft 4 features including: |
| 75 | * * anyOf, oneOf (since WordPress 5.6.0) |
| 76 | * * Basic validation: type, format, enum, pattern, constraints |
| 77 | * * Object/array validation: properties, items, additionalProperties |
| 78 | * - Does NOT support: allOf, not, if/then/else (conditional validation) |
| 79 | * |
| 80 | * This function configures AJV (Draft 7/2019-09) to work with WordPress schemas: |
| 81 | * - Transforms WordPress Draft 03/04 schemas to Draft 7/2019-09 syntax |
| 82 | * - Converts 'required: true' on individual properties to 'required' array at object level |
| 83 | * - Adds all standard JSON Schema formats using ajv-formats package |
| 84 | * - Adds custom error messages using ajv-errors package |
| 85 | * - Adds WordPress-specific custom formats (text-field, textarea-field) |
| 86 | * - Disables schema validation to avoid conflicts with WordPress schema extensions |
| 87 | * - Enables advanced validation features that WordPress ignores |
| 88 | * |
| 89 | * Validation Features Available: |
| 90 | * |
| 91 | * Backend (WordPress REST API) + Frontend (AJV): |
| 92 | * - Type validation (string, integer, boolean, number, array, object, null) |
| 93 | * - Required field validation |
| 94 | * - Format validation (email, date-time, uri, ip, hex-color, uuid, text-field, textarea-field, and all standard formats via ajv-formats) |
| 95 | * - Enum validation |
| 96 | * - Constraint validation (minLength, maxLength, minimum, maximum, etc.) |
| 97 | * - Pattern validation (regex) |
| 98 | * - Array validation (items, minItems, maxItems, uniqueItems) |
| 99 | * - Object validation (properties, additionalProperties, patternProperties) |
| 100 | * - Schema composition: anyOf, oneOf (WordPress 5.6.0+) |
| 101 | * |
| 102 | * Frontend Only (AJV Draft 7/2019-09): |
| 103 | * - Conditional validation (if/then/else) |
| 104 | * - Advanced schema composition (allOf, not) |
| 105 | * - Advanced references ($ref with complex paths) |
| 106 | * - Dependent required fields |
| 107 | * - Dynamic validation based on other field values |
| 108 | * - More comprehensive format validation |
| 109 | * |
| 110 | * References: |
| 111 | * - WordPress REST API Schema Documentation: https://developer.wordpress.org/rest-api/extending-the-rest-api/schema/ |
| 112 | * - It only supports a subset of the draft-04 and draft-03 meta-schemas: https://developer.wordpress.org/news/2024/07/json-schema-in-wordpress/#wordpress-rest-api |
| 113 | * - WordPress 5.6.0 anyOf/oneOf Support: https://core.trac.wordpress.org/ticket/51025 |
| 114 | * - Implementation Changeset: https://core.trac.wordpress.org/changeset/49246 |
| 115 | * - WordPress uses 'readonly' (lowercase) vs JSON Schema 'readOnly': https://core.trac.wordpress.org/ticket/56152 |
| 116 | * - WordPress 5.5.0 UUID format support: https://core.trac.wordpress.org/ticket/50053 |
| 117 | * - WordPress 5.9.0 text-field/textarea-field formats: https://core.trac.wordpress.org/changeset/49246 |
| 118 | * - rest_validate_value_from_schema(): https://developer.wordpress.org/reference/functions/rest_validate_value_from_schema/ |
| 119 | * - rest_get_allowed_schema_keywords(): https://developer.wordpress.org/reference/functions/rest_get_allowed_schema_keywords/ |
| 120 | * |
| 121 | * @returns Configured AJV instance (Draft 7/2019-09) for WordPress compatibility |
| 122 | */ |
| 123 | function configureAjvForWordPress() { |
| 124 | // Use standard AJV (Draft 7/2019-09) and transform WordPress schemas to be compatible |
| 125 | const AjvClass = require('ajv').default || require('ajv'); |
| 126 | |
| 127 | const ajv = new AjvClass({ |
| 128 | // Disable schema validation to avoid conflicts with WordPress schemas |
| 129 | validateSchema: false, |
| 130 | // Disable strict mode to allow WordPress extensions like 'readonly' |
| 131 | strict: false, |
| 132 | // Enable all errors for better debugging |
| 133 | allErrors: true, |
| 134 | verbose: true, |
| 135 | }); |
| 136 | |
| 137 | // Add all standard JSON Schema formats using ajv-formats |
| 138 | addFormats(ajv); |
| 139 | |
| 140 | // Add custom error messages support using ajv-errors |
| 141 | addErrors(ajv); |
| 142 | |
| 143 | // Add WordPress-specific custom formats that are not in the standard |
| 144 | ajv.addFormat('text-field', true); // WordPress custom format - no validation, only sanitization |
| 145 | ajv.addFormat('textarea-field', true); // WordPress custom format - no validation, only sanitization |
| 146 | ajv.addFormat('integer', true); // WordPress custom format - no validation, only sanitization |
| 147 | ajv.addFormat('boolean', true); // WordPress custom format - no validation, only sanitization |
| 148 | |
| 149 | // Transform WordPress schemas to be compatible with Draft 7/2019-09 |
| 150 | // This converts Draft 03/04 syntax (required: true on properties) to Draft 7 syntax |
| 151 | const originalCompile = ajv.compile.bind(ajv); |
| 152 | ajv.compile = function (schema: JSONSchemaType<any>) { |
| 153 | try { |
| 154 | const transformedSchema = transformWordPressSchemaToDraft7(schema); |
| 155 | return originalCompile(transformedSchema); |
| 156 | } catch (error) { |
| 157 | console.error('Schema transformation error:', error); |
| 158 | return originalCompile(schema); |
| 159 | } |
| 160 | }; |
| 161 | |
| 162 | return ajv; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Transform WordPress schema from Draft 03/04 syntax to Draft 7/2019-09 syntax |
| 167 | * |
| 168 | * This function converts WordPress REST API schemas (Draft 03/04) to be compatible |
| 169 | * with AJV (Draft 7/2019-09). The transformation includes: |
| 170 | * - Converts 'required: true' on individual properties to 'required' array at object level |
| 171 | * - Updates $schema reference from Draft 04 to Draft 7/2019-09 |
| 172 | * - Removes readonly/readOnly fields from validation (they shouldn't be validated by frontend) |
| 173 | * - Conditionally removes enum from nullable fields when value is null to prevent AJV conflicts |
| 174 | * - Preserves all advanced features (if/then/else, allOf, etc.) that WordPress ignores |
| 175 | * but AJV can use for enhanced frontend validation |
| 176 | * |
| 177 | * Key benefit: WordPress schemas can include advanced validation rules (if/then/else, allOf, not) |
| 178 | * that are ignored by the backend but utilized by the frontend for better UX. |
| 179 | */ |
| 180 | function transformWordPressSchemaToDraft7(schema: JSONSchemaType<any>, data?: any): JSONSchemaType<any> { |
| 181 | if (!schema || typeof schema !== 'object') { |
| 182 | return schema; |
| 183 | } |
| 184 | |
| 185 | const transformed = JSON.parse(JSON.stringify(schema)); |
| 186 | |
| 187 | // Update $schema reference to Draft 7/2019-09 |
| 188 | if (transformed.$schema) { |
| 189 | transformed.$schema = 'https://json-schema.org/draft/2019-09/schema'; |
| 190 | } |
| 191 | |
| 192 | if (transformed.properties && typeof transformed.properties === 'object') { |
| 193 | const requiredFields: string[] = []; |
| 194 | const errorMessages: any = {}; |
| 195 | |
| 196 | Object.keys(transformed.properties).forEach((key) => { |
| 197 | const prop = transformed.properties[key]; |
| 198 | |
| 199 | // Early return if prop is not a valid object |
| 200 | if (!prop || typeof prop !== 'object') { |
| 201 | return; |
| 202 | } |
| 203 | |
| 204 | // Converts 'required: true' on individual properties to 'required' array at object level |
| 205 | if (prop.required === true) { |
| 206 | requiredFields.push(key); |
| 207 | delete prop.required; |
| 208 | } |
| 209 | |
| 210 | // Remove readonly/readOnly fields from validation (they shouldn't be validated by frontend) |
| 211 | if (prop.readonly === true || prop.readOnly === true) { |
| 212 | delete transformed.properties[key]; |
| 213 | return; |
| 214 | } |
| 215 | |
| 216 | // For WordPress Array type + enum (like honorific), conditionally remove enum based on current value |
| 217 | // This prevents AJV conflicts when nullable fields have null values |
| 218 | if (Array.isArray(prop.type) && prop.enum) { |
| 219 | const currentValue = data && data[key]; |
| 220 | const allowsNull = prop.type.includes('null'); |
| 221 | if (currentValue === null && allowsNull) { |
| 222 | delete prop.enum; |
| 223 | } |
| 224 | } |
| 225 | |
| 226 | // Add custom error messages for each property |
| 227 | errorMessages[key] = getCustomErrorMessage(prop, key); |
| 228 | }); |
| 229 | |
| 230 | if (requiredFields.length > 0) { |
| 231 | transformed.required = requiredFields; |
| 232 | } |
| 233 | |
| 234 | // Add error messages to the schema |
| 235 | if (Object.keys(errorMessages).length > 0) { |
| 236 | transformed.errorMessage = { |
| 237 | properties: errorMessages, |
| 238 | required: __('Required fields are missing.', 'give'), |
| 239 | _: __('Please check the form for errors.', 'give'), |
| 240 | }; |
| 241 | } |
| 242 | } |
| 243 | |
| 244 | return transformed; |
| 245 | } |
| 246 | |
| 247 | /** |
| 248 | * Generate custom error messages for schema properties using ajv-errors |
| 249 | * |
| 250 | * This function creates specific error messages for different validation types |
| 251 | * based on the property schema, providing better user experience. |
| 252 | * |
| 253 | * @param prop - The property schema object |
| 254 | * @param fieldName - The name of the field |
| 255 | * @returns Custom error message string for the field |
| 256 | */ |
| 257 | function getCustomErrorMessage(prop: any, fieldName: string): string { |
| 258 | // Priority order: format > type > constraints > generic |
| 259 | |
| 260 | // Format validation messages (highest priority) |
| 261 | if (prop.format) { |
| 262 | switch (prop.format) { |
| 263 | case 'email': |
| 264 | return sprintf(__('%s must be a valid email address.', 'give'), fieldName); |
| 265 | case 'uri': |
| 266 | return sprintf(__('%s must be a valid URL.', 'give'), fieldName); |
| 267 | case 'date-time': |
| 268 | return sprintf(__('%s must be a valid date and time.', 'give'), fieldName); |
| 269 | case 'uuid': |
| 270 | return sprintf(__('%s must be a valid UUID.', 'give'), fieldName); |
| 271 | case 'hex-color': |
| 272 | return sprintf(__('%s must be a valid color code.', 'give'), fieldName); |
| 273 | default: |
| 274 | return sprintf(__('%s format is invalid.', 'give'), fieldName); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | // Type validation messages |
| 279 | if (prop.type) { |
| 280 | if (prop.type === 'string') { |
| 281 | return sprintf(__('%s must be text.', 'give'), fieldName); |
| 282 | } else if (prop.type === 'number') { |
| 283 | return sprintf(__('%s must be a number.', 'give'), fieldName); |
| 284 | } else if (prop.type === 'integer') { |
| 285 | return sprintf(__('%s must be a whole number.', 'give'), fieldName); |
| 286 | } else if (prop.type === 'boolean') { |
| 287 | return sprintf(__('%s must be true or false.', 'give'), fieldName); |
| 288 | } else if (prop.type === 'array') { |
| 289 | return sprintf(__('%s must be a list.', 'give'), fieldName); |
| 290 | } else if (prop.type === 'object') { |
| 291 | return sprintf(__('%s must be an object.', 'give'), fieldName); |
| 292 | } |
| 293 | } |
| 294 | |
| 295 | // Enum validation messages |
| 296 | if (prop.enum && Array.isArray(prop.enum)) { |
| 297 | return sprintf(__('%s must be one of: %s', 'give'), fieldName, prop.enum.join(', ')); |
| 298 | } |
| 299 | |
| 300 | // Constraint validation messages |
| 301 | if (prop.minLength !== undefined) { |
| 302 | return sprintf(__('%s must be at least %d characters long.', 'give'), fieldName, prop.minLength); |
| 303 | } |
| 304 | if (prop.maxLength !== undefined) { |
| 305 | return sprintf(__('%s must be no more than %d characters long.', 'give'), fieldName, prop.maxLength); |
| 306 | } |
| 307 | if (prop.minimum !== undefined) { |
| 308 | return sprintf(__('%s must be at least %s.', 'give'), fieldName, prop.minimum); |
| 309 | } |
| 310 | if (prop.maximum !== undefined) { |
| 311 | return sprintf(__('%s must be no more than %s.', 'give'), fieldName, prop.maximum); |
| 312 | } |
| 313 | if (prop.pattern) { |
| 314 | return sprintf(__('%s format is invalid.', 'give'), fieldName); |
| 315 | } |
| 316 | |
| 317 | // Generic fallback |
| 318 | return sprintf(__('%s is invalid.', 'give'), fieldName); |
| 319 | } |
| 320 | |
| 321 | /** |
| 322 | * Transform form data to be compatible with JSON Schema validation |
| 323 | * |
| 324 | * This function handles common form data issues that occur when HTML forms send |
| 325 | * data that doesn't match the expected schema types: |
| 326 | * - Converts string numbers to actual numbers based on schema type definitions |
| 327 | * - Handles enum fields with null/empty string values (converts to null if schema allows) |
| 328 | * - Removes non-required fields that are not present in form data |
| 329 | * - Recursively processes nested objects and arrays |
| 330 | * |
| 331 | * This ensures that form data matches the schema expectations for validation, |
| 332 | * working with both WordPress backend validation and AJV frontend validation. |
| 333 | */ |
| 334 | function transformFormDataForValidation(data: any, schema: JSONSchemaType<any>): any { |
| 335 | if (!data || !schema || typeof data !== 'object') { |
| 336 | return data || {}; |
| 337 | } |
| 338 | const transformed = JSON.parse(JSON.stringify(data)); // Deep clone |
| 339 | |
| 340 | // Recursively transform nested objects |
| 341 | function transformObject(obj: any, schemaObj: any): any { |
| 342 | if (!obj || !schemaObj || typeof obj !== 'object') { |
| 343 | return obj; |
| 344 | } |
| 345 | |
| 346 | const result = {...obj}; |
| 347 | |
| 348 | if (schemaObj.properties) { |
| 349 | Object.keys(schemaObj.properties).forEach((key) => { |
| 350 | const propSchema = schemaObj.properties[key]; |
| 351 | const value = result[key]; |
| 352 | |
| 353 | // Skip validation for fields that are not present in form data and not required |
| 354 | const isRequired = Array.isArray(schemaObj.required) && schemaObj.required.includes(key); |
| 355 | const isFieldPresent = value !== undefined && value !== null; |
| 356 | |
| 357 | // Remove fields that are not present and not required from the result |
| 358 | // Note: We allow empty strings ('') to be present so they can be saved to clear fields |
| 359 | if (!isFieldPresent && !isRequired) { |
| 360 | delete result[key]; |
| 361 | return; // Skip processing this field |
| 362 | } |
| 363 | |
| 364 | // Only process fields that are present in the form OR are required |
| 365 | if (propSchema && (isFieldPresent || isRequired)) { |
| 366 | // Handle number types |
| 367 | if (propSchema.type === 'number' && typeof value === 'string') { |
| 368 | // Convert string to number |
| 369 | const numValue = parseFloat(value); |
| 370 | if (!isNaN(numValue)) { |
| 371 | result[key] = numValue; |
| 372 | } |
| 373 | } else if (propSchema.type === 'integer' && typeof value === 'string') { |
| 374 | // Convert string to integer |
| 375 | const intValue = parseInt(value, 10); |
| 376 | if (!isNaN(intValue)) { |
| 377 | result[key] = intValue; |
| 378 | } |
| 379 | } |
| 380 | // Handle boolean types |
| 381 | else if (propSchema.type === 'boolean') { |
| 382 | if (typeof value === 'string') { |
| 383 | result[key] = value === 'true' || value === '1' || value === 'yes'; |
| 384 | } else if (typeof value === 'number') { |
| 385 | result[key] = value === 1; |
| 386 | } |
| 387 | } |
| 388 | // Handle enum types - ensure value is valid |
| 389 | else if (propSchema.enum && Array.isArray(propSchema.enum)) { |
| 390 | // Check if null is allowed (when type includes 'null') |
| 391 | const allowsNull = Array.isArray(propSchema.type) && propSchema.type.includes('null'); |
| 392 | |
| 393 | // If value is null, undefined, or empty string and null is allowed, set to null |
| 394 | if ((value === null || value === undefined || value === '') && allowsNull) { |
| 395 | result[key] = null; |
| 396 | } |
| 397 | // If value is not in enum and not null/empty, try to find a close match |
| 398 | else if (!propSchema.enum.includes(value)) { |
| 399 | // If value is not in enum, try to find a close match or use first valid value |
| 400 | const stringValue = String(value).toLowerCase(); |
| 401 | const validValue = propSchema.enum.find( |
| 402 | (enumValue) => String(enumValue).toLowerCase() === stringValue |
| 403 | ); |
| 404 | if (validValue !== undefined) { |
| 405 | result[key] = validValue; |
| 406 | } else if (propSchema.enum.length > 0) { |
| 407 | // Use first enum value as fallback |
| 408 | result[key] = propSchema.enum[0]; |
| 409 | } |
| 410 | } |
| 411 | } |
| 412 | // Handle oneOf schemas (like createdAt/updatedAt for donations) |
| 413 | else if (propSchema.oneOf && Array.isArray(propSchema.oneOf)) { |
| 414 | result[key] = transformOneOfValue(value, propSchema.oneOf); |
| 415 | } |
| 416 | // Handle array types with string/null (like createdAt/renewsAt for subscriptions) |
| 417 | else if ( |
| 418 | Array.isArray(propSchema.type) && |
| 419 | propSchema.type.includes('string') && |
| 420 | propSchema.format === 'date-time' |
| 421 | ) { |
| 422 | // For subscription date fields that expect string or null |
| 423 | if (value === null || value === undefined) { |
| 424 | result[key] = null; |
| 425 | } else if (typeof value === 'string') { |
| 426 | // Handle ISO 8601 date strings (with or without timezone) |
| 427 | // Examples: '2025-07-15T16:34:57', '2025-07-15T16:34:57Z', '2025-07-15T16:34:57.000Z' |
| 428 | const date = new Date(value); |
| 429 | if (!isNaN(date.getTime())) { |
| 430 | // Ensure the string is in proper ISO format |
| 431 | const isoString = date.toISOString(); |
| 432 | result[key] = isoString; |
| 433 | } else { |
| 434 | result[key] = null; |
| 435 | } |
| 436 | } else if (value instanceof Date) { |
| 437 | result[key] = value.toISOString(); |
| 438 | } else { |
| 439 | result[key] = null; |
| 440 | } |
| 441 | } |
| 442 | // Handle nested objects |
| 443 | else if ( |
| 444 | (propSchema.type === 'object' || |
| 445 | (Array.isArray(propSchema.type) && propSchema.type.includes('object'))) && |
| 446 | propSchema.properties |
| 447 | ) { |
| 448 | // Recursively transform nested objects |
| 449 | result[key] = transformObject(value, propSchema); |
| 450 | } |
| 451 | } |
| 452 | }); |
| 453 | } |
| 454 | |
| 455 | return result; |
| 456 | } |
| 457 | |
| 458 | // Call transformObject to process the data |
| 459 | return transformObject(transformed, schema); |
| 460 | } |
| 461 | |
| 462 | /** |
| 463 | * Transform values for oneOf schemas (like createdAt/updatedAt) |
| 464 | */ |
| 465 | function transformOneOfValue(value: any, oneOfSchemas: any[]): any { |
| 466 | // If value is already null, return null |
| 467 | if (value === null || value === undefined) { |
| 468 | return null; |
| 469 | } |
| 470 | |
| 471 | // Check each oneOf schema to see which one matches |
| 472 | for (const schema of oneOfSchemas) { |
| 473 | // If schema expects a string |
| 474 | if (schema.type === 'string') { |
| 475 | if (typeof value === 'string') { |
| 476 | // Check if it's a valid ISO date-time string |
| 477 | const date = new Date(value); |
| 478 | if (!isNaN(date.getTime())) { |
| 479 | return value; // Return as string if valid |
| 480 | } |
| 481 | } |
| 482 | } |
| 483 | // If schema expects an object with date property (like createdAt/updatedAt) |
| 484 | else if (schema.type === 'object' && schema.properties && schema.properties.date) { |
| 485 | // If value is already an object with date property, validate and return |
| 486 | if (typeof value === 'object' && value.date) { |
| 487 | const date = new Date(value.date); |
| 488 | if (!isNaN(date.getTime())) { |
| 489 | // Convert to ISO 8601 format if needed |
| 490 | const isoDate = date.toISOString(); |
| 491 | return { |
| 492 | date: isoDate, |
| 493 | timezone: value.timezone || 'UTC', |
| 494 | timezone_type: value.timezone_type || 3, |
| 495 | }; |
| 496 | } |
| 497 | } |
| 498 | // If value is a string, convert to object format |
| 499 | else if (typeof value === 'string') { |
| 500 | const date = new Date(value); |
| 501 | if (!isNaN(date.getTime())) { |
| 502 | return { |
| 503 | date: date.toISOString(), |
| 504 | timezone: 'UTC', |
| 505 | timezone_type: 3, |
| 506 | }; |
| 507 | } |
| 508 | } |
| 509 | } |
| 510 | // If schema expects null |
| 511 | else if (schema.type === 'null') { |
| 512 | if (value === null) { |
| 513 | return null; |
| 514 | } |
| 515 | } |
| 516 | } |
| 517 | |
| 518 | // If value is a Date object, convert to object format |
| 519 | if (value instanceof Date) { |
| 520 | return { |
| 521 | date: value.toISOString(), |
| 522 | timezone: 'UTC', |
| 523 | timezone_type: 3, |
| 524 | }; |
| 525 | } |
| 526 | |
| 527 | // If value is a string that looks like a date, convert to object format |
| 528 | if (typeof value === 'string') { |
| 529 | const date = new Date(value); |
| 530 | if (!isNaN(date.getTime())) { |
| 531 | return { |
| 532 | date: value, |
| 533 | timezone: 'UTC', |
| 534 | timezone_type: 3, |
| 535 | }; |
| 536 | } |
| 537 | } |
| 538 | |
| 539 | // Fallback: return null |
| 540 | return null; |
| 541 | } |
| 542 |