UNPKG

json-machete

Version:
776 lines (766 loc) • 35.8 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } const utils = require('@graphql-tools/utils'); const JsonPointer = _interopDefault(require('json-pointer')); const crossHelpers = require('@graphql-mesh/cross-helpers'); const urlJoin = _interopDefault(require('url-join')); const fetch = require('@whatwg-node/fetch'); const utils$1 = require('@graphql-mesh/utils'); const toJsonSchema = _interopDefault(require('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, }); } 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' && obj.$ref; } 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) : crossHelpers.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, cwd) { if (isURL(path)) { return path; } if (isURL(cwd)) { return normalizeUrl(urlJoin(cwd, path)); } if (crossHelpers.path.isAbsolute(path)) { return path; } return crossHelpers.path.join(cwd, path); } function getCwd(path) { return isURL(path) ? getCwdForUrl(path) : crossHelpers.path.dirname(path); } // eslint-disable-next-line @typescript-eslint/ban-types async function dereferenceObject(obj, { cwd = crossHelpers.process.cwd(), externalFileCache = new Map(), refMap = new Map(), root = obj, fetchFn: fetch$1 = fetch.fetch, importFn = utils$1.defaultImportFn, logger = new utils$1.DefaultLogger('dereferenceObject'), headers, } = {}) { var _a; if (obj != null && typeof obj === 'object') { if (isRefObject(obj)) { const $ref = obj.$ref; if (refMap.has($ref)) { return refMap.get($ref); } else { if (crossHelpers.process.env.DEBUG) { console.log(`Dereferencing `, obj); } 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 utils$1.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 if (externalFile.definitions) { for (const definitionName in externalFile.definitions) { const definition = externalFile.definitions[definitionName]; if (!definition.title) { definition.title = definitionName; } } } if ((_a = externalFile.components) === null || _a === void 0 ? void 0 : _a.schemas) { for (const definitionName in externalFile.components.schemas) { const definition = externalFile.components.schemas[definitionName]; if (!definition.title) { definition.title = definitionName; } } } } 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, }); refMap.set($ref, 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 (typeof resolvedObj === 'object' && !resolvedObj.$ref) { refMap.set($ref, resolvedObj); } if (resolvedObj && !resolvedObj.$resolvedRef) { resolvedObj.$resolvedRef = refPath; } const result = await dereferenceObject(resolvedObj, { cwd, externalFileCache, refMap, root, fetchFn: fetch$1, importFn, logger, headers, }); refMap.set($ref, result); if (result && !result.$resolvedRef) { result.$resolvedRef = refPath; } return result; } } } else { 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, }); } } } } 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 utils.AggregateError(breakingChanges.map(breakingChange => new Error(breakingChange)), `Breaking changes are found:\n${breakingChanges.join('\n')}`); } } const asArray = (value) => (Array.isArray(value) ? value : [value]); const reservedTypeNames = ['Query', 'Mutation', 'Subscription']; const JSONSchemaStringFormats = [ 'date', 'hostname', 'regex', 'json-pointer', 'relative-json-pointer', 'uri-reference', 'uri-template', 'date-time', 'time', 'email', 'ipv4', 'ipv6', 'uri', ]; const AnySchema = { title: 'Any', oneOf: [ { type: 'string' }, { type: 'integer' }, { type: 'boolean' }, { type: 'number' }, { type: 'object', additionalProperties: true }, ], }; const titleResolvedRefReservedMap = new WeakMap(); function removeTitlesAndResolvedRefs(schema) { if (typeof schema === 'object' && schema != null && !titleResolvedRefReservedMap.has(schema)) { if (!schema.$comment) { const titleReserved = schema.title; if (titleReserved) { schema.title = undefined; } const resolvedRefReserved = schema.$resolvedRef; if (resolvedRefReserved) { schema.$resolvedRef = undefined; } titleResolvedRefReservedMap.set(schema, { title: titleReserved, $resolvedRef: resolvedRefReserved, }); for (const key in schema) { if (key === 'properties') { for (const propertyName in schema.properties) { schema[key][propertyName] = removeTitlesAndResolvedRefs(schema[key][propertyName]); } } else { schema[key] = removeTitlesAndResolvedRefs(schema[key]); } } } } return schema; } function deduplicateJSONSchema(schema, seenMap = new Map()) { if (typeof schema === 'object' && schema != null) { if (!schema.$comment) { const stringified = utils.inspect(schema.properties || schema) .split('[Circular]') .join('[Object]'); const seen = seenMap.get(stringified); if (seen) { return seen; } seenMap.set(stringified, schema); for (const key in schema) { if (key === 'properties') { for (const propertyName in schema.properties) { schema.properties[propertyName] = deduplicateJSONSchema(schema.properties[propertyName], seenMap); } } else { schema[key] = deduplicateJSONSchema(schema[key], seenMap); } } } } return schema; } const visited = new WeakSet(); function addTitlesAndResolvedRefs(schema) { if (typeof schema === 'object' && schema != null && !visited.has(schema)) { visited.add(schema); if (!schema.$comment) { const reservedTitleAndResolveRef = titleResolvedRefReservedMap.get(schema); if (reservedTitleAndResolveRef) { if (!schema.title && reservedTitleAndResolveRef.title) { schema.title = reservedTitleAndResolveRef.title; } if (!schema.$resolvedRef && reservedTitleAndResolveRef.$resolvedRef) { schema.$resolvedRef = reservedTitleAndResolveRef.$resolvedRef; } } for (const key in schema) { if (key === 'properties') { for (const propertyName in schema.properties) { schema.properties[propertyName] = addTitlesAndResolvedRefs(schema.properties[propertyName]); } } else { schema[key] = addTitlesAndResolvedRefs(schema[key]); } } } } return schema; } async function getDeduplicatedTitles(schema) { const duplicatedTypeNames = new Set(); const seenTypeNames = new Set(); await visitJSONSchema(schema, { leave: subSchema => { if (typeof subSchema === 'object' && subSchema.title) { if (seenTypeNames.has(subSchema.title)) { duplicatedTypeNames.add(subSchema.title); } else { seenTypeNames.add(subSchema.title); } } return subSchema; }, }, { visitedSubschemaResultMap: new WeakMap(), path: '', }); return duplicatedTypeNames; } async function healJSONSchema(schema, options = {}) { let readySchema = schema; if (!(options === null || options === void 0 ? void 0 : options.noDeduplication)) { const schemaWithoutResolvedRefAndTitles = removeTitlesAndResolvedRefs(schema); const deduplicatedSchemaWithoutResolvedRefAndTitles = (options === null || options === void 0 ? void 0 : options.noDeduplication) ? schema : deduplicateJSONSchema(schemaWithoutResolvedRefAndTitles); const deduplicatedSchema = addTitlesAndResolvedRefs(deduplicatedSchemaWithoutResolvedRefAndTitles); readySchema = deduplicatedSchema; } const duplicatedTypeNames = await getDeduplicatedTitles(readySchema); return visitJSONSchema(readySchema, { enter: async function healSubschema(subSchema, { path }) { if (typeof subSchema === 'object') { if (crossHelpers.process.env.DEBUG) { console.log(`Healing ${path}`); } // We don't support following properties delete subSchema.readOnly; delete subSchema.writeOnly; const keys = Object.keys(subSchema); if (keys.length === 0) { subSchema.type = 'object'; subSchema.additionalProperties = true; } if (typeof subSchema.additionalProperties === 'object') { delete subSchema.additionalProperties.readOnly; delete subSchema.additionalProperties.writeOnly; if (Object.keys(subSchema.additionalProperties).length === 0) { subSchema.additionalProperties = true; } } if (subSchema.allOf != null && subSchema.allOf.length === 1) { const realSubschema = subSchema.allOf[0]; delete subSchema.allOf; return realSubschema; } if (subSchema.anyOf != null && subSchema.anyOf.length === 1) { const realSubschema = subSchema.anyOf[0]; delete subSchema.anyOf; return realSubschema; } if (subSchema.oneOf != null && subSchema.oneOf.length === 1) { const realSubschema = subSchema.oneOf[0]; delete subSchema.oneOf; return realSubschema; } if (subSchema.description != null) { subSchema.description = subSchema.description.trim(); if (keys.length === 1) { 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) { const realSubschema = subSchema.items; delete subSchema.items; return realSubschema; } if (subSchema.properties && subSchema.type !== 'object') { subSchema.type = 'object'; } if (duplicatedTypeNames.has(subSchema.title)) { delete subSchema.title; } if (typeof subSchema.example === 'object' && !subSchema.type) { subSchema.type = 'object'; } // Try to find the type if (!subSchema.type) { // If required exists without properties if (subSchema.required && !subSchema.properties && !subSchema.anyOf && !subSchema.allOf) { // 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) { subSchema.type = 'object'; } // Items only exist in arrays if (subSchema.items) { subSchema.type = 'array'; // Items should be an object if (Array.isArray(subSchema.items)) { if (subSchema.items.length === 0) { subSchema.items = subSchema.items[0]; } else { subSchema.items = { oneOf: subSchema.items, }; } } } if (subSchema.format === 'int64') { subSchema.type = 'integer'; } if (subSchema.format) { subSchema.type = 'string'; } } 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) { subSchema.format = format; } } } if (subSchema.format === 'dateTime') { subSchema.format = 'date-time'; } if (subSchema.type === 'string' && subSchema.format) { if (!JSONSchemaStringFormats.includes(subSchema.format)) { delete subSchema.format; } } if (subSchema.required) { if (!Array.isArray(subSchema.required)) { delete subSchema.required; } } // If it is an object type but no properties given while example is available if (((subSchema.type === 'object' && !subSchema.properties) || !subSchema.type) && subSchema.example) { const generatedSchema = toJsonSchema(subSchema.example, { required: false, objects: { additionalProperties: false, }, strings: { detectFormat: true, }, arrays: { mode: 'first', }, }); const healedGeneratedSchema = await healJSONSchema(generatedSchema, options); subSchema.type = asArray(healedGeneratedSchema.type)[0]; subSchema.properties = healedGeneratedSchema.properties; // If type for properties is already given, use it if (typeof subSchema.additionalProperties === 'object') { for (const propertyName in subSchema.properties) { subSchema.properties[propertyName] = subSchema.additionalProperties; } } } 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; let 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) { subSchema.title = pathBasedName; // Otherwise use the format name } else if (subSchema.format) { subSchema.title = subSchema.format; } break; case 'number': case 'integer': if (subSchema.enum) { subSchema.title = pathBasedName; // Otherwise use the format name } else if (subSchema.format) { subSchema.title = subSchema.format; } break; case 'array': break; default: subSchema.title = subSchema.title || pathBasedName; } // If type name is reserved, add a suffix if (reservedTypeNames.includes(pathBasedName)) { pathBasedName += '_'; } } if (subSchema.type === 'object' && subSchema.properties && Object.keys(subSchema.properties).length === 0) { delete subSchema.properties; subSchema.additionalProperties = true; } } return subSchema; }, }, { visitedSubschemaResultMap: new WeakMap(), path: '', }); } async function referenceJSONSchema(schema) { const initialDefinitions = {}; const { $ref: initialRef } = await visitJSONSchema(schema, { enter: (subSchema, { path }) => { if (typeof subSchema === 'object') { if (crossHelpers.process.env.DEBUG) { console.log(`Referencing ${path}`); } // Remove $id refs delete subSchema.$id; if (subSchema.$ref) { return subSchema; } else if (subSchema.title) { 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') { console.warn(`${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, }; } exports.compareJSONSchemas = compareJSONSchemas; exports.dereferenceObject = dereferenceObject; exports.getAbsolutePath = getAbsolutePath; exports.getCwd = getCwd; exports.healJSONSchema = healJSONSchema; exports.referenceJSONSchema = referenceJSONSchema; exports.resolvePath = resolvePath; exports.visitJSONSchema = visitJSONSchema;