UNPKG

@graphql-mesh/openapi

Version:
1,265 lines (1,258 loc) • 220 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } const utils = require('@graphql-mesh/utils'); const utils$1 = require('@graphql-tools/utils'); const graphql = require('graphql'); const graphqlScalars = require('graphql-scalars'); const Swagger2OpenAPI = require('swagger2openapi'); const JsonPointer = _interopDefault(require('json-pointer')); const pluralize = _interopDefault(require('pluralize')); const JSONPath = require('jsonpath-plus'); const qs = _interopDefault(require('qs')); const formurlencoded = _interopDefault(require('form-urlencoded')); const urlJoin = _interopDefault(require('url-join')); const fetch = require('@whatwg-node/fetch'); const deepEqual = _interopDefault(require('deep-equal')); const crossHelpers = require('@graphql-mesh/cross-helpers'); const store = require('@graphql-mesh/store'); const openapiDiff = _interopDefault(require('openapi-diff')); const stringInterpolation = require('@graphql-mesh/string-interpolation'); /* eslint-disable @typescript-eslint/ban-types */ // 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 = {})); /* eslint-disable @typescript-eslint/ban-types */ /* eslint-disable no-sequences */ // 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 arbitray 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: `Ignore security scheme`, }; /** * Utilities that are specific to OpenAPI-to-GraphQL */ function handleWarning({ mitigationType, message, mitigationAddendum, path, data, logger, }) { 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}`; logger.debug(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; }); } /* eslint-disable no-useless-escape */ // 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/; const CONTENT_TYPE_JSON_RX = /^application\/(.*)json$/; /** * 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}'`); } } /** * Resolves on a validated OAS 3 for the given spec (OAS 2 or OAS 3), or rejects * if errors occur. */ async function getValidOAS3(spec) { if (typeof spec.swagger === 'string' && spec.swagger === '2.0') { const { openapi } = await Swagger2OpenAPI.convertObj(spec, { patch: true, warnOnly: true, }); return openapi; } else { return spec; } } /** * Counts the number of operations in an OAS. */ function countOperations(oas) { let numOps = 0; for (const path in oas.paths) { for (const method in oas.paths[path]) { if (isHttpMethod(method)) { numOps++; if (oas.paths[path][method].callbacks) { for (const cbName in oas.paths[path][method].callbacks) { for (const _ 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 (const path in oas.paths) { for (const 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 (const path in oas.paths) { for (const 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 (const path in oas.paths) { for (const method in oas.paths[path]) { if (isHttpMethod(method) && method.toLowerCase() !== HTTP_METHODS.get && oas.paths[path][method].callbacks) { for (const cbName in oas.paths[path][method].callbacks) { for (const _ in oas.paths[path][method].callbacks[cbName]) { numOps++; } } } } } return numOps; } /** * Resolves the given reference in the given object. */ function resolveRef(ref, oas) { try { return JsonPointer.get(oas, ref.replace('#/', '/')); } catch (e) { if (e.message.startsWith('Invalid reference')) { return undefined; } throw e; } } /** * Returns the base URL to use for the given operation. */ function getBaseUrl(operation, logger) { const httpLogger = logger.child('http'); // 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) { httpLogger.debug(`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) { httpLogger.debug(`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 (const 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 (const 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 * * Does not consider allOf, anyOf, oneOf, or not (handled separately) */ function getSchemaTargetGraphQLType(schema, data) { // 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 'json'; } else { return 'object'; } } // CASE: array if (schema.type === 'array' || 'items' in schema) { return 'list'; } // CASE: enum if (Array.isArray(schema.enum)) { return 'enum'; } // CASE: a type is present if (typeof schema.type === 'string') { // Special edge cases involving the schema format if (typeof schema.format === 'string') { /** * CASE: 64 bit int - return number instead of integer, leading to use of * GraphQLFloat, which can support 64 bits: */ if (schema.type === 'integer' && schema.format === 'int64') { return 'number'; // 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 'id'; } } return schema.type; } return null; } function isIdParam(part) { return /^{.*(id|name|key).*}$/gi.test(part); } function isSingularParam(part, nextPart) { return `\{${pluralize.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('/'); const 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(pluralize.singular(part)); } else { return path + capitalize(part); } } else { return path; } }, ''); return pathNoPathParams; } /** * Returns JSON-compatible schema required by the given operation */ function getRequestBodyObject(operation, oas) { var _a, _b; if (typeof operation.requestBody === 'object') { let requestBodyObject = operation.requestBody; // Make sure we have a RequestBodyObject: if (typeof requestBodyObject.$ref === 'string') { requestBodyObject = resolveRef(requestBodyObject.$ref, oas); } else { requestBodyObject = requestBodyObject; } if (typeof requestBodyObject.content === 'object') { const content = requestBodyObject.content; const contentTypes = Object.keys(content); const jsonContentType = (_a = contentTypes .find(contentType => CONTENT_TYPE_JSON_RX.test(contentType.toString()))) === null || _a === void 0 ? void 0 : _a.toString(); const formDataContentType = (_b = contentTypes .find(contentType => contentType.toString().includes('application/x-www-form-urlencoded'))) === null || _b === void 0 ? void 0 : _b.toString(); // Prioritize content-type JSON if (jsonContentType) { return { payloadContentType: jsonContentType, requestBodyObject, }; } else if (formDataContentType) { return { payloadContentType: formDataContentType, requestBodyObject, }; } else { return { payloadContentType: contentTypes[0].toString(), requestBodyObject, }; } } } return { payloadContentType: null, requestBodyObject: null }; } /** * 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, operation, oas) { const { payloadContentType, requestBodyObject } = getRequestBodyObject(operation, oas); if (payloadContentType) { let payloadSchema = requestBodyObject.content[payloadContentType].schema; // Get resource name from different sources let fromRef; if ('$ref' in payloadSchema) { fromRef = payloadSchema.$ref.split('/').pop(); payloadSchema = resolveRef(payloadSchema.$ref, oas); } let payloadSchemaNames = { fromRef, fromSchema: payloadSchema.title, fromPath: inferResourceNameFromPath(path), }; // Determine if request body is required: const payloadRequired = typeof requestBodyObject.required === 'boolean' ? requestBodyObject.required : false; /** * 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 */ if (!CONTENT_TYPE_JSON_RX.test(payloadContentType.toString()) && !payloadContentType.includes('application/x-www-form-urlencoded') && !payloadContentType.includes('*/*')) { const saneContentTypeName = uncapitalize(payloadContentType.split('/').reduce((name, term) => { return name + capitalize(term); })); payloadSchemaNames = { fromPath: saneContentTypeName, }; let description = `String represents payload of content type '${payloadContentType}'`; if ('description' in payloadSchema && typeof payloadSchema.description === 'string') { description += `\n\nOriginal top level description: '${payloadSchema.description}'`; } payloadSchema = { description, type: 'string', }; } return { payloadContentType, payloadSchema, payloadSchemaNames, payloadRequired, }; } return { payloadRequired: false, }; } /** * Returns JSON-compatible schema produced by the given operation */ function getResponseObject(operation, statusCode, oas) { if (typeof operation.responses === 'object') { const responses = operation.responses; if (typeof responses[statusCode] === 'object') { let responseObject = responses[statusCode]; // Make sure we have a ResponseObject: if (typeof responseObject.$ref === 'string') { responseObject = resolveRef(responseObject.$ref, oas); } else { responseObject = responseObject; } if (responseObject.content && typeof responseObject.content !== 'undefined') { const content = responseObject.content; const contentTypes = Object.keys(content); const isJsonContent = contentTypes.some(contentType => CONTENT_TYPE_JSON_RX.test(contentType.toString())); // Prioritize content-type JSON if (isJsonContent) { return { responseContentType: 'application/json', responseObject, }; } else { // Pick first (random) content type const randomContentType = contentTypes[0].toString(); return { responseContentType: randomContentType, responseObject, }; } } } } return { responseContentType: null, responseObject: null }; } /** * 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) { const statusCode = getResponseStatusCode(path, method, operation, oas, data, options.logger); if (!statusCode) { return {}; } const { responseContentType, responseObject } = getResponseObject(operation, statusCode, oas); if (responseContentType) { const contentTypes = Object.keys(responseObject.content); const availableSimilarContentType = contentTypes.find(contentType => contentType.toString().includes(responseContentType)); let responseSchema = responseObject.content[availableSimilarContentType || contentTypes[0]].schema; let fromRef; if (responseSchema) { if ('$ref' in responseSchema) { fromRef = responseSchema.$ref.split('/').pop(); responseSchema = resolveRef(responseSchema.$ref, oas); } } else if (options.allowUndefinedSchemaRefTags) { options.logger.info(`${path}:${method.toUpperCase()}:${statusCode}`); fromRef = 'Unknown'; responseSchema = { description: `Placeholder for missing ${path}:${method.toUpperCase()}:${statusCode} schema ref`, type: options.defaultUndefinedSchemaType || 'object', }; } else { throw new Error(`${path}:${method.toUpperCase()}:${statusCode} has an undefined schema ref`); } const responseSchemaNames = { fromRef, fromSchema: responseSchema.title, fromPath: inferResourceNameFromPath(path), }; /** * Edge case: if response body content-type is not application/json, do not * parse. */ if (!CONTENT_TYPE_JSON_RX.test(responseContentType.toString()) && !responseContentType.includes('*/*')) { let description = 'Placeholder to access non-application/json response bodies'; if ('description' in responseSchema && typeof responseSchema.description === 'string') { description += `\n\nOriginal top level description: '${responseSchema.description}'`; } responseSchema = { description, type: 'string', }; } return { responseContentType, responseSchema, responseSchemaNames, statusCode, }; } else { /** * GraphQL requires that objects must have some properties. * * To allow some operations (such as those with a 204 HTTP code) to be * included in the GraphQL interface, we added the fillEmptyResponses * option, which will simply create a placeholder to allow access. */ if (options.fillEmptyResponses) { return { responseSchemaNames: { fromPath: inferResourceNameFromPath(path), }, responseContentType: 'application/json', responseSchema: { description: 'Placeholder to support operations with no response schema', type: 'object', }, }; } return {}; } } /** * Returns a success status code for the given operation */ function getResponseStatusCode(path, method, operation, oas, data, logger) { var _a; const translationLogger = logger.child('translation'); if (typeof operation.responses === 'object') { const codes = Object.keys(operation.responses); const successCodes = codes.filter(code => { return code === 'default' || SUCCESS_STATUS_RX.test(code.toString()); }); if (successCodes.length === 1) { return successCodes[0].toString(); } else if (successCodes.length > 1) { handleWarning({ mitigationType: MitigationTypes.MULTIPLE_RESPONSES, message: `Operation '${formatOperationString(method, path, (_a = oas.info) === null || _a === void 0 ? void 0 : _a.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, logger: translationLogger, }); return successCodes[0].toString(); } } return null; } /** * Returns a hash containing the links in the given operation. */ function getLinks(path, method, operation, oas, data, logger) { const links = {}; const statusCode = getResponseStatusCode(path, method, operation, oas, data, logger); if (!statusCode) { return links; } if (typeof operation.responses === 'object') { const responses = operation.responses; if (typeof responses[statusCode] === 'object') { let response = responses[statusCode]; if (typeof response.$ref === 'string') { response = resolveRef(response.$ref, oas); } // Here, we can be certain we have a ResponseObject: response = response; if (typeof response.links === 'object') { const epLinks = response.links; for (const linkKey in epLinks) { let link = epLinks[linkKey]; // Make sure we have LinkObjects: if (typeof link.$ref === 'string') { link = resolveRef(link.$ref, oas); } else { link = link; } links[linkKey] = link; } } } } return links; } /** * Returns the list of parameters in the given operation. */ function getParameters(path, method, operation, pathItem, oas, logger) { const translationLogger = logger.child('translation'); let parameters = []; if (!isHttpMethod(method)) { translationLogger.debug(() => `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 (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 (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) { const 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 (const schemeKey in oas.components.securitySchemes) { const obj = oas.components.securitySchemes[schemeKey]; // Ensure we have actual SecuritySchemeObject: if (typeof obj.$ref === 'string') { // Result of resolution will be SecuritySchemeObject: securitySchemes[schemeKey] = resolveRef(obj.$ref, oas); } else { // We already have a SecuritySchemeObject: securitySchemes[schemeKey] = obj; } } } 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 (const secReq of globalSecurity) { for (const 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 (const secReq of localSecurity) { for (const 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"; })(CaseStyle || (CaseStyle = {})); /** * First sanitizes given string and then also camel-cases it. */ function sanitize(str, caseStyle) { /** * Used in conjunction to simpleNames, which only removes illegal * characters and preserves casing */ if (caseStyle === CaseStyle.simple) { return str.replace(/[^a-zA-Z0-9_]/gi, ''); } /** * Remove all GraphQL unsafe characters */ const regex = caseStyle === CaseStyle.ALL_CAPS ? /[^a-zA-Z0-9_]/g // ALL_CAPS has underscores : /[^a-zA-Z0-9]/g; const operators = { '<=': 'less_than_or_equal_to', '>=': 'greater_than_or_equal_to', '<': 'less_than', '>': 'greater_than', '=': 'equal_to', }; for (const operator in operators) { str = str.replace(operator, operators[operator]); } let sanitized = str.split(regex).reduce((path, part) => { if (caseStyle === CaseStyle.ALL_CAPS) { return path + '_' + part; } else { return path + capitalize(part); } }); switch (caseStyle) { case CaseStyle.PascalCase: // The first character in PascalCase should be uppercase sanitized = capitalize(sanitized); break; case CaseStyle.camelCase: // The first character in camelCase should be lowercase sanitized = uncapitalize(sanitized); break; case CaseStyle.ALL_CAPS: sanitized = sanitized.toUpperCase(); break; } // Special case: we cannot start with number, and cannot be empty: if (/^[0-9]/.test(sanitized) || sanitized === '') { sanitized = '_' + sanitized; } return sanitized; } /** * Sanitizes the given string and stores the sanitized-to-original mapping in * the given mapping. */ function storeSaneName(saneStr, str, mapping, logger) { if (saneStr in mapping && str !== mapping[saneStr]) { const translationLogger = logger.child('translation'); // TODO: Follow warning model translationLogger.debug(() => `Warning: '${str}' and '${mapping[saneStr]}' both sanitize ` + `to '${saneStr}' - collision possible. Desanitize to '${str}'.`); let appendix = 2; while (`${saneStr}${appendix}` in mapping) { appendix++; } return storeSaneName(`${saneStr}${appendix}`, str, mapping, logger); } mapping[saneStr] = str; return saneStr; } /** * Determines if the given "method" is indeed an operation. Alternatively, the * method could point to other types of information (e.g., parameters, servers). */ function isHttpMethod(method) { return Object.keys(HTTP_METHODS).includes(method.toLowerCase()); } /** * Formats a string that describes an operation in the form: * {name of OAS} {HTTP method in ALL_CAPS} {operation path} * * Also used in preprocessing.ts where Operation objects are being constructed */ function formatOperationString(method, path, title) { if (title) { return `${title} ${method.toUpperCase()} ${path}`; } else { return `${method.toUpperCase()} ${path}`; } } /** * Capitalizes a given string */ function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Uncapitalizes a given string */ function uncapitalize(str) { return str.charAt(0).toLowerCase() + str.slice(1); } /** * For operations that do not have an operationId, generate one */ function generateOperationId(method, path) { return sanitize(`${method} ${path}`, CaseStyle.camelCase); } /* eslint-disable no-case-declarations */ /* * If operationType is Subscription, creates and returns a resolver object that * contains subscribe to perform subscription and resolve to execute payload * transformation */ function getSubscribe({ operation, payloadName, data, baseUrl, connectOptions, pubsub, logger, }) { var _a; const translationLogger = logger.child('translation'); const pubSubLogger = logger.child('pubsub'); // Determine the appropriate URL: if (typeof baseUrl === 'undefined') { baseUrl = getBaseUrl(operation, logger); } // Return custom resolver if it is defined const customResolvers = data.options.customSubscriptionResolvers; const title = (_a = operation.oas.info) === null || _a === void 0 ? void 0 : _a.title; const path = operation.path; const method = operation.method; if (typeof customResolvers === 'object' && typeof customResolvers[title] === 'object' && typeof customResolvers[title][path] === 'object' && typeof customResolvers[title][path][method] === 'object' && typeof customResolvers[title][path][method].subscribe === 'function') { translationLogger.debug(`Use custom publish resolver for ${operation.operationString}`); return customResolvers[title][path][method].subscribe; } return (root, args, context, info) => { try { /** * Determine possible topic(s) by resolving callback path * * GraphQL produces sanitized payload names, so we have to sanitize before * lookup here */ const paramName = sanitize(payloadName, CaseStyle.camelCase); const resolveData = {}; if (payloadName && typeof payloadName === 'string') { // The option genericPayloadArgName will change the payload name to "requestBody" const sanePayloadName = data.options.genericPayloadArgName ? 'requestBody' : sanitize(payloadName, CaseStyle.camelCase); if (sanePayloadName in args) { if (typeof args[sanePayloadName] === 'object') { const rawPayload = desanitizeObjectKeys(args[sanePayloadName], data.saneMap); resolveData.usedPayload = rawPayload; } else { const rawPayload = JSON.parse(args[sanePayloadName]); resolveData.usedPayload = rawPayload; } } } if (connectOptions) { resolveData.usedRequestOptions = connectOptions; } else { resolveData.usedRequestOptions = { method: resolveData.usedPayload.method ? resolveData.usedPayload.method : method.toUpperCase(), }; } pubSubLogger.debug(`Subscription schema: `, resolveData.usedPayload); let value = path; let paramNameWithoutLocation = paramName; if (paramName.indexOf('.') !== -1) { paramNameWithoutLocation = paramName.split('.')[1]; } // See if the callback path contains constants expression if (value.search(/{|}/) === -1) { args[paramNameWithoutLocation] = isRuntimeExpression(value) ? resolveRuntimeExpression(paramName, value, resolveData, root, args, logger) : value; } else { // Replace callback expression with appropriate values const cbParams = value.match(/{([^}]*)}/g); pubSubLogger.debug(`Analyzing subscription path: ${cbParams.toString()}`); cbParams.forEach(cbParam => { value = value.replace(cbParam, resolveRuntimeExpression(paramName, cbParam.substring(1, cbParam.length - 1), resolveData, root, args, logger)); }); args[paramNameWithoutLocation] = value; } const topic = args[paramNameWithoutLocation] || 'test'; pubSubLogger.debug(`Subscribing to: ${topic}`); return pubsub.asyncIterator(topic); } catch (e) { console.error(e); throw e; } }; } /* * If operationType is Subscription, creates and returns a resolver function * triggered after a message has been published to the corresponding subscribe * topic(s) to execute payload transformation */ function getPublishResolver({ operation, responseName, data, logger, }) { var _a; const translationLogger = logger.child('translation'); const pubSubLogger = logger.child('pubsub'); // Return custom resolver if it is defined const customResolvers = data.options.customSubscriptionResolvers; const title = (_a = operation.oas.info) === null || _a === void 0 ? void 0 : _a.title; const path = operation.path; const method = operation.method; if (typeof customResolvers === 'object' && typeof customResolvers[title] === 'object' && typeof customResolvers[title][path] === 'object' && typeof customResolvers[title][path][method] === 'object' && typeof customResolvers[title][path][method].resolve === 'function') { translationLogger.debug(`Use custom publish resolver for ${operation.operationString}`); return customResolvers[title][path][method].resolve; } return (payload, args, context, info) => { // Validate and format based on operation.responseDefinition const typeOfResponse = operation.responseDefinition.targetGraphQLType; pubSubLogger.debug(`Message received: `, responseName, typeOfResponse, payload); let responseBody; let saneData; if (typeof payload === 'object') { if (typeOfResponse === 'object') { if (Buffer.isBuffer(payload)) { try { responseBody = JSON.parse(payload.toString()); } catch (e) { const errorString = `Cannot JSON parse payload` + `operation ${operation.operationString} ` + `even though it has content-type 'application/json'`; pubSubLogger.debug(errorString); return null; } } else { responseBody = payload; } saneData = sanitizeObjectKeys(payload); } else if ((Buffer.isBuffer(payload) || Array.isArray(payload)) && typeOfResponse === 'string') { saneData = payload.toString(); } } else if (typeof payload === 'string') { if (typeOfResponse === 'object') { try { responseBody = JSON.parse(payload); saneData = sanitizeObjectKeys(responseBody); } catch (e) { const errorString = `Cannot JSON parse payload` + `operation ${operation.operationString} ` + `even though it has content-type 'application/json'`; pubSubLogger.debug(errorString); return null; } } else if (typeOfResponse === 'string') { saneData = payload; } } pubSubLogger.debug(`Message forwarded: `, saneData || payload); return saneData || payload; }; } /** * Creates and returns a resolver function that performs API requests for the * given GraphQL query */ function getResolver(getResolverParams, logger) { var _a; const translationLogger = logger.child('translation'); const httpLogger = logger.child('http'); let { operation, argsFromLink = {}, payloadName, data, baseUrl, requestOptions, fetch: fetchFn = data.options.fetch, qs: customQs, } = getResolverParams(); // Determine the appropriate URL: if (typeof baseUrl === 'undefined') { baseUrl = getBaseUrl(operation, logger); } // Return custom resolver if it is defined const customResolvers = data.options.customResolvers; const title = (_a = operation.oas.info) === null || _a === void 0 ? void 0 : _a.title; const path = operation.path; const method = operation.method; if (typeof customResolvers === 'object' && typeof customResolvers[title] === 'object' && typeof customResolvers[title][path] === 'object' && typeof customResolvers[title][path][method] === 'function') { translationLogger.debug(`Use custom resolver for ${operation.operationString}`); return customResolvers[title][path][method]; } // Return resolve function: return async (root, args, ctx, info = {}) => { /** * Retch resolveData from possibly existing _openAPIToGraphQL * * NOTE: _openAPIToGraphQL is an object used to pass security info and data * from previous resolvers */ let resolveData = {}; if (root && typeof root === 'object' && typeof root._openAPIToGraphQL === 'object' && typeof root._openAPIToGraphQL.data === 'object') { const parentIdentifier = getParentIdentifier(info); if (!(parentIdentifier.length === 0) && parentIdentifier in root._openAPIToGraphQL.data) { /** * Resolving link params may change the usedParams, but these changes * should not be present in the parent _openAPIToGraphQL, therefore copy * the object */ resolveData = JSON.parse(JSON.stringify(root._openAPIToGraphQL.data[parentIdentifier])); } } if (typeof resolveData.usedParams === 'undefined') { resolveData.usedParams = {}; } /** * Handle default values of parameters, if they have not yet been defined by * the user. */ operation.parameters.forEach(param => { const paramName = sanitize(param.name, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple); if (typeof args[paramName] === 'undefined' && param.schema && typeof param.schema === 'object') { let schema = param.schema; if (schema && schema.$ref && typeof schema.$ref === 'string') { schema = resolveRef(schema.$ref, operation.oas); } if (schema && schema.default && typeof schema.default !== 'undefined') { args[paramName] = schema.default; } } }); // Handle arguments provided by links for (const paramName in argsFromLink) { const saneParamName = sanitize(paramName, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple); let value = argsFromLink[paramName]; /** * see if the link parameter contains constants that are appended to the link parameter * * e.g. instead of: * $response.body#/employerId * * it could be: * abc_{$response.body#/employerId} */ if (value.search(/{|}/) === -1) { args[saneParamName] = isRuntimeExpression(value) ? resolveLinkParameter(paramName, value, resolveData, root, args, logger)