@omnigraph/openapi
Version:
380 lines (372 loc) • 20.1 kB
JavaScript
;
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;