UNPKG

openapi-to-graphql-harshith

Version:

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

1,232 lines (1,228 loc) 227 kB
import { GraphQLError, GraphQLScalarType, GraphQLID, GraphQLBoolean, GraphQLFloat, GraphQLInt, GraphQLString, GraphQLObjectType, GraphQLInputObjectType, GraphQLUnionType, GraphQLList, GraphQLEnumType, GraphQLNonNull, GraphQLSchema } from 'graphql'; import { GraphQLUpload } from 'graphql-upload'; import { GraphQLJSON, GraphQLBigInt } from 'graphql-scalars'; import { convertObj } from 'swagger2openapi'; import { validate } from 'oas-validator'; import debug, { debug as debug$1 } from 'debug'; import { JsonPointer } from 'json-ptr'; import { singular } from 'pluralize'; import stream from 'stream'; import { JSONPath } from 'jsonpath-plus'; import { get } from 'jsonpointer'; import formurlencoded from 'form-urlencoded'; import { PubSub } from 'graphql-subscriptions'; import urljoin from 'url-join'; import FormData from 'form-data'; import deepEqual from 'deep-equal'; import crossFetch from 'cross-fetch'; // 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 var GraphQLOperationType; (function (GraphQLOperationType) { GraphQLOperationType[GraphQLOperationType["Query"] = 0] = "Query"; GraphQLOperationType[GraphQLOperationType["Mutation"] = 1] = "Mutation"; GraphQLOperationType[GraphQLOperationType["Subscription"] = 2] = "Subscription"; })(GraphQLOperationType || (GraphQLOperationType = {})); // 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 var TargetGraphQLType; (function (TargetGraphQLType) { // scalars TargetGraphQLType["string"] = "string"; TargetGraphQLType["integer"] = "integer"; TargetGraphQLType["float"] = "float"; TargetGraphQLType["boolean"] = "boolean"; TargetGraphQLType["id"] = "id"; TargetGraphQLType["bigint"] = "bigint"; TargetGraphQLType["upload"] = "upload"; // JSON TargetGraphQLType["json"] = "json"; // non-scalars TargetGraphQLType["object"] = "object"; TargetGraphQLType["list"] = "list"; TargetGraphQLType["enum"] = "enum"; TargetGraphQLType["anyOfObject"] = "anyOfObject"; TargetGraphQLType["oneOfUnion"] = "oneOfUnion"; })(TargetGraphQLType || (TargetGraphQLType = {})); // 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 var MitigationTypes; (function (MitigationTypes) { /** * Problems with the OAS * * Should be caught by the module oas-validator */ MitigationTypes["INVALID_OAS"] = "INVALID_OAS"; MitigationTypes["UNNAMED_PARAMETER"] = "UNNAMED_PARAMETER"; // General problems MitigationTypes["AMBIGUOUS_UNION_MEMBERS"] = "AMBIGUOUS_UNION_MEMBERS"; MitigationTypes["CANNOT_GET_FIELD_TYPE"] = "CANNOT_GET_FIELD_TYPE"; MitigationTypes["COMBINE_SCHEMAS"] = "COMBINE_SCHEMAS"; MitigationTypes["DUPLICATE_FIELD_NAME"] = "DUPLICATE_FIELD_NAME"; MitigationTypes["DUPLICATE_LINK_KEY"] = "DUPLICATE_LINK_KEY"; MitigationTypes["INVALID_HTTP_METHOD"] = "INVALID_HTTP_METHOD"; MitigationTypes["INPUT_UNION"] = "INPUT_UNION"; MitigationTypes["MISSING_RESPONSE_SCHEMA"] = "MISSING_RESPONSE_SCHEMA"; MitigationTypes["MISSING_SCHEMA"] = "MISSING_SCHEMA"; MitigationTypes["MULTIPLE_RESPONSES"] = "MULTIPLE_RESPONSES"; MitigationTypes["NON_APPLICATION_JSON_SCHEMA"] = "NON_APPLICATION_JSON_SCHEMA"; MitigationTypes["OBJECT_MISSING_PROPERTIES"] = "OBJECT_MISSING_PROPERTIES"; MitigationTypes["UNKNOWN_TARGET_TYPE"] = "UNKNOWN_TARGET_TYPE"; MitigationTypes["UNRESOLVABLE_SCHEMA"] = "UNRESOLVABLE_SCHEMA"; MitigationTypes["UNSUPPORTED_HTTP_SECURITY_SCHEME"] = "UNSUPPORTED_HTTP_SECURITY_SCHEME"; MitigationTypes["UNSUPPORTED_JSON_SCHEMA_KEYWORD"] = "UNSUPPORTED_JSON_SCHEMA_KEYWORD"; MitigationTypes["CALLBACKS_MULTIPLE_OPERATION_OBJECTS"] = "CALLBACKS_MULTIPLE_OPERATION_OBJECTS"; // Links MitigationTypes["AMBIGUOUS_LINK"] = "AMBIGUOUS_LINK"; MitigationTypes["LINK_NAME_COLLISION"] = "LINK_NAME_COLLISION"; MitigationTypes["UNRESOLVABLE_LINK"] = "UNRESOLVABLE_LINK"; // Multiple OAS MitigationTypes["DUPLICATE_OPERATIONID"] = "DUPLICATE_OPERATIONID"; MitigationTypes["DUPLICATE_SECURITY_SCHEME"] = "DUPLICATE_SECURITY_SCHEME"; MitigationTypes["MULTIPLE_OAS_SAME_TITLE"] = "MULTIPLE_OAS_SAME_TITLE"; // Options MitigationTypes["CUSTOM_RESOLVER_UNKNOWN_OAS"] = "CUSTOM_RESOLVER_UNKNOWN_OAS"; MitigationTypes["CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD"] = "CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD"; MitigationTypes["LIMIT_ARGUMENT_NAME_COLLISION"] = "LIMIT_ARGUMENT_NAME_COLLISION"; // Miscellaneous MitigationTypes["OAUTH_SECURITY_SCHEME"] = "OAUTH_SECURITY_SCHEME"; })(MitigationTypes || (MitigationTypes = {})); const mitigations = { /** * Problems with the OAS * * Should be caught by the module oas-validator */ INVALID_OAS: 'Ignore issue and continue.', UNNAMED_PARAMETER: 'Ignore parameter.', // General problems AMBIGUOUS_UNION_MEMBERS: 'Ignore issue and continue.', CANNOT_GET_FIELD_TYPE: 'Ignore field and continue.', COMBINE_SCHEMAS: 'Ignore combine schema keyword and continue.', DUPLICATE_FIELD_NAME: 'Ignore field and maintain preexisting field.', DUPLICATE_LINK_KEY: 'Ignore link and maintain preexisting link.', INPUT_UNION: 'The data will be stored in an arbitrary JSON type.', INVALID_HTTP_METHOD: 'Ignore operation and continue.', MISSING_RESPONSE_SCHEMA: 'Ignore operation.', MISSING_SCHEMA: 'Use arbitrary JSON type.', MULTIPLE_RESPONSES: 'Select first response object with successful status code (200-299).', NON_APPLICATION_JSON_SCHEMA: 'Ignore schema', OBJECT_MISSING_PROPERTIES: 'The (sub-)object will be stored in an arbitrary JSON type.', UNKNOWN_TARGET_TYPE: 'The data will be stored in an arbitrary JSON type.', UNRESOLVABLE_SCHEMA: 'Ignore and continue. May lead to unexpected behavior.', UNSUPPORTED_HTTP_SECURITY_SCHEME: 'Ignore security scheme.', UNSUPPORTED_JSON_SCHEMA_KEYWORD: 'Ignore keyword and continue.', CALLBACKS_MULTIPLE_OPERATION_OBJECTS: 'Select arbitrary operation object', // Links AMBIGUOUS_LINK: `Use first occurance of '#/'.`, LINK_NAME_COLLISION: 'Ignore link and maintain preexisting field.', UNRESOLVABLE_LINK: 'Ignore link.', // Multiple OAS DUPLICATE_OPERATIONID: 'Ignore operation and maintain preexisting operation.', DUPLICATE_SECURITY_SCHEME: 'Ignore security scheme and maintain preexisting scheme.', MULTIPLE_OAS_SAME_TITLE: 'Ignore issue and continue.', // Options CUSTOM_RESOLVER_UNKNOWN_OAS: 'Ignore this set of custom resolvers.', CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD: 'Ignore this set of custom resolvers.', LIMIT_ARGUMENT_NAME_COLLISION: `Do not override existing 'limit' argument.`, // Miscellaneous OAUTH_SECURITY_SCHEME: `Do not create OAuth viewer. OAuth support is provided using the 'tokenJSONpath' option.` }; /** * Utilities that are specific to OpenAPI-to-GraphQL */ function handleWarning({ mitigationType, message, mitigationAddendum, path, data, log }) { const mitigation = mitigations[mitigationType]; const warning = { type: mitigationType, message, mitigation: mitigationAddendum ? `${mitigation} ${mitigationAddendum}` : mitigation }; if (path) { warning['path'] = path; } if (data.options.strict) { throw new Error(`${warning.type} - ${warning.message}`); } else { const output = `Warning: ${warning.message} - ${warning.mitigation}`; if (typeof log === 'function') { log(output); } else { console.log(output); } data.options.report.warnings.push(warning); } } // Code provided by codename- from StackOverflow // Link: https://stackoverflow.com/a/29622653 function sortObject(o) { return Object.keys(o) .sort() .reduce((r, k) => ((r[k] = o[k]), r), {}); } /** * Finds the common property names between two objects */ function getCommonPropertyNames(object1, object2) { return Object.keys(object1).filter((propertyName) => { return propertyName in object2; }); } // Copyright IBM Corp. 2018. All Rights Reserved. const httpLog = debug('http'); const preprocessingLog = debug('preprocessing'); const translationLog = debug('translation'); // OAS constants var HTTP_METHODS; (function (HTTP_METHODS) { HTTP_METHODS["get"] = "get"; HTTP_METHODS["put"] = "put"; HTTP_METHODS["post"] = "post"; HTTP_METHODS["patch"] = "patch"; HTTP_METHODS["delete"] = "delete"; HTTP_METHODS["options"] = "options"; HTTP_METHODS["head"] = "head"; })(HTTP_METHODS || (HTTP_METHODS = {})); const SUCCESS_STATUS_RX = /2[0-9]{2}|2XX/; var OAS_GRAPHQL_EXTENSIONS; (function (OAS_GRAPHQL_EXTENSIONS) { OAS_GRAPHQL_EXTENSIONS["TypeName"] = "x-graphql-type-name"; OAS_GRAPHQL_EXTENSIONS["FieldName"] = "x-graphql-field-name"; OAS_GRAPHQL_EXTENSIONS["EnumMapping"] = "x-graphql-enum-mapping"; })(OAS_GRAPHQL_EXTENSIONS || (OAS_GRAPHQL_EXTENSIONS = {})); /** * Given an HTTP method, convert it to the HTTP_METHODS enum */ function methodToHttpMethod(method) { switch (method.toLowerCase()) { case 'get': return HTTP_METHODS.get; case 'put': return HTTP_METHODS.put; case 'post': return HTTP_METHODS.post; case 'patch': return HTTP_METHODS.patch; case 'delete': return HTTP_METHODS.delete; case 'options': return HTTP_METHODS.options; case 'head': return HTTP_METHODS.head; default: throw new Error(`Invalid HTTP method '${method}'`); } } function isOas2(spec) { return typeof spec.swagger === 'string' && /^2/.test(spec.swagger); } function isOas3(spec) { return typeof spec.openapi === 'string' && /^3/.test(spec.openapi); } /** * Resolves on a validated OAS 3 for the given spec (OAS 2 or OAS 3), or rejects * if errors occur. */ async function getValidOAS3(spec, oasValidatorOptions, swagger2OpenAPIOptions) { // CASE: translate if (isOas2(spec)) { preprocessingLog(`Received Swagger - going to translate to OpenAPI Specification...`); try { const { openapi } = await convertObj(spec, swagger2OpenAPIOptions); return openapi; } catch (error) { throw new Error(`Could not convert Swagger '${spec.info.title}' to OpenAPI Specification. ${error.message}`); } // CASE: validate } else if (isOas3(spec)) { preprocessingLog(`Received OpenAPI Specification - going to validate...`); await validate(spec, oasValidatorOptions); } else { throw new Error(`Invalid specification provided`); } return spec; } /** * Counts the number of operations in an OAS. */ function countOperations(oas) { let numOps = 0; for (let path in oas.paths) { for (let method in oas.paths[path]) { if (isHttpMethod(method)) { numOps++; if (oas.paths[path][method].callbacks) { for (let cbName in oas.paths[path][method].callbacks) { for (let cbPath in oas.paths[path][method].callbacks[cbName]) { numOps++; } } } } } } return numOps; } /** * Counts the number of operations that translate to queries in an OAS. */ function countOperationsQuery(oas) { let numOps = 0; for (let path in oas.paths) { for (let method in oas.paths[path]) { if (isHttpMethod(method) && method.toLowerCase() === HTTP_METHODS.get) { numOps++; } } } return numOps; } /** * Counts the number of operations that translate to mutations in an OAS. */ function countOperationsMutation(oas) { let numOps = 0; for (let path in oas.paths) { for (let method in oas.paths[path]) { if (isHttpMethod(method) && method.toLowerCase() !== HTTP_METHODS.get) { numOps++; } } } return numOps; } /** * Counts the number of operations that translate to subscriptions in an OAS. */ function countOperationsSubscription(oas) { let numOps = 0; for (let path in oas.paths) { for (let method in oas.paths[path]) { if (isHttpMethod(method) && method.toLowerCase() !== HTTP_METHODS.get && oas.paths[path][method].callbacks) { for (let cbName in oas.paths[path][method].callbacks) { for (let cbPath in oas.paths[path][method].callbacks[cbName]) { numOps++; } } } } } return numOps; } /** * Resolves the given reference in the given object. */ function resolveRef(ref, oas) { return JsonPointer.get(oas, ref); } /** * 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 && typeof schema.$ref === 'string') { if (schema.$ref in references) { return references[schema.$ref]; } const reference = schema.$ref; schema = resolveRef(schema.$ref, oas); references[reference] = schema; } /** * TODO: Is there a better method to copy the schema? * * Copy the schema */ const collapsedSchema = JSON.parse(JSON.stringify(schema)); // Resolve allOf if (Array.isArray(collapsedSchema.allOf)) { collapsedSchema.allOf.forEach((memberSchema) => { const collapsedMemberSchema = resolveAllOf(memberSchema, references, data, oas); // Collapse type if applicable if (collapsedMemberSchema.type) { if (!collapsedSchema.type) { collapsedSchema.type = collapsedMemberSchema.type; // Check for incompatible schema type } else if (collapsedSchema.type !== collapsedMemberSchema.type) { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_SCHEMA, message: `Resolving 'allOf' field in schema '${JSON.stringify(collapsedSchema)}' ` + `results in incompatible schema type.`, data, log: preprocessingLog }); } } // Collapse properties if applicable if ('properties' in collapsedMemberSchema) { if (!('properties' in collapsedSchema)) { collapsedSchema.properties = {}; } Object.entries(collapsedMemberSchema.properties).forEach(([propertyName, property]) => { if (!(propertyName in collapsedSchema.properties)) { collapsedSchema.properties[propertyName] = property; // Conflicting property } else { handleWarning({ mitigationType: MitigationTypes.UNRESOLVABLE_SCHEMA, message: `Resolving 'allOf' field in schema '${JSON.stringify(collapsedSchema)}' ` + `results in incompatible property field '${propertyName}'.`, data, log: preprocessingLog }); } }); } // Collapse oneOf if applicable if ('oneOf' in collapsedMemberSchema) { if (!('oneOf' in collapsedSchema)) { collapsedSchema.oneOf = []; } collapsedMemberSchema.oneOf.forEach((oneOfProperty) => { collapsedSchema.oneOf.push(oneOfProperty); }); } // Collapse anyOf if applicable if ('anyOf' in collapsedMemberSchema) { if (!('anyOf' in collapsedSchema)) { collapsedSchema.anyOf = []; } collapsedMemberSchema.anyOf.forEach((anyOfProperty) => { collapsedSchema.anyOf.push(anyOfProperty); }); } // Collapse required if applicable if ('required' in collapsedMemberSchema) { if (!('required' in collapsedSchema)) { collapsedSchema.required = []; } collapsedMemberSchema.required.forEach((requiredProperty) => { if (!collapsedSchema.required.includes(requiredProperty)) { collapsedSchema.required.push(requiredProperty); } }); } }); } return collapsedSchema; } /** * Returns the base URL to use for the given operation. */ function getBaseUrl(operation) { // Check for servers: if (!Array.isArray(operation.servers) || operation.servers.length === 0) { throw new Error(`No servers defined for operation '${operation.operationString}'`); } // Check for local servers if (Array.isArray(operation.servers) && operation.servers.length > 0) { const url = buildUrl(operation.servers[0]); if (Array.isArray(operation.servers) && operation.servers.length > 1) { httpLog(`Warning: Randomly selected first server '${url}'`); } return url.replace(/\/$/, ''); } const oas = operation.oas; if (Array.isArray(oas.servers) && oas.servers.length > 0) { const url = buildUrl(oas.servers[0]); if (Array.isArray(oas.servers) && oas.servers.length > 1) { httpLog(`Warning: Randomly selected first server '${url}'`); } return url.replace(/\/$/, ''); } throw new Error('Cannot find a server to call'); } /** * Returns the default URL for a given OAS server object. */ function buildUrl(server) { let url = server.url; // Replace with variable defaults, if applicable if (typeof server.variables === 'object' && Object.keys(server.variables).length > 0) { for (let variableKey in server.variables) { // TODO: check for default? Would be invalid OAS url = url.replace(`{${variableKey}}`, server.variables[variableKey].default.toString()); } } return url; } /** * Returns object/array/scalar where all object keys (if applicable) are * sanitized. */ function sanitizeObjectKeys(obj, // obj does not necessarily need to be an object caseStyle = CaseStyle.camelCase) { const cleanKeys = (obj) => { // Case: no (response) data if (obj === null || typeof obj === 'undefined') { return null; // Case: array } else if (Array.isArray(obj)) { return obj.map(cleanKeys); // Case: object } else if (typeof obj === 'object') { const res = {}; for (const key in obj) { const saneKey = sanitize(key, caseStyle); if (Object.prototype.hasOwnProperty.call(obj, key)) { res[saneKey] = cleanKeys(obj[key]); } } return res; // Case: scalar } else { return obj; } }; return cleanKeys(obj); } /** * Desanitizes keys in given object by replacing them with the keys stored in * the given mapping. */ function desanitizeObjectKeys(obj, mapping = {}) { const replaceKeys = (obj) => { if (obj === null) { return null; } else if (Array.isArray(obj)) { return obj.map(replaceKeys); } else if (typeof obj === 'object') { const res = {}; for (let key in obj) { if (key in mapping) { const rawKey = mapping[key]; if (Object.prototype.hasOwnProperty.call(obj, key)) { res[rawKey] = replaceKeys(obj[key]); } } else { res[key] = replaceKeys(obj[key]); } } return res; } else { return obj; } }; return replaceKeys(obj); } /** * Returns the GraphQL type that the provided schema should be made into */ function getSchemaTargetGraphQLType(schemaOrRef, data, oas) { let schema; if ('$ref' in schemaOrRef && typeof schemaOrRef.$ref === 'string') { schema = resolveRef(schemaOrRef.$ref, oas); } else { schema = schemaOrRef; } // TODO: Need to resolve allOf here as well. // CASE: Check for nested or concurrent anyOf and oneOf if ( // TODO: Should also consider if the member schema contains type data (Array.isArray(schema.anyOf) && Array.isArray(schema.oneOf)) || // anyOf and oneOf used concurrently hasNestedAnyOfUsage(schema, oas) || hasNestedOneOfUsage(schema, oas)) { handleWarning({ mitigationType: MitigationTypes.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 }); return TargetGraphQLType.json; } if (Array.isArray(schema.anyOf)) { return GetAnyOfTargetGraphQLType(schema, data, oas); } if (Array.isArray(schema.oneOf)) { return GetOneOfTargetGraphQLType(schema, data, oas); } // CASE: enum if (Array.isArray(schema.enum)) { return TargetGraphQLType.enum; } // CASE: object if (schema.type === 'object' || typeof schema.properties === 'object') { // TODO: additionalProperties is more like a flag than a type itself // CASE: arbitrary JSON if (typeof schema.additionalProperties === 'object') { return TargetGraphQLType.json; } else { return TargetGraphQLType.object; } } // CASE: array if (schema.type === 'array' || 'items' in schema) { return TargetGraphQLType.list; } // Special edge cases involving the schema format if (typeof schema.format === 'string') { if (schema.type === 'integer' && schema.format === 'int64') { return TargetGraphQLType.bigint; // CASE: file upload } else if (schema.type === 'string' && schema.format === 'binary') { return TargetGraphQLType.upload; // CASE: id } else if (schema.type === 'string' && (schema.format === 'uuid' || // Custom ID format (Array.isArray(data.options.idFormats) && data.options.idFormats.includes(schema.format)))) { return TargetGraphQLType.id; } } switch (schema.type) { case 'string': return TargetGraphQLType.string; case 'number': return TargetGraphQLType.float; case 'integer': return TargetGraphQLType.integer; case 'boolean': return TargetGraphQLType.boolean; // Error: unsupported schema type } return null; } /** * 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(schema, oas) { // TODO: Should also consider if the member schema contains type data return (Array.isArray(schema.oneOf) && schema.oneOf.some((memberSchemaOrRef) => { let memberSchema; if ('$ref' in memberSchemaOrRef && typeof memberSchemaOrRef.$ref === 'string') { memberSchema = resolveRef(memberSchemaOrRef.$ref, oas); } else { memberSchema = memberSchemaOrRef; } return ( /** * anyOf and oneOf are nested * * Nested oneOf would result in nested unions which are not allowed by * GraphQL */ Array.isArray(memberSchema.anyOf) || Array.isArray(memberSchema.oneOf)); })); } /** * 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(schema, oas) { // TODO: Should also consider if the member schema contains type data return (Array.isArray(schema.anyOf) && schema.anyOf.some((memberSchemaOrRef) => { let memberSchema; if ('$ref' in memberSchemaOrRef && typeof memberSchemaOrRef.$ref === 'string') { memberSchema = resolveRef(memberSchemaOrRef.$ref, oas); } else { memberSchema = memberSchemaOrRef; } return ( // anyOf and oneOf are nested Array.isArray(memberSchema.anyOf) || Array.isArray(memberSchema.oneOf)); })); } function GetAnyOfTargetGraphQLType(schema, data, oas) { // Identify the type of the base schema, meaning ignoring the anyOf const schemaWithNoAnyOf = { ...schema }; delete schemaWithNoAnyOf.anyOf; const baseTargetType = getSchemaTargetGraphQLType(schemaWithNoAnyOf, data, oas); // Target GraphQL types of all the member schemas const memberTargetTypes = []; schema.anyOf.forEach((memberSchema) => { const memberTargetType = getSchemaTargetGraphQLType(memberSchema, data, oas); if (memberTargetType !== null) { memberTargetTypes.push(memberTargetType); } }); if (memberTargetTypes.length > 0) { const firstMemberTargetType = memberTargetTypes[0]; const consistentMemberTargetTypes = memberTargetTypes.every((targetType) => { return targetType === firstMemberTargetType; }); if (consistentMemberTargetTypes) { if (baseTargetType !== null) { if (baseTargetType === firstMemberTargetType) { if (baseTargetType === 'object') { // Base schema and member schema types are object types return TargetGraphQLType.anyOfObject; } else { // Base schema and member schema types but no object types return baseTargetType; } } else { // Base schema and member schema types are not consistent return TargetGraphQLType.json; } } else { if (firstMemberTargetType === TargetGraphQLType.object) { return TargetGraphQLType.anyOfObject; } else { return firstMemberTargetType; } } } else { // Member schema types are not consistent return TargetGraphQLType.json; } } else { // No member schema types, therefore use the base schema type return baseTargetType; } } function GetOneOfTargetGraphQLType(schema, data, oas) { // Identify the type of the base schema, meaning ignoring the oneOf const schemaWithNoOneOf = { ...schema }; delete schemaWithNoOneOf.oneOf; const baseTargetType = getSchemaTargetGraphQLType(schemaWithNoOneOf, data, oas); // Target GraphQL types of all the member schemas const memberTargetTypes = []; schema.oneOf.forEach((memberSchema) => { const collapsedMemberSchema = resolveAllOf(memberSchema, {}, data, oas); const memberTargetType = getSchemaTargetGraphQLType(collapsedMemberSchema, data, oas); if (memberTargetType !== null) { memberTargetTypes.push(memberTargetType); } }); if (memberTargetTypes.length > 0) { const firstMemberTargetType = memberTargetTypes[0]; const consistentMemberTargetTypes = memberTargetTypes.every((targetType) => { return targetType === firstMemberTargetType; }); if (consistentMemberTargetTypes) { if (baseTargetType !== null) { if (baseTargetType === firstMemberTargetType) { if (baseTargetType === 'object') { // Base schema and member schema types are object types return TargetGraphQLType.oneOfUnion; } else { // Base schema and member schema types but no object types return baseTargetType; } } else { // Base schema and member schema types are not consistent return TargetGraphQLType.json; } } else { if (firstMemberTargetType === TargetGraphQLType.object) { return TargetGraphQLType.oneOfUnion; } else { return firstMemberTargetType; } } } else { // Member schema types are not consistent return TargetGraphQLType.json; } } else { // No member schema types, therefore use the base schema type return baseTargetType; } } function isIdParam(part) { return /^{.*(id|name|key).*}$/gi.test(part); } function isSingularParam(part, nextPart) { return `\{${singular(part)}\}` === nextPart; } /** * Infers a resource name from the given URL path. * * For example, turns "/users/{userId}/car" into "userCar". */ function inferResourceNameFromPath(path) { const parts = path.split('/'); let pathNoPathParams = parts.reduce((path, part, i) => { if (!/{/g.test(part)) { if (parts[i + 1] && (isIdParam(parts[i + 1]) || isSingularParam(part, parts[i + 1]))) { return path + capitalize(singular(part)); } else { return path + capitalize(part); } } else { return path; } }, ''); return pathNoPathParams; } /** * Returns the request schema (if any) for the given operation, * a dictionary of names from different sources (if available), and whether the * request schema is required for the operation. */ function getRequestSchemaAndNames(path, method, operation, oas) { var _a; let payloadContentType; // randomly selected content-type, prioritizing application/json let requestBodyObject; // request object let payloadSchema; // request schema with given content-type let payloadSchemaNames; // dictionary of names let payloadRequired = false; // Get request body const requestBodyObjectOrRef = operation === null || operation === void 0 ? void 0 : operation.requestBody; if (typeof requestBodyObjectOrRef === 'object' && requestBodyObjectOrRef !== null) { // Resolve reference if applicable. Make sure we have a RequestBodyObject: if ('$ref' in requestBodyObjectOrRef && typeof requestBodyObjectOrRef.$ref === 'string') { requestBodyObject = resolveRef(requestBodyObjectOrRef.$ref, oas); } else { requestBodyObject = requestBodyObjectOrRef; } if (typeof requestBodyObject === 'object' && requestBodyObject !== null) { // Determine if request body is required: payloadRequired = typeof (requestBodyObject === null || requestBodyObject === void 0 ? void 0 : requestBodyObject.required) === 'boolean' ? requestBodyObject === null || requestBodyObject === void 0 ? void 0 : requestBodyObject.required : false; // Determine content-type const content = requestBodyObject === null || requestBodyObject === void 0 ? void 0 : requestBodyObject.content; if (typeof content === 'object' && content !== null && Object.keys(content).length > 0) { // Prioritize content-type JSON if ('application/json' in content) { payloadContentType = 'application/json'; } else if ('application/x-www-form-urlencoded' in content) { payloadContentType = 'application/x-www-form-urlencoded'; } else { // Pick first (random) content type const randomContentType = Object.keys(content)[0]; payloadContentType = randomContentType; } if (payloadContentType === 'application/json' || payloadContentType === '*/*' || payloadContentType === 'application/x-www-form-urlencoded' || payloadContentType === 'multipart/form-data') { // Name extracted from a reference, if applicable let fromRef; // Determine payload schema const payloadSchemaOrRef = (_a = content === null || content === void 0 ? void 0 : content[payloadContentType]) === null || _a === void 0 ? void 0 : _a.schema; if (typeof payloadSchemaOrRef === 'object' && payloadSchemaOrRef !== null) { // Resolve payload schema reference if applicable if ('$ref' in payloadSchemaOrRef && typeof payloadSchemaOrRef.$ref === 'string') { fromRef = payloadSchemaOrRef.$ref.split('/').pop(); payloadSchema = resolveRef(payloadSchemaOrRef.$ref, oas); } else { payloadSchema = payloadSchemaOrRef; } } // Determine possible schema names payloadSchemaNames = { fromExtension: payloadSchema[OAS_GRAPHQL_EXTENSIONS.TypeName], fromRef, fromSchema: payloadSchema === null || payloadSchema === void 0 ? void 0 : payloadSchema.title, fromPath: inferResourceNameFromPath(path) }; /** * Edge case: if request body content-type is not application/json or * application/x-www-form-urlencoded, do not parse it. * * Instead, treat the request body as a black box and send it as a string * with the proper content-type header */ } else { const saneContentTypeName = uncapitalize(payloadContentType.split('/').reduce((name, term) => { return name + capitalize(term); })); let description = `String represents payload of content type '${payloadContentType}'`; if (typeof (payloadSchema === null || payloadSchema === void 0 ? void 0 : payloadSchema.description) === 'string') { description += `\n\nOriginal top level description: '${payloadSchema.description}'`; } // Replacement schema to avoid parsing payloadSchema = { description, type: 'string' }; // Determine possible schema names payloadSchemaNames = { fromPath: saneContentTypeName }; } } } } return { payloadContentType, payloadSchema, payloadSchemaNames, payloadRequired }; } /** * Returns the response schema for the given operation, * a successful status code, and a dictionary of names from different sources * (if available). */ function getResponseSchemaAndNames(path, method, operation, oas, data, options) { var _a, _b, _c; let responseContentType; // randomly selected content-type, prioritizing application/json let responseObject; // response object let responseSchema; // response schema with given content-type let responseSchemaNames; // dictionary of names const statusCode = getResponseStatusCode(path, method, operation, oas, data); // Get response object const responseObjectOrRef = (_a = operation === null || operation === void 0 ? void 0 : operation.responses) === null || _a === void 0 ? void 0 : _a[statusCode]; if (typeof responseObjectOrRef === 'object' && responseObjectOrRef !== null) { if ('$ref' in responseObjectOrRef && typeof responseObjectOrRef.$ref === 'string') { responseObject = resolveRef(responseObjectOrRef.$ref, oas); } else { responseObject = responseObjectOrRef; } // Determine content-type if (typeof responseObject === 'object' && responseObject !== null) { const content = responseObject === null || responseObject === void 0 ? void 0 : responseObject.content; if (typeof content === 'object' && content !== null && Object.keys(content).length > 0) { // Prioritize content-type JSON if ('application/json' in content) { responseContentType = 'application/json'; } else { // Pick first (random) content type const randomContentType = Object.keys(content)[0]; responseContentType = randomContentType; } if (responseContentType === 'application/json' || responseContentType === '*/*') { // Name from reference, if applicable let fromRef; // Determine response schema const responseSchemaOrRef = (_c = (_b = responseObject === null || responseObject === void 0 ? void 0 : responseObject.content) === null || _b === void 0 ? void 0 : _b[responseContentType]) === null || _c === void 0 ? void 0 : _c.schema; // Resolve response schema reference if applicable if ('$ref' in responseSchemaOrRef && typeof responseSchemaOrRef.$ref === 'string') { fromRef = responseSchemaOrRef.$ref.split('/').pop(); responseSchema = resolveRef(responseSchemaOrRef.$ref, oas); } else { responseSchema = responseSchemaOrRef; } // Determine possible schema names responseSchemaNames = { fromExtension: responseSchema[OAS_GRAPHQL_EXTENSIONS.TypeName], fromRef, fromSchema: responseSchema === null || responseSchema === void 0 ? void 0 : responseSchema.title, fromPath: inferResourceNameFromPath(path) }; /** * Edge case: if response body content-type is not application/json, * do not parse. */ } else { let description = 'Placeholder to access non-application/json response bodies'; if (typeof (responseSchema === null || responseSchema === void 0 ? void 0 : responseSchema.description) === 'string') { description += `\n\nOriginal top level description: '${responseSchema.description}'`; } // Replacement schema to avoid parsing responseSchema = { description, type: 'string' }; // Determine possible schema names responseSchemaNames = { fromExtension: responseSchema === null || responseSchema === void 0 ? void 0 : responseSchema[OAS_GRAPHQL_EXTENSIONS.TypeName], fromSchema: responseSchema === null || responseSchema === void 0 ? void 0 : responseSchema.title, fromPath: inferResourceNameFromPath(path) }; } return { responseContentType, responseSchema, responseSchemaNames, statusCode }; } } } // No response schema if (options.fillEmptyResponses) { return { responseSchemaNames: { fromPath: inferResourceNameFromPath(path) }, responseSchema: { description: 'Placeholder to support operations with no response schema', type: 'string' } }; } else { return {}; } } /** * Returns a success status code for the given operation */ function getResponseStatusCode(path, method, operation, oas, data) { if (typeof operation.responses === 'object' && operation.responses !== null) { const codes = Object.keys(operation.responses); const successCodes = codes.filter((code) => { return SUCCESS_STATUS_RX.test(code); }); if (successCodes.length === 1) { return successCodes[0]; } else if (successCodes.length > 1) { // Select a random success code handleWarning({ mitigationType: MitigationTypes.MULTIPLE_RESPONSES, message: `Operation '${formatOperationString(method, path, oas.info.title)}' ` + `contains multiple possible successful response object ` + `(HTTP code 200-299 or 2XX). Only one can be chosen.`, mitigationAddendum: `The response object with the HTTP code ` + `${successCodes[0]} will be selected`, data, log: translationLog }); return successCodes[0]; } } } /** * Returns a hash containing the links in the given operation. */ function getLinks(path, method, operation, oas, data) { const links = {}; const statusCode = getResponseStatusCode(path, method, operation, oas, data); if (!statusCode) { return links; } if (typeof operation.responses === 'object') { const responses = operation.responses; if (typeof responses[statusCode] === 'object') { const responseObjectOrRef = responses[statusCode]; let response; if ('$ref' in responseObjectOrRef && typeof responseObjectOrRef.$ref === 'string') { response = resolveRef(responseObjectOrRef.$ref, oas); } else { response = responseObjectOrRef; } if (typeof response.links === 'object') { const epLinks = response.links; for (let linkKey in epLinks) { const linkObjectOrRef = epLinks[linkKey]; let link; if ('$ref' in linkObjectOrRef && typeof linkObjectOrRef.$ref === 'string') { link = resolveRef(linkObjectOrRef.$ref, oas); } else { link = linkObjectOrRef; } links[linkKey] = link; } } } } return links; } /** * Returns the list of parameters in the given operation. */ function getParameters(path, method, operation, pathItem, oas) { let parameters = []; if (!isHttpMethod(method)) { translationLog(`Warning: attempted to get parameters for ${method} ${path}, ` + `which is not an operation.`); return parameters; } // First, consider parameters in Path Item Object: const pathParams = pathItem.parameters; if (Array.isArray(pathParams)) { const pathItemParameters = pathParams.map((p) => { if ('$ref' in p && typeof p.$ref === 'string') { // Here we know we have a parameter object: return resolveRef(p.$ref, oas); } else { // Here we know we have a parameter object: return p; } }); parameters = parameters.concat(pathItemParameters); } // Second, consider parameters in Operation Object: const opObjectParameters = operation.parameters; if (Array.isArray(opObjectParameters)) { const operationParameters = opObjectParameters.map((p) => { if ('$ref' in p && typeof p.$ref === 'string') { // Here we know we have a parameter object: return resolveRef(p.$ref, oas); } else { // Here we know we have a parameter object: return p; } }); parameters = parameters.concat(operationParameters); } return parameters; } /** * Returns an array of server objects for the operation at the given path and * method. Considers in the following order: global server definitions, * definitions at the path item, definitions at the operation, or the OAS * default. */ function getServers(operation, pathItem, oas) { let servers = []; // Global server definitions: if (Array.isArray(oas.servers) && oas.servers.length > 0) { servers = oas.servers; } // First, consider servers defined on the path if (Array.isArray(pathItem.servers) && pathItem.servers.length > 0) { servers = pathItem.servers; } // Second, consider servers defined on the operation if (Array.isArray(operation.servers) && operation.servers.length > 0) { servers = operation.servers; } // Default, in case there is no server: if (servers.length === 0) { let server = { url: '/' // TODO: avoid double-slashes }; servers.push(server); } return servers; } /** * Returns a map of security scheme definitions, identified by keys. Resolves * possible references. */ function getSecuritySchemes(oas) { // Collect all security schemes: const securitySchemes = {}; if (typeof oas.components === 'object' && typeof oas.components.securitySchemes === 'object') { for (let schemeKey in oas.components.securitySchemes) { const securitySchemeOrRef = oas.components.securitySchemes[schemeKey]; // Ensure we have actual SecuritySchemeObject: if ('$ref' in securitySchemeOrRef && typeof securitySchemeOrRef.$ref === 'string') { // Result of resolution will be SecuritySchemeObject: securitySchemes[schemeKey] = resolveRef(securitySchemeOrRef.$ref, oas); } else { // We already have a SecuritySchemeObject: securitySchemes[schemeKey] = securitySchemeOrRef; } } } return securitySchemes; } /** * Returns the list of sanitized keys of non-OAuth2 security schemes * required by the operation at the given path and method. */ function getSecurityRequirements(operation, securitySchemes, oas) { const results = []; // First, consider global requirements const globalSecurity = oas.security; if (globalSecurity && typeof globalSecurity !== 'undefined') { for (let secReq of globalSecurity) { for (let schemaKey in secReq) { if (securitySchemes[schemaKey] && typeof securitySchemes[schemaKey] === 'object' && securitySchemes[schemaKey].def.type !== 'oauth2') { results.push(schemaKey); } } } } // Second, consider operation requirements const localSecurity = operation.security; if (localSecurity && typeof localSecurity !== 'undefined') { for (let secReq of localSecurity) { for (let schemaKey in secReq) { if (securitySchemes[schemaKey] && typeof securitySchemes[schemaKey] === 'object' && securitySchemes[schemaKey].def.type !== 'oauth2') { if (!results.includes(schemaKey)) { results.push(schemaKey); } } } } } return results; } var CaseStyle; (function (CaseStyle) { CaseStyle[CaseStyle["simple"] = 0] = "simple"; CaseStyle[CaseStyle["PascalCase"] = 1] = "PascalCase"; CaseStyle[CaseStyle["camelCase"] = 2] = "camelCase"; CaseStyle[CaseStyle["ALL_CAPS"] = 3] = "ALL_CAPS"; // Used for enum values })(CaseStyle || (CaseSt