Giter Site home page Giter Site logo

sisense / graphql2rest Goto Github PK

View Code? Open in Web Editor NEW
313.0 15.0 36.0 1.62 MB

GraphQL to REST converter: automatically generate a RESTful API from your existing GraphQL API

License: MIT License

JavaScript 100.00%
graphql-to-rest graphql-tools graphql-api rest-api restful-api rest express-router graphql-server api-gateway graphql-js

graphql2rest's Introduction

GraphQL2REST

Automatically generate a RESTful API from your existing GraphQL API

Sisense-open-source License: MIT npm-image Tweet

GraphQL2REST is a Node.js library that reads your GraphQL schema and a user-provided manifest file and automatically generates an Express router with fully RESTful HTTP routes — a full-fledged REST API.

Author: Roy Mor

Why?

  • You have an existing GraphQL API, but need to expose it as REST because that's what your API users want

  • You want to develop a new GraphQL API and get REST on top of it, for free

  • You want to benefit from GraphQL internally while exposing REST externally as a public API

GraphQL2REST allows you to fully configure and customize your REST API, which may sit on top of a very different GraphQL layer (see features).


Table of Contents

Installation

npm:

npm i graphql2rest

yarn:

yarn add graphql2rest

Usage

Basic example:

Given a simple GraphQL schema:

type Query {
	getUser(userId: UUID!): User
}

type Mutation {
	createUser(name: String!, userData: UserDataInput): User
	removeUser(userId: UUID!): Boolean
}

Add REST endpoints to the manifest.json file:

{
	"endpoints": {
		"/users/:userId": {
			"get": {
				"operation": "getUser"
			},
			"delete": {
				"operation": "removeUser",
				"successStatusCode": 202
			}
		},
		"/users": {
			"post": {
				"operation": "createUser",
				"successStatusCode": 201
			}
		}
	}
}

In your code:

import GraphQL2REST from 'graphql2rest';
import { execute } from 'graphql'; // or any GraphQL execute function (assumes apollo-link by default)
import { schema } from './myGraphQLSchema.js'; 

const gqlGeneratorOutputFolder = path.resolve(__dirname, './gqlFilesFolder'); 
const manifestFile = path.resolve(__dirname, './manifest.json');

GraphQL2REST.generateGqlQueryFiles(schema, gqlGeneratorOutputFolder); // a one time pre-processing step

const restRouter = GraphQL2REST.init(schema, execute, { gqlGeneratorOutputFolder, manifestFile });

// restRouter now has our REST API attached
const app = express();
app.use('/api', restRouter);

(Actual route prefix, file paths etc should be set first via options object or in config/defaults.json)

Resulting API:

POST /api/users              --> 201 CREATED
GET /api/users/{userId}      --> 200 OK
DELETE /api/users/{userId}   --> 202 ACCEPTED

// Example:

GET /api/users/1234?fields=name,role

Will invoke getUser query and return only 'name' and 'role' fields in the REST response.
The name of the filter query param ("fields" here) can be changed via configuration. 

For more examples and usage, please refer to the Tutorial.


Features

  • Use any type of GraphQL server - you provide the execute() function
  • Default "RESTful" logic for error identification, determining status codes and response formatting
  • Customize response format (and error responses format too, separately)
  • Custom parameter mapping (REST params can be different than GraphQL parameter names)
  • Customize success status codes for each REST endpoint
  • Map custom GraphQL error codes to HTTP response status codes
  • Map a single REST endpoint to multiple GraphQL operations, with conditional logic to determine mapping
  • Hide specific fields from responses
  • Run custom middleware function on incoming requests before they are sent to GraphQL
  • Client can filter on all fields of a response, in all REST endpoints, using built-in filter
  • Built-in JMESPath support (JSON query language) for client filter queries
  • GraphQL server can be local or remote (supports apollo-link and fetch to forward request to a remote server)
  • Embed your own winston-based logger

How GraphQL2REST works

GraphQL2REST exposes two public functions:

  • generateGqlQueryFiles() - GraphQL schema pre-processing
  • init() - generate Express router at runtime

First, GraphQL2REST needs to do some one-time preprocessing. It reads your GraphQL schema and generates .gql files containing all client operations (queries and mutations). These are "fully-exploded" GraphQL client queries which expand all fields in all nesting levels and all possible variables, per each Query or Mutation type.

