nest-graphql-endpoint
v0.8.1
Published
Nest an external GraphQL Endpoint
Downloads
354
Readme
Nest an external GraphQL Endpoint (Like GitHub's v4 API)
How it works
https://www.parabol.co/blog/nest-github-api-in-graphql-schema
Install
yarn add nest-graphql-endpoint
What's it do?
Merges a remote graphql endpoint into your schema. For example, let's say your app has a GitHub integration. Each user has used OAuth2 to allow your app to access GitHub on their behalf. You have stored that token on the User object in your database. Now, you want to get their name from your app & their bio from GitHub. You also want the bio of all their friends, too. This package will let you write the following query:
query {
user(id: 'abc') {
name
github {
errors {
message
}
query {
# These values come from GitHub
viewer {
bio
}
}
}
friends {
github {
errors {
message
}
query {
viewer {
bio
}
}
}
}
todos {
... on _extGitHubIssue {
# These values come from GitHub, too
number
repository {
nameWithOwner
}
}
}
}
}
Example with your own endpoint
import {schema} from './mySchema'
import nestGraphQLEndpoint from 'nest-graphql-endpoint'
import {print} from 'graphql'
import schemaIDL from './remoteSchemaIDL'
return nestGraphQLEndpoint({
parentSchema: schema,
parentType: 'User',
fieldName: 'github',
resolveEndpointContext: (source) => ({accessToken: source.accessToken}),
executor: (document, variables, context) => {
// Example executor fn not for production! See nestGitHubEndpoint for a production-ready executor
return fetch('https://foo.co', {
headers: {
Authorization: `Bearer ${context.accessToken}`,
},
body: JSON.stringify({query: print(document), variables}),
})
},
prefix: '_extEndpoint',
batchKey: 'accessToken',
schemaIDL,
})
As a convenience, this package includes a helper for GitHub's API. If you'd like to add other endpoints, please make a PR.
Example using GitHub's API
import {schema} from './mySchema'
import nestGitHubEndpoint from 'nest-graphql-endpoint/lib/nestGitHubEndpoint'
return nestGitHubEndpoint({
parentSchema: schema,
parentType: 'User',
fieldName: 'github',
// Assumes a `githubToken` is on the User object
// GitHub preview is being phased out, but still requires a custom header to access preview features
resolveEndpointContext: (source) => ({
accessToken: source.githubToken,
headers: {Accept: 'application/vnd.github.bane-preview+json'},
}),
}).schema
Note: Requires Node v15.0.0 to use the native AbortController
Example field resolution
Say each User
has a list of todos
and some of those come from Jira & others come from GitHub.
type User {
id: ID!
todos: [Todo]
}
interface Todo {
id: ID!
}
type _extGitHubIssue implements Todo
type JiraIssue implements Todo
// Front-end
const UserQuery = graphql`
query UserQuery($userId: ID!) {
user(id: $userId) {
todos {
__typename
... on JiraIssue {
descriptionHTML
}
... on _extGitHubIssue {
bodyHTML
number
respository {
nameWithOwner
}
}
}
}
`;
const ShowTodo = (props) => {
const data = useQuery(UserQuery)
if (!data) return null
return data.todos.map((todo) => {
if todo.__typename === 'JiraIssue') {
return <JiraTodo todo={todo}/>
}
return <GitHubTodo todo={todo}/>
})
}
// Backend
const {schema, githubRequest} = nestGitHubEndpoint({...})
const todoResolver = (source, args, context, info) => {
if (source.type === 'github') {
const ghContext = {accessToken: '123'}
const {nameWithOwner, issueNumber} = source
const [repoOwner, repoName] = nameWithOwner.split('/')
const wrapper = `
{
repository(owner: "${repoOwner}", name: "${repoName}") {
issue(number: ${issueNumber}) {
...info
}
}
}`
const {data, errors} = await githubRequest(wrapper, ghContext, context, info)
return data // returns
}
}
How it works
- It extends your schema with a type that contains
{errors, query, mutation}
- Given a remote IDL, it prefixes the
__typename
so there are no conflicts with your own schema - It batches all requests by the
batchKey
so only 1 request is made per key. In the above example, this is the accessToken. - For each batched request, it removes the
__typename
prefix and merges the fragments, variableDefinitions, and variables. - In the event of a name conflict, it will alias fields before the request is fetched.
- When the endpoint responds, it will de-alias the response, re-apply the
__typename
prefix, and filter the errors by path - For field resolvers, it removes any types that don't exist on the endpoint & then sends the request.
Limnitations
- FragmentDefinitions are assumed equal if they have matching names (Relay compiler guarantees this)
- @octokit/graphql-schema example is pinned at v14 to support CJS
License
MIT