Comparing version 5.6.0 to 5.7.0
138
lib/index.js
'use strict'; | ||
const Assert = require('assert'); | ||
const Boom = require('boom'); | ||
@@ -8,2 +7,3 @@ const Graphql = require('graphql'); | ||
const Merge = require('lodash.merge'); | ||
const Utils = require('./utils'); | ||
const Package = require('../package.json'); | ||
@@ -23,9 +23,11 @@ | ||
exports.register = function (server, options) { | ||
const settings = Object.assign({}, internals.defaults, options); | ||
const settings = Merge({}, internals.defaults, options); | ||
let schema = options.schema; | ||
const resolvers = Merge({}, options.resolvers); | ||
server.expose('resolvers', {}); | ||
server.expose('settings', settings); | ||
server.decorate('server', 'makeExecutableSchema', Utils.makeExecutableSchema); | ||
server.decorate('server', 'registerSchema', internals.registerSchema); | ||
if (schema && typeof schema === 'string') { | ||
schema = exports.makeExecutableSchema({ schema, resolvers }); | ||
if (settings.schema) { | ||
server.registerSchema({ schema: settings.schema, resolvers: settings.resolvers }); | ||
} | ||
@@ -35,45 +37,7 @@ | ||
type: 'onPreStart', | ||
method: () => { | ||
const resolver = (prefix = '') => { | ||
return async (payload, request, ast) => { | ||
const url = `${prefix}/${ast.fieldName}`; | ||
const res = await request.server.inject({ | ||
method: 'graphql', | ||
url, | ||
payload, | ||
headers: request.headers | ||
}); | ||
if (res.statusCode < 400) { | ||
return res.result; | ||
} | ||
return new Boom(res.result.message, { | ||
statusCode: res.statusCode, | ||
data: { | ||
error: res.result.error, | ||
url | ||
} | ||
}); | ||
}; | ||
}; | ||
server.table().forEach((route) => { | ||
if (route.method !== 'graphql') { | ||
return; | ||
} | ||
const prefix = route.realm.modifiers.route.prefix; | ||
const path = prefix ? route.path.substr(prefix.length + 1) : route.path.substr(1); | ||
resolvers[path] = resolver(prefix); | ||
}); | ||
server.expose('resolvers', resolvers); | ||
} | ||
method: internals.onPreStart | ||
}); | ||
server.expose('schema', schema); | ||
server.expose('settings', settings); | ||
const tags = ['graphql']; | ||
const route = { | ||
server.route({ | ||
method: '*', | ||
@@ -86,6 +50,4 @@ path: settings.graphqlPath, | ||
} | ||
}; | ||
}); | ||
server.route(route); | ||
if (settings.graphiqlPath) { | ||
@@ -108,56 +70,50 @@ server.route({ | ||
// Inspired by graphql-tools | ||
exports.makeExecutableSchema = ({ schema, resolvers = {}, preResolve }) => { | ||
const parsed = Graphql.parse(schema); | ||
const astSchema = Graphql.buildASTSchema(parsed, { commentDescriptions: true }); | ||
exports.makeExecutableSchema = Utils.makeExecutableSchema; | ||
for (const resolverName of Object.keys(resolvers)) { | ||
const type = astSchema.getType(resolverName); | ||
if (!type) { | ||
continue; | ||
} | ||
const typeResolver = resolvers[resolverName]; | ||
internals.onPreStart = function (server) { | ||
const resolver = (prefix = '') => { | ||
return async (payload, request, ast) => { | ||
const url = `${prefix}/${ast.fieldName}`; | ||
const res = await request.server.inject({ | ||
method: 'graphql', | ||
url, | ||
payload, | ||
headers: request.headers | ||
}); | ||
// go through field resolvers for the parent resolver type | ||
for (const fieldName of Object.keys(typeResolver)) { | ||
let fieldResolver = typeResolver[fieldName]; | ||
Assert(typeof fieldResolver === 'function', `${resolverName}.${fieldName} resolver must be a function`); | ||
if (typeof preResolve === 'function') { | ||
fieldResolver = internals.wrapResolve(preResolve, fieldResolver); | ||
if (res.statusCode < 400) { | ||
return res.result; | ||
} | ||
if (type instanceof Graphql.GraphQLScalarType) { | ||
type[fieldName] = fieldResolver; | ||
continue; | ||
} | ||
return new Boom(res.result.message, { | ||
statusCode: res.statusCode, | ||
data: { | ||
error: res.result.error, | ||
url | ||
} | ||
}); | ||
}; | ||
}; | ||
if (type instanceof Graphql.GraphQLEnumType) { | ||
const fieldType = type.getValue(fieldName); | ||
Assert(fieldType, `${resolverName}.${fieldName} enum definition missing from schema`); | ||
fieldType.value = fieldResolver; | ||
continue; | ||
} | ||
// no need to set resolvers unless we are dealing with a type that needs resolvers | ||
if (!(type instanceof Graphql.GraphQLObjectType) && !(type instanceof Graphql.GraphQLInterfaceType)) { | ||
continue; | ||
} | ||
const fields = type.getFields(); | ||
fields[fieldName].resolve = fieldResolver; | ||
server.table().forEach((route) => { | ||
if (route.method !== 'graphql') { | ||
return; | ||
} | ||
} | ||
return astSchema; | ||
const prefix = route.realm.modifiers.route.prefix; | ||
const path = prefix ? route.path.substr(prefix.length + 1) : route.path.substr(1); | ||
server.plugins.graphi.resolvers[path] = resolver(prefix); | ||
}); | ||
}; | ||
internals.wrapResolve = function (preResolve, resolve) { | ||
return (root, args, request) => { | ||
const context = preResolve(root, args, request); | ||
internals.registerSchema = function ({ schema = {}, resolvers = {} }) { | ||
const server = this; | ||
if (typeof schema === 'string') { | ||
schema = Utils.makeExecutableSchema({ schema, resolvers }); | ||
} | ||
return resolve.call(context, root, args, request); | ||
}; | ||
server.plugins.graphi.resolvers = Merge(server.plugins.graphi.resolvers, resolvers); | ||
server.plugins.graphi.schema = Utils.mergeSchemas(server.plugins.graphi.schema, schema); | ||
}; | ||
internals.graphqlHandler = async function (request, h) { | ||
@@ -164,0 +120,0 @@ if (request.method.toUpperCase() === 'OPTIONS') { |
{ | ||
"name": "graphi", | ||
"version": "5.6.0", | ||
"version": "5.7.0", | ||
"description": "hapi graphql plugin", | ||
@@ -40,4 +40,5 @@ "main": "lib", | ||
"graphql-server-module-graphiql": "1.3.x", | ||
"graphql-tools": "v3.0.0-beta.0", | ||
"lodash.merge": "4.6.x" | ||
} | ||
} |
@@ -18,2 +18,9 @@ # graphi | ||
The following decorations are made to the hapi server to make it easier to use a single graphi plugin with multiple other plugins depending on it. | ||
- `server.registerSchema({ schema, resolvers })` - similar to the original registration options for the plugin, but this will merge the schema with any prior schema that is already registered with the server. This is useful for combining multiple graphql schemas/resolvers together into a single server. | ||
- `server.makeExecutableSchema({ schema, resolvers, preResolve })` - combine resolvers with the schema definition into a `GraphQLSchema`. | ||
The follow properties are exported directly when you `require('graphi')` | ||
- `graphql` - exported Graphql module that graphi uses | ||
@@ -20,0 +27,0 @@ - `makeExecutableSchema({ schema, resolvers, preResolve })` - combine resolvers with the schema definition into a `GraphQLSchema`. |
@@ -1229,31 +1229,113 @@ 'use strict'; | ||
it('converts a graphql schema with custom joi directives', () => { | ||
it('errors when resolver missing from schema', () => { | ||
const schema = ` | ||
input Someone { | ||
name: String @JoiString(min: 1) | ||
type Person { | ||
firstname: String! | ||
lastname: String! | ||
email: String! | ||
} | ||
interface IPerson { | ||
firstname: String | ||
type Query { | ||
person(firstname: String!): Person! | ||
} | ||
`; | ||
type Person implements IPerson { | ||
firstname: String! @JoiString(min: 1) | ||
let err; | ||
try { | ||
Graphi.makeExecutableSchema({ schema, resolvers: { Query: { human: () => {} } } }); | ||
} catch (ex) { | ||
err = ex; | ||
} | ||
expect(err).to.be.error(); | ||
}); | ||
}); | ||
describe('server.registerGraph()', () => { | ||
it('will merge multiple schemas together', async () => { | ||
const getPerson = function (args, request) { | ||
expect(args.firstname).to.equal('billy'); | ||
expect(request.path).to.equal('/graphql'); | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
}; | ||
const createPerson = function (args, request) { | ||
expect(args.firstname).to.equal('billy'); | ||
expect(args.lastname).to.equal('jean'); | ||
expect(request.path).to.equal('/graphql'); | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
}; | ||
const schema1 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
email: String! | ||
description: People | ||
ability: Ability | ||
search: SearchResult | ||
} | ||
scalar People | ||
type Query { | ||
person(firstname: String!): Person! | ||
} | ||
`; | ||
enum Ability { | ||
COOK | ||
PROGRAM | ||
const schema2 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
} | ||
union SearchResult = Person | String | ||
type Mutation { | ||
createPerson(firstname: String!, lastname: String!): Person! | ||
} | ||
type Query { | ||
people: [Person] | ||
} | ||
`; | ||
const resolvers1 = { | ||
person: getPerson, | ||
people: () => {} | ||
}; | ||
const resolvers2 = { | ||
createPerson | ||
}; | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi, options: { schema: schema1, resolvers: resolvers1 } }); | ||
server.registerSchema({ schema: schema2, resolvers: resolvers2 }); | ||
await server.initialize(); | ||
const payload1 = { query: 'mutation { createPerson(firstname: "billy", lastname: "jean") { lastname } }' }; | ||
const res1 = await server.inject({ method: 'POST', url: '/graphql', payload: payload1 }); | ||
expect(res1.statusCode).to.equal(200); | ||
expect(res1.result.data.createPerson.lastname).to.equal('jean'); | ||
const payload2 = { query: 'query { person(firstname: "billy") { lastname } }' }; | ||
const res2 = await server.inject({ method: 'POST', url: '/graphql', payload: payload2 }); | ||
expect(res2.statusCode).to.equal(200); | ||
expect(res2.result.data.person.lastname).to.equal('jean'); | ||
}); | ||
it('will merge new query resolvers', async () => { | ||
const getPerson = function (args, request) { | ||
expect(args.firstname).to.equal('billy'); | ||
expect(request.path).to.equal('/graphql'); | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
}; | ||
const getPeople = function (args, request) { | ||
return [{ firstname: 'billy', lastname: 'jean' }]; | ||
}; | ||
const schema1 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
} | ||
type Query { | ||
person(firstname: String!): Person! | ||
@@ -1263,35 +1345,54 @@ } | ||
const resolvers = { | ||
Query: { | ||
person: () => {} | ||
}, | ||
People: { | ||
description: () => {} | ||
}, | ||
Ability: { | ||
COOK: () => {} | ||
}, | ||
Person: { | ||
ability: () => {}, | ||
description: () => {}, | ||
search: () => {} | ||
}, | ||
IPerson: { | ||
firstname: () => {} | ||
}, | ||
Someone: { | ||
name: () => {} | ||
const schema2 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
} | ||
type Query { | ||
people: [Person] | ||
} | ||
`; | ||
const resolvers1 = { | ||
person: getPerson | ||
}; | ||
const executable = Graphi.makeExecutableSchema({ schema, resolvers }); | ||
expect(executable instanceof Graphi.graphql.GraphQLSchema).to.be.true(); | ||
const resolvers2 = { | ||
people: getPeople | ||
}; | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi, options: { schema: schema1, resolvers: resolvers1 } }); | ||
server.registerSchema({ schema: schema2, resolvers: resolvers2 }); | ||
await server.initialize(); | ||
const payload1 = { query: 'query { people { lastname } }' }; | ||
const res1 = await server.inject({ method: 'POST', url: '/graphql', payload: payload1 }); | ||
expect(res1.statusCode).to.equal(200); | ||
expect(res1.result.data.people[0].lastname).to.equal('jean'); | ||
const payload2 = { query: 'query { person(firstname: "billy") { lastname } }' }; | ||
const res2 = await server.inject({ method: 'POST', url: '/graphql', payload: payload2 }); | ||
expect(res2.statusCode).to.equal(200); | ||
expect(res2.result.data.person.lastname).to.equal('jean'); | ||
}); | ||
it('errors when resolver missing from schema', () => { | ||
const schema = ` | ||
it('will merge new query resolvers when neither was registered with the plugin', async () => { | ||
const getPerson = function (args, request) { | ||
expect(args.firstname).to.equal('billy'); | ||
expect(request.path).to.equal('/graphql'); | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
}; | ||
const getPeople = function (args, request) { | ||
return [{ firstname: 'billy', lastname: 'jean' }]; | ||
}; | ||
const schema1 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
email: String! | ||
} | ||
@@ -1304,14 +1405,40 @@ | ||
let err; | ||
try { | ||
Graphi.makeExecutableSchema({ schema, resolvers: { Query: { human: () => {} } } }); | ||
} catch (ex) { | ||
err = ex; | ||
} | ||
const schema2 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
} | ||
expect(err).to.be.error(); | ||
type Query { | ||
people: [Person] | ||
} | ||
`; | ||
const resolvers1 = { | ||
person: getPerson | ||
}; | ||
const resolvers2 = { | ||
people: getPeople | ||
}; | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi }); | ||
server.registerSchema({ schema: schema1, resolvers: resolvers1 }); | ||
server.registerSchema({ schema: schema2, resolvers: resolvers2 }); | ||
await server.initialize(); | ||
const payload1 = { query: 'query { people { lastname } }' }; | ||
const res1 = await server.inject({ method: 'POST', url: '/graphql', payload: payload1 }); | ||
expect(res1.statusCode).to.equal(200); | ||
expect(res1.result.data.people[0].lastname).to.equal('jean'); | ||
const payload2 = { query: 'query { person(firstname: "billy") { lastname } }' }; | ||
const res2 = await server.inject({ method: 'POST', url: '/graphql', payload: payload2 }); | ||
expect(res2.statusCode).to.equal(200); | ||
expect(res2.result.data.person.lastname).to.equal('jean'); | ||
}); | ||
}); | ||
// auth token strategy plugin | ||
@@ -1318,0 +1445,0 @@ |
70639
8
1421
108
5
+ Addedgraphql-tools@v3.0.0-beta.0