This is achieved by running the generateGqlQueryFiles() function:

GraphQL2REST.generateGqlQueryFiles(schema, '/gqlFilesFolder');

Now the /gqlFilesFolder contains an index.js file and subfolders for queries and mutations, containing .gql files corresponding to GraphQL operations. Use path.resolve(__dirname, <PATH>) for relative paths.

generateGqlQueryFiles() has to be executed just once, or when the GraphQL schema changes (it can be executed offline by a separate script or at "build time").


After generateGqlQueryFiles() has been executed once, GraphQL2REST init() can be invoked to create REST endpoints dynamically at runtime.

init() loads all .gql files into memory, reads the manifest.json file and uses Express router to generate REST endpoint routes associated with the GraphQL operations and rules defines in the manifest. init() returns an Express router mounted with all REST API endpoints.


The init() function

GraphQL2REST.init() is the entry point that creates REST routes at runtime.

It only takes two mandatory parameters: your GraphQL schema and the GraphQL server execute function (whatever your specific GraphQL server implementation provides, or an Apollo Link function).

GraphQL2REST.init(
	schema: GraphQLSchema,
	executeFn: Function,

	options?: Object,
	formatErrorFn?: Function,
	formatDataFn?: Function,
	expressRouter?: Function)

GraphQL arguments are passed to executeFn() in Apollo Link/fetch style, meaning one object as argument: { query, variables, context, operationName }.

options defines various settings (see below). If undefined, default values will be used.

formatErrorFn is an optional function to custom format GraphQL error responses.

formatDataFn is an optional function to custom format non-error GraphQL responses (data). If not provided, default behavior is to strip the encapsulating 'data:' property and the name of the GraphQL operation, and omit the 'errors' array from successful responses.

expressRouter is an express.Router() instance to attach new routes on. If not provided, a new Express instance will be returned.

The Manifest File

REST API endpoints and their behavior are defined in the manifest file (normally manifest.json ). It is used to map HTTP REST routes to GraphQL operations and define error code mappings. See a full example here.

The endpoints section

The endpoints object lists the REST endpoints to generate:

"endpoints": {
	"/tweets/:id": {  // <--- HTTP route path; path parameters in Express notation
		"get": {      // <--- HTTP method (get, post, patch, put, delete)
			"operation": "getTweetById", // <--- name of GraphQL query or mutation
		}
	}
}

Route path, HTTP method and operation name are mandatory.

GraphQL2REST lets you map a single REST endpoint to multiple GraphQL operations by using an array of operations (operations[] array instead of the operation field).

Additional optional fields:

  • "params": Used to map parameters in the REST request to GraphQL arguments in the corresponding query or mutation. Lets you rename parameters so the REST API can use different naming than the underlying GraphQL layer. If omitted, parameters will be passed as is by default. [Learn more]

  • "successStatusCode": Success status code. If omitted, success status code is 200 OK by default. [Learn more]

  • "condition": Conditions on the request parameters. GraphQL operation will be invoked only if the condition is satisfied. Condition is expressed using MongoDB query language query operators. [Learn more]

  • "hide": Array of fields in response to hide. These fields in the GraphQL response will always be filtered out in the REST response. [Learn more]

  • "wrapRequestBodyWith": Wrap the request body with this property (or multiple nested objects expressed in dot notation) before passing the REST request to GraphQL. Lets you map the entire HTTP body to a specific GraphQL Input argument. Helpful when we want the REST body to be flat, but the GraphQL operation expects the input to be wrapped within an object. [Learn more]

  • "requestMiddlewareFunction": Name of a middleware function (in the middleware.js module) to call before passing the request to the GraphQL server. This function receives the express req object and returns a modified version of it. [Learn more]


Another example:

// Mutation updateUserData(userOid: UUID!, newData: userDataInput!): User
// input userDataInput { name: String, birthday: Date }
// type User { id: UUID!, name: String, birthday: Date, internalSecret: String }

