json-machete
Version:
424 lines (423 loc) • 21.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.healJSONSchema = exports.AnySchema = void 0;
const tslib_1 = require("tslib");
const visitJSONSchema_js_1 = require("./visitJSONSchema.js");
const to_json_schema_1 = tslib_1.__importDefault(require("to-json-schema"));
const utils_1 = require("@graphql-mesh/utils");
const utils_2 = require("@graphql-tools/utils");
const asArray = (value) => (Array.isArray(value) ? value : [value]);
const JSONSchemaStringFormats = [
'date',
'hostname',
'regex',
'json-pointer',
'relative-json-pointer',
'uri-reference',
'uri-template',
'date-time',
'time',
'email',
'ipv4',
'ipv6',
'uri',
'uuid',
'binary',
'byte',
'int64',
'int32',
'unix-time',
'double',
'float',
'decimal',
];
exports.AnySchema = {
title: 'Any',
oneOf: [
{ type: 'string' },
{ type: 'integer' },
{ type: 'boolean' },
{ type: 'number' },
{ type: 'object', additionalProperties: true },
],
};
async function healJSONSchema(schema, { logger = new utils_1.DefaultLogger('healJSONSchema') } = {}) {
const schemaByTitle = new Map();
const anySchemaOneOfInspect = (0, utils_2.inspect)(exports.AnySchema.oneOf);
return (0, visitJSONSchema_js_1.visitJSONSchema)(schema, {
enter: async function healSubschema(subSchema, { path }) {
if (typeof subSchema === 'object') {
if (subSchema.title === 'Any' ||
(subSchema.oneOf && (0, utils_2.inspect)(subSchema.oneOf) === anySchemaOneOfInspect)) {
return exports.AnySchema;
}
if (subSchema.title) {
if (Object.keys(subSchema).length === 2 && subSchema.type) {
delete subSchema.title;
}
else {
const existingSubSchema = schemaByTitle.get(subSchema.title);
if (existingSubSchema) {
if ((0, utils_2.inspect)(subSchema) === (0, utils_2.inspect)(existingSubSchema)) {
return existingSubSchema;
}
else {
delete subSchema.title;
}
}
else {
schemaByTitle.set(subSchema.title, subSchema);
}
}
}
else if (Object.keys(subSchema).length === 1 &&
subSchema.type &&
!Array.isArray(subSchema.type)) {
return subSchema;
}
const keys = Object.keys(subSchema).filter(key => key !== 'readOnly' && key !== 'writeOnly');
if (keys.length === 0) {
logger.debug(`${path} has an empty definition. Adding an object definition.`);
subSchema.type = 'object';
subSchema.additionalProperties = true;
}
if (typeof subSchema.additionalProperties === 'object') {
const additionalPropertiesKeys = Object.keys(subSchema.additionalProperties).filter(key => key !== 'readOnly' && key !== 'writeOnly');
if (additionalPropertiesKeys.length === 0 ||
(additionalPropertiesKeys.length === 1 &&
subSchema.additionalProperties.type === 'string')) {
logger.debug(`${path} has an empty additionalProperties object. So this is invalid. Replacing it with 'true'`);
subSchema.additionalProperties = true;
}
}
// Really edge case, but we need to support it
if (subSchema.allOf != null &&
subSchema.allOf.length === 1 &&
subSchema.allOf[0].oneOf &&
subSchema.oneOf) {
subSchema.oneOf.push(...subSchema.allOf[0].oneOf);
delete subSchema.allOf;
}
// If they have title, it makes sense to keep them to reflect the schema in a better way
if (!subSchema.title) {
if (subSchema.anyOf != null &&
subSchema.anyOf.length === 1 &&
!subSchema.properties &&
!subSchema.allOf &&
!subSchema.oneOf) {
logger.debug(`${path} has an "anyOf" definition with only one element. Removing it.`);
const realSubschema = subSchema.anyOf[0];
delete subSchema.anyOf;
subSchema = realSubschema;
}
if (subSchema.allOf != null &&
subSchema.allOf.length === 1 &&
!subSchema.properties &&
!subSchema.anyOf &&
!subSchema.oneOf) {
logger.debug(`${path} has an "allOf" definition with only one element. Removing it.`);
const realSubschema = subSchema.allOf[0];
delete subSchema.allOf;
subSchema = realSubschema;
}
}
if (subSchema.oneOf != null &&
subSchema.oneOf.length === 1 &&
!subSchema.properties &&
!subSchema.anyOf &&
!subSchema.allOf) {
logger.debug(`${path} has an "oneOf" definition with only one element. Removing it.`);
const realSubschema = subSchema.oneOf[0];
delete subSchema.oneOf;
subSchema = realSubschema;
}
if (subSchema.description != null) {
subSchema.description = subSchema.description.trim();
if (keys.length === 1) {
logger.debug(`${path} has a description definition but has nothing else. Adding an object definition.`);
subSchema.type = 'object';
subSchema.additionalProperties = true;
}
}
// Some JSON Schemas use this broken pattern and refer the type using `items`
if (subSchema.type === 'object' && subSchema.items) {
logger.debug(`${path} has an object definition but with "items" which is not valid. So setting "items" to the actual definition.`);
const realSubschema = subSchema.items;
delete subSchema.items;
subSchema = realSubschema;
}
if (subSchema.properties && subSchema.type !== 'object') {
logger.debug(`${path} has "properties" with no type defined. Adding a type property with "object" value.`);
subSchema.type = 'object';
}
if (typeof subSchema.example === 'object' && !subSchema.type) {
logger.debug(`${path} has an example object but no type defined. Setting type to "object".`);
subSchema.type = 'object';
}
// Items only exist in arrays
if (subSchema.items) {
logger.debug(`${path} has an items definition but no type defined. Setting type to "array".`);
subSchema.type = 'array';
if (subSchema.properties) {
delete subSchema.properties;
}
// Items should be an object
if (Array.isArray(subSchema.items)) {
if (subSchema.items.length === 0) {
logger.debug(`${path} has an items array with a single value. Setting items to an object.`);
subSchema.items = subSchema.items[0];
}
else {
logger.debug(`${path} has an items array with multiple values. Setting items to an object with oneOf definition.`);
subSchema.items = {
oneOf: subSchema.items,
};
}
}
}
// Try to find the type
if (!subSchema.type) {
logger.debug(`${path} has no type defined. Trying to find it.`);
// If required exists without properties
if (Array.isArray(subSchema.required) &&
!subSchema.properties &&
!subSchema.anyOf &&
!subSchema.allOf) {
logger.debug(`${path} has a required definition but no properties or oneOf/allOf. Setting missing properties with Any schema.`);
// Add properties
subSchema.properties = {};
for (const missingPropertyName of subSchema.required) {
subSchema.properties[missingPropertyName] = exports.AnySchema;
}
}
// Properties only exist in objects
if (subSchema.properties ||
subSchema.patternProperties ||
'additionalProperties' in subSchema) {
logger.debug(`${path} has properties or patternProperties or additionalProperties. Setting type to "object".`);
subSchema.type = 'object';
}
switch (subSchema.format) {
case 'int64':
case 'int32':
logger.debug(`${path} has a format of ${subSchema.format}. Setting type to "integer".`);
subSchema.type = 'integer';
break;
case 'float':
case 'double':
logger.debug(`${path} has a format of ${subSchema.format}. Setting type to "number".`);
subSchema.type = 'number';
break;
default:
if (subSchema.format != null) {
logger.debug(`${path} has a format of ${subSchema.format}. Setting type to "string".`);
subSchema.type = 'string';
}
}
if (subSchema.minimum != null || subSchema.maximum != null) {
logger.debug(`${path} has a minimum or maximum. Setting type to "number".`);
subSchema.type = 'number';
}
}
if (subSchema.type === 'string' &&
!subSchema.format &&
(subSchema.examples || subSchema.example)) {
const examples = asArray(subSchema.examples || subSchema.example || []);
if (examples === null || examples === void 0 ? void 0 : examples.length) {
const { format } = (0, to_json_schema_1.default)(examples[0]);
if (format && format !== 'utc-millisec' && format !== 'style') {
logger.debug(`${path} has a format of ${format} according to the example. Setting type to "string".`);
subSchema.format = format;
}
}
}
if (subSchema.format === 'dateTime') {
logger.debug(`${path} has a format of dateTime. It should be "date-time".`);
subSchema.format = 'date-time';
}
if (subSchema.format) {
if (!JSONSchemaStringFormats.includes(subSchema.format)) {
logger.debug(`${path} has a format of ${subSchema.format}. It should be one of ${JSONSchemaStringFormats.join(', ')}.`);
delete subSchema.format;
}
}
if (subSchema.required) {
if (!Array.isArray(subSchema.required)) {
logger.debug(`${path} has a required definition but it is not an array. Removing it.`);
delete subSchema.required;
}
}
// If it is an object type but no properties given while example is available
if (((subSchema.type === 'object' &&
!subSchema.properties &&
!subSchema.allOf &&
!subSchema.anyOf &&
!subSchema.oneOf) ||
!subSchema.type) &&
(subSchema.example || subSchema.examples)) {
const examples = [];
if (subSchema.example) {
examples.push(subSchema.example);
}
if (subSchema.examples) {
examples.push(...subSchema.examples);
}
const generatedSchema = (0, to_json_schema_1.default)(examples[0], {
required: false,
objects: {
additionalProperties: false,
},
strings: {
detectFormat: true,
},
arrays: {
mode: 'first',
},
postProcessFnc(type, schema, value, defaultFunc) {
if (schema.type === 'object' &&
!schema.properties &&
Object.keys(value).length === 0) {
return exports.AnySchema;
}
return defaultFunc(type, schema, value);
},
});
subSchema.type = asArray(generatedSchema.type)[0];
if (generatedSchema.properties) {
subSchema.properties = generatedSchema.properties;
}
if (generatedSchema.items) {
subSchema.items = generatedSchema.items;
}
// If type for properties is already given, use it
logger.debug(`${path} has an example but no type defined. Setting type to ${subSchema.type}.`);
// if (typeof subSchema.additionalProperties === 'object') {
// for (const propertyName in subSchema.properties) {
// subSchema.properties[propertyName] = subSchema.additionalProperties;
// }
// }
}
if (subSchema.enum && subSchema.enum.length === 1 && subSchema.type !== 'boolean') {
subSchema.const = subSchema.enum[0];
logger.debug(`${path} has an enum but with a single value. Converting it to const.`);
delete subSchema.enum;
}
if (subSchema.enum && subSchema.enum.includes(null)) {
logger.debug(`${path} has "null" value. Converting it to nullable.`);
subSchema.enum = subSchema.enum.filter((e) => e != null);
subSchema.nullable = true;
}
if (subSchema.const === null && subSchema.type !== 'null') {
logger.debug(`${path} has a const definition of null. Setting type to "null".`);
subSchema.type = 'null';
delete subSchema.const;
}
if (!subSchema.title &&
!subSchema.$ref &&
subSchema.type !== 'array' &&
!subSchema.items) {
const realPath = subSchema.$resolvedRef || path;
// Try to get definition name if missing
const splitByDefinitions = realPath.includes('/components/schemas/')
? realPath.split('/components/schemas/')
: realPath.split('/definitions/');
const maybeDefinitionBasedPath = splitByDefinitions.length > 1
? splitByDefinitions[splitByDefinitions.length - 1]
: realPath;
const pathBasedName = maybeDefinitionBasedPath
.split('~1')
.join('/')
.split('/properties')
.join('')
.split('-')
.join('_')
.split('/')
.filter(Boolean)
.join('_');
switch (subSchema.type) {
case 'string':
// If it has special pattern, use path based name because it is specific
if (subSchema.pattern ||
subSchema.maxLength ||
subSchema.minLength ||
subSchema.enum) {
logger.debug(`${path} has a pattern or maxLength or minLength or enum but no title. Setting it to ${pathBasedName}`);
subSchema.title = pathBasedName;
// Otherwise use the format name
}
break;
case 'number':
case 'integer':
if (subSchema.enum || subSchema.pattern) {
logger.debug(`${path} has an enum or pattern but no title. Setting it to ${pathBasedName}`);
subSchema.title = pathBasedName;
// Otherwise use the format name
}
break;
case 'array':
break;
case 'boolean':
// pattern is unnecessary for boolean
if (subSchema.pattern) {
logger.debug(`${path} has a pattern for a boolean type. Removing it.`);
delete subSchema.pattern;
}
// enum is unnecessary for boolean
if (subSchema.enum) {
logger.debug(`${path} is an enum but a boolean type. Removing it.`);
delete subSchema.enum;
}
break;
default:
logger.debug(`${path} has no title. Setting it to ${pathBasedName}`);
subSchema.title = subSchema.title || pathBasedName;
}
if (subSchema.const) {
subSchema.title = subSchema.const.toString() + '_const';
const existingSubSchema = schemaByTitle.get(subSchema.title);
if (existingSubSchema) {
return existingSubSchema;
}
schemaByTitle.set(subSchema.title, subSchema);
}
}
if (subSchema.type === 'object' &&
subSchema.properties &&
Object.keys(subSchema.properties).length === 0) {
logger.debug(`${path} has an empty properties object. Removing it and adding "additionalProperties": true.`);
delete subSchema.properties;
subSchema.additionalProperties = true;
}
if (subSchema.properties) {
const propertyValues = Object.values(subSchema.properties);
if (propertyValues.every(property => property.writeOnly && !property.readOnly)) {
subSchema.writeOnly = true;
}
if (propertyValues.every(property => property.readOnly && !property.writeOnly)) {
subSchema.readOnly = true;
}
}
if (subSchema.pattern) {
// Fix non JS patterns
const javaToJsPattern = {
'\\p{Digit}': '[0-9]',
'\\p{Alpha}': '[a-zA-Z]',
'\\p{Alnum}': '[a-zA-Z0-9]',
'\\p{ASCII}': '[\\x00-\\x7F]',
};
for (const javaPattern in javaToJsPattern) {
if (subSchema.pattern.includes(javaPattern)) {
const jsPattern = javaToJsPattern[javaPattern];
subSchema.pattern = subSchema.pattern.split(javaPattern).join(jsPattern);
}
}
}
}
return subSchema;
},
}, {
visitedSubschemaResultMap: new WeakMap(),
path: '',
});
}
exports.healJSONSchema = healJSONSchema;