openapi-to-graphql-cli
Version:
CLI for turning APIs described by OpenAPI Specifications (OAS) into GraphQL interfaces
224 lines (222 loc) • 8.74 kB
JavaScript
import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import cors from 'cors';
import path from 'path';
import fs from 'fs';
import yaml from 'js-yaml';
import { printSchema } from 'graphql';
import { Command } from 'commander';
import { fetch } from 'cross-fetch';
import { createGraphQLSchema } from 'openapi-to-graphql';
const app = express();
const program = new Command();
program
.version(require('../package.json').version)
.usage('<OAS JSON file path(s) and/or remote url(s)> [options]')
.arguments('<path(s) and/or url(s)>')
.option('-s, --strict', 'throw an error if OpenAPI-to-GraphQL cannot run without compensating for errors or missing data in the OAS')
.option('--save <file path>', 'save schema to path and do not start server')
// Resolver options
.option('-p, --port <port>', 'select the port where the server will start', parseInt)
.option('-u, --url <url>', 'select the base url which paths will be built on')
.option('--cors', 'enable Cross-origin resource sharing (CORS)')
// Schema options
.option('-o, --operationIdFieldNames', 'create field names based on the operationId')
.option('-f, --fillEmptyResponses', 'create placeholder schemas for operations with no response body rather than ignore them')
.option('--addLimitArgument', 'add a limit argument on fields returning lists of objects/lists to control the data size')
.option('--genericPayloadArgName', "Sets argument name for the payload of a mutation to 'requestBody'")
.option('--simpleNames', 'Only remove illegal characters from names in the OAS and ignore casing and formatting')
.option('--simpleEnumValues', 'Only remove illegal characters from enum values in the OAS and ignore casing and formatting')
.option('--singularNames', 'Experimental feature that will create more meaningful names from the operation path')
// Resolver options
.option('-H, --header <key:value>', 'add headers to every request; repeatable flag; set using key:value notation', collect, [])
.option('-Q, --queryString <key:value>', 'add query parameters to every request; repeatable flag; set using key:value notation', collect, [])
// Authentication options
.option('--no-viewer', 'do not create GraphQL viewer objects for passing authentication credentials')
// Logging options
.option('--no-extensions', 'do not add extentions, containing information about failed REST calls, to the GraphQL errors objects')
.option('--no-equivalentToMessages', 'do not append information about the underlying REST operations to the description of fields')
.parse(process.argv);
const filePaths = program.args;
if (typeof filePaths === 'undefined' || filePaths.length === 0) {
console.error('No path(s) provided');
console.error('Please refer to the help manual (openapi-to-graphql -h) for more information');
process.exit(1);
}
// Load the OASs based off of the provided paths
Promise.all(filePaths.map((filePath) => {
return new Promise((resolve, reject) => {
// Check if the file exists
if (fs.existsSync(path.resolve(filePath))) {
try {
resolve(readFile(path.resolve(filePath)));
}
catch (error) {
reject(error);
}
// Check if file is in a remote location
}
else if (filePath.match(/^https?/g)) {
getRemoteFileSpec(filePath)
.then((remoteContent) => {
resolve(remoteContent);
})
.catch((error) => {
reject(error);
});
// Cannot determine location of file
}
else {
reject(`File path '${filePath}' is invalid`);
}
});
}))
.then((oass) => {
// Select the port on which to host the GraphQL server
const portNumber = program.port ? program.port : 3000;
const options = {
...program.opts(),
/**
* Assemble headers and query strings so that they are in the proper format
* for the OpenAPI-to-GraphQL library
*/
...{
headers: parseKeyValuePairs(program.header),
qs: parseKeyValuePairs(program.queryString)
}
};
startGraphQLServer(oass, options, portNumber);
})
.catch((error) => {
console.error(error);
process.exit(1);
});
/**
* For list arguments, collect all values and store them in a list
*
* @param value the current value
* @param previous the store of all values
*/
function collect(value, previous) {
return previous.concat([value]);
}
/**
* Returns content of read JSON/YAML file.
*
* @param {string} path Path to file to read
* @return {object} Content of read file
*/
function readFile(path) {
if (/json$/.test(path)) {
return JSON.parse(fs.readFileSync(path, 'utf8'));
}
else if (/yaml$/.test(path) || /yml$/.test(path)) {
return yaml.safeLoad(fs.readFileSync(path, 'utf8'));
}
else {
throw new Error(`Failed to parse JSON/YAML. Ensure file '${path}' has ` +
`the correct extension (i.e. '.json', '.yaml', or '.yml).`);
}
}
/**
* reads a remote file content using http protocol
* @param {string} url specifies a valid URL path including the port number
*/
async function getRemoteFileSpec(uri) {
const res = await fetch(uri);
const body = await res.text();
if (res.status < 200 && res.status <= 300) {
throw new Error(`Could not retrieve file. Received unsuccessful status code '${res.status}.`);
}
else {
if (typeof body === 'string') {
try {
return JSON.parse(body);
}
catch (e) {
try {
return yaml.safeLoad(body);
}
catch (f) {
console.error(`JSON parse error: ${e}\nYAML parse error: ${f}`);
}
}
}
throw new Error(`Cannot parse remote file`);
}
}
/**
* generates a GraphQL schema and starts the GraphQL server on the specified port
* @param {object} oas the OAS specification file
* @param {number} port the port number to listen on on this server
*/
function startGraphQLServer(oas, options, port) {
// Create GraphQL interface
createGraphQLSchema(oas, options)
.then(({ schema, report }) => {
console.log(JSON.stringify(report, null, 2));
// Save local file if required
if (program.save) {
writeSchema(schema);
}
else {
// Enable CORS
if (program.cors) {
app.use(cors());
}
// Mounting graphql endpoint using the middleware express-graphql
app.use('/graphql', graphqlHTTP({
schema,
graphiql: true
}));
// Initiating the server on the port specified by user or the default one
app.listen(port, () => {
console.log(`GraphQL accessible at: http://localhost:${port}/graphql`);
});
}
})
.catch((err) => {
console.log('OpenAPI-to-GraphQL creation event error:', err.message);
});
}
/**
* saves a grahpQL schema generated by OpenAPI-to-GraphQL to a file
* @param {createGraphQLSchema} schema
*/
function writeSchema(schema) {
fs.writeFile(program.save, printSchema(schema), (err) => {
if (err)
throw err;
console.log(`OpenAPI-to-GraphQL successfully saved your schema at ${program.save}`);
});
}
/**
* Parse key value pairs in the form `key:string`
*
* @param keyValues Raw unparsed key value pairs from the CLI
*/
function parseKeyValuePairs(keyValues) {
const parsedKeyValues = {};
if (Array.isArray(keyValues)) {
keyValues.forEach((keyValue) => {
const separator = keyValue.indexOf(':');
if (separator === -1) {
console.warn(`The key value pair '${keyValue}' does not have a ':' separating ` +
`the key from the value. It will be ignored.`);
}
else {
const key = keyValue.substr(0, separator);
// Trim, may have leading white space
const value = keyValue.substr(separator + 1).trim();
if (key in parsedKeyValues) {
console.warn(`Multiple key value pairs have the same key '${key}'. ` +
`The key value pair '${keyValue}' will be ignored.`);
}
else {
parsedKeyValues[key] = value;
}
}
});
}
return parsedKeyValues;
}