diff --git a/community/boilerplates/remote-schemas/rest-wrapper/.gitignore b/community/boilerplates/remote-schemas/rest-wrapper/.gitignore new file mode 100644 index 00000000000..10a291cf43c --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +*.zip +package-lock.json +my-rest-api/.git diff --git a/community/boilerplates/remote-schemas/rest-wrapper/README.md b/community/boilerplates/remote-schemas/rest-wrapper/README.md new file mode 100644 index 00000000000..fed6e08fd37 --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/README.md @@ -0,0 +1,101 @@ +# REST wrapper - Boilerplate to write a GraphQL server that wraps a REST API + +This boilerplate gives an example of writing a GraphQL service to wrap some pre-existing REST API. +You can add this REST wrapper as a Remote Schema in Hasura. + +## Stack + +Node 8.10 + +Apollo Server (GraphQL framework) + +## REST API + +The REST API is implemented in [my-rest-api](my-rest-api/) folder. It has the following APIs: + +``` +GET /users +GET /users/:userId +POST /users +``` + +The `GET /users` endpoint also takes an optional query param i.e. `GET /users?name=abc` and the `POST /users` endpoint expects a valid JSON payload in the body. + +## GraphQL API + +We will convert the above REST API into the following GraphQL API: + +``` +type User { + id: String! + name: String! + balance: Int! +} + +type Query { + getUser(id: String!): User + users(name: String): [User] +} + +type Mutation { + addUser(name: String!, balance: Int!): User +} +``` + +## How to wrap + +In our GraphQL service, we have defined a new API for each REST endpoint. This is what our mapping looks like: + +| REST | GraphQL | +|---------------------|------------------------------------------------| +| GET /users | users (name: String) : [User] | +| GET /users/:userId | getUser(id: String!): User | +| POST /users | addUser(name: String!, balance: Int!): User | + +We would have to write a resolver for each API. This is what a typical resolver looks like, for e.g `getUser` : + +``` +getUser: async (_, { id }) => { + return await getData(restAPIEndpoint + '/users/' + id); +} +``` + +## Deployment (Using Heroku) + +You need a Heroku account and heroku-cli installed. Execute the following commands in a terminal: + +1. Log into Heroku + +```bash +heroku login +``` + +2. Create REST API app + +```bash +# in my-rest-api directory (community/boilerplates/remote-schemas/rest-wrapper/my-rest-api) +heroku create +``` + +3. Deploy REST API app + +```bash +git push heroku master +``` + +4. The above step will return an endpoint for your REST API. Update the constant `restAPIEndpoint` in `index.js` with this endpoint. + +5. Create GRAPHQL API app + +```bash +# in current directory (community/boilerplates/remote-schemas/rest-wrapper) +heroku create +``` + +6. Deploy GRAPHQL API app + +```bash +git push heroku master +``` + +The final step will also return a HTTPS URL in the output. Now, you can go to Hasura console and add this URL as a Remote Schema to allow querying it via Hasura. diff --git a/community/boilerplates/remote-schemas/rest-wrapper/helpers.js b/community/boilerplates/remote-schemas/rest-wrapper/helpers.js new file mode 100644 index 00000000000..ed2d30c5fb0 --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/helpers.js @@ -0,0 +1,44 @@ +const { ApolloError } = require('apollo-server'); +const fetch = require('node-fetch'); + +const getData = async url => { + try { + const res = await fetch(url); + const json = await res.json(); + if(isHTTPError(res.status)) { + throw new ApolloError(json, "http-status-error", {statusCode: res.status, error: json}); + } + console.log(json); + return json; + } catch (error) { + console.log(JSON.stringify(error)); + throw error; + } +}; + +const postData = async (url, body) => { + try { + const res = await fetch(url, { + method: 'post', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }); + const json = await res.json(); + if(isHTTPError(res.status)) { + throw new ApolloError(json, "http-status-error", {statusCode: res.status, error: json}); + } + console.log(json); + return json; + } catch (error) { + console.log(JSON.stringify(error)); + throw error; + } +}; + +const isHTTPError = status => { + return !((status >= 200) && (status < 300)); +}; + +exports.getData = getData; +exports.postData = postData; + diff --git a/community/boilerplates/remote-schemas/rest-wrapper/index.js b/community/boilerplates/remote-schemas/rest-wrapper/index.js new file mode 100644 index 00000000000..50c94f39b3a --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/index.js @@ -0,0 +1,52 @@ +const { ApolloServer } = require('apollo-server'); +const gql = require('graphql-tag'); +const {getData, postData} = require('./helpers'); + +const typeDefs = gql` + type User { + id: String! + name: String! + balance: Int! + } + + type Query { + getUser(id: String!): User + users(name: String): [User] + } + + type Mutation { + addUser(name: String!, balance: Int!): User + } +`; + +// replace with actual REST endpoint +const restAPIEndpoint = 'https://rest-user-api.herokuapp.com'; + +const resolvers = { + Query: { + getUser: async (_, { id }) => { + return await getData(restAPIEndpoint + '/users/' + id); + }, + + users: async (_, { name }) => { + var nameParams = ''; + if (name) { + nameParams = '?name=' + name; + } + return await getData(restAPIEndpoint + '/users' + nameParams ); + } + }, + + Mutation: { + addUser: async (_, { name, balance } ) => { + return await postData(restAPIEndpoint + '/users', { name, balance } ); + } + } +}; + +const schema = new ApolloServer({ typeDefs, resolvers }); + +schema.listen({ port: process.env.PORT || 4000 }).then(({ url }) => { + console.log(`schema ready at ${url}`); +}); + diff --git a/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/.gitignore b/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/.gitignore new file mode 100644 index 00000000000..504afef81fb --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/index.js b/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/index.js new file mode 100644 index 00000000000..8194c3932bc --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/index.js @@ -0,0 +1,45 @@ +const express = require('express'); +const bodyParser = require("body-parser"); +const uuidv4 = require('uuid/v4'); +const app = express(); +const port = process.env.PORT || 3000; + +app.use(bodyParser.json()); + +var users = []; + +app.get('/', (req, res) => res.json('Hello World!')); + +app.get('/users/:userId', (req, res) => { + var idParam = req.params.userId; + var result = users.find(user => idParam ? user.id == idParam: false); + if(result) { + res.json(result); + } else { + res.status(404).json("user not found"); + } +}); + +app.get('/users', (req, res) => { + var nameParam = req.query.name; + var result = users.filter(user => nameParam ? user.name == nameParam : true); + res.json(result); +}); + +app.post('/users', (req, res) => { + var user = req.body; + user.id = user.id ? user.id : uuidv4(); + + if (user.id && user.name && user.balance) { + if (user.balance >= 100) { + users.push(user); + res.json(user); + } else { + res.status(400).json('minimum balance required: 100'); + } + } else { + res.status(400).json('invalid parameters'); + } +}); + +app.listen(port, () => console.log(`Example app listening on port ${port}!`)); diff --git a/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/package.json b/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/package.json new file mode 100644 index 00000000000..54536275d1d --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/my-rest-api/package.json @@ -0,0 +1,16 @@ +{ + "name": "rest", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node index.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "express": "^4.16.4", + "uuid": "^3.3.2" + } +} diff --git a/community/boilerplates/remote-schemas/rest-wrapper/package.json b/community/boilerplates/remote-schemas/rest-wrapper/package.json new file mode 100644 index 00000000000..15c7fdaf1c0 --- /dev/null +++ b/community/boilerplates/remote-schemas/rest-wrapper/package.json @@ -0,0 +1,17 @@ +{ + "name": "rest-remote-schema", + "version": "1.0.0", + "description": "This is a GraphQL backend to wrap a REST API.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "apollo-server": "^2.4.8", + "graphql": "^14.1.1", + "graphql-tag": "^2.10.1", + "node-fetch": "^2.3.0" + } +}