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