PluginProbe ʕ •ᴥ•ʔ
GiveWP – Donation Plugin and Fundraising Platform / trunk
GiveWP – Donation Plugin and Fundraising Platform vtrunk
4.16.2 4.16.1 4.16.0 4.15.5 4.15.4 4.15.3 4.15.2 4.15.1 4.15.0 2.3.0 2.3.1 2.3.2 2.30.0 2.31.0 2.31.1 2.32.0 2.33.0 2.33.1 2.33.2 2.33.3 2.33.4 2.33.5 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.4.7 2.5.0 2.5.1 2.5.10 2.5.11 2.5.12 2.5.13 2.5.2 2.5.3 2.5.4 2.5.5 2.5.6 2.5.7 2.5.8 2.5.9 2.6.0 2.6.1 2.6.2 2.6.3 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.8.0 2.8.1 2.9.0 2.9.1 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.1.2 3.10.0 3.11.0 3.12.0 3.12.1 3.12.2 3.12.3 3.13.0 3.14.0 3.14.1 3.14.2 3.15.0 3.15.1 3.16.0 3.16.1 3.16.2 3.16.3 3.16.4 3.16.5 3.17.0 3.17.1 3.17.2 3.18.0 3.19.0 3.19.1 3.19.2 3.19.3 3.19.4 3.2.0 3.2.1 3.2.2 3.20.0 3.21.0 3.21.1 3.22.0 3.22.1 3.22.2 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.6.0 3.6.1 3.6.2 3.7.0 3.8.0 3.9.0 4.0.0 4.1.0 4.1.1 4.10.0 4.10.1 4.11.0 4.12.0 4.13.0 4.13.1 4.13.2 4.14.0 4.14.1 4.14.2 4.14.3 4.14.4 4.14.5 4.14.6 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.1 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 trunk 1.9.0 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.1.0 2.1.1 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.10.0 2.10.1 2.10.2 2.10.3 2.10.4 2.11.0 2.11.1 2.11.2 2.11.3 2.12.0 2.12.1 2.12.2 2.12.3 2.13.0 2.13.1 2.13.2 2.13.3 2.13.4 2.14.0 2.15.0 2.16.0 2.16.1 2.17.0 2.17.1 2.17.3 2.18.0 2.18.1 2.19.1 2.19.2 2.19.3 2.19.4 2.19.5 2.19.6 2.19.7 2.19.8 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.20.0 2.20.1 2.20.2 2.21.0 2.21.1 2.21.2 2.21.3 2.21.4 2.22.0 2.22.1 2.22.2 2.22.3 2.23.0 2.23.1 2.23.2 2.24.0 2.24.1 2.24.2 2.25.0 2.25.1 2.25.2 2.25.3 2.26.0 2.27.0 2.27.1 2.27.2 2.27.3 2.28.0 2.29.0 2.29.1 2.29.2
give / src / Admin / ajv.ts
give / src / Admin Last commit date
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