"endpoints": {
	"/users/:id": {
		"patch": {
			"operation": "updateUserData",
			"params": {   // <-- map or rename some params
				"userOid": "id" // <-- value of :id will be passed to userOid in mutation
			},
			"successStatusCode": 202  // <-- customize success status code (202 is strange here but valid)
			"wrapRequestBodyWith": "newData", // <-- allow flat REST request body
			"hide": ["internalSecret"] // <-- array of fields to omit from final REST response
		}
	}
}
// PATCH /users/{userId}, body = {"name": "Joe", "birthday": "1990-1-14"}
// Response: 202 ACCEPTED
// { "id": THE_USERID, "name": "Joe", "birthday": "1990-1-14"} // "internalSecret" omitted

The errors section

The optional “errors” object lets you map GraphQL error codes to HTTP status codes, and add an optional additional error message. The first error element in GraphQL's errors array is used for this mapping.

Example:

"errors": {
	"errorCodes": {
		"UNAUTHENTICATED": {
			"httpCode": 401,
			"errorDescription": "Forbidden: Unauthorized access",
		}
	}
}

In this example, responses from GraphQL that have an errors[0].extension.code field with the value "UNAUTHENTICATED" produce a 401 Unauthorized HTTP status code, and the error description string above is included in the JSON response sent by the REST router.

For GraphQL error codes that have no mappings (or if the "errors" object is missing from manifest.json), a 400 Bad Request HTTP status code is returned by default for client errors, and a 500 Internal Server Error is returned for errors in the server or uncaught exceptions.

Configuration

Settings can be configured in the options object provided to init(). For any fields not specified in the options object, or if options is not provided to init(), values from the config/defaults.json file are used.

const gql2restOptions  = {
	apiPrefix: '/api/v2', //sets the API base path url
	manifestFile: './api-v2-manifest.json', //pathname of manifest file
	gqlGeneratorOutputFolder: './gqls', //.gql files folder (generated by generateGqlQueryFiles())
	middlewaresFile:  './middlewares.js', //optional middlewares module for modifying requests
	filterFieldName: 'fields', //global query parameter name for filtering (default is 'fields'),
	graphqlErrorCodeObjPath: 'errors[0].extensions.code', //property for GraphQL error code upon error
	logger: myCustomLogger //optional Winston-based logger function
};

const expressRouter = GraphQL2REST.init(schema, execute, gql2restOptions);

Use path.resolve(__dirname, <PATH>) for relative paths.

All fields in options are optional, but init() will not be able to run without a valid manifest file and gqlGeneratorOutputFolder previously populated by generateGqlQueryFiles().

The depth limit of generated client queries can be set in the pre-processing step. This might be needed for very large schemas, when the schema has circular references or the GraphQL server has a strict query depth limit.

Tutorial

Running tests

npm test

Or, for tests with coverage:

npm run test:coverage

Benefits

  • GraphQL2REST lets you create a truly RESTful API that might be very different than the original, unchanged GraphQL API, without writing a single line of code.

  • The resulting REST API enjoys the built-in data validation provided by GraphQL due to its strong type system. Executing a REST API call with missing or incorrect parameters automatically results in an informative error provided by GraphQL (which can be custom formatted to look like REST).

  • An old REST API can be migrated to a new GraphQL API gradually, by first building the GraphQL API and using GraphQL2REST to generate a REST API on top of it, seamlessly. That new REST API will have the same interface as the old one, and the new implementation can then be tested, endpoints migrated in stages, until a full migration to the underlying GraphQL API takes place.

Limitations and Known Issues

• No support for subscriptions yet – only queries and mutations

Acknowledgments

Contact

For inquiries contact author Roy Mor (roy.mor.email at gmail.com).

Release History

  • 0.6.1
    • First release as open source
  • 1.0.1
    • First major release

Contributing

See CONTRIBUTING.md

License

Distributed under MIT License. See LICENSE for more information.

(c) Copyright 2020 Sisense Ltd

graphql2rest's People

Contributors

dependabot[bot] avatar nipeshkc7 avatar roy-mor avatar sergsisense avatar tjsiron avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

graphql2rest's Issues

Passing query params into gql

Hi

