remote-graph v0.0.3
GraphQL Remote Type
remote-graph is a library that helps you to execute remote GraphQL resolvers in a remote schema as if they are local and is build on top of graphql-js.
In contrast of Apollo Federation, it needs zero modification of your local schema nor any modification of the remote schema.
As a byproduct, the zero-modification principle enables you to merge 3rd party schemas, which you do not own, into your schema. Therefore, your frontend or mobile clients can treat in-house schema and 3rd party schemas as one unified API.
Example
The example below merges 3 public GraphQL schema into one. They are: 1. https://graphql-explorer.githubapp.com/graphql/proxy (GitHub) 2. https://countries.trevorblades.com/ 3. https://graphql.anilist.co/
First, you import several helper functions from RemoteResolver.js.
// Part 1
import { RemoteResolver, MapArgument, Transport } from '../RemoteResolver'; // This Lib
import { HTTP } from '../Transport'; // This Lib
import { buildSchema, graphql } from 'graphql';Second, define your schema. If you want to use parts of a remote schema, define those parts in your local schema.
// Part 2
let source = `
type Query {
me: MeOnGithub! # GitHub
countries(byName: String): [Country] # https://countries.trevorblades.com/
getAnimes(sort: [MediaSort]): Media # https://graphql.anilist.co/
}
# GitHub
# Notice GitHub GraphQL doesn't have this type. It is renamed from GitHub's "User"
# With this library, you can freely rename remote types.
type MeOnGithub {
login: String
}
# https://countries.trevorblades.com/
type Country {
name: String
}
# https://graphql.anilist.co/
enum MediaSort {
SCORE
POPULARITY
}
type Media {
id: Int
isAdult: Boolean
title: MediaTitle
}
type MediaTitle {
romaji: String
native: String
}
`;
let schema = buildSchema(source);Third, define remote resolvers.
// Part 3
async function ResolverFactory() {
const viewers = await RemoteResolver(
HTTP(
'https://graphql-explorer.githubapp.com/graphql/proxy',
{
cookie: 'needed for GitHub auth',
'X-CSRF-Token': 'needed for GitHub auth',
},
"include",
),
`query`,
`viewer`);RemoteResolver(transport: Transport, operationName: string, remoteField: string) constructs a resolver that will automatically resolve remote data.
Transport is an interface which locates the remote schema. You can implement a different transport if you don't use HTTP.
operationName and remoteField define the entry point of the remote field/resolver/type. For example, you can issue a query { user { shoppingCart { items } } } to domain1.com/gql, where shoppingCart returns a ShoppingCart type that is in another schema/server domain2.com/gql. The remote schema may look like
type Query {
getShoppingCartByUserId(id: ID): ShoppingCart
}
type ShoppingCart {
items: String
}getShoppingCartByUserId is the entry point of ShoppingCart type. Therefore, you can define your remote resolver in domain1.com/gql as
let resolvers = {
user: {
shoppingCart: await RemoteResolver(HTTP('domain2.com/gql'), `query`, `getShoppingCartByUserId`)
}
}Now let's fill up other remote resolvers.
// Part 4
const countries = await RemoteResolver(
HTTP('https://countries.trevorblades.com/'),
`query`,
`countries`);
const Media = await RemoteResolver(
HTTP('https://graphql.anilist.co/'),
'query',
'Media'
);
return {
// GitHub, mapped `query{me{...}}` to GitHub `query{viewer{...}}` to
me: viewers,
countries: async function (args, ctx, info) {
const remoteResult = await countries(args, ctx, MapArgument(info, {}));
if (Object.keys(args).length > 0) {
return remoteResult.filter((country) => {
return country.name.includes(args.byName);
});
}
return remoteResult;
},
getAnimes: Media,
};
}Here is another feature of RemoteResolver. In https://countries.trevorblades.com/, query { countries {...} } has no arguments. There is no way to filter output. You can modify your local schema to allow arguments for this field. That's why we defined
type Query {
countries(byName: String): [Country] # https://countries.trevorblades.com/
}We can do apply the arguments after we get data from remote. RemoteResolver allows you to have imperative fine control over your resolvers.
countries: async function (args, ctx, info) {
const remoteResult = await countries(args, ctx, MapArgument(info, {}));
if (Object.keys(args).length > 0) {
return remoteResult.filter((country) => {
return country.name.includes(args.byName);
});
}
return remoteResult;
},Now, let put everything together and issue queries to 3 remote schema as if they are 1 local schema.
async function f() {
let res = await graphql(schema,
`
query ($sort: MediaSort) {
me {
login
}
countries(byName: "ina") {
name
}
getAnimes(sort: [$sort]) {
id
isAdult
title {
romaji
native
}
}
}
`,
await ResolverFactory(),
null, // context
{"sort": "SCORE"}
);
console.log(res.data);
console.log(res.errors);
}
f();Nice Features
- Batching Remote Queries
If you have a queryquery { x y z }wherexandybelong to the same remote server, this lib will compile only 1 remote query that batches them together. Batching happens automatically and zero-configuration is needed.
Todo
- Full query language support.
RemoteResolverdoes not support full GraphQL query language yet. For example, it does not support Fragments and Interface. - Add
@remotedirective to allow better ahead of time type checking, readability. An draft design is
type Query {
me: User
}
type User @remote(url: "github.com/graphql-api", entry: "Query.viewer") {
login: String
age: Int @local # should allow local schema to expand a remote type.
}Mutationhas no semantic difference withQueryso it's already supported. I need to think about how to supportSubscription.- Remote schema auto generation. Instead of copy & paste remote schema into your local. Maybe I should write a tool to generate schema language source from the introspected schema.
- Somebody please comes out a better name for this library.