18 Mar 2018

A first try at GraphQL

Introducing Forge-QL: A GraphQL wrapper server/client demo around Autodesk Forge Web API's ...

 

    GraphQL has become too mainstream to ignore it and it may even be the next big thing about client/server communication and how we built and consume Web APIs. The technology is defined as follow on its official page:

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools. 

    This article is by no mean planning to give you an introduction to what GraphQL is about as it is fairly well done in many places over the web, including the Introduction to GraphQL, so spend a bit of time there if you haven't heard about or tried it yet.

    I was planning to take a closer for a while but a recent training I delivered on Forge was the occasion to find some time for it. Not that Forge is already exposing a GraphQL API yet - who knows what may come in the future - but I thought it could be interesting to see what it would take to wrap an existing Forge REST API with a custom GraphQL server in order to expose it to a client and leverage its features. In the worst case I would gain some experience in one of the upcoming most popular technology in 2018, this definitely doesn't look like a waste of time...

    After reading the very well done tutorial above, I looked for GraphQL/node.js/mongoDB samples and stumbled across this very handy article: GraphQL and MongoDB — a quick example. I found it to be a really good resource in the way that it gets straight to the point "My advice: don’t read this post. Instead, take 5 minutes ... clone the project, build and run it, and play with the graphiql interface yourself".

    I also knew that Apollo was the middleware of choice when dealing with GraphQL, for having spotted multiple articles mentioning the name.

   After few days of work I was able to produce a working demo of a GraphQL server wrapping the Forge DataManagement API and expose it to a React+Apollo client. Here are the highlights of that demo:

    First of all the schema, exposing the data contract in order to be able to browse through Hubs, Projects, Folders, Items and Versions:    

const typeDefs = [`

    type Query {
      itemVersions (projectId: String!, itemId: String!): ItemVersionsResponse
      folderContent (projectId: String!, folderId: String!): FolderContentResponse
      topFolders (hubId: String!, projectId: String!): FolderContentResponse
      project (hubId: String!, projectId: String!): ProjectResponse
      folder (projectId: String!, folderId: String!): FolderResponse
      projects (hubId: String!): ProjectsResponse
      hub (hubId: String!): HubResponse
      hubs: HubsResponse
    }

    type ItemVersionsResponse {
      data: [Version]
      error: Error
    }

    type FolderContentResponse {
      data: [FolderOrItem]
      error: Error
    }

    type ProjectsResponse {
      data: [Project]
      error: Error
    }

    type ProjectResponse {
      data: Project
      error: Error
    }

    type FolderResponse {
      data: Folder
      error: Error
    }

    type HubsResponse {
      error: Error
      data: [Hub]
    }

    type HubResponse {
      error: Error
      data: Hub
    }

    type Hub {
      attributes: Attributes
      id: String
    }

    type Project {
      attributes: Attributes
      id: String
    }

    type Folder {
      attributes: Attributes
      id: String
    }

    type FolderOrItem {
      attributes: Attributes
      id: String
    }

    type Item {
      attributes: Attributes
      id: String
    }

    type Version {
      relationships: Relationships
      attributes: Attributes
      id: String
    }

    type Relationships {
      derivatives: Derivatives
    }

    type Derivatives {
      data: Data
    }

    type Data {
      id: String
    }

    type Attributes {
      extension: Extension
      displayName: String
      name: String
    }

    type Extension {
      version: String
      type: String
    }

    type Error {
      message: String
    }

    schema {
      query: Query
    }
`]

export default typeDefs

    Then the implementation of the GraphQL resolver, basically a simple wrapper around the Forge REST API calls:

import ServiceManager from '../../services/SvcManager'
import {graphqlExpress} from 'graphql-server-express'
import {makeExecutableSchema} from 'graphql-tools'
import typeDefs from './typeDefs'

///////////////////////////////////////////////////////////
//
//
///////////////////////////////////////////////////////////
const onError = (ex) => {

  return {
    data: null,
    error: ex
  }
}

///////////////////////////////////////////////////////////
//
//
///////////////////////////////////////////////////////////
const onResponse = (res) => {

  if (res.statusCode === 200) {

    return {
      data: res.body.data,
      error: null
    }
  }

  return {
    data: null,
    error: {}
  }
}

