Consensys / ethql

A GraphQL interface to Ethereum :fire:
Apache License 2.0
623 stars 85 forks source link

ethql should be embeddable in other node applications #44

Closed raulk closed 6 years ago

raulk commented 6 years ago

ethql should be published as an npm module that other projects can import and add to their applications. This is a placeholder description that will be enhanced as the requirement is specified further.

raulk commented 6 years ago

@pcardune – it would be great if you could add your thoughts here!

pcardune commented 6 years ago

I think the primary use case for this is when you are developing an application that is a mix between a decentralized web app and a centralized web app, and you want to query all the data through a single API.

Our application collects data from a bunch of different data sources for which the ethereum blockchain is just one. On any given page, we might be querying data from 3 different sources. To reduce the number of round trips, it's important for all these data sources to be queryable through a single graphql endpoint.

There are three ways (that I know of) that you can combine multiple data sources in a single graphql API:

  1. Have separate graphql endpoints for each data source and merge the associated schemas using the mergeSchemas and makeRemoteExecutableSchema utilities from the graphql-tools library. There are two major downsides to this approach:

    • Deploying these services is more complex because you have to run a separate server for each graphql endpoint.
    • Additional latency from proxying queries through to multiple graphql endpoints.
    • It's tricky to intermix these APIs. You can use schema delegation, but it's kind of complicated.
  2. Have a single graphql endpoint where you directly import GraphQLSchema objects and merge them with mergeSchemas. This solves some problems and not others:

    • You only have to run one server, so deployment is simpler
    • You don't have any additional latency from proxying through to other servers
    • You still have to use schema delegation to intermix the APIs
  3. Have a single graphql endpoint where you directly import resolvers, which then get combined into a single GraphQLSchema. This solves all the problems in the above two scenarios.

I think ethql should support all three of the above scenarios. Here is an example of what this might look like in code:

Case 1: proxy to a separate ethql server

const EthQL = require('ethql');
const express = require('express');
const graphqlHTTP = require('express-graphql');
const fetch = require('node-fetch');
const { HttpLink } = require('apollo-link-http');
const {
  mergeSchemas,
  makeRemoteExecutableSchema,
  transformSchema,
  introspectSchema,
  RenameTypes,
} = require('graphql-tools');
const otherSchema = require('./someSchema');

export default async function startServer() {
  await EthQL.startServer(4000);
  const link = new HttpLink({ uri: 'http://localhost:4000', fetch });
  const ethqlSchema = makeRemoteExecutableSchema({
    schema: introspectSchema(link),
    link,
  });
  const schema = mergeSchemas({
    schemas: [
      transformSchema(ethqlSchema, [new RenameTypes(name => `EthQL${name}`)]),
      otherSchema,
    ],
  });

  const app = express();

  app.use('/graphql', graphqlHTTP({ schema }));
  app.listen(4001);
}

Case 2: delegate to ethql schema without starting a separate server

const { schema: ethqlSchema } = require('ethql');
const express = require('express');
const graphqlHTTP = require('express-graphql');
const { mergeSchemas, transformSchema, RenameTypes } = require('graphql-tools');
const otherSchema = require('./someSchema');

export default async function startServer() {
  const schema = mergeSchemas({
    schemas: [
      transformSchema(ethqlSchema, [new RenameTypes(name => `EthQL${name}`)]),
      otherSchema,
    ],
  });

  const app = express();

  app.use('/graphql', graphqlHTTP({ schema }));
  app.listen(4001);
}

Case 3: directly embedding ethql fields / resolvers into your own graphql object types

const { accountField: ethqlAccountField } = require('ethql');
const { GraphQLSchema, GraphQLObjectType } = require('graphql');
const express = require('express');
const graphqlHTTP = require('express-graphql');
const otherField = require('./otherField');

export default async function startServer() {
  const schema = new GraphQLSchema({
    query: new GraphQLObjectType({
      name: 'Query',
      fields: () => ({
        account: ethqlAccountField,
        otherField: otherField,
      }),
    }),
  });

  const app = express();

  app.use('/graphql', graphqlHTTP({ schema }));
  app.listen(4001);
}

I think at the very least, ethql should support case 2. Case 3 will require a bit more thought to figure out the granularity of the APIs that ethql would export.