UNPKG

@ardatan/openapi-to-graphql

Version:

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

470 lines 21.2 kB
"use strict"; // 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); const graphql_1 = require("graphql"); // Imports: const schema_builder_1 = require("./schema_builder"); const resolver_builder_1 = require("./resolver_builder"); const GraphQLTools = require("./graphql_tools"); const preprocessor_1 = require("./preprocessor"); const Oas3Tools = require("./oas_3_tools"); const auth_builder_1 = require("./auth_builder"); const debug_1 = require("debug"); const utils_1 = require("./utils"); const translationLog = debug_1.default('translation'); /** * Creates a GraphQL interface from the given OpenAPI Specification (2 or 3). */ function createGraphQLSchema(spec, options) { return __awaiter(this, void 0, void 0, function* () { if (typeof options === 'undefined') { options = {}; } // Setting default options options.strict = typeof options.strict === 'boolean' ? options.strict : false; // Schema options options.operationIdFieldNames = typeof options.operationIdFieldNames === 'boolean' ? options.operationIdFieldNames : false; options.fillEmptyResponses = typeof options.fillEmptyResponses === 'boolean' ? options.fillEmptyResponses : false; options.addLimitArgument = typeof options.addLimitArgument === 'boolean' ? options.addLimitArgument : false; options.genericPayloadArgName = typeof options.genericPayloadArgName === 'boolean' ? options.genericPayloadArgName : false; options.simpleNames = typeof options.simpleNames === 'boolean' ? options.simpleNames : false; options.singularNames = typeof options.singularNames === 'boolean' ? options.singularNames : false; // Authentication options options.viewer = typeof options.viewer === 'boolean' ? options.viewer : true; options.sendOAuthTokenInQuery = typeof options.sendOAuthTokenInQuery === 'boolean' ? options.sendOAuthTokenInQuery : false; // Logging options options.provideErrorExtensions = typeof options.provideErrorExtensions === 'boolean' ? options.provideErrorExtensions : true; options.equivalentToMessages = typeof options.equivalentToMessages === 'boolean' ? options.equivalentToMessages : true; options.fetch = typeof options.fetch === 'function' ? options.fetch : yield Promise.resolve().then(() => require('cross-fetch')).then(m => m.fetch); options.resolverMiddleware = typeof options.resolverMiddleware === 'function' ? options.resolverMiddleware : (resolverFactoryParams, factory) => factory(resolverFactoryParams); options['report'] = { warnings: [], numOps: 0, numOpsQuery: 0, numOpsMutation: 0, numQueriesCreated: 0, numMutationsCreated: 0 }; options.skipSchemaValidation = typeof options.skipSchemaValidation === 'boolean' ? options.skipSchemaValidation : false; let oass; if (Array.isArray(spec)) { /** * Convert all non-OAS 3.0.x into OAS 3.0.x */ oass = yield Promise.all(spec.map(ele => { return Oas3Tools.getValidOAS3(ele, options); })); } else { /** * Check if the spec is a valid OAS 3.0.x * If the spec is OAS 2.0, attempt to translate it into 3.0.x, then try to * translate the spec into a GraphQL schema */ oass = [yield Oas3Tools.getValidOAS3(spec, options)]; } const { schema, report } = yield translateOpenAPIToGraphQL(oass, options); return { schema, report }; }); } exports.createGraphQLSchema = createGraphQLSchema; /** * Creates a GraphQL interface from the given OpenAPI Specification 3.0.x */ function translateOpenAPIToGraphQL(oass, { strict, report, // Schema options operationIdFieldNames, fillEmptyResponses, addLimitArgument, idFormats, selectQueryOrMutationField, genericPayloadArgName, simpleNames, singularNames, // Resolver options headers, qs, requestOptions, baseUrl, customResolvers, fetch, resolverMiddleware, // Authentication options viewer, tokenJSONpath, sendOAuthTokenInQuery, // Logging options provideErrorExtensions, equivalentToMessages }) { return __awaiter(this, void 0, void 0, function* () { const options = { strict, report, // Schema options operationIdFieldNames, fillEmptyResponses, addLimitArgument, idFormats, selectQueryOrMutationField, genericPayloadArgName, simpleNames, singularNames, // Resolver options headers, qs, requestOptions, baseUrl, customResolvers, fetch, resolverMiddleware, // Authentication options viewer, tokenJSONpath, sendOAuthTokenInQuery, // Logging options provideErrorExtensions, equivalentToMessages }; translationLog(`Options: ${JSON.stringify(options)}`); /** * Extract information from the OASs and put it inside a data structure that * is easier for OpenAPI-to-GraphQL to use */ const data = preprocessor_1.preprocessOas(oass, options); preliminaryChecks(options, data); /** * Create GraphQL fields for every operation and structure them based on their * characteristics (query vs. mutation, auth vs. non-auth). */ let queryFields = {}; let mutationFields = {}; let authQueryFields = {}; let authMutationFields = {}; Object.entries(data.operations).forEach(([operationId, operation]) => { translationLog(`Process operation '${operation.operationString}'...`); let field = getFieldForOperation(operation, options.baseUrl, data, requestOptions); const saneOperationId = Oas3Tools.sanitize(operationId, Oas3Tools.CaseStyle.camelCase); // Check if the operation should be added as a Query or Mutation field if (!operation.isMutation) { let fieldName = !singularNames ? Oas3Tools.uncapitalize(operation.responseDefinition.graphQLTypeName) : Oas3Tools.sanitize(Oas3Tools.inferResourceNameFromPath(operation.path), Oas3Tools.CaseStyle.camelCase); if (operation.inViewer) { for (let securityRequirement of operation.securityRequirements) { if (typeof authQueryFields[securityRequirement] !== 'object') { authQueryFields[securityRequirement] = {}; } // Avoid overwriting fields that return the same data: if (fieldName in authQueryFields[securityRequirement] || /** * If the option is set operationIdFieldNames, the fieldName is * forced to be the operationId */ operationIdFieldNames) { fieldName = Oas3Tools.storeSaneName(saneOperationId, operationId, data.saneMap); } if (fieldName in authQueryFields[securityRequirement]) { utils_1.handleWarning({ typeKey: 'DUPLICATE_FIELD_NAME', message: `Multiple operations have the same name ` + `'${fieldName}' and security requirement ` + `'${securityRequirement}'. GraphQL field names must be ` + `unique so only one can be added to the authentication ` + `viewer. Operation '${operation.operationString}' will be ignored.`, data, log: translationLog }); } else { authQueryFields[securityRequirement][fieldName] = field; } } } else { // Avoid overwriting fields that return the same data: if (fieldName in queryFields || /** * If the option is set operationIdFieldNames, the fieldName is * forced to be the operationId */ operationIdFieldNames) { fieldName = Oas3Tools.storeSaneName(saneOperationId, operationId, data.saneMap); } if (fieldName in queryFields) { utils_1.handleWarning({ typeKey: 'DUPLICATE_FIELD_NAME', message: `Multiple operations have the same name ` + `'${fieldName}'. GraphQL field names must be ` + `unique so only one can be added to the Query object. ` + `Operation '${operation.operationString}' will be ignored.`, data, log: translationLog }); } else { queryFields[fieldName] = field; } } } else { let saneFieldName; if (!singularNames) { /** * Use operationId to avoid problems differentiating operations with the * same path but differnet methods */ saneFieldName = Oas3Tools.storeSaneName(saneOperationId, operationId, data.saneMap); } else { const fieldName = `${operation.method}${Oas3Tools.inferResourceNameFromPath(operation.path)}`; saneFieldName = Oas3Tools.storeSaneName(Oas3Tools.sanitize(fieldName, Oas3Tools.CaseStyle.camelCase), fieldName, data.saneMap); } if (operation.inViewer) { for (let securityRequirement of operation.securityRequirements) { if (typeof authMutationFields[securityRequirement] !== 'object') { authMutationFields[securityRequirement] = {}; } if (saneFieldName in authMutationFields[securityRequirement]) { utils_1.handleWarning({ typeKey: 'DUPLICATE_FIELD_NAME', message: `Multiple operations have the same name ` + `'${saneFieldName}' and security requirement ` + `'${securityRequirement}'. GraphQL field names must be ` + `unique so only one can be added to the authentication ` + `viewer. Operation '${operation.operationString}' will be ignored.`, data, log: translationLog }); } else { authMutationFields[securityRequirement][saneFieldName] = field; } } } else { if (saneFieldName in mutationFields) { utils_1.handleWarning({ typeKey: 'DUPLICATE_FIELD_NAME', message: `Multiple operations have the same name ` + `'${saneFieldName}'. GraphQL field names must be ` + `unique so only one can be added to the Mutation object. ` + `Operation '${operation.operationString}' will be ignored.`, data, log: translationLog }); } else { mutationFields[saneFieldName] = field; } } } }); // Sorting fields queryFields = utils_1.sortObject(queryFields); mutationFields = utils_1.sortObject(mutationFields); authQueryFields = utils_1.sortObject(authQueryFields); Object.keys(authQueryFields).forEach(key => { authQueryFields[key] = utils_1.sortObject(authQueryFields[key]); }); authMutationFields = utils_1.sortObject(authMutationFields); Object.keys(authMutationFields).forEach(key => { authMutationFields[key] = utils_1.sortObject(authMutationFields[key]); }); /** * Count created queries / mutations */ options.report.numQueriesCreated = Object.keys(queryFields).length + Object.keys(authQueryFields).reduce((sum, key) => { return sum + Object.keys(authQueryFields[key]).length; }, 0); options.report.numMutationsCreated = Object.keys(mutationFields).length + Object.keys(authMutationFields).reduce((sum, key) => { return sum + Object.keys(authMutationFields[key]).length; }, 0); /** * Organize created queries / mutations into viewer objects. */ if (Object.keys(authQueryFields).length > 0) { Object.assign(queryFields, auth_builder_1.createAndLoadViewer(authQueryFields, data, false)); } if (Object.keys(authMutationFields).length > 0) { Object.assign(mutationFields, auth_builder_1.createAndLoadViewer(authMutationFields, data, true)); } /** * Build up the schema */ const schemaConfig = { query: Object.keys(queryFields).length > 0 ? new graphql_1.GraphQLObjectType({ name: 'Query', description: 'The start of any query', fields: queryFields }) : GraphQLTools.getEmptyObjectType('Query'), mutation: Object.keys(mutationFields).length > 0 ? new graphql_1.GraphQLObjectType({ name: 'Mutation', description: 'The start of any mutation', fields: mutationFields }) : null }; /** * Fill in yet undefined object types to avoid GraphQLSchema from breaking. * * The reason: once creating the schema, the 'fields' thunks will resolve and * if a field references an undefined object types, GraphQL will throw. */ Object.entries(data.operations).forEach(([opId, operation]) => { if (typeof operation.responseDefinition.graphQLType === 'undefined') { operation.responseDefinition.graphQLType = GraphQLTools.getEmptyObjectType(operation.responseDefinition.graphQLTypeName); } }); const schema = new graphql_1.GraphQLSchema(schemaConfig); return { schema, report: options.report }; }); } /** * Creates the field object for the given operation. */ function getFieldForOperation(operation, baseUrl, data, requestOptions) { // Create GraphQL Type for response: const type = schema_builder_1.getGraphQLType({ def: operation.responseDefinition, data, operation }); // Create resolve function: const payloadSchemaName = operation.payloadDefinition ? operation.payloadDefinition.graphQLInputObjectTypeName : null; const resolverFactoryParams = { operation, payloadName: payloadSchemaName, data, baseUrl, requestOptions }; const resolve = data.options.resolverMiddleware(resolverFactoryParams, resolver_builder_1.getResolver); // Create args: const args = schema_builder_1.getArgs({ /** * Even though these arguments seems redundent because of the operation * argument, the function cannot be refactored because it is also used to * create arguments for links. The operation argument is really used to pass * data to other functions. */ requestPayloadDef: operation.payloadDefinition, parameters: operation.parameters, operation, data }); return { type, resolve, args, description: operation.description }; } /** * Ensures that the options are valid */ function preliminaryChecks(options, data) { // Check if OASs have unique titles const titles = data.oass.map(oas => { return oas.info.title; }); // Find duplicates among titles new Set(titles.filter((title, index) => { return titles.indexOf(title) !== index; })).forEach(title => { utils_1.handleWarning({ typeKey: 'MULTIPLE_OAS_SAME_TITLE', message: `Multiple OAS share the same title '${title}'`, data, log: translationLog }); }); // Check customResolvers if (typeof options.customResolvers === 'object') { // Check that all OASs that are referenced in the customResolvers are provided Object.keys(options.customResolvers) .filter(title => { // If no OAS contains this title return !data.oass.some(oas => { return title === oas.info.title; }); }) .forEach(title => { utils_1.handleWarning({ typeKey: 'CUSTOM_RESOLVER_UNKNOWN_OAS', message: `Custom resolvers reference OAS '${title}' but no such ` + `OAS was provided`, data, log: translationLog }); }); // TODO: Only run the following test on OASs that exist. See previous check. Object.keys(options.customResolvers).forEach(title => { // Get all operations from a particular OAS const operations = Object.values(data.operations).filter(operation => { return title === operation.oas.info.title; }); Object.keys(options.customResolvers[title]).forEach(path => { Object.keys(options.customResolvers[title][path]).forEach(method => { if (!operations.some(operation => { return path === operation.path && method === operation.method; })) { utils_1.handleWarning({ typeKey: 'CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD', message: `A custom resolver references an operation with ` + `path '${path}' and method '${method}' but no such operation ` + `exists in OAS '${title}'`, data, log: translationLog }); } }); }); }); } } var oas_3_tools_1 = require("./oas_3_tools"); exports.sanitize = oas_3_tools_1.sanitize; exports.CaseStyle = oas_3_tools_1.CaseStyle; var graphql_2 = require("./types/graphql"); exports.GraphQLOperationType = graphql_2.GraphQLOperationType; //# sourceMappingURL=index.js.map