UNPKG

openapi-to-graphql-harshith

Version:

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

1,454 lines (1,287 loc) 43.2 kB
// 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 /** * Functions to translate JSON schema to GraphQL (input) object types. */ // Type imports: import { PreprocessingData } from './types/preprocessing_data' import { Operation, DataDefinition, TargetGraphQLType } from './types/operation' import { Oas3, SchemaObject, ParameterObject, ReferenceObject, LinkObject } from './types/oas3' import { Args } from './types/graphql' import { GraphQLScalarType, GraphQLObjectType, GraphQLString, GraphQLID, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLNonNull, GraphQLList, GraphQLInputObjectType, GraphQLEnumType, GraphQLFieldConfigMap, GraphQLOutputType, GraphQLUnionType, GraphQLInputType, GraphQLInputFieldConfigMap } from 'graphql' import { GraphQLUpload } from 'graphql-upload' // Imports: import { GraphQLBigInt, GraphQLJSON } from 'graphql-scalars' import * as Oas3Tools from './oas_3_tools' import { getResolver, OPENAPI_TO_GRAPHQL } from './resolver_builder' import { createDataDef } from './preprocessor' import debug from 'debug' import { handleWarning, sortObject, MitigationTypes } from './utils' import crossFetch from 'cross-fetch' type GetArgsParams<TSource, TContext, TArgs> = { requestPayloadDef?: DataDefinition parameters: ParameterObject[] operation?: Operation data: PreprocessingData<TSource, TContext, TArgs> fetch: typeof crossFetch } type CreateOrReuseComplexTypeParams<TSource, TContext, TArgs> = { def: DataDefinition operation?: Operation iteration?: number // Count of recursions used to create type isInputObjectType?: boolean // Does not require isInputObjectType because unions must be composed of objects data: PreprocessingData<TSource, TContext, TArgs> // Data produced by preprocessing fetch: typeof crossFetch } type CreateOrReuseSimpleTypeParams<TSource, TContext, TArgs> = { def: DataDefinition data: PreprocessingData<TSource, TContext, TArgs> } type CreateFieldsParams<TSource, TContext, TArgs> = { def: DataDefinition links: { [key: string]: LinkObject } operation: Operation iteration: number isInputObjectType: boolean data: PreprocessingData<TSource, TContext, TArgs> fetch: typeof crossFetch } type LinkOpRefToOpIdParams<TSource, TContext, TArgs> = { links: { [key: string]: LinkObject } linkKey: string operation: Operation data: PreprocessingData<TSource, TContext, TArgs> } /** * We need to slightly modify the GraphQLJSON type. * * We need to remove the _openAPIToGraphQL or else we will leak data about * the API requests. Therefore, we need to change the serialize() function * in the GraphQLJSON type. */ const CleanGraphQLJSON = new GraphQLScalarType({ ...GraphQLJSON.toConfig(), serialize: (value) => { let cleanValue /** * If the value is an object and contains the _openAPIToGraphQL, * make a copy of the object without said field. * * NOTE: The value will only contain the _openAPIToGraphQL field if * an OAS operation is determined to return an arbitrary JSON type. * Not if a property of the return type contains an arbitrary JSON * type. */ if ( value && typeof value === 'object' && typeof value[OPENAPI_TO_GRAPHQL] === 'object' ) { cleanValue = { ...value } delete cleanValue[OPENAPI_TO_GRAPHQL] /** * As a GraphQLJSON type, the value can also be a scalar or array or * an object without the _openAPIToGraphQL field. In that case, * just use the original value. */ } else { cleanValue = value } // Use original serialize() function but with clean value return GraphQLJSON.serialize(cleanValue) } }) const translationLog = debug('translation') /** * Creates and returns a GraphQL type for the given JSON schema. */ export function getGraphQLType<TSource, TContext, TArgs>({ def, operation, data, iteration = 0, isInputObjectType = false, fetch }: CreateOrReuseComplexTypeParams<TSource, TContext, TArgs>): | GraphQLOutputType | GraphQLInputType { const name = isInputObjectType ? def.graphQLInputObjectTypeName : def.graphQLTypeName // Avoid excessive iterations if (iteration === 50) { throw new Error(`GraphQL type ${name} has excessive nesting of other types`) } switch (def.targetGraphQLType) { // CASE: object - create object type case TargetGraphQLType.object: case TargetGraphQLType.anyOfObject: return createOrReuseOt({ def, operation, data, iteration, isInputObjectType, fetch }) // CASE: union - create union type case TargetGraphQLType.oneOfUnion: return createOrReuseUnion({ def, operation, data, iteration, fetch }) // CASE: list - create list type case TargetGraphQLType.list: return createOrReuseList({ def, operation, data, iteration, isInputObjectType, fetch }) // CASE: enum - create enum type case TargetGraphQLType.enum: return createOrReuseEnum({ def, data }) // CASE: scalar - return scalar type case TargetGraphQLType.string: def.graphQLType = GraphQLString return def.graphQLType case TargetGraphQLType.integer: def.graphQLType = GraphQLInt return def.graphQLType case TargetGraphQLType.float: def.graphQLType = GraphQLFloat return def.graphQLType case TargetGraphQLType.boolean: def.graphQLType = GraphQLBoolean return def.graphQLType case TargetGraphQLType.id: def.graphQLType = GraphQLID return def.graphQLType case TargetGraphQLType.json: def.graphQLType = CleanGraphQLJSON return def.graphQLType case TargetGraphQLType.bigint: def.graphQLType = GraphQLBigInt return def.graphQLType case TargetGraphQLType.upload: def.graphQLType = GraphQLUpload return def.graphQLType } } /** * Creates an (input) object type or return an existing one, and stores it * in data * * A returned GraphQLObjectType has the following internal structure: * * new GraphQLObjectType({ * name // Optional name of the type * description // Optional description of type * fields // REQUIRED returning fields * type // REQUIRED definition of the field type * args // Optional definition of types * resolve // Optional function defining how to obtain this type * }) */ function createOrReuseOt<TSource, TContext, TArgs>({ def, operation, data, iteration, isInputObjectType, fetch }: CreateOrReuseComplexTypeParams<TSource, TContext, TArgs>): | GraphQLObjectType | GraphQLInputObjectType { // Try to reuse a preexisting (input) object type // CASE: query - reuse object type if (!isInputObjectType) { if (def.graphQLType && typeof def.graphQLType !== 'undefined') { translationLog( `Reuse object type '${def.graphQLTypeName}'` + (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : '') ) return def.graphQLType as GraphQLObjectType | GraphQLInputObjectType } // CASE: mutation - reuse input object type } else { if ( def.graphQLInputObjectType && typeof def.graphQLInputObjectType !== 'undefined' ) { translationLog( `Reuse input object type '${def.graphQLInputObjectTypeName}'` + (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : '') ) return def.graphQLInputObjectType as GraphQLInputObjectType } } // Cannot reuse preexisting (input) object type, therefore create one const schema = def.schema const description = schema.description // CASE: query - create object type if (!isInputObjectType) { translationLog( `Create object type '${def.graphQLTypeName}'` + (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : '') ) def.graphQLType = new GraphQLObjectType({ name: def.graphQLTypeName, description, fields: () => { return createFields({ def, links: def.links, operation, data, iteration, isInputObjectType: false, fetch }) as GraphQLFieldConfigMap<TSource, TContext> } }) return def.graphQLType // CASE: mutation - create input object type } else { translationLog( `Create input object type '${def.graphQLInputObjectTypeName}'` + (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : '') ) def.graphQLInputObjectType = new GraphQLInputObjectType({ name: def.graphQLInputObjectTypeName, description, fields: () => { return createFields({ def, links: {}, operation, data, iteration, isInputObjectType: true, fetch }) as GraphQLInputFieldConfigMap } }) return def.graphQLInputObjectType } } /** * Creates a union type or return an existing one, and stores it in data */ function createOrReuseUnion<TSource, TContext, TArgs>({ def, operation, data, iteration, fetch }: CreateOrReuseComplexTypeParams<TSource, TContext, TArgs>): GraphQLUnionType { // Try to reuse existing union type if (typeof def.graphQLType !== 'undefined') { translationLog( `Reuse union type '${def.graphQLTypeName}'` + (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : '') ) return def.graphQLType as GraphQLUnionType } else { translationLog( `Create union type '${def.graphQLTypeName}'` + (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : '') ) const schema = def.schema const description = typeof schema.description !== 'undefined' ? schema.description : 'No description available.' const memberTypeDefinitions = def.subDefinitions as DataDefinition[] const types = Object.values(memberTypeDefinitions).map( (memberTypeDefinition) => { return getGraphQLType({ def: memberTypeDefinition, operation, data, iteration: iteration + 1, isInputObjectType: false, fetch }) as GraphQLObjectType } ) /** * Check for ambiguous member types * * i.e. member types that can be confused with each other. */ checkAmbiguousMemberTypes(def, types, data) def.graphQLType = new GraphQLUnionType({ name: def.graphQLTypeName, description, types, resolveType: (source, context, info) => { const properties = Object.keys(source) // Remove custom _openAPIToGraphQL property used to pass data .filter((property) => property !== '_openAPIToGraphQL') /** * Find appropriate member type * * TODO: currently, the check is performed by only checking the property * names. In the future, we should also check the types of those * properties. * * TODO: there is a chance a that an intended member type cannot be * identified if, for whatever reason, the return data is a superset * of the fields specified in the OAS */ return types.find((type) => { const typeFields = Object.keys(type.getFields()) // The type should be a superset of the properties if (properties.length <= typeFields.length) { return properties.every((property) => typeFields.includes(property)) } return false }) } }) return def.graphQLType } } /** * Check for ambiguous member types * * i.e. member types that can be confused with each other. */ function checkAmbiguousMemberTypes<TSource, TContext, TArgs>( def: DataDefinition, types: GraphQLObjectType[], data: PreprocessingData<TSource, TContext, TArgs> ): void { types.sort((a, b) => { const aFieldLength = Object.keys(a.getFields()).length const bFieldLength = Object.keys(b.getFields()).length if (aFieldLength < bFieldLength) { return -1 } else if (aFieldLength < bFieldLength) { return 1 } else { return 0 } }) for (let i = 0; i < types.length - 1; i++) { const currentType = types[i] for (let j = i + 1; j < types.length; j++) { const otherType = types[j] // TODO: Check the value, not just the field name if ( Object.keys(currentType.getFields()).every((field) => { return Object.keys(otherType.getFields()).includes(field) }) ) { handleWarning({ mitigationType: MitigationTypes.AMBIGUOUS_UNION_MEMBERS, message: `Union created from schema '${JSON.stringify(def)}' contains ` + `member types such as '${currentType}' and '${otherType}' ` + `which are ambiguous. Ambiguous member types can cause ` + `problems when trying to resolve types.`, data, log: translationLog }) return } } } } /** * Creates a list type or returns an existing one, and stores it in data */ function createOrReuseList<TSource, TContext, TArgs>({ def, operation, iteration, isInputObjectType, data, fetch }: CreateOrReuseComplexTypeParams<TSource, TContext, TArgs>): GraphQLList<any> { const name = isInputObjectType ? def.graphQLInputObjectTypeName : def.graphQLTypeName // Try to reuse existing Object Type if ( !isInputObjectType && def.graphQLType && typeof def.graphQLType !== 'undefined' ) { translationLog(`Reuse GraphQLList '${def.graphQLTypeName}'`) return def.graphQLType as GraphQLList<any> } else if ( isInputObjectType && def.graphQLInputObjectType && typeof def.graphQLInputObjectType !== 'undefined' ) { translationLog(`Reuse GraphQLList '${def.graphQLInputObjectTypeName}'`) return def.graphQLInputObjectType as GraphQLList<any> } // Create new List Object Type translationLog(`Create GraphQLList '${def.graphQLTypeName}'`) // Get definition of the list item, which should be in the sub definitions const itemDef = def.subDefinitions as DataDefinition // Equivalent to schema.items const itemsSchema = itemDef.schema // Equivalent to `{name}ListItem` const itemsName = itemDef.graphQLTypeName const itemsType = getGraphQLType({ def: itemDef, data, operation, iteration: iteration + 1, isInputObjectType, fetch }) if (itemsType !== null) { const listObjectType = new GraphQLList(itemsType) // Store newly created list type if (!isInputObjectType) { def.graphQLType = listObjectType } else { def.graphQLInputObjectType = listObjectType } return listObjectType } else { throw new Error( `Cannot create list item object type '${itemsName}' in list ` + `'${name}' with schema '${JSON.stringify(itemsSchema)}'.` ) } } /** * Creates an enum type or returns an existing one, and stores it in data */ function createOrReuseEnum<TSource, TContext, TArgs>({ def, data }: CreateOrReuseSimpleTypeParams<TSource, TContext, TArgs>): GraphQLEnumType { /** * Try to reuse existing enum type * * Enum types do not have an input variant so only check def.ot */ if (def.graphQLType && typeof def.graphQLType !== 'undefined') { translationLog(`Reuse GraphQLEnumType '${def.graphQLTypeName}'`) return def.graphQLType as GraphQLEnumType } else { translationLog(`Create GraphQLEnumType '${def.graphQLTypeName}'`) const values = {} const extensionEnumMapping = def.schema[Oas3Tools.OAS_GRAPHQL_EXTENSIONS.EnumMapping] || {} def.schema.enum.forEach((enumValue) => { const enumValueString = enumValue.toString() const extensionEnumValue = extensionEnumMapping[enumValueString] if (!Oas3Tools.isSanitized(extensionEnumValue)) { throw new Error( `Cannot create enum value "${extensionEnumValue}".\nYou ` + `provided "${extensionEnumValue}" in ` + `${Oas3Tools.OAS_GRAPHQL_EXTENSIONS.EnumMapping}, but it is not ` + `GraphQL-safe."` ) } const emumValue = extensionEnumValue || Oas3Tools.sanitize( enumValueString, !data.options.simpleEnumValues ? Oas3Tools.CaseStyle.ALL_CAPS : Oas3Tools.CaseStyle.simple ) if (extensionEnumValue in values) { throw new Error( `Cannot create enum value "${extensionEnumValue}".\nYou ` + `provided "${extensionEnumValue}" in ` + `${Oas3Tools.OAS_GRAPHQL_EXTENSIONS.EnumMapping}, but it ` + `conflicts with another value "${extensionEnumValue}".` ) } values[emumValue] = { value: enumValue } }) // Store newly created Enum Object Type def.graphQLType = new GraphQLEnumType({ name: def.graphQLTypeName, values }) return def.graphQLType } } /** * Creates the fields object to be used by an (input) object type */ function createFields<TSource, TContext, TArgs>({ def, links, operation, data, iteration, isInputObjectType, fetch }: CreateFieldsParams<TSource, TContext, TArgs>): | GraphQLFieldConfigMap<any, any> | GraphQLInputFieldConfigMap { let fields: GraphQLFieldConfigMap<any, any> = {} const fieldTypeDefinitions = def.subDefinitions as { [fieldName: string]: DataDefinition } // Create fields for properties for (let fieldName in fieldTypeDefinitions) { const fieldTypeDefinition = fieldTypeDefinitions[fieldName] const fieldSchema = fieldTypeDefinition.schema // Get object type describing the property const objectType = getGraphQLType({ def: fieldTypeDefinition, operation, data, iteration: iteration + 1, isInputObjectType, fetch }) const requiredProperty = typeof def.required === 'object' && def.required.includes(fieldName) // Finally, add the object type to the fields (using sanitized field name) if (objectType) { const extensionFieldName = fieldSchema?.[Oas3Tools.OAS_GRAPHQL_EXTENSIONS.FieldName] if (!Oas3Tools.isSanitized(extensionFieldName)) { throw new Error( `Cannot create field with name "${extensionFieldName}".\nYou ` + `provided "${extensionFieldName}" in ` + `${Oas3Tools.OAS_GRAPHQL_EXTENSIONS.FieldName}, but it is not ` + `GraphQL-safe."` ) } if (extensionFieldName && extensionFieldName in fields) { throw new Error( `Cannot create field with name "${extensionFieldName}".\nYou ` + `provided "${extensionFieldName}" in ` + `${Oas3Tools.OAS_GRAPHQL_EXTENSIONS.FieldName}, but it ` + `conflicts with another field named "${extensionFieldName}".` ) } const saneFieldName = extensionFieldName || Oas3Tools.sanitize( fieldName, !data.options.simpleNames ? Oas3Tools.CaseStyle.camelCase : Oas3Tools.CaseStyle.simple ) const sanePropName = Oas3Tools.storeSaneName( saneFieldName, fieldName, data.saneMap ) fields[sanePropName] = { type: requiredProperty ? new GraphQLNonNull(objectType) : (objectType as GraphQLOutputType), description: typeof fieldSchema === 'object' ? fieldSchema.description : null } } else { handleWarning({ mitigationType: MitigationTypes.CANNOT_GET_FIELD_TYPE, message: `Cannot obtain GraphQL type for field '${fieldName}' in ` + `GraphQL type '${JSON.stringify(def.schema)}'.`, data, log: translationLog }) } } if ( typeof links === 'object' && // Links are present !isInputObjectType // Only object type (input object types cannot make use of links) ) { for (let saneLinkKey in links) { translationLog(`Create link '${saneLinkKey}'...`) // Check if key is already in fields if (saneLinkKey in fields) { handleWarning({ mitigationType: MitigationTypes.LINK_NAME_COLLISION, message: `Cannot create link '${saneLinkKey}' because parent ` + `object type already contains a field with the same ` + `(sanitized) name.`, data, log: translationLog }) } else { const link = links[saneLinkKey] // Get linked operation let linkedOpId // TODO: href is yet another alternative to operationRef and operationId if (typeof link.operationId === 'string') { linkedOpId = link.operationId } else if (typeof link.operationRef === 'string') { linkedOpId = linkOpRefToOpId({ links, linkKey: saneLinkKey, operation, data }) } /** * linkedOpId may not be initialized because operationRef may lead to an * operation object that does not have an operationId */ if (typeof linkedOpId === 'string' && linkedOpId in data.operations) { const linkedOp = data.operations[linkedOpId] // Determine parameters provided via link let argsFromLink = link.parameters // Get arguments that are not provided by the linked operation let dynamicParams = linkedOp.parameters if (typeof argsFromLink === 'object') { dynamicParams = dynamicParams.filter((param) => { return typeof argsFromLink[param.name] === 'undefined' }) } // Get resolve function for link const linkResolver = getResolver({ operation: linkedOp, argsFromLink: argsFromLink as { [key: string]: string }, data, baseUrl: data.options.baseUrl, requestOptions: data.options.requestOptions, fileUploadOptions: data.options.fileUploadOptions, fetch }) // Get arguments for link const args = getArgs({ parameters: dynamicParams, operation: linkedOp, data, fetch }) // Get response object type const resObjectType = linkedOp.responseDefinition.graphQLType !== undefined ? linkedOp.responseDefinition.graphQLType : (getGraphQLType({ def: linkedOp.responseDefinition, operation, data, iteration: iteration + 1, isInputObjectType: false, fetch }) as GraphQLOutputType) let description = link.description if (data.options.equivalentToMessages) { if (typeof description !== 'string') { description = `Equivalent to ${linkedOp.operationString}` } else { description += `\n\nEquivalent to ${linkedOp.operationString}` } } // Finally, add the object type to the fields (using sanitized field name) // TODO: check if fields already has this field name fields[saneLinkKey] = { type: resObjectType, resolve: linkResolver, args, description } } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `Cannot resolve target of link '${saneLinkKey}'`, data, log: translationLog }) } } } } fields = sortObject(fields) return fields } /** * Returns the operationId that an operationRef is associated to * * NOTE: If the operation does not natively have operationId, this function * will try to produce an operationId the same way preprocessor.js does it. * * Any changes to constructing operationIds in preprocessor.js should be * reflected here. */ function linkOpRefToOpId<TSource, TContext, TArgs>({ links, linkKey, operation, data }: LinkOpRefToOpIdParams<TSource, TContext, TArgs>): string { const link = links[linkKey] if (typeof link.operationRef === 'string') { // TODO: external refs const operationRef = link.operationRef let linkLocation let linkRelativePathAndMethod /** * Example relative path: '#/paths/~12.0~1repositories~1{username}/get' * Example absolute path: 'https://na2.gigantic-server.com/#/paths/~12.0~1repositories~1{username}/get' * Extract relative path from relative path */ if (operationRef.substring(0, 8) === '#/paths/') { linkRelativePathAndMethod = operationRef // Extract relative path from absolute path } else { /** * '#' may exist in other places in the path * '/#/' is more likely to point to the beginning of the path */ const firstPathIndex = operationRef.indexOf('#/paths/') // Found a relative path candidate if (firstPathIndex !== -1) { // Check to see if there are other relative path candidates const lastPathIndex = operationRef.lastIndexOf('#/paths/') if (firstPathIndex !== lastPathIndex) { handleWarning({ mitigationType: MitigationTypes.AMBIGUOUS_LINK, message: `The link '${linkKey}' in operation '${operation.operationString}' ` + `contains an ambiguous operationRef '${operationRef}', ` + `meaning it has multiple instances of the string '#/paths/'`, data, log: translationLog }) return } linkLocation = operationRef.substring(0, firstPathIndex) linkRelativePathAndMethod = operationRef.substring(firstPathIndex) // Cannot find relative path candidate } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The link '${linkKey}' in operation '${operation.operationString}' ` + `does not contain a valid path in operationRef '${operationRef}', ` + `meaning it does not contain a string '#/paths/'`, data, log: translationLog }) return } } // Infer operationId from relative path if (typeof linkRelativePathAndMethod === 'string') { let linkPath let linkMethod: Oas3Tools.HTTP_METHODS /** * NOTE: I wish we could extract the linkedOpId by matching the * linkedOpObject with an operation in data and extracting the operationId * there but that does not seem to be possible especiially because you * need to know the operationId just to access the operations so what I * have to do is reconstruct the operationId the same way preprocessing * does it */ /** * linkPath should be the path followed by the method * * Find the slash that divides the path from the method */ const pivotSlashIndex = linkRelativePathAndMethod.lastIndexOf('/') // Check if there are any '/' in the linkPath if (pivotSlashIndex !== -1) { // Get method // Check if there is a method at the end of the linkPath if (pivotSlashIndex !== linkRelativePathAndMethod.length - 1) { try { // Start at +1 because we do not want the starting '/' linkMethod = Oas3Tools.methodToHttpMethod( linkRelativePathAndMethod.substring(pivotSlashIndex + 1) ) } catch { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The operationRef '${operationRef}' contains an ` + `invalid HTTP method '${linkMethod}'`, data, log: translationLog }) return } // There is no method at the end of the path } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The operationRef '${operationRef}' does not contain an` + `HTTP method`, data, log: translationLog }) return } /** * Get path * * Substring starts at index 8 and ends at pivotSlashIndex to exclude * the '/'s at the ends of the path * * TODO: improve removing '/#/paths'? */ linkPath = linkRelativePathAndMethod.substring(8, pivotSlashIndex) /** * linkPath is currently a JSON Pointer * * Revert the escaped '/', represented by '~1', to form intended path */ linkPath = linkPath.replace(/~1/g, '/') // Find the right oas const oas = typeof linkLocation === 'undefined' ? operation.oas : getOasFromLinkLocation(linkLocation, link, data) // If the link was external, make sure that an OAS could be identified if (typeof oas !== 'undefined') { if (typeof linkMethod === 'string' && typeof linkPath === 'string') { let linkedOpId if (linkPath in oas.paths && linkMethod in oas.paths[linkPath]) { const linkedOpObject = oas.paths[linkPath][linkMethod] if ('operationId' in linkedOpObject) { linkedOpId = linkedOpObject.operationId } } if (typeof linkedOpId !== 'string') { linkedOpId = Oas3Tools.generateOperationId(linkMethod, linkPath) } if (linkedOpId in data.operations) { return linkedOpId } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The link '${linkKey}' references an operation with ` + `operationId '${linkedOpId}' but no such operation exists. ` + `Note that the operationId may be autogenerated but ` + `regardless, the link could not be matched to an operation.`, data, log: translationLog }) return } // Path and method could not be found } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `Cannot identify path and/or method, '${linkPath} and ` + `'${linkMethod}' respectively, from operationRef ` + `'${operationRef}' in link '${linkKey}'`, data, log: translationLog }) return } // External link could not be resolved } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The link '${link.operationRef}' references an external OAS ` + `but it was not provided`, data, log: translationLog }) return } // Cannot split relative path into path and method sections } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `Cannot extract path and/or method from operationRef ` + `'${operationRef}' in link '${linkKey}'`, data, log: translationLog }) return } // Cannot extract relative path from absolute path } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `Cannot extract path and/or method from operationRef ` + `'${operationRef}' in link '${linkKey}'`, data, log: translationLog }) return } } } /** * Determin if an argument should be created if the argument has already been * provided through the options */ function skipArg<TSource, TContext, TArgs>( parameter: ParameterObject, operation: Operation, data: PreprocessingData<TSource, TContext, TArgs> ): boolean { if (typeof data.options === 'object') { switch (parameter.in) { case 'header': // Check header option if ( typeof data.options.headers === 'object' && parameter.name in data.options.headers ) { return true } else if (typeof data.options.headers === 'function') { const headers = data.options.headers( operation.method, operation.path, operation.oas.info.title ) if (typeof headers === 'object') { return true } // Check requestOptions option } else if (typeof data.options.requestOptions === 'object') { if ( typeof data.options.requestOptions.headers === 'object' && parameter.name in data.options.requestOptions.headers ) { return true } else if ( typeof data.options.requestOptions.headers === 'function' ) { const headers = data.options.requestOptions.headers( operation.method, operation.path, operation.oas.info.title ) if (typeof headers === 'object') { return true } } } break case 'query': // Check header option if ( typeof data.options.qs === 'object' && parameter.name in data.options.qs ) { return true // Check requestOptions option } else if ( typeof data.options.requestOptions === 'object' && typeof data.options.requestOptions.qs === 'object' && parameter.name in data.options.requestOptions.qs ) { return true } break } } return false } /** * Creates the arguments for resolving a field * * Arguments that are provided via options will be ignored */ export function getArgs<TSource, TContext, TArgs>({ requestPayloadDef, parameters, operation, data, fetch }: GetArgsParams<TSource, TContext, TArgs>): Args { let args = {} // Handle params: parameters.forEach((parameter) => { // We need at least a name if (typeof parameter.name !== 'string') { handleWarning({ mitigationType: MitigationTypes.INVALID_OAS, message: `The operation '${operation.operationString}' contains a ` + `parameter '${JSON.stringify(parameter)}' with no 'name' property`, data, log: translationLog }) return } // If this parameter is provided via options, ignore if (skipArg(parameter, operation, data)) { return } /** * Determine type of parameter * * The type of the parameter can either be contained in the "schema" field * or the "content" field (but not both) */ let schema: SchemaObject | ReferenceObject if (typeof parameter.schema === 'object') { schema = parameter.schema } else if (typeof parameter.content === 'object') { if ( typeof parameter.content['application/json'] === 'object' && typeof parameter.content['application/json'].schema === 'object' ) { schema = parameter.content['application/json'].schema } else { handleWarning({ mitigationType: MitigationTypes.NON_APPLICATION_JSON_SCHEMA, message: `The operation '${operation.operationString}' contains a ` + `parameter '${JSON.stringify(parameter)}' that has a 'content' ` + `property but no schemas in application/json format. The ` + `parameter will not be created`, data, log: translationLog }) return } } else { // Invalid OAS according to 3.0.2 handleWarning({ mitigationType: MitigationTypes.INVALID_OAS, message: `The operation '${operation.operationString}' contains a ` + `parameter '${JSON.stringify(parameter)}' with no 'schema' or ` + `'content' property`, data, log: translationLog }) return } /** * Resolving the reference is necessary later in the code and by doing it, * we can avoid doing it a second time in resolveRev() */ if ('$ref' in schema) { schema = Oas3Tools.resolveRef(schema.$ref, operation.oas) } const paramDef = createDataDef( { fromSchema: parameter.name, fromExtension: schema[Oas3Tools.OAS_GRAPHQL_EXTENSIONS.TypeName] }, schema as SchemaObject, true, data, operation.oas ) const type = getGraphQLType({ def: paramDef, operation, data, iteration: 0, isInputObjectType: true, fetch }) /** * Sanitize the argument name * * NOTE: when matching these parameters back to requests, we need to again * use the real parameter name */ const saneName = Oas3Tools.sanitize( parameter.name, !data.options.simpleNames ? Oas3Tools.CaseStyle.camelCase : Oas3Tools.CaseStyle.simple ) // Parameters are not required when a default exists: let hasDefault = false if (typeof parameter.schema === 'object') { let schema = parameter.schema if ('$ref' in schema) { schema = Oas3Tools.resolveRef<SchemaObject>(schema.$ref, operation.oas) } if (typeof schema.default !== 'undefined') { hasDefault = true } } const paramRequired = parameter.required && !hasDefault args[saneName] = { type: paramRequired ? new GraphQLNonNull(type) : type, description: parameter.description // Might be undefined } }) // Add limit argument if ( data.options.addLimitArgument && typeof operation.responseDefinition === 'object' && operation.responseDefinition.schema.type === 'array' && // Only add limit argument to lists of object types, not to lists of scalar types ((operation.responseDefinition.subDefinitions as DataDefinition).schema .type === 'object' || (operation.responseDefinition.subDefinitions as DataDefinition).schema .type === 'array') ) { // Make sure slicing arguments will not overwrite preexisting arguments if ('limit' in args) { handleWarning({ mitigationType: MitigationTypes.LIMIT_ARGUMENT_NAME_COLLISION, message: `The 'limit' argument cannot be added ` + `because of a preexisting argument in ` + `operation ${operation.operationString}`, data, log: translationLog }) } else { args['limit'] = { type: GraphQLInt, description: `Auto-generated argument that limits the size of ` + `returned list of objects/list, selecting the first \`n\` ` + `elements of the list` } } } // Handle request payload (if present): if (typeof requestPayloadDef === 'object') { const reqObjectType = getGraphQLType({ def: requestPayloadDef, data, operation, isInputObjectType: true, // Request payloads will always be an input object type, fetch }) // Sanitize the argument name const saneName = data.options.genericPayloadArgName ? 'requestBody' : Oas3Tools.uncapitalize(requestPayloadDef.graphQLInputObjectTypeName) // Already sanitized const reqRequired = typeof operation === 'object' && typeof operation.payloadRequired === 'boolean' ? operation.payloadRequired : false args[saneName] = { type: reqRequired ? new GraphQLNonNull(reqObjectType) : reqObjectType, // TODO: addendum to the description explaining this is the request body description: requestPayloadDef.schema.description } } args = sortObject(args) return args } /** * Used in the context of links, specifically those using an external operationRef * If the reference is an absolute reference, determine the type of location * * For example, name reference, file path, web-hosted OAS link, etc. */ function getLinkLocationType(linkLocation: string): string { // TODO: currently we only support the title as a link location return 'title' } /** * Used in the context of links, specifically those using an external operationRef * Based on the location of the OAS, retrieve said OAS */ function getOasFromLinkLocation<TSource, TContext, TArgs>( linkLocation: string, link: LinkObject, data: PreprocessingData<TSource, TContext, TArgs> ): Oas3 { // May be an external reference switch (getLinkLocationType(linkLocation)) { case 'title': // Get the possible const possibleOass = data.oass.filter((oas) => { return oas.info.title === linkLocation }) // Check if there are an ambiguous OASs if (possibleOass.length === 1) { // No ambiguity return possibleOass[0] } else if (possibleOass.length > 1) { // Some ambiguity handleWarning({ mitigationType: MitigationTypes.AMBIGUOUS_LINK, message: `The operationRef '${link.operationRef}' references an ` + `OAS '${linkLocation}' but multiple OASs share the same title`, data, log: translationLog }) } else { // No OAS had the expected title handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The operationRef '${link.operationRef}' references an ` + `OAS '${linkLocation}' but no such OAS was provided`, data, log: translationLog }) } break // // TODO // case 'url': // break // // TODO // case 'file': // break // TODO: should title be default? // In cases of names like api.io default: handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_LINK, message: `The link location of the operationRef ` + `'${link.operationRef}' is currently not supported\n` + `Currently only the title of the OAS is supported`, data, log: translationLog }) } }