I was trying to pass some parameters to gql through REST and my schema looks like this:
type Query{ foo(paramA : typeA, paramB: typeB) : returnType!

Here typeA is defined as
input typeA { key: String value: String operation: String }
I need to pass key, value and operation as query parameters

What I tried:
Using a middleware function that modifies the query params to the required format.

const middlewareFunc = (req, route, verb, operation) => { if (req.query.key && req.query.value && req.query.operation) { req.query = { paramA : {key : req.query.key, value : req.query.value, operation : req.query.operation } }; } }

The parameters aren't being passed down to GQL as expected
Can anyone help me on this, it be of great help.
Thanks in advance

TypeError: field.type.inspect is not a function

Hi guys,

I´m trying to test your pretty cool graphql2rest. In my environment I got always same error:

error: [gqlgenerator] Encountered error trying to generate GQL files for GraphQL operations. Aborting
error: TypeError: field.type.inspect is not a function

So I installed the example without any other code. I added the modules via

yarn add express body-parser
yarn add path graphql graphql2rest

which results in packages.conf:

{
  "dependencies": {
    "body-parser": "^1.19.0",
    "express": "^4.17.1",
    "graphql": "^16.0.1",
    "graphql2rest": "^0.6.4",
    "path": "^0.12.7"
  }
}

Executing ends up in the error

myuser@MacBook-Pro graphql2rest % node index.js
info: GQLGenerator initializing...
info: GQLGenerator initialized with query depthLimit of 1000
info: [gqlgenerator] Creating folder /Users/myuser/graphql2rest/myGqlFiles/
error: [gqlgenerator] Encountered error trying to generate GQL files for GraphQL operations. Aborting
error: TypeError: field.type.inspect is not a function
    at generateQuery (/Users/myuser/graphql2rest/node_modules/graphql2rest/src/gqlgenerator/schemaParser.js:70:33)
    at /Users/myuser/graphql2rest/node_modules/graphql2rest/src/gqlgenerator/index.js:123:23
    at Array.forEach (<anonymous>)
    at generateFile (/Users/myuser/graphql2rest/node_modules/graphql2rest/src/gqlgenerator/index.js:122:19)
    at generateQueries (/Users/myuser/graphql2rest/node_modules/graphql2rest/src/gqlgenerator/index.js:147:3)
    at Object.generateGqlQueryFiles (/Users/myuser/graphql2rest/node_modules/graphql2rest/src/gqlgenerator/index.js:59:3)
    at Object.<anonymous> (/Users/myuser/graphql2rest/index.js:77:14)
    at Module._compile (internal/modules/cjs/loader.js:1068:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1097:10)
    at Module.load (internal/modules/cjs/loader.js:933:32)
error: FATAL: Error while attempting to load index.js from gqlGeneratorOutputFolder /Users/myuser/graphql2rest/myGqlFiles. Absolute path: "/Users/myuser/graphql2rest/myGqlFiles/".

                Function generateGqlQueryFiles() must be executed at least once before init() is invoked, to generate .gql query files and index from your schema. Did you run it?

                If you did and you specified a custom folder name, make sure you provide the "options" parameter to init(), and that "options.gqlGeneratorOutputFolder" is set to the correct folder.
...

My environment is

  • MacOS 10.15.7
  • Node.js v14.17.0 also tested with new LTS v16.13.1

Error determining logic needs improvement

Hi everyone!

First I'd like to thank you for creating and sharing this module, it's very useful. I've come across one bug so I'd like to share it.

Actual: When used with apollo-server and apollo rest data sources, logic doesn't determine the error correctly, it returns false even if it's an error.

Expected: isError should return true when response object has errors

Details:: Specifically this line of code should be changed

if (!response.data && response.errors) return true;

should be

if (response.errors && Array.isArray(response.errors) && response.errors.length > 0) {
   return true;
}

It's because the response that comes from RestDataSource has both error and data properties.

Issue in generated GQL files

Hi

My query looks like

queryA(filter: typeAFilter, sort:[typeASort], limit:Limit) : returnTypeA!

returnTypeA is defined as

type returnTypeA{
    members:[typeA]
    count: Int
}

Where typeA is

type typeA {
    name: String!
    id: int64
    queryB(filter: typeBFilter, sort:[typeBSort] ,limit:Limit) : returntypeB!
}

The queryA.gql file created after genrarateGQLFiles() looks like:

query queryA($filter: typeBFilter, $sort: [typeBSort], $limit: Limit,
            $filter2: typeAFilter, $sort2: [typeASort], $limit2: Limit){
    queryA(filter: $filter2, sort: $sort2, limit: $limit2){
        members {
            .
            .
            .
            queryB(filter: $filter, sort: $sort, limit: $limit){
                 
            }
     }
}

The problem is that if I want to write a generic middleware function for limit, it breaks as queryA expects the structure to be passed in as limit2, and not limit.
Why is this be happening, and how can I make the middleware function generic?

Thanks in advance :)

Query parameters don't get converted to appropriate GraphQL types

Description

When passing arguments in a query string, there is no type conversion step. So strings don't get converted into integers, etc. For example, an HTTP request like this one:

GET https://myapi.com/projects?limit=100

would attempt to pass the limit parameter as a string "100" instead of consulting the GraphQL schema and realizing it is supposed to take an Int type instead.

It's possible to define a middleware that converts these, however I'd like to suggest it as a feature instead. Most GraphQL built-in scalar types I think could be automatically converted, but obviously custom scalars or non-scalars would still require custom middleware to be built.

Another workaround is to use the request body instead of the query string, as JSON allows you to specify scalars without ambiguity.

I would be happy to take a stab at creating a pull request for this if there's no immediate objection to the idea.

Steps to Reproduce

Set up a graphql2rest project with the following GraphQL Schema:

type Project {
  id: ID!
}

type Query {
  projects(limit: Int): [Project!]!
}

this manifest.json:

{
  "endpoints": {
    "/projects": {
      "get": {
        "operation": "projects"
      }
    }
  }
}

and then execute this HTTP request:

GET https://myapi.com/projects?limit=100

You should get a GraphQL error like the following:

Variable "$limit" got invalid value "100"; Expected type Int. Int cannot represent non-integer value: "100"

Hangs on large schema?

I have a pretty large GraphQL schema (schema-main.graphql is 107kloc) and the generation hangs out at thi stage:

info: GQLGenerator initializing...
info: GQLGenerator initialized with query depthLimit of 1000
info: [gqlgenerator] Creating folder /Users/vincent/Code/sterblue1/packages/apps/graphile/src/rest/generated-graphql-files/

Here is the TS file being executed:

import GraphQL2REST from "graphql2rest";
import { execute } from "graphql"; // or any GraphQL execute function (assumes apollo-link by default)
import { buildSchema } from "graphql";
import path from "path";
import fs from "fs";

const schema = buildSchema(
  fs.readFileSync(path.resolve(__dirname, "../../schema-main.graphql"), {
    encoding: "utf8"
  })
);

const gqlGeneratorOutputFolder = path.resolve(
  __dirname,
  "./generated-graphql-files"
);
const manifestFile = path.resolve(__dirname, "./manifest.json");

export const restRouter = GraphQL2REST.generateGqlQueryFiles(
  schema,
  gqlGeneratorOutputFolder
); 

Add types file for logger integration

This package allows integrating with an external winston-compatible logger by passing in the logger function. Logger type definition/validation is currently duck typed - it's better to add types file for this.

Rest API document

Hey is there a support for rest api doc ?

If it is there can you please share samples?

Split test module into smaller modules

Currently all unit tests reside in one file and it is too large. The test module should be split into smaller, separate files, based on area/feature being tested (gql-generator tests, router tests, various capabilities etc).

When invoking npm test all tests from all these files should be executed.

Test components should be shared across files as much as possible (mocha, chai, runMiddleware, mocks etc).

Generate only relevant gql files

GraphQL files are generated for the whole GraphQL schema. Wouldn't it be nice to generate only what we need (operations that are defined in the manifest.json)?

Field Filtering Is Done After Object Is Returned

First, thank you for this project. It works better for large GraphQL projects than SOFA.

When I initially read the docs, I assumed that passing in values to fields would adjust the GraphQL query itself, and not request the data from the server in the first place.

However, after trying it out and reviewing the code, it looks like GraphQL2Rest filters fields after the data is returned.
We have some objects with a very large amount of field options (100+), but only a few are needed at any given time, so this is causing our server to load a lot of unneeded/unwanted data.

  1. Is there a way to adjust the GraphQL query itself to only request the fields that are in the fields param?

  2. If not, would you be able to point me to the right place in the code base to make this modification?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.