gql-gateway

    1.0.10 • Public • Published

    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

    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

    npm install --save gql-gateway

    Getting started

    Basic - Serve a basic GraphQL Gateway from public services

    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

    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 :

    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:

    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:

    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:
        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:
    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:
    query {
      get_users {
        firstname,
        lastname,
        products {
          name,
          type
        }
      }
    }
    

    Install

    npm i gql-gateway

    DownloadsWeekly Downloads

    3

    Version

    1.0.10

    License

    MIT

    Unpacked Size

    15.9 kB

    Total Files

    7

    Last publish

    Collaborators

    • segpacto