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