///////////////////////////////////////////////////////////
//
//
///////////////////////////////////////////////////////////
const api = () => {

  const forgeSvc = ServiceManager.getService('ForgeSvc')

  const dmSvc = ServiceManager.getService('DMSvc')

  const resolvers = {
    Query: {
      hubs: async (root, args, {session}) => {
   
        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await dmSvc.getHubs(token)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      },
      hub: async (root, {hubId}, {session}) => {

        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await dmSvc.getHub(token, hubId)

          return onResponse(res)

        } catch (ex) {

          return onError(ex)
        }
      },
      project: async (root, {hubId, projectId}, {session}) => {

        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await dmSvc.getProject(
            token, hubId, projectId)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      },
      projects: async (root, {hubId}, {session}) => {

        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await dmSvc.getProjects(token, hubId)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      },
      topFolders: async (root, {hubId, projectId}, {session}) => {

        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await await dmSvc.getProjectTopFolders(
            token, hubId, projectId)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      },
      folderContent: async (root, {projectId, folderId}, {session}) => {
      
        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await dmSvc.getFolderContent(
            token, projectId, folderId)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      },
      folder: async (root, {projectId, folderId}, {session}) => {

        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await await dmSvc.getFolder(
            token, projectId, folderId)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      },
      itemVersions: async (root, {projectId, itemId}, {session}) => {

        try {

          const token = 
            await forgeSvc.get3LeggedTokenMaster(
              session)

          const res = await await dmSvc.getItemVersions(
            token, projectId, itemId)

          return onResponse (res)

        } catch (ex) {

          return onError(ex)
        }
      }
    }
  }

  return makeExecutableSchema({
    resolvers,
    typeDefs
  })
}

export default api


     My server is using a simple node.js/express setup but Apollo is also providing some dedicated server technology Apollo Engine which I haven't experimented yet.

    And finally the React client which is using several Apollo packages, including react-apollo, apollo-client, apollo-link, apollo-link-state and apollo-link-http:

import registerServiceWorker from './registerServiceWorker'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { withClientState } from 'apollo-link-state'
import { ApolloProvider } from 'react-apollo'
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { onError } from 'apollo-link-error'
import { ApolloLink } from 'apollo-link'
import {client as config} from 'c0nfig'
import ReactDOM from 'react-dom'
import merge from 'lodash/merge'
import React from 'react'
import 'babel-polyfill'
import {
  BrowserRouter as Router
} from 'react-router-dom'

// Apollo Resolvers
import appResolver from 'resolvers/app'

// App container
import App from 'containers/App'

const cache = new InMemoryCache()

const errorLink = onError(({ graphQLErrors, networkError }) => {
  if (graphQLErrors) {
    graphQLErrors.map(({ message, locations, path }) =>
      console.log(
        `[GraphQL error]: ` + 
        `Message: ${message}`
        `Location: ${locations}` +
        `Path: ${path}`
      )
    )
  }

  if (networkError) console.log(`[Network error]: ${networkError}`);
});

const stateLink = withClientState({ 
  ...merge(appResolver), 
  cache 
})

const httpLink = new HttpLink({ 
  // headers: {
  //   authorization: `Bearer ${ACCESS_TOKEN}`
  // },
  uri: `${config.apiUrl}/graphql`,
  credentials: 'include'
})

const link = ApolloLink.from([
  errorLink,
  stateLink,
  httpLink
])

const client = new ApolloClient({
  cache,
  link
})

ReactDOM.render(
  <ApolloProvider client={client}>
    <Router>
      <App/>
    </Router>
  </ApolloProvider>,
  document.getElementById('root')
)

registerServiceWorker()

   Here is how a couple of GraphQL queries to my DataManagement wrapper look like:

# Retrieve List of Hubs
query HubsQuery {
  hubs {
    data {
      attributes {
        extension {
          type
        }
        name
      }
      id
    }
    error {
      message
    }
  }
}
# Retrieve folder content for a specific (projectId, folderId) pair
query FolderContent ($projectId: String!, $folderId: String!) {
  folderContent (projectId: $projectId, folderId: $folderId) {
    data {
      attributes {
        extension {
          type
        }
        displayName
        name
      }
      id
    }
    error {
      message
    }
  }
}

   The full sample source code is available at https://github.com/leefsmp/forge-ql and I also deployed that as a live demo at https://forge-ql.autodesk.io. Sign-in there with your A360 credentials and browse through your data, you can also display models in the viewer ;)

graphql demo

Related Article