An open API service indexing awesome lists of open source software.

https://github.com/segpacto/gql-gateway

gql-gateway
https://github.com/segpacto/gql-gateway

graphql-gateway graphql-schemas graphql-server swagger swagger-specifications

Last synced: 4 months ago
JSON representation

gql-gateway

Awesome Lists containing this project

README

        

## Apollo GraphQL Gateway

### Description
This module provides a GraphQL Gateway that allows the interaction with Swagger based REST APIs, by autogenerating and merging their GraphQL schemas. 🚀
Through this gateway, it is possible to easily establish aggregations between the downstream REST services using GraphQL generated types, queries and mutations.

#### Related topics
- [Swagger](https://swagger.io/docs/)
- [GraphQl Schema Stitching](https://www.apollographql.com/docs/apollo-server/features/schema-stitching/)
- [GraphQl Schema Delegation](https://www.apollographql.com/docs/apollo-server/features/schema-delegation/)

### How this GraphQL-Gateway actually works?
1. Read and parse the Swagger specifications from all given endpoints.
2. For each Swagger specification auto-generate the GraphQL Types, Queries and Mutations; as well as auto-generate the APIs based resolvers.
3. Merge our local GraphQl definitions containing the aggregations and extensions along with the previous generated schemas.
4. Serve an Apollo GraphQl server with all agreggations.

## Getting started

### Installation
```js
npm install --save gql-gateway
```

### Getting started

#### Basic - Serve a basic GraphQL Gateway from public services
```js
const gateway = require('gql-gateway')

const endpointsList = [
{ name: 'petstore_service', url: 'https://petstore.swagger.io/v2/swagger.json' },
{ name: 'fruits_service', url: 'https://api.predic8.de/shop/swagger' }
]

gateway({ endpointsList })
.then(server => server.listen(5000))
.then(console.log('Service is now running at port: 5000'))
.catch(err => console.log(err))
```

#### Advanced - Adding aggregations
```js
const gateway = require('gql-gateway')

const localSchema = `
extend type Order {
pet: Pet
}
`

const resolvers = {
/*
Query : {
....
}
*/
Order: {
pet: {
fragment: '... on Order {petId}',
async resolve (order, args, context, info) {
const schema = await context.resolveSchema('pet_service')

return info.mergeInfo.delegateToSchema({
schema,
operation: 'query',
fieldName: 'getPetById',
args: { petId: order.petId },
context,
info
})
}
}
}
}

const config = {
port: 5000,
playgroundBasePath: 'gql-gateway'
}

const endpointsList = [
{ name: 'pet_service', url: 'https://petstore.swagger.io/v2/swagger.json' }
]

const apolloServerConfig = { playground: { endpoint: config.playgroundBasePath } }

gateway({ resolvers, localSchema, endpointsList, apolloServerConfig })
.then(server => server.listen(config.port))
.then(console.log(`Service is now running at port: ${config.port}`))
.catch(err => console.log(err))
```

**What just happened?**
- On `localSchema` we declare the aggregations that we would like to have by extending the original schemas (to get the original schemas, queries and mutations it is recommended to publish the service and then take a look at them before start adding aggregations).
- On `resolvers` we declare the way how to resolve the model `Order`, for this we use graphql `delegations`, where we specify on which of the autogenerated queries or mutations we relay to obtain the `pet` property in `Order`, in this case `getPetById`.

> Note that on the fragment part we declare `petId` as required field to obtain the `pet` property, so `petId` is going to be injected from the `Order` to the resolver even if it haven't been requested originally.

### Configuration options explained

| Name | Default | Description |
| -------------------- | --------------------------- | --------------- |
| `localSchema` | `empty` | Schema that contains the aggregations that we want to establish between the REST API services |
| `resolvers` | `empty` | Resolvers that implement delegation. See samples above |
| `endpointsList` | `required` | Contains a list of `json` swagger endpoints where to retrieve definitions to build the graphql schemas. `Minimum` one element|
| `apolloServerConfig` | `empty` | Apollo Server configuration (https://www.apollographql.com/docs/apollo-server/api/apollo-server/#apolloserver)|
| `contextConfig` | `empty` | Object that contains middlewares and also used to inject data into the Context (https://www.apollographql.com/docs/apollo-server/api/apollo-server/#apolloserver) |
| `logger` | `console` | Default logger is the console |

#### Format of `localSchema` parameters:
| Name | Default | Description |
| ---------- | ----------- | ------------- |
| `name` | `required` | Is used to identify the service |
| `url` | `required` | Url of the service swagger in `json` format |
| `headers` | `empty` | Headers passed to request the json swagger service, in case any kind of particular auth is needed |
| `onLoaded` | `empty` | Function that process the swaggerJson once is loaded so changes on the flight can be introduced. If passed `Must` return the swaggerJson back |

#### Format of function `onLoaded` parameters:
| Name | Default | Description |
| ------------- | --------------------- | --------------------------------------- |
| `swaggerJson` | `Swagger JSON schema` | Contains the loaded Swagger Json schema |
| `service` | `object` | Contains the `localSchema` that was loaded |

> onLoaded `function` Ex :
```js
const onLoaded = (swaggerJson, service) => {
swaggerJson.schemes = ['http', 'https']
return swaggerJson
}
const endpointsList = [
{ name: 'pet_service', url: 'https://petstore.swagger.io/v2/swagger.json', onLoaded }
]
```

#### Using the `apolloServerConfig` parameter:
```js
const apolloServerConfig = {
playground: {
endpoint: config.playgroundBasePath
}
}
```

### Technical Explanation
Below, we describe how to interact between services swagger based using agreggations(relations).
In this example we take the `User` and `Product` services as example.

#### The `User` service:
```
...
paths:
"/users":
get:
description: "Return an Array of existing users"
responses:
'200':
description: "successful operation"
schema:
type: array
items:
"$ref": "#/definitions/User"
...
definitions :
User:
type: object
properties:
userId:
type: string
firstname:
type: string
lastname:
type: string
...
```

#### The `Product` service:
```
...
paths:
paths:
'/products/{userId}':
get:
tags:
- Product
parameters:
- name: userId
in: path
description: ID of the user to fetch last products
required: true
type: string
summary: Return a summary of the last products
description: Return a sumary of the user products
responses:
'200':
description: successful operation
schema:
type: array
items:
"$ref": "#/definitions/Product"
...
definitions :
Product:
type: object
properties:
productId:
type: string
userId:
type: string
name:
type: string
type:
type: string
...
```

Once the graphql gateway read from those services their swagger specification, our server generates the following:
```graph
type Queries {
get_products_userId(userId: String!): Products!
get_users(): [User]!
}
```

#### Custom aggregations / relations
The next step is to extend the GraphQL definitions to introduce our custom global aggregations:

- Extend `GraphQl` Types definitions:
```graph
extend type User {
products: Product
}

# You can always declare the relation in one direction
extend type Product {
user: User
}
```
> This will automatically indicate to the GraphQl server that the Type `User` will have another field named products, actually the `Product` service relation.

- Finally, extend `GraphQl` resolvers:
```js
User: {
products: {
fragment: '... on User {userId}',
async resolve (user, args, context, info) {
return info.mergeInfo.delegateToSchema({
schema: userSchema,
operation: 'query',
fieldName: 'get_products_userId',
args: {
userId: user.userId // here we hook the relation identifier
},
context,
info
})
}
}
},
```

- And now we can magically query:

```graph
query {
get_users {
firstname,
lastname,
products {
name,
type
}
}
}
```