UNPKG

@omnigraph/openapi

Version:
380 lines (372 loc) • 20.1 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); const jsonSchema = require('@omnigraph/json-schema'); const utils = require('@graphql-mesh/utils'); const jsonMachete = require('json-machete'); const changeCase = require('change-case'); const stringInterpolation = require('@graphql-mesh/string-interpolation'); const crossHelpers = require('@graphql-mesh/cross-helpers'); function getFieldNameFromPath(path, method, responseTypeSchemaRef) { // Replace identifiers with "by" path = path.split('{').join('by_').split('}').join(''); const [actualPartsStr, allQueryPartsStr] = path.split('?'); const actualParts = actualPartsStr.split('/').filter(Boolean); let fieldNameWithoutMethod = actualParts.join('_'); // If path doesn't give any field name without identifiers, we can use the return type with HTTP Method name if ((!fieldNameWithoutMethod || fieldNameWithoutMethod.startsWith('by')) && responseTypeSchemaRef) { const refArr = responseTypeSchemaRef.split('/'); // lowercase looks better in the schema const prefix = changeCase.camelCase(refArr[refArr.length - 1]); if (fieldNameWithoutMethod) { fieldNameWithoutMethod = prefix + '_' + fieldNameWithoutMethod; } else { fieldNameWithoutMethod = prefix; } } if (allQueryPartsStr) { const queryParts = allQueryPartsStr.split('&'); for (const queryPart of queryParts) { const [queryName] = queryPart.split('='); fieldNameWithoutMethod += '_' + 'by' + '_' + queryName; } } // get_ doesn't look good in field names const methodPrefix = method.toLowerCase(); if (methodPrefix === 'get') { return fieldNameWithoutMethod; } if (fieldNameWithoutMethod) { return methodPrefix + '_' + fieldNameWithoutMethod; } return methodPrefix; } async function getJSONSchemaOptionsFromOpenAPIOptions({ oasFilePath, fallbackFormat, cwd, fetch: fetchFn, baseUrl, schemaHeaders, operationHeaders, selectQueryOrMutationField = [], logger = new utils.DefaultLogger('getJSONSchemaOptionsFromOpenAPIOptions'), }) { var _a, _b, _c, _d, _e, _f; const fieldTypeMap = {}; for (const { fieldName, type } of selectQueryOrMutationField) { fieldTypeMap[fieldName] = type; } const schemaHeadersFactory = stringInterpolation.getInterpolatedHeadersFactory(schemaHeaders); logger === null || logger === void 0 ? void 0 : logger.debug(`Fetching OpenAPI Document from ${oasFilePath}`); const oasOrSwagger = typeof oasFilePath === 'string' ? await utils.readFileOrUrl(oasFilePath, { cwd, fallbackFormat, headers: schemaHeadersFactory({ env: crossHelpers.process.env }), fetch: fetchFn, importFn: utils.defaultImportFn, logger, }) : oasFilePath; const operations = []; if ('servers' in oasOrSwagger) { baseUrl = baseUrl || oasOrSwagger.servers[0].url; } for (const relativePath in oasOrSwagger.paths) { const pathObj = oasOrSwagger.paths[relativePath]; const pathParameters = pathObj.parameters; for (const method in pathObj) { if (method === 'parameters') { continue; } const methodObj = pathObj[method]; const operationConfig = { method: method.toUpperCase(), path: relativePath, type: method.toUpperCase() === 'GET' ? 'query' : 'mutation', field: methodObj.operationId && utils.sanitizeNameForGraphQL(methodObj.operationId), description: methodObj.description || methodObj.summary, schemaHeaders, operationHeaders, responseByStatusCode: {}, }; operations.push(operationConfig); let allParams; if (methodObj.parameters && Array.isArray(methodObj.parameters)) { allParams = [...(pathParameters || []), ...methodObj.parameters]; } else { allParams = { ...(pathParameters || {}), ...(methodObj.parameters || {}), }; } for (const paramObjIndex in allParams) { let paramObj = allParams[paramObjIndex]; if ('$ref' in paramObj) { paramObj = jsonMachete.resolvePath(paramObj.$ref.split('#')[1], oasOrSwagger); } const argName = utils.sanitizeNameForGraphQL(paramObj.name); switch (paramObj.in) { case 'query': if (method.toUpperCase() === 'GET') { const requestSchema = (operationConfig.requestSchema = operationConfig.requestSchema || { type: 'object', properties: {}, }); requestSchema.properties[paramObj.name] = paramObj.schema || ((_b = (_a = paramObj.content) === null || _a === void 0 ? void 0 : _a['application/json']) === null || _b === void 0 ? void 0 : _b.schema) || paramObj; if (!requestSchema.properties[paramObj.name].title) { requestSchema.properties[paramObj.name].name = paramObj.name; } if (!requestSchema.properties[paramObj.name].description) { requestSchema.properties[paramObj.name].description = paramObj.description; } if (requestSchema.properties.__typename) { delete requestSchema.properties.__typename; } if (paramObj.required) { requestSchema.required = requestSchema.required || []; requestSchema.required.push(paramObj.name); } // Fix the reference if ((_c = requestSchema.properties[paramObj.name].$ref) === null || _c === void 0 ? void 0 : _c.startsWith('#')) { requestSchema.properties[paramObj.name].$ref = `${oasFilePath}${requestSchema.properties[paramObj.name].$ref}`; } } else { if (!operationConfig.path.includes('?')) { operationConfig.path += '?'; } if (operationConfig.path !== '?') { operationConfig.path += '&'; } operationConfig.path += `${paramObj.name}={args.${argName}}`; } break; case 'path': { // If it is in the path, let JSON Schema handler put it operationConfig.path = operationConfig.path.replace(`{${paramObj.name}}`, `{args.${argName}}`); break; } case 'header': { operationConfig.headers = operationConfig.headers || {}; operationConfig.headers[paramObj.name] = `{args.${argName}}`; break; } case 'cookie': { operationConfig.headers = operationConfig.headers || {}; operationConfig.headers.cookie = operationConfig.headers.cookie || ''; const cookieParams = operationConfig.headers.cookie.split('; '); cookieParams.push(`${paramObj.name}={args.${argName}}`); operationConfig.headers.cookie = cookieParams.join('; '); break; } case 'body': if (paramObj.schema && Object.keys(paramObj.schema).length > 0) { operationConfig.requestSchema = `${oasFilePath}#/paths/${relativePath .split('/') .join('~1')}/${method}/parameters/${paramObjIndex}/schema`; } if (paramObj.example) { operationConfig.requestSample = paramObj.example; } if (paramObj.examples) { operationConfig.requestSample = Object.values(paramObj.examples)[0]; } break; } switch (((_d = paramObj.schema) === null || _d === void 0 ? void 0 : _d.type) || paramObj.type) { case 'string': operationConfig.argTypeMap = operationConfig.argTypeMap || {}; operationConfig.argTypeMap[argName] = 'String'; break; case 'integer': operationConfig.argTypeMap = operationConfig.argTypeMap || {}; operationConfig.argTypeMap[argName] = 'Int'; break; case 'number': operationConfig.argTypeMap = operationConfig.argTypeMap || {}; operationConfig.argTypeMap[argName] = 'Float'; break; case 'boolean': operationConfig.argTypeMap = operationConfig.argTypeMap || {}; operationConfig.argTypeMap[argName] = 'Boolean'; break; } if (paramObj.required) { operationConfig.argTypeMap = operationConfig.argTypeMap || {}; operationConfig.argTypeMap[argName] = operationConfig.argTypeMap[argName] || 'ID'; operationConfig.argTypeMap[argName] += '!'; } } if ('requestBody' in methodObj) { const requestBodyObj = methodObj.requestBody; if ('content' in requestBodyObj) { const contentKey = Object.keys(requestBodyObj.content)[0]; const contentSchema = (_e = requestBodyObj.content[contentKey]) === null || _e === void 0 ? void 0 : _e.schema; if (contentSchema && Object.keys(contentSchema).length > 0) { operationConfig.requestSchema = `${oasFilePath}#/paths/${relativePath .split('/') .join('~1')}/${method}/requestBody/content/${contentKey === null || contentKey === void 0 ? void 0 : contentKey.toString().split('/').join('~1')}/schema`; } const examplesObj = (_f = requestBodyObj.content[contentKey]) === null || _f === void 0 ? void 0 : _f.examples; if (examplesObj) { operationConfig.requestSample = Object.values(examplesObj)[0]; } } } const responseByStatusCode = operationConfig.responseByStatusCode; // Handling multiple response types for (const responseKey in methodObj.responses) { const responseObj = methodObj.responses[responseKey]; let schemaObj; if ('content' in responseObj) { const contentKey = Object.keys(responseObj.content)[0]; schemaObj = responseObj.content[contentKey].schema; if (schemaObj && Object.keys(schemaObj).length > 0) { responseByStatusCode[responseKey] = responseByStatusCode[responseKey] || {}; responseByStatusCode[responseKey].responseSchema = `${oasFilePath}#/paths/${relativePath .split('/') .join('~1')}/${method}/responses/${responseKey}/content/${contentKey === null || contentKey === void 0 ? void 0 : contentKey.toString().split('/').join('~1')}/schema`; } const examplesObj = responseObj.content[contentKey].examples; if (examplesObj) { responseByStatusCode[responseKey] = responseByStatusCode[responseKey] || {}; responseByStatusCode[responseKey].responseSample = Object.values(examplesObj)[0]; } const example = responseObj.content[contentKey].example; if (example) { responseByStatusCode[responseKey] = responseByStatusCode[responseKey] || {}; responseByStatusCode[responseKey].responseSample = example; } } else if ('schema' in responseObj) { schemaObj = responseObj.schema; if (schemaObj && Object.keys(schemaObj).length > 0) { responseByStatusCode[responseKey] = responseByStatusCode[responseKey] || {}; responseByStatusCode[responseKey].responseSchema = `${oasFilePath}#/paths/${relativePath .split('/') .join('~1')}/${method}/responses/${responseKey}/schema`; } } else if ('examples' in responseObj) { const examples = Object.values(responseObj.examples); responseByStatusCode[responseKey] = responseByStatusCode[responseKey] || {}; responseByStatusCode[responseKey].responseSample = examples[0]; } else if (responseKey.toString() === '204') { responseByStatusCode[responseKey] = responseByStatusCode[responseKey] || {}; responseByStatusCode[responseKey].responseSchema = { type: 'null', description: responseObj.description, }; } if ('links' in responseObj) { const dereferencedLinkObj = await jsonMachete.dereferenceObject({ links: responseObj.links, }, { cwd, root: oasOrSwagger, fetchFn, logger, headers: schemaHeaders, }); responseByStatusCode[responseKey].links = responseByStatusCode[responseKey].links || {}; for (const linkName in dereferencedLinkObj.links) { const linkObj = responseObj.links[linkName]; if ('$ref' in linkObj) { throw new Error('Unexpected $ref in dereferenced link object'); } const args = {}; for (const parameterName in linkObj.parameters || {}) { const parameterExp = linkObj.parameters[parameterName].split('-').join('_'); args[utils.sanitizeNameForGraphQL(parameterName)] = parameterExp.startsWith('$') ? `{root.${parameterExp}}` : parameterExp.split('$').join('root.$'); } if ('operationRef' in linkObj) { const [externalPath, ref] = linkObj.operationRef.split('#'); if (externalPath) { if (crossHelpers.process.env.DEBUG) { console.warn(`Skipping external operation reference ${linkObj.operationRef}\n Use additionalTypeDefs and additionalResolvers instead.`); } } else { const actualOperation = jsonMachete.resolvePath(ref, oasOrSwagger); if (actualOperation.operationId) { const fieldName = utils.sanitizeNameForGraphQL(actualOperation.operationId); responseByStatusCode[responseKey].links[linkName] = { fieldName, args, description: linkObj.description, }; } else { console.warn('Missing operationId skipping...'); } } } else if ('operationId' in linkObj) { responseByStatusCode[responseKey].links[linkName] = { fieldName: utils.sanitizeNameForGraphQL(linkObj.operationId), args, description: linkObj.description, }; } } } if (!operationConfig.field) { methodObj.operationId = getFieldNameFromPath(relativePath, method, schemaObj === null || schemaObj === void 0 ? void 0 : schemaObj.$ref); // Operation ID might not be avaiable so let's generate field name from path and response type schema operationConfig.field = utils.sanitizeNameForGraphQL(methodObj.operationId); } // Give a better name to the request input object if (typeof operationConfig.requestSchema === 'object' && !operationConfig.requestSchema.title) { operationConfig.requestSchema.title = operationConfig.field + '_request'; } } if (fieldTypeMap[operationConfig.field]) { operationConfig.type = fieldTypeMap[operationConfig.field]; } } } return { operations, baseUrl, cwd, fetch: fetchFn, schemaHeaders, operationHeaders, }; } /** * Creates a local GraphQLSchema instance from a OpenAPI Document. * Everytime this function is called, the OpenAPI file and its dependencies will be resolved on runtime. * If you want to avoid this, use `createBundle` function to create a bundle once and save it to a storage * then load it with `loadGraphQLSchemaFromBundle`. */ async function loadGraphQLSchemaFromOpenAPI(name, options) { const extraJSONSchemaOptions = await getJSONSchemaOptionsFromOpenAPIOptions(options); return jsonSchema.loadGraphQLSchemaFromJSONSchemas(name, { ...options, ...extraJSONSchemaOptions, }); } /** * Creates a bundle by downloading and resolving the internal references once * to load the schema locally later */ async function createBundle(name, openApiLoaderOptions) { const { operations, baseUrl, cwd, fetch, schemaHeaders, operationHeaders } = await getJSONSchemaOptionsFromOpenAPIOptions(openApiLoaderOptions); return jsonSchema.createBundle(name, { operations, baseUrl, cwd, fetch, schemaHeaders, operationHeaders, ignoreErrorResponses: openApiLoaderOptions.ignoreErrorResponses, logger: openApiLoaderOptions.logger, }); } Object.defineProperty(exports, 'getGraphQLSchemaFromBundle', { enumerable: true, get: function () { return jsonSchema.getGraphQLSchemaFromBundle; } }); exports.createBundle = createBundle; exports.default = loadGraphQLSchemaFromOpenAPI; exports.getJSONSchemaOptionsFromOpenAPIOptions = getJSONSchemaOptionsFromOpenAPIOptions;