openapi-to-graphql-harshith
Version:
Generates a GraphQL schema for a given OpenAPI Specification (OAS)
1,233 lines (1,226 loc) • 229 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
const graphql = require('graphql');
const graphqlUpload = require('graphql-upload');
const graphqlScalars = require('graphql-scalars');
const Swagger2OpenAPI = require('swagger2openapi');
const OASValidator = require('oas-validator');
const debug = require('debug');
const debug__default = _interopDefault(debug);
const jsonptr = require('json-ptr');
const pluralize = require('pluralize');
const stream = _interopDefault(require('stream'));
const jsonpathPlus = require('jsonpath-plus');
const JSONPointer = require('jsonpointer');
const formurlencoded = _interopDefault(require('form-urlencoded'));
const graphqlSubscriptions = require('graphql-subscriptions');
const urljoin = _interopDefault(require('url-join'));
const FormData = _interopDefault(require('form-data'));
const deepEqual = _interopDefault(require('deep-equal'));
const crossFetch = _interopDefault(require('cross-fetch'));
// Copyright IBM Corp. 2018. All Rights Reserved.
(function (GraphQLOperationType) {
GraphQLOperationType[GraphQLOperationType["Query"] = 0] = "Query";
GraphQLOperationType[GraphQLOperationType["Mutation"] = 1] = "Mutation";
GraphQLOperationType[GraphQLOperationType["Subscription"] = 2] = "Subscription";
})(exports.GraphQLOperationType || (exports.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__default('http');
const preprocessingLog = debug__default('preprocessing');
const translationLog = debug__default('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 Swagger2OpenAPI.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 OASValidator.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 jsonptr.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 = exports.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 `\{${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('/');
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(pluralize.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;
}
(function (CaseStyle) {
CaseStyle[CaseStyle["simple"] = 0] = "simple";
CaseStyle[CaseStyle["PascalCase"] = 1] = "PascalCase";
CaseStyle[CaseStyle["camelCase"] = 2] = "camelCase";
CaseSt