@graphql-mesh/openapi
Version:
1,265 lines (1,258 loc) • 220 kB
JavaScript
'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)