@simpleapps-com/augur-api
Version:
TypeScript client library for Augur microservices API endpoints
153 lines • 5.65 kB
JavaScript
import { z } from 'zod';
/**
* Flexible Schema Utilities for Handling Inconsistent API Outputs
*
* During microservice API refactoring, many endpoints return inconsistent formats.
* These utilities provide standardized patterns for handling common inconsistencies.
*/
/**
* Creates a schema that accepts both array and object formats
* Common pattern: server sometimes returns [] and sometimes {}
*/
export const flexibleArrayOrObject = (itemSchema) => z.union([
z.array(itemSchema), // Preferred format
z.record(z.unknown()), // Legacy/inconsistent format
z.record(z.never()).optional(), // Empty object fallback
]);
/**
* Creates a schema that accepts both array and record formats for collections
* Handles cases where APIs return either array of items or object with key-value pairs
*/
export const flexibleCollection = (itemSchema) => z.union([
z.array(itemSchema), // Standard array format
z.record(itemSchema), // Object with typed values
z.array(z.unknown()), // Fallback array with unknown items
z.record(z.unknown()), // Fallback object with unknown values
]);
/**
* Creates a schema for fields that might be string, number, or null
* Common during API migrations when data types are being standardized
*/
export const flexibleStringOrNumber = () => z.union([z.string(), z.number(), z.null(), z.undefined()]).transform(val => {
if (val === null || val === undefined)
return null;
return String(val);
});
/**
* Creates a schema for boolean fields that might come as string, number, or boolean
* Handles legacy APIs that return "1"/"0", true/false, or "true"/"false"
*/
export const flexibleBoolean = () => z.union([z.boolean(), z.string(), z.number(), z.null(), z.undefined()]).transform(val => {
if (typeof val === 'boolean')
return val;
if (typeof val === 'string')
return val.toLowerCase() === 'true' || val === '1';
if (typeof val === 'number')
return val === 1;
return false;
});
/**
* Creates a schema for date fields that might be string, Date, or null
* Handles various date format inconsistencies
*/
export const flexibleDate = () => z.union([z.string(), z.date(), z.null(), z.undefined()]).transform(val => {
if (val === null || val === undefined)
return null;
if (val instanceof Date)
return val.toISOString();
return String(val);
});
/**
* Creates a flexible schema for profile/metadata fields that vary between endpoints
* This is the pattern we found with profileValues - sometimes array, sometimes object
*/
export const flexibleProfileData = () => z
.union([
z.record(z.union([z.string(), z.array(z.string())])), // Object format with string or array values
z.array(z.unknown()), // Array format (common case)
z.null(), // No profile data
z.undefined(), // Field missing
])
.optional();
/**
* Creates a flexible schema that gracefully handles unexpected extra fields
* Uses passthrough to allow additional properties during API evolution
*/
export const flexibleObject = (shape) => z.object(shape).passthrough();
/**
* Creates a schema that handles nested data that might be normalized or denormalized
* Common when APIs are being refactored from flat to nested structures
*/
export const flexibleNested = (simpleSchema, complexSchema) => z.union([
simpleSchema, // Simple/flat format
complexSchema, // Complex/nested format
]);
/**
* Pre-built schema for common metadata fields that are inconsistent during refactoring
* Handles count normalization and bidirectional sync between total and totalResults
*/
export const flexibleMetadataFields = z
.object({
count: z.number().optional().default(0),
total: z.number().optional().default(0),
totalResults: z.number().optional().default(0),
})
.transform(data => {
// Apply the same normalization logic as BaseResponseSchema
const count = data.count || 0;
// Bidirectional sync between total and totalResults
let total = data.total;
let totalResults = data.totalResults || 0;
// If total is 0 and totalResults > 0, set total to totalResults
if (total === 0 && totalResults > 0) {
total = totalResults;
}
// If totalResults is 0 and total > 0, set totalResults to total
else if (totalResults === 0 && total > 0) {
totalResults = total;
}
return {
count,
total,
totalResults,
};
});
/**
* Utility for wrapping existing schemas to be more flexible during API refactoring
* Adds .catch() to provide fallback values instead of throwing validation errors
*/
export const makeFlexible = (schema, fallbackValue) => schema.catch(fallbackValue);
/**
* Common patterns for user-related data that varies across services
*/
export const flexibleUserFields = {
profileValues: flexibleProfileData(),
customFields: flexibleArrayOrObject(z.unknown()),
permissions: flexibleCollection(z.string()),
groups: flexibleArrayOrObject(z
.object({
id: z.number(),
title: z.string(),
})
.passthrough()),
};
/**
* Common patterns for product/item data that varies across services
*/
export const flexibleProductFields = {
attributes: flexibleCollection(z
.object({
attributeId: z.string(),
value: z.string(),
})
.passthrough()),
categories: flexibleArrayOrObject(z
.object({
categoryUid: z.number(),
categoryDesc: z.string(),
})
.passthrough()),
metadata: flexibleProfileData(),
customData: flexibleArrayOrObject(z.unknown()),
};
//# sourceMappingURL=flexible-schemas.js.map