Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kbrandwijk committed Nov 7, 2017
0 parents commit beb14f2
Show file tree
Hide file tree
Showing 8 changed files with 289 additions and 0 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
GRAPHCOOL_ENDPOINT=http://localhost:60000/simple/v1/cj9larpve000c0196iathtcl2
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
dist
yarn.lock
package-lock.json
.vscode
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# graphql-gateway-tools

A set of tools to help you build a GraphQL Gateway using remote schemas and schema stitching.

## SchemaGenerator

A higher order helper for creating a merged schema. It makes it easy to compose a schema, by providing methods to add remote schema endpoints, local GraphQL schemas, partial type definitions, and multiple resolver functions.

```
const schemaGenerator = new SchemaGenerator()
// Create a remoteExecutableSchema by specifying the endpoint address
await schemaGenerator.registerEndpoint({uri: 'http://myendpointaddress/graphql'})
// Create a remoteExecutableSchema by specifying endpoint adddress and introspection schema:
await schemaGenerator.registerEndpoint({uri: 'http://myendpointaddress/graphql', introspectionSchema: myIntrospectionSchema})
// Create a remoteExecutableSchema by passing in an ApolloLink instance
await schemaGenerator.registerEndpoint({link: myApolloLink})
// Create a remoteExecutableSchema that uses an Authorization bearer token
await schemaGenerator.registerEndpoint({uri: 'http://myendpointaddress/graphql', authenticationToken: 'ey.......'})
// Add a schema
schemaGenerator.registerSchema(schema: myGraphQLSchema)
// Add a type definition
schemaGenerator.registerTypeDefinition(typeDefs: myTypeDefinitionString)
// Add a resolver function
schemaGenerator.registerResolver(resolverFunction: myResolverFunction)
// Generate the merged schema
const mySchema = schemaGenerator.generateSchema()
```

See the [examples](./examples) folder for a complete example (coming soon).

## addTypeNameField

If you add an Interface Types to your merged schema, you have to manually add the `__typeName` field to your resolvers. This helper function makes it easy to do so.

```
// Assuming you have created a remote schema mySchema with types Car and Boat
const typeDefs = `
interface Verhicle {
maxSpeed: Float
}
extend type Car implements Vehicle { }
extend type Boat implements Vehicle { }
extend type Query {
allVehicles: [Verhicle]
}`
const schema = mergeSchemas({
schemas: [mySchema],
resolvers: mergeInfo => ({
Query: {
allVehicles: {
async resolve(parent, args, context, info){
const newInfo = addTypeNameField(info)
const cars = mergeInfo.delegate('query', 'allCars', args, context, info)
const boats = mergeInfo.delgate('query', 'allBoats', args, context, info)
return [...cars, ...boats]
}
}
}
})
})
```

## addFields(mergeInfo: MergeInfo, fields: Array<FieldNode | string>)

A generic helper for adding fields to the resolveInfo, by passing in a fieldName, or a complete FieldNode.

```
const myField: FieldNode = { kind: 'Field', name: { kind: 'Name', value: 'myField' } }
const anotherField: 'anotherField'
addFields(mergeInfo, [myField, anotherField])
```
2 changes: 2 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { SchemaGenerator } from './src/SchemaGenerator'
export { addTypeNameField, delegateHelper } from './src/delegateHelpers'
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "graphql-gateway-tools",
"version": "0.1.0",
"scripts": {
"prepublish": "npm run build",
"build": "rm -rf dist && tsc -d",
"start": "./node_modules/.bin/ts-node ./test/server.ts"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"license": "GPL-3.0",
"author": "Kim Brandwijk <[email protected]>",
"homepage": "https://github.com/kbrandwijk/graphql-gateway-tools#readme",
"repository": {
"type": "git",
"url": "https://github.com/kbrandwijk/graphql-gateway-tools.git"
},
"bugs": {
"url": "https://github.com/kbrandwijk/graphql-gateway-tools/issues"
},
"files": [
"dist",
"package.json",
"README.md"
],
"peerDependencies": {
"graphql": "^0.11.7"
},
"dependencies": {
"apollo-link": "^1.0.0",
"apollo-link-http": "^1.1.0",
"graphql-tools": "^2.7.2",
"lodash": "^4.17.4",
"node-fetch": "^1.7.3"
},
"devDependencies": {
"@types/node": "^8.0.49",
"@types/zen-observable": "^0.5.3",
"typescript": "^2.6.1",
"ts-node": "^3.3.0"
}
}
84 changes: 84 additions & 0 deletions src/SchemaGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { createHttpLink } from 'apollo-link-http'
import { ApolloLink } from 'apollo-link'
import fetch from 'node-fetch'
import { GraphQLSchema } from 'graphql'
import { introspectSchema, makeRemoteExecutableSchema, mergeSchemas } from 'graphql-tools'
import { MergeInfo } from 'graphql-tools/dist/stitching/mergeSchemas'
import { IResolvers } from 'graphql-tools/dist/Interfaces'
import { merge } from 'lodash'

