UNPKG

@ardatan/openapi-to-graphql

Version:

Generates a GraphQL schema for a given OpenAPI Specification (OAS)

1,054 lines 48.4 kB
"use strict"; // Copyright IBM Corp. 2018. All Rights Reserved. // Node module: openapi-to-graphql // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT Object.defineProperty(exports, "__esModule", { value: true }); // Imports: const Oas3Tools = require("./oas_3_tools"); const deepEqual = require("deep-equal"); const debug_1 = require("debug"); const utils_1 = require("./utils"); const graphql_1 = require("./types/graphql"); const preprocessingLog = debug_1.default('preprocessing'); /** * Extract information from the OAS and put it inside a data structure that * is easier for OpenAPI-to-GraphQL to use */ function preprocessOas(oass, options) { const data = { usedTypeNames: [ 'Query', 'Mutation' // Used by OpenAPI-to-GraphQL for root-level element ], defs: [], operations: {}, saneMap: {}, security: {}, options, oass }; oass.forEach(oas => { // Store stats on OAS: data.options.report.numOps += Oas3Tools.countOperations(oas); data.options.report.numOpsMutation += Oas3Tools.countOperationsMutation(oas); data.options.report.numOpsQuery += Oas3Tools.countOperationsQuery(oas); // Get security schemes const currentSecurity = getProcessedSecuritySchemes(oas, data); const commonSecurityPropertyName = utils_1.getCommonPropertyNames(data.security, currentSecurity); commonSecurityPropertyName.forEach(propertyName => { utils_1.handleWarning({ typeKey: 'DUPLICATE_SECURITY_SCHEME', message: `Multiple OASs share security schemes with the same name '${propertyName}'`, mitigationAddendum: `The security scheme from OAS ` + `'${currentSecurity[propertyName].oas.info.title}' will be ignored`, data, log: preprocessingLog }); }); // Do not overwrite preexisting security schemes data.security = Object.assign(Object.assign({}, currentSecurity), data.security); // Process all operations for (let path in oas.paths) { for (let method in oas.paths[path]) { // Only consider Operation Objects if (!Oas3Tools.isOperation(method)) { continue; } const endpoint = oas.paths[path][method]; const operationString = oass.length === 1 ? Oas3Tools.formatOperationString(method, path) : Oas3Tools.formatOperationString(method, path, oas.info.title); // Determine description let description = endpoint.description; if ((typeof description !== 'string' || description === '') && typeof endpoint.summary === 'string') { description = endpoint.summary; } if (data.options.equivalentToMessages) { // Description may not exist if (typeof description !== 'string') { description = ''; } description += `\n\nEquivalent to ${operationString}`; } // Hold on to the operationId const operationId = typeof endpoint.operationId !== 'undefined' ? endpoint.operationId : Oas3Tools.generateOperationId(method, path); // Request schema const { payloadContentType, payloadSchema, payloadSchemaNames, payloadRequired } = Oas3Tools.getRequestSchemaAndNames(path, method, oas); const payloadDefinition = payloadSchema && typeof payloadSchema !== 'undefined' ? createDataDef(payloadSchemaNames, payloadSchema, true, data, undefined, oas) : undefined; // Response schema const { responseContentType, responseSchema, responseSchemaNames, statusCode } = Oas3Tools.getResponseSchemaAndNames(path, method, oas, data, options); if (!responseSchema || typeof responseSchema !== 'object') { utils_1.handleWarning({ typeKey: 'MISSING_RESPONSE_SCHEMA', message: `Operation ${operationString} has no (valid) response schema. ` + `You can use the fillEmptyResponses option to create a ` + `placeholder schema`, data, log: preprocessingLog }); continue; } // Links const links = Oas3Tools.getEndpointLinks(path, method, oas, data); const responseDefinition = createDataDef(responseSchemaNames, responseSchema, false, data, links, oas); // Parameters const parameters = Oas3Tools.getParameters(path, method, oas); // Security protocols const securityRequirements = options.viewer ? Oas3Tools.getSecurityRequirements(path, method, data.security, oas) : []; // Servers const servers = Oas3Tools.getServers(path, method, oas); // Whether to place this operation into an authentication viewer const inViewer = securityRequirements.length > 0 && data.options.viewer !== false; /** * Whether the operation should be added as a Query or Mutation field. * By default, all GET operations are Query fields and all other * operations are Mutation fields. */ let isMutation = method.toLowerCase() !== 'get'; // Option selectQueryOrMutationField can override isMutation if (typeof options.selectQueryOrMutationField === 'object' && typeof options.selectQueryOrMutationField[oas.info.title] === 'object' && typeof options.selectQueryOrMutationField[oas.info.title][path] === 'object' && typeof options.selectQueryOrMutationField[oas.info.title][path][method] === 'number' // This is an TS enum, which is translated to have a integer value ) { isMutation = options.selectQueryOrMutationField[oas.info.title][path][method] === graphql_1.GraphQLOperationType.Mutation; } // Store determined information for operation const operation = { operationId, operationString, description, path, method: method.toLowerCase(), payloadContentType, payloadDefinition, payloadRequired, responseContentType, responseDefinition, parameters, securityRequirements, servers, inViewer, isMutation, statusCode, oas }; /** * Handle operationId property name collision * May occur if multiple OAS are provided */ if (operationId in data.operations) { utils_1.handleWarning({ typeKey: 'DUPLICATE_OPERATIONID', message: `Multiple OASs share operations with the same operationId '${operationId}'`, mitigationAddendum: `The operation from the OAS '${operation.oas.info.title}' will be ignored`, data, log: preprocessingLog }); } else { data.operations[operationId] = operation; } } } }); return data; } exports.preprocessOas = preprocessOas; /** * Extracts the security schemes from given OAS and organizes the information in * a data structure that is easier for OpenAPI-to-GraphQL to use * * Here is the structure of the data: * { * {string} [sanitized name] { Contains information about the security protocol * {string} rawName Stores the raw security protocol name * {object} def Definition provided by OAS * {object} parameters Stores the names of the authentication credentials * NOTE: Structure will depend on the type of the protocol * (e.g. basic authentication, API key, etc.) * NOTE: Mainly used for the AnyAuth viewers * {object} schema Stores the GraphQL schema to create the viewers * } * } * * Here is an example: * { * MyApiKey: { * rawName: "My_api_key", * def: { ... }, * parameters: { * apiKey: MyKeyApiKey * }, * schema: { ... } * } * MyBasicAuth: { * rawName: "My_basic_auth", * def: { ... }, * parameters: { * username: MyBasicAuthUsername, * password: MyBasicAuthPassword, * }, * schema: { ... } * } * } */ function getProcessedSecuritySchemes(oas, data) { const result = {}; const security = Oas3Tools.getSecuritySchemes(oas); // Loop through all the security protocols for (let key in security) { const protocol = security[key]; // Determine the schema and the parameters for the security protocol let schema; let parameters = {}; let description; switch (protocol.type) { case 'apiKey': description = `API key credentials for the security protocol '${key}'`; if (data.oass.length > 1) { description += ` in ${oas.info.title}`; } parameters = { apiKey: Oas3Tools.sanitize(`${key}_apiKey`, Oas3Tools.CaseStyle.camelCase) }; schema = { type: 'object', description, properties: { apiKey: { type: 'string' } } }; break; case 'http': switch (protocol.scheme) { /** * TODO: HTTP has a number of authentication types * * See http://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml */ case 'basic': description = `Basic auth credentials for security protocol '${key}'`; parameters = { username: Oas3Tools.sanitize(`${key}_username`, Oas3Tools.CaseStyle.camelCase), password: Oas3Tools.sanitize(`${key}_password`, Oas3Tools.CaseStyle.camelCase) }; schema = { type: 'object', description, properties: { username: { type: 'string' }, password: { type: 'string' } } }; break; default: utils_1.handleWarning({ typeKey: 'UNSUPPORTED_HTTP_SECURITY_SCHEME', message: `Currently unsupported HTTP authentication protocol ` + `type 'http' and scheme '${protocol.scheme}' in OAS ` + `'${oas.info.title}'`, data, log: preprocessingLog }); } break; // TODO: Implement case 'openIdConnect': utils_1.handleWarning({ typeKey: 'UNSUPPORTED_HTTP_SECURITY_SCHEME', message: `Currently unsupported HTTP authentication protocol ` + `type 'openIdConnect' in OAS '${oas.info.title}'`, data, log: preprocessingLog }); break; case 'oauth2': utils_1.handleWarning({ typeKey: 'OAUTH_SECURITY_SCHEME', message: `OAuth security scheme found in OAS '${oas.info.title}'. ` + `OAuth support is provided using the 'tokenJSONpath' option`, data, log: preprocessingLog }); // Continue because we do not want to create an OAuth viewer continue; default: utils_1.handleWarning({ typeKey: 'UNSUPPORTED_HTTP_SECURITY_SCHEME', message: `Unsupported HTTP authentication protocol` + `type '${protocol.type}' in OAS '${oas.info.title}'`, data, log: preprocessingLog }); } // Add protocol data to the output result[key] = { rawName: key, def: protocol, parameters, schema, oas }; } return result; } /** * Method to either create a new or reuse an existing, centrally stored data * definition. Data definitions are objects that hold a schema (= JSON schema), * an otName (= String to use as the name for object types), and an iotName * (= String to use as the name for input object types). Eventually, data * definitions also hold an ot (= the object type for the schema) and an iot * (= the input object type for the schema). * * Either names or preferredName should exist. */ function createDataDef(names, schema, isInputObjectType, data, links, oas) { const preferredName = getPreferredName(names); // Basic validation test if (typeof schema !== 'object') { utils_1.handleWarning({ typeKey: 'MISSING_SCHEMA', message: `Could not create data definition for schema with ` + `preferred name '${preferredName}' and schema '${JSON.stringify(schema)}'`, data, log: preprocessingLog }); // TODO: Does this change make the option fillEmptyResponses obsolete? return { preferredName, schema: null, required: [], links: null, subDefinitions: null, graphQLTypeName: null, graphQLInputObjectTypeName: null, targetGraphQLType: 'json' }; } else { if ('$ref' in schema) { schema = Oas3Tools.resolveRef(schema['$ref'], oas); } const saneLinks = {}; if (typeof links === 'object') { Object.keys(links).forEach(linkKey => { saneLinks[Oas3Tools.sanitize(linkKey, !data.options.simpleNames ? Oas3Tools.CaseStyle.camelCase : Oas3Tools.CaseStyle.simple)] = links[linkKey]; }); } // Determine the index of possible existing data definition const index = getSchemaIndex(preferredName, schema, data.defs); if (index !== -1) { // Found existing data definition and fetch it const existingDataDef = data.defs[index]; /** * Collapse links if possible, i.e. if the current operation has links, * combine them with the prexisting ones */ if (typeof saneLinks !== 'undefined') { if (typeof existingDataDef.links !== 'undefined') { // Check if there are any overlapping links Object.keys(existingDataDef.links).forEach(saneLinkKey => { if (typeof saneLinks[saneLinkKey] !== 'undefined' && !deepEqual(existingDataDef.links[saneLinkKey], saneLinks[saneLinkKey])) { utils_1.handleWarning({ typeKey: 'DUPLICATE_LINK_KEY', message: `Multiple operations with the same response body share the same sanitized ` + `link key '${saneLinkKey}' but have different link definitions ` + `'${JSON.stringify(existingDataDef.links[saneLinkKey])}' and ` + `'${JSON.stringify(saneLinks[saneLinkKey])}'.`, data, log: preprocessingLog }); } }); /** * Collapse the links * * Avoid overwriting preexisting links */ existingDataDef.links = Object.assign(Object.assign({}, saneLinks), existingDataDef.links); } else { // No preexisting links, so simply assign the links existingDataDef.links = saneLinks; } } return existingDataDef; } else { // Else, define a new name, store the def, and return it const name = getSchemaName(names, data.usedTypeNames); // Store and sanitize the name const saneName = !data.options.simpleNames ? Oas3Tools.sanitize(name, Oas3Tools.CaseStyle.PascalCase) : Oas3Tools.capitalize(Oas3Tools.sanitize(name, Oas3Tools.CaseStyle.simple)); const saneInputName = Oas3Tools.capitalize(saneName + 'Input'); Oas3Tools.storeSaneName(saneName, name, data.saneMap); /** * TODO: is there a better way of copying the schema object? * * Perhaps, just copy it at the root level (operation schema) */ const collapsedSchema = resolveAllOf(schema, {}, data, oas); const targetGraphQLType = Oas3Tools.getSchemaTargetGraphQLType(collapsedSchema, data); const def = { preferredName, /** * Note that schema may contain $ref or schema composition (e.g. allOf) * * TODO: the schema is used in getSchemaIndex, which allows us to check * whether a dataDef has already been created for that particular * schema and name pair. The look up should resolve references but * currently, it does not. */ schema, required: [], targetGraphQLType, subDefinitions: undefined, links: saneLinks, graphQLTypeName: saneName, graphQLInputObjectTypeName: saneInputName }; // Used type names and defs of union and object types are pushed during creation if (targetGraphQLType === 'object' || targetGraphQLType === 'list' || targetGraphQLType === 'enum') { data.usedTypeNames.push(saneName); data.usedTypeNames.push(saneInputName); // Add the def to the master list data.defs.push(def); } // We currently only support simple cases of anyOf and oneOf if ( // TODO: Should also consider if the member schema contains type data (Array.isArray(collapsedSchema.anyOf) && Array.isArray(collapsedSchema.oneOf)) || // anyOf and oneOf used concurrently hasNestedAnyOfUsage(collapsedSchema, oas) || hasNestedOneOfUsage(collapsedSchema, oas)) { utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${JSON.stringify(schema)}' contains either both ` + `'anyOf' and 'oneOf' or nested 'anyOf' and 'oneOf' which ` + `is currently not supported.`, mitigationAddendum: `Use arbitrary JSON type instead.`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; return def; } // oneOf will ideally be turned into a union type if (Array.isArray(collapsedSchema.oneOf)) { const oneOfDataDef = createDataDefFromOneOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas); if (typeof oneOfDataDef === 'object') { return oneOfDataDef; } } /** * anyOf will ideally be turned into an object type * * Fields common to all member schemas will be made non-null */ if (Array.isArray(collapsedSchema.anyOf)) { const anyOfDataDef = createDataDefFromAnyOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas); if (typeof anyOfDataDef === 'object') { return anyOfDataDef; } } if (targetGraphQLType) { switch (targetGraphQLType) { case 'list': if (typeof collapsedSchema.items === 'object') { // Break schema down into component parts // I.e. if it is an list type, create a reference to the list item type // Or if it is an object type, create references to all of the field types let itemsSchema = collapsedSchema.items; let itemsName = `${name}ListItem`; if ('$ref' in itemsSchema) { itemsName = collapsedSchema.items['$ref'].split('/').pop(); } const subDefinition = createDataDef( // Is this the correct classification for this name? It does not matter in the long run. { fromRef: itemsName }, itemsSchema, isInputObjectType, data, undefined, oas); // Add list item reference def.subDefinitions = subDefinition; } break; case 'object': def.subDefinitions = {}; if (typeof collapsedSchema.properties === 'object' && Object.keys(collapsedSchema.properties).length > 0) { addObjectPropertiesToDataDef(def, collapsedSchema, def.required, isInputObjectType, data, oas); } else { utils_1.handleWarning({ typeKey: 'OBJECT_MISSING_PROPERTIES', message: `Schema ${JSON.stringify(schema)} does not have ` + `any properties`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; } break; } } else { // No target GraphQL type utils_1.handleWarning({ typeKey: 'UNKNOWN_TARGET_TYPE', message: `No GraphQL target type could be identified for schema '${JSON.stringify(schema)}'.`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; } return def; } } } exports.createDataDef = createDataDef; /** * Returns the index of the data definition object in the given list that * contains the same schema and preferred name as the given one. Returns -1 if * that schema could not be found. */ function getSchemaIndex(preferredName, schema, dataDefs) { /** * TODO: instead of iterating through the whole list every time, create a * hashing function and store all of the DataDefinitions in a hashmap. */ for (let index = 0; index < dataDefs.length; index++) { const def = dataDefs[index]; /** * TODO: deepEquals is not sufficient. We also need to resolve references. * However, deepEquals should work for vast majority of cases. */ if (preferredName === def.preferredName && deepEqual(schema, def.schema)) { return index; } } // The schema could not be found in the master list return -1; } /** * Determines the preferred name to use for schema regardless of name collisions. * * In other words, determines the ideal name for a schema. * * Similar to getSchemaName() except it does not check if the name has already * been taken. */ function getPreferredName(names) { if (typeof names.preferred === 'string') { return Oas3Tools.sanitize(names.preferred, Oas3Tools.CaseStyle.PascalCase); // CASE: preferred name already known } else if (typeof names.fromRef === 'string') { return Oas3Tools.sanitize(names.fromRef, Oas3Tools.CaseStyle.PascalCase); // CASE: name from reference } else if (typeof names.fromSchema === 'string') { return Oas3Tools.sanitize(names.fromSchema, Oas3Tools.CaseStyle.PascalCase); // CASE: name from schema (i.e., "title" property in schema) } else if (typeof names.fromPath === 'string') { return Oas3Tools.sanitize(names.fromPath, Oas3Tools.CaseStyle.PascalCase); // CASE: name from path } else { return 'PlaceholderName'; // CASE: placeholder name } } /** * Determines name to use for schema from previously determined schemaNames and * considering not reusing existing names. */ function getSchemaName(names, usedNames) { if (Object.keys(names).length === 1 && typeof names.preferred === 'string') { throw new Error(`Cannot create data definition without name(s), excluding the preferred name.`); } let schemaName; // CASE: name from reference if (typeof names.fromRef === 'string') { const saneName = Oas3Tools.sanitize(names.fromRef, Oas3Tools.CaseStyle.PascalCase); if (!usedNames.includes(saneName)) { schemaName = names.fromRef; } } // CASE: name from schema (i.e., "title" property in schema) if (!schemaName && typeof names.fromSchema === 'string') { const saneName = Oas3Tools.sanitize(names.fromSchema, Oas3Tools.CaseStyle.PascalCase); if (!usedNames.includes(saneName)) { schemaName = names.fromSchema; } } // CASE: name from path if (!schemaName && typeof names.fromPath === 'string') { const saneName = Oas3Tools.sanitize(names.fromPath, Oas3Tools.CaseStyle.PascalCase); if (!usedNames.includes(saneName)) { schemaName = names.fromPath; } } // CASE: all names are already used - create approximate name if (!schemaName) { schemaName = Oas3Tools.sanitize(typeof names.fromRef === 'string' ? names.fromRef : typeof names.fromSchema === 'string' ? names.fromSchema : typeof names.fromPath === 'string' ? names.fromPath : 'PlaceholderName', Oas3Tools.CaseStyle.PascalCase); } if (usedNames.includes(schemaName)) { let appendix = 2; /** * GraphQL Objects cannot share the name so if the name already exists in * the master list append an incremental number until the name does not * exist anymore. */ while (usedNames.includes(`${schemaName}${appendix}`)) { appendix++; } schemaName = `${schemaName}${appendix}`; } return schemaName; } /** * Add the properties to the data definition */ function addObjectPropertiesToDataDef(def, schema, required, isInputObjectType, data, oas) { /** * Resolve all required properties * * TODO: required may contain duplicates, which is not necessarily a problem */ if (Array.isArray(schema.required)) { schema.required.forEach(requiredProperty => { required.push(requiredProperty); }); } for (let propertyKey in schema.properties) { let propSchemaName = propertyKey; let propSchema = schema.properties[propertyKey]; if ('$ref' in propSchema) { propSchemaName = propSchema['$ref'].split('/').pop(); propSchema = Oas3Tools.resolveRef(propSchema['$ref'], oas); } if (!(propertyKey in def.subDefinitions)) { const subDefinition = createDataDef({ fromRef: propSchemaName, fromSchema: propSchema.title // TODO: Currently not utilized because of fromRef but arguably, propertyKey is a better field name and title is a better type name }, propSchema, isInputObjectType, data, undefined, oas); // Add field type references def.subDefinitions[propertyKey] = subDefinition; } else { utils_1.handleWarning({ typeKey: 'DUPLICATE_FIELD_NAME', message: `By way of resolving 'allOf', multiple schemas contain ` + `properties with the same name, preventing consolidation. Cannot ` + `add property '${propertyKey}' from schema '${JSON.stringify(schema)}' ` + `to dataDefinition '${JSON.stringify(def)}'`, data, log: preprocessingLog }); } } } /** * Recursively traverse a schema and resolve allOf by appending the data to the * parent schema */ function resolveAllOf(schema, references, data, oas) { // Dereference schema if ('$ref' in schema) { const referenceLocation = schema['$ref']; schema = Oas3Tools.resolveRef(schema['$ref'], oas); if (referenceLocation in references) { return references[referenceLocation]; } else { // Store references in case of circular allOf references[referenceLocation] = schema; } } const collapsedSchema = JSON.parse(JSON.stringify(schema)); // Resolve allOf if (Array.isArray(collapsedSchema.allOf)) { collapsedSchema.allOf.forEach(memberSchema => { // Collapse type if applicable const resolvedSchema = resolveAllOf(memberSchema, references, data, oas); if (resolvedSchema.type) { if (!collapsedSchema.type) { collapsedSchema.type = resolvedSchema.type; // Add type if applicable } else if (collapsedSchema.type !== resolvedSchema.type) { // Incompatible schema type utils_1.handleWarning({ typeKey: 'UNRESOLVABLE_SCHEMA', message: `Resolving 'allOf' field in schema '${collapsedSchema}' ` + `results in incompatible schema type from partial schema '${resolvedSchema}'.`, data, log: preprocessingLog }); } } // Collapse properties if applicable if ('properties' in resolvedSchema) { if (!('properties' in collapsedSchema)) { collapsedSchema.properties = {}; } Object.entries(resolvedSchema.properties).forEach(([propertyName, property]) => { if (propertyName in collapsedSchema) { // Conflicting property utils_1.handleWarning({ typeKey: 'UNRESOLVABLE_SCHEMA', message: `Resolving 'allOf' field in schema '${collapsedSchema}' ` + `results in incompatible property field from partial schema '${resolvedSchema}'.`, data, log: preprocessingLog }); } else { collapsedSchema.properties[propertyName] = property; } }); } // Collapse oneOf if applicable if ('oneOf' in resolvedSchema) { if (!('oneOf' in collapsedSchema)) { collapsedSchema.oneOf = []; } resolvedSchema.oneOf.forEach(oneOfProperty => { collapsedSchema.oneOf.push(oneOfProperty); }); } // Collapse anyOf if applicable if ('anyOf' in resolvedSchema) { if (!('anyOf' in collapsedSchema)) { collapsedSchema.anyOf = []; } resolvedSchema.anyOf.forEach(anyOfProperty => { collapsedSchema.anyOf.push(anyOfProperty); }); } // Collapse required if applicable if ('required' in resolvedSchema) { if (!('required' in collapsedSchema)) { collapsedSchema.required = []; } resolvedSchema.required.forEach(requiredProperty => { if (!collapsedSchema.required.includes(requiredProperty)) { collapsedSchema.required.push(requiredProperty); } }); } }); } return collapsedSchema; } /** * In the context of schemas that use keywords that combine member schemas, * collect data on certain aspects so it is all in one place for processing. */ function getMemberSchemaData(schemas, data, oas) { const result = { allTargetGraphQLTypes: [], allProperties: [], allRequired: [] }; schemas.forEach(schema => { // Dereference schemas if ('$ref' in schema) { schema = Oas3Tools.resolveRef(schema['$ref'], oas); } // Consolidate target GraphQL type const memberTargetGraphQLType = Oas3Tools.getSchemaTargetGraphQLType(schema, data); if (memberTargetGraphQLType) { result.allTargetGraphQLTypes.push(memberTargetGraphQLType); } // Consolidate properties if (schema.properties) { result.allProperties.push(schema.properties); } // Consolidate required if (schema.required) { result.allRequired = result.allRequired.concat(schema.required); } }); return result; } /** * Check to see if there are cases of nested oneOf fields in the member schemas * * We currently cannot handle complex cases of oneOf and anyOf */ function hasNestedOneOfUsage(collapsedSchema, oas) { // TODO: Should also consider if the member schema contains type data return (Array.isArray(collapsedSchema.oneOf) && collapsedSchema.oneOf.some(memberSchema => { // anyOf and oneOf are nested if ('$ref' in memberSchema) { memberSchema = Oas3Tools.resolveRef(memberSchema['$ref'], oas); } return (Array.isArray(memberSchema.anyOf) || Array.isArray(memberSchema.oneOf) // Nested oneOf would result in nested unions which are not allowed by GraphQL ); })); } /** * Check to see if there are cases of nested anyOf fields in the member schemas * * We currently cannot handle complex cases of oneOf and anyOf */ function hasNestedAnyOfUsage(collapsedSchema, oas) { // TODO: Should also consider if the member schema contains type data return (Array.isArray(collapsedSchema.anyOf) && collapsedSchema.anyOf.some(memberSchema => { // anyOf and oneOf are nested if ('$ref' in memberSchema) { memberSchema = Oas3Tools.resolveRef(memberSchema['$ref'], oas); } return (Array.isArray(memberSchema.anyOf) || Array.isArray(memberSchema.oneOf)); })); } /** * Create a data definition for anyOf is applicable * * anyOf should resolve into an object that contains the superset of all * properties from the member schemas */ function createDataDefFromAnyOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas) { const anyOfData = getMemberSchemaData(collapsedSchema.anyOf, data, oas); if (anyOfData.allTargetGraphQLTypes.some(memberTargetGraphQLType => { return memberTargetGraphQLType === 'object'; })) { // Every member type should be an object if (anyOfData.allTargetGraphQLTypes.every(memberTargetGraphQLType => { return memberTargetGraphQLType === 'object'; }) && anyOfData.allProperties.length > 0 // Redundant check ) { // Ensure that parent schema is compatiable with oneOf if (def.targetGraphQLType === null || def.targetGraphQLType === 'object') { const allProperties = {}; const incompatibleProperties = new Set(); /** * TODO: Check for consistent properties across all member schemas and * make them into non-nullable properties by manipulating the * required field */ if (typeof collapsedSchema.properties === 'object') { Object.keys(collapsedSchema.properties).forEach(propertyName => { allProperties[propertyName] = [ collapsedSchema.properties[propertyName] ]; }); } // Check if any member schema has conflicting properties anyOfData.allProperties.forEach(properties => { Object.keys(properties).forEach(propertyName => { if (!incompatibleProperties.has(propertyName) && // Has not been already identified as a problematic property typeof allProperties[propertyName] === 'object' && allProperties[propertyName].some(property => { // Property does not match a recorded one return !deepEqual(property, properties[propertyName]); })) { incompatibleProperties.add(propertyName); } // Add property in the store if (!(propertyName in allProperties)) { allProperties[propertyName] = []; } allProperties[propertyName].push(properties[propertyName]); }); }); def.subDefinitions = {}; if (typeof collapsedSchema.properties === 'object' && Object.keys(collapsedSchema.properties).length > 0) { addObjectPropertiesToDataDef(def, collapsedSchema, def.required, isInputObjectType, data, oas); } anyOfData.allProperties.forEach(properties => { Object.keys(properties).forEach(propertyName => { if (!incompatibleProperties.has(propertyName)) { // Dereferenced by processing anyOfData const propertySchema = properties[propertyName]; const subDefinition = createDataDef({ fromRef: propertyName, fromSchema: propertySchema.title // TODO: Currently not utilized because of fromRef but arguably, propertyKey is a better field name and title is a better type name }, propertySchema, isInputObjectType, data, undefined, oas); /** * Add field type references * There should not be any collisions */ def.subDefinitions[propertyName] = subDefinition; } }); }); // Add in incompatible properties incompatibleProperties.forEach(propertyName => { // TODO: add description def.subDefinitions[propertyName] = { targetGraphQLType: 'json' }; }); data.usedTypeNames.push(saneName); data.usedTypeNames.push(saneInputName); data.defs.push(def); def.targetGraphQLType = 'object'; return def; } else { // The parent schema is incompatible with the member schemas utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${JSON.stringify(def.schema)}' contains 'anyOf' and ` + `some member schemas are object types so create a GraphQL ` + `object type but the parent schema is a non-object type ` + `so they are not compatible.`, mitigationAddendum: `Use arbitrary JSON type instead.`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; return def; } } else { // The member schemas are not all object types utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${def.schema}' contains 'anyOf' and ` + `some member schemas are object types so create a GraphQL ` + `object type but some member schemas are non-object types ` + `so they are not compatible.`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; return def; } } } function createDataDefFromOneOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas) { const oneOfData = getMemberSchemaData(collapsedSchema.oneOf, data, oas); if (oneOfData.allTargetGraphQLTypes.some(memberTargetGraphQLType => { return memberTargetGraphQLType === 'object'; })) { // unions must be created from object types if (oneOfData.allTargetGraphQLTypes.every(memberTargetGraphQLType => { return memberTargetGraphQLType === 'object'; }) && oneOfData.allProperties.length > 0 // Redundant check ) { // Ensure that parent schema is compatiable with oneOf if (def.targetGraphQLType === null || def.targetGraphQLType === 'object') { def.subDefinitions = []; collapsedSchema.oneOf.forEach(memberSchema => { // Dereference member schema let fromRef; if ('$ref' in memberSchema) { fromRef = memberSchema['$ref'].split('/').pop(); memberSchema = Oas3Tools.resolveRef(memberSchema['$ref'], oas); } // Member types of GraphQL unions must be object types if (Oas3Tools.getSchemaTargetGraphQLType(memberSchema, data) === 'object') { const subDefinition = createDataDef({ fromRef, fromSchema: memberSchema.title, fromPath: `${saneName}Member` }, memberSchema, isInputObjectType, data, undefined, oas); def.subDefinitions.push(subDefinition); } else { utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so ` + `create a GraphQL union type but member schema '${JSON.stringify(memberSchema)}' ` + `is not an object type and union member types must be ` + `object base types.`, data, log: preprocessingLog }); } }); // Not all member schemas may have been turned into GraphQL member types if (def.subDefinitions.length > 0 && def.subDefinitions.every(subDefinition => { return subDefinition.targetGraphQLType === 'object'; })) { // Ensure all member schemas have been verified as object types data.usedTypeNames.push(saneName); data.usedTypeNames.push(saneInputName); data.defs.push(def); def.targetGraphQLType = 'union'; return def; } else { utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so ` + `create a GraphQL union type but all member schemas are not` + `object types and union member types must be object types.`, mitigationAddendum: `Use arbitrary JSON type instead.`, data, log: preprocessingLog }); // Default arbitrary JSON type def.targetGraphQLType = 'json'; return def; } } else { // The parent schema is incompatible with the member schemas utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so create ` + `a GraphQL union type but the parent schema is a non-object ` + `type and member types must be object types.`, mitigationAddendum: `Use arbitrary JSON type instead.`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; return def; } } else { // The member schemas are not all object types utils_1.handleWarning({ typeKey: 'COMBINE_SCHEMAS', message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so create ` + `a GraphQL union type but some member schemas are non-object ` + `types and union member types must be object types.`, mitigationAddendum: `Use arbitrary JSON type instead.`, data, log: preprocessingLog }); def.targetGraphQLType = 'json'; return def; } } } //# sourceMappingURL=preprocessor.js.map