UNPKG

json-machete

Version:
849 lines (841 loc) • 41.9 kB
import { AggregateError, inspect } from '@graphql-tools/utils'; import JsonPointer from 'json-pointer'; import { path, process } from '@graphql-mesh/cross-helpers'; import urlJoin from 'url-join'; import { fetch } from '@whatwg-node/fetch'; import { defaultImportFn, DefaultLogger, readFileOrUrl } from '@graphql-mesh/utils'; import toJsonSchema from 'to-json-schema'; const identicalFn = (a) => a; const objectFields = [ 'additionalProperties', 'additionalItems', 'contains', 'else', 'if', 'items', 'not', 'then', ]; const dictFields = ['anyOf', 'allOf', 'oneOf', 'definitions', 'properties', 'patternProperties']; async function visitJSONSchema(schema, { enter = identicalFn, leave = identicalFn, }, { visitedSubschemaResultMap, path } = { visitedSubschemaResultMap: new WeakMap(), path: '', }) { var _a; if (typeof schema === 'object') { if (!visitedSubschemaResultMap.has(schema)) { const enterResult = await enter(schema, { visitedSubschemaResultMap, path, }); visitedSubschemaResultMap.set(schema, enterResult); for (const key of objectFields) { if (enterResult[key]) { enterResult[key] = await visitJSONSchema(enterResult[key], { enter, leave }, { visitedSubschemaResultMap, path: `${path}/${key}`, }); } } for (const key of dictFields) { if (enterResult[key]) { const entries = Object.entries(enterResult[key]); for (const [itemKey, itemValue] of entries) { enterResult[key][itemKey] = await visitJSONSchema(itemValue, { enter, leave }, { visitedSubschemaResultMap, path: `${path}/${key}/${itemKey}` }); } } } if ((_a = enterResult.components) === null || _a === void 0 ? void 0 : _a.schema) { const entries = Object.entries(enterResult.components.schemas); for (const [schemaName, subSchema] of entries) { enterResult.components.schemas[schemaName] = await visitJSONSchema(subSchema, { enter, leave }, { visitedSubschemaResultMap, path: `${path}/components/schemas/${schemaName}` }); } } const leaveResult = await leave(enterResult, { visitedSubschemaResultMap, path, }); visitedSubschemaResultMap.set(schema, leaveResult); return leaveResult; } return visitedSubschemaResultMap.get(schema); } const enterResult = await enter(schema, { visitedSubschemaResultMap, path, }); return leave(enterResult, { visitedSubschemaResultMap, path, }); } function handleUntitledDefinitions(schemaDocument) { var _a, _b, _c; const seen = new Map(); function handleDefinitions(definitions) { for (const definitionName in definitions) { const definition = definitions[definitionName]; if (!definition.$ref) { if (!definition.title) { definition.title = definitionName; } else { const seenDefinition = seen.get(definition.title); if (seenDefinition) { definition.title = definitionName; seenDefinition.definition.title = seenDefinition.definitionName; } seen.set(definition.title, { definitionName, definition }); } } } } if (schemaDocument.definitions) { handleDefinitions(schemaDocument.definitions); } if ((_a = schemaDocument.components) === null || _a === void 0 ? void 0 : _a.schemas) { handleDefinitions(schemaDocument.components.schemas); } const bodyTypeMap = { responses: 'response', requestBodies: 'request', }; for (const bodyType in bodyTypeMap) { const bodies = (_b = schemaDocument.components) === null || _b === void 0 ? void 0 : _b[bodyType]; if (bodies) { for (const bodyName in bodies) { const body = bodies[bodyName]; if (body.content) { for (const contentType in body.content) { const contentObj = body.content[contentType]; const contentSchema = contentObj.schema; if (contentSchema && !contentSchema.$ref) { if (!contentSchema.title) { contentSchema.title = bodyName; if (contentType !== 'application/json') { contentSchema.title += `_${contentType.split('/')[1]}`; } const suffix = bodyTypeMap[bodyType]; contentSchema.title += '_' + suffix; } if (body.description && !contentSchema.description) { contentSchema.description = body.description; } } } } } } } const inputTypeMap = { parameters: 'parameter', headers: 'header', }; for (const inputType in inputTypeMap) { const inputs = (_c = schemaDocument.components) === null || _c === void 0 ? void 0 : _c[inputType]; if (inputs) { for (const inputName in inputs) { const input = inputs[inputName]; const inputSchema = input.schema; if (inputSchema && !inputSchema.$ref) { if (!inputSchema.title) { const suffix = inputTypeMap[inputType]; inputSchema.title = inputName + '_' + suffix; } if (input.description && !inputSchema.description) { inputSchema.description = input.description; } } } } } } const resolvePath = (path, root) => { var _a; try { return JsonPointer.get(root, path); } catch (e) { if ((_a = e.message) === null || _a === void 0 ? void 0 : _a.startsWith('Invalid reference')) { return undefined; } throw e; } }; function isRefObject(obj) { return typeof obj === 'object' && typeof obj.$ref === 'string'; } function isURL(str) { return /^https?:\/\//.test(str); } const getAbsolute$Ref = (given$ref, baseFilePath) => { const [givenExternalFileRelativePath, givenRefPath] = given$ref.split('#'); if (givenExternalFileRelativePath) { const cwd = isURL(baseFilePath) ? getCwdForUrl(baseFilePath) : path.dirname(baseFilePath); const givenExternalFilePath = getAbsolutePath(givenExternalFileRelativePath, cwd); if (givenRefPath) { return `${givenExternalFilePath}#${givenRefPath}`; } return givenExternalFilePath; } return `${baseFilePath}#${givenRefPath}`; }; function getCwdForUrl(url) { const urlParts = url.split('/'); urlParts.pop(); return urlParts.join('/'); } function normalizeUrl(url) { return new URL(url).toString(); } function getAbsolutePath(path$1, cwd) { if (isURL(path$1)) { return path$1; } if (isURL(cwd)) { return normalizeUrl(urlJoin(cwd, path$1)); } if (path.isAbsolute(path$1)) { return path$1; } return path.join(cwd, path$1); } function getCwd(path$1) { return isURL(path$1) ? getCwdForUrl(path$1) : path.dirname(path$1); } // eslint-disable-next-line @typescript-eslint/ban-types async function dereferenceObject(obj, { cwd = process.cwd(), externalFileCache = new Map(), refMap = new Map(), root = obj, fetchFn: fetch$1 = fetch, importFn = defaultImportFn, logger = new DefaultLogger('dereferenceObject'), resolvedObjects = new WeakSet(), headers, } = {}) { if (obj != null && typeof obj === 'object') { if (isRefObject(obj)) { const $ref = obj.$ref; if (refMap.has($ref)) { return refMap.get($ref); } else { logger.debug(`Resolving ${$ref}`); const [externalRelativeFilePath, refPath] = $ref.split('#'); if (externalRelativeFilePath) { const externalFilePath = getAbsolutePath(externalRelativeFilePath, cwd); const newCwd = getCwd(externalFilePath); let externalFile = externalFileCache.get(externalFilePath); if (!externalFile) { externalFile = await readFileOrUrl(externalFilePath, { fetch: fetch$1, headers, cwd, importFn, logger, }).catch(() => { throw new Error(`Unable to load ${externalRelativeFilePath} from ${cwd}`); }); externalFileCache.set(externalFilePath, externalFile); // Title should not be overwritten by the title given from the reference // Usually Swagger and OpenAPI Schemas have this handleUntitledDefinitions(externalFile); } const result = await dereferenceObject(refPath ? { $ref: `#${refPath}`, } : externalFile, { cwd: newCwd, externalFileCache, refMap: new Proxy(refMap, { get: (originalRefMap, key) => { switch (key) { case 'has': return (given$ref) => { const original$Ref = getAbsolute$Ref(given$ref, externalFilePath); return originalRefMap.has(original$Ref); }; case 'get': return (given$ref) => { const original$Ref = getAbsolute$Ref(given$ref, externalFilePath); return originalRefMap.get(original$Ref); }; case 'set': return (given$ref, val) => { const original$Ref = getAbsolute$Ref(given$ref, externalFilePath); return originalRefMap.set(original$Ref, val); }; } throw new Error('Not implemented ' + key.toString()); }, }), fetchFn: fetch$1, importFn, logger, headers, root: externalFile, resolvedObjects, }); refMap.set($ref, result); resolvedObjects.add(result); if (result && !result.$resolvedRef) { result.$resolvedRef = refPath; } if (obj.title && !result.title) { result.title = obj.title; } return result; } else { const resolvedObj = resolvePath(refPath, root); if (resolvedObjects.has(resolvedObj)) { refMap.set($ref, resolvedObj); return resolvedObj; } /* if (resolvedObj && !resolvedObj.$resolvedRef) { resolvedObj.$resolvedRef = refPath; } */ const result = await dereferenceObject(resolvedObj, { cwd, externalFileCache, refMap, root, fetchFn: fetch$1, importFn, logger, headers, resolvedObjects, }); if (!result) { return obj; } resolvedObjects.add(result); refMap.set($ref, result); if (!result.$resolvedRef) { result.$resolvedRef = refPath; } return result; } } } else { if (!resolvedObjects.has(obj)) { resolvedObjects.add(obj); for (const key in obj) { const val = obj[key]; if (typeof val === 'object') { obj[key] = await dereferenceObject(val, { cwd, externalFileCache, refMap, root, fetchFn: fetch$1, headers, resolvedObjects, }); } } } } } return obj; } async function compareJSONSchemas(oldSchema, newSchema) { const breakingChanges = []; await visitJSONSchema(oldSchema, { enter: (oldSubSchema, { path }) => { var _a, _b, _c, _d; if (typeof newSchema === 'object') { const newSubSchema = resolvePath(path, newSchema); if (typeof oldSubSchema === 'boolean') { if (newSubSchema !== oldSubSchema) { breakingChanges.push(`${path} is changed from ${oldSubSchema} to ${newSubSchema}`); } } else { if (oldSubSchema.$ref) { if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.$ref) !== oldSubSchema.$ref) { breakingChanges.push(`${path}/$ref is changed from ${oldSubSchema.$ref} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.$ref}`); } } if (oldSubSchema.const) { if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.const) !== oldSubSchema.const) { breakingChanges.push(`${path}/const is changed from ${oldSubSchema.const} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.const}`); } } if (oldSubSchema.enum) { for (const enumValue of oldSubSchema.enum) { if (!((_a = newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.enum) === null || _a === void 0 ? void 0 : _a.includes(enumValue))) { breakingChanges.push(`${path}/enum doesn't have ${enumValue} anymore`); } } } if (oldSubSchema.format) { if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.format) !== oldSubSchema.format) { breakingChanges.push(`${path}/format is changed from ${oldSubSchema.format} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.format}`); } } if (oldSubSchema.maxLength) { if (oldSubSchema.maxLength > (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.maxLength)) { breakingChanges.push(`${path}/maxLength is changed from ${oldSubSchema.maxLength} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.maxLength}`); } } if (oldSubSchema.minLength) { if (oldSubSchema.minLength < (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.minLength)) { breakingChanges.push(`${path}/minLength is changed from ${oldSubSchema.minLength} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.minLength}`); } } if (oldSubSchema.pattern) { if (((_b = newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.pattern) === null || _b === void 0 ? void 0 : _b.toString()) !== oldSubSchema.pattern.toString()) { breakingChanges.push(`${path}/pattern is changed from ${oldSubSchema.pattern} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.pattern}`); } } if (oldSubSchema.properties) { for (const propertyName in oldSubSchema.properties) { if (((_c = newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.properties) === null || _c === void 0 ? void 0 : _c[propertyName]) == null) { breakingChanges.push(`${path}/properties doesn't have ${propertyName}`); } } } if (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.required) { for (const propertyName of newSubSchema.required) { if (!((_d = oldSubSchema.required) === null || _d === void 0 ? void 0 : _d.includes(propertyName))) { breakingChanges.push(`${path}/required has ${propertyName} an extra`); } } } if (oldSubSchema.title) { if ((newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.title) !== oldSubSchema.title) { breakingChanges.push(`${path}/title is changed from ${oldSubSchema.title} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.title}`); } } if (oldSubSchema.type) { if (typeof (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type) === 'string' ? (newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type) !== oldSubSchema.type : Array.isArray(newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type) ? Array.isArray(oldSubSchema.type) ? oldSubSchema.type.some(typeName => !(newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type.includes(typeName))) : !(newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type.includes(oldSubSchema.type)) : true) { breakingChanges.push(`${path}/type is changed from ${oldSubSchema.type} to ${newSubSchema === null || newSubSchema === void 0 ? void 0 : newSubSchema.type}`); } } } } return oldSubSchema; }, }, { visitedSubschemaResultMap: new WeakMap(), path: '', }); if (breakingChanges.length > 0) { throw new AggregateError(breakingChanges.map(breakingChange => new Error(breakingChange)), `Breaking changes are found:\n${breakingChanges.join('\n')}`); } } 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', ]; const AnySchema = { title: 'Any', oneOf: [ { type: 'string' }, { type: 'integer' }, { type: 'boolean' }, { type: 'number' }, { type: 'object', additionalProperties: true }, ], }; async function healJSONSchema(schema, { logger = new DefaultLogger('healJSONSchema') } = {}) { const schemaByTitle = new Map(); const anySchemaOneOfInspect = inspect(AnySchema.oneOf); return visitJSONSchema(schema, { enter: async function healSubschema(subSchema, { path }) { if (typeof subSchema === 'object') { if (subSchema.title === 'Any' || (subSchema.oneOf && inspect(subSchema.oneOf) === anySchemaOneOfInspect)) { return 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 (inspect(subSchema) === 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 (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] = 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 } = toJsonSchema(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 = asArray(subSchema.examples || subSchema.example || []); const generatedSchema = toJsonSchema(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 AnySchema; } return defaultFunc(type, schema, value); }, }); subSchema.type = asArray(generatedSchema.type)[0]; subSchema.properties = generatedSchema.properties; // 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.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; } } } return subSchema; }, }, { visitedSubschemaResultMap: new WeakMap(), path: '', }); } async function referenceJSONSchema(schema, logger = new DefaultLogger('referenceJSONSchema')) { const initialDefinitions = {}; const { $ref: initialRef } = await visitJSONSchema(schema, { enter: (subSchema, { path }) => { if (typeof subSchema === 'object') { // Remove $id refs delete subSchema.$id; if (subSchema.$ref) { return subSchema; } else if (subSchema.title) { logger.debug(`Referencing ${path}`); if (subSchema.title in initialDefinitions) { let cnt = 2; while (`${subSchema.title}${cnt}` in initialDefinitions) { cnt++; } const definitionProp = `${subSchema.title}${cnt}`.split(' ').join('_SPACE_'); initialDefinitions[definitionProp] = subSchema; return { $ref: `#/definitions/${definitionProp}`, ...subSchema, }; } else { const definitionProp = subSchema.title.split(' ').join('_SPACE_'); initialDefinitions[definitionProp] = subSchema; return { $ref: `#/definitions/${definitionProp}`, ...subSchema, }; } } else if (subSchema.type === 'object') { logger.debug(`${path} cannot be referenced because it has no title`); } } return subSchema; }, }); const { definitions: finalDefinitions } = await visitJSONSchema({ definitions: initialDefinitions, }, { enter: subSchema => { if (typeof subSchema === 'object') { if (subSchema.$ref) { return { $ref: subSchema.$ref, }; } } return subSchema; }, }); return { $ref: initialRef, definitions: finalDefinitions, }; } export { AnySchema, compareJSONSchemas, dereferenceObject, getAbsolutePath, getCwd, handleUntitledDefinitions, healJSONSchema, referenceJSONSchema, resolvePath, visitJSONSchema };