export class SchemaGenerator {
private _schemas: (GraphQLSchema | string)[] = []
private _resolvers: ((mergeInfo: MergeInfo) => IResolvers)[] = []

async registerEndpoint({uri, introspectionSchema, link, authenticationToken}: {
uri?: string,
introspectionSchema?: GraphQLSchema,
link?: ApolloLink,
authenticationToken?: (context) => string }): Promise<GraphQLSchema> {


if (link === undefined) {
const httpLink: ApolloLink = createHttpLink({ uri, fetch })

if (authenticationToken !== undefined) {
link = new ApolloLink((operation, forward) => {
operation.setContext((context) => {

if (context && authenticationToken(context)) {
console.log(authenticationToken(context))
return {
headers: {
'Authorization': `Bearer ${authenticationToken(context)}`,
}
}
}
else {
return null
}
})
return forward!(operation);
}).concat(httpLink)
}
else {
link = httpLink
}
}

if (introspectionSchema === undefined) {
introspectionSchema = await introspectSchema(link)
}

const executableSchema = makeRemoteExecutableSchema({ schema: introspectionSchema, link })
this._schemas.push(executableSchema)

return executableSchema
}

registerSchema(schema: GraphQLSchema) {
this._schemas.push(schema)
}

registerTypeDefinition(typeDefs: string) {
this._schemas.push(typeDefs)
}

registerResolver(resolverFunction: any) {
this._resolvers.push(resolverFunction)
}

generateSchema() {
const resolvers = mergeInfo => {
const resolverObject = {}
merge(resolverObject, ...this._resolvers.map(r => r(mergeInfo)))
return resolverObject
}

const finalSchema = mergeSchemas({
schemas: this._schemas,
resolvers
})

return finalSchema
}
}
54 changes: 54 additions & 0 deletions src/delegateHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { DocumentNode, GraphQLResolveInfo, OperationDefinitionNode, parse, FieldNode } from 'graphql'
import { MergeInfo } from 'graphql-tools/dist/stitching/mergeSchemas';

/*
mergeInfo.delegate only allows you to pass in an operationName and variables.
It does not allow you to define query fields, because they are inferred from the user query.
If you need to delegate a query that is unrelated to the user query, you need to provide
the fields you need. This helper does that, based on a provided query.
*/
export const delegateHelper = (mergeInfo: MergeInfo) => ({
delegateQuery: async (
query: string,
args: { [key: string]: any },
context: { [key: string]: any },
info: GraphQLResolveInfo): Promise<any> => {
const document: DocumentNode = parse(query)

const operationDefinition: OperationDefinitionNode = document.definitions[0] as OperationDefinitionNode
const operationType: "query" | "mutation" = operationDefinition.operation
const operationName:string = (operationDefinition.selectionSet.selections[0] as any).name.value
const fields: [FieldNode] = (operationDefinition.selectionSet.selections[0] as any).selectionSet.selections

const newInfo: GraphQLResolveInfo = JSON.parse(JSON.stringify(info));
newInfo.fieldNodes[0].selectionSet!.selections = fields

return await mergeInfo.delegate(
operationType, operationName, args, context, newInfo
)
}
})

export const addTypeNameField: GraphQLResolveInfo = (info: GraphQLResolveInfo) => {
const field: FieldNode =
{ kind: 'Field', name: { kind: 'Name', value: '__typename' } }

return addFields(info, [field])
}

export const addFields: GraphQLResolveInfo = (info: GraphQLResolveInfo, fields: [FieldNode | string]) => {
const newInfo = JSON.parse(JSON.stringify(info));

for (const field of fields) {
if (typeof field === 'string'){
newInfo.fieldNodes[0].selectionSet.selections.push({
kind: 'Field', name: { kind: 'Name', value: field }
})
}
else {
newInfo.fieldNodes[0].selectionSet.selections.push(field)
}
}

return newInfo
}
13 changes: 13 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"declaration": true,
"module": "commonjs",
"strictNullChecks": true,
"target": "es6",
"sourceMap": true,
"outDir": "./dist"
},
"exclude": [
"node_modules"
]
}

0 comments on commit beb14f2

Please sign in to comment.