Comparing version 5.7.0 to 6.0.0
@@ -38,3 +38,2 @@ 'use strict'; | ||
const tags = ['graphql']; | ||
server.route({ | ||
@@ -44,3 +43,2 @@ method: '*', | ||
config: { | ||
tags, | ||
auth: settings.authStrategy, | ||
@@ -56,3 +54,2 @@ handler: internals.graphqlHandler | ||
config: { | ||
tags, | ||
auth: settings.graphiAuthStrategy, | ||
@@ -73,7 +70,7 @@ handler: internals.graphiqlHandler | ||
internals.onPreStart = function (server) { | ||
const resolver = (prefix = '') => { | ||
const resolver = ({ prefix = '', method = 'graphql' }) => { | ||
return async (payload, request, ast) => { | ||
const url = `${prefix}/${ast.fieldName}`; | ||
const res = await request.server.inject({ | ||
method: 'graphql', | ||
method, | ||
url, | ||
@@ -99,8 +96,10 @@ payload, | ||
server.table().forEach((route) => { | ||
if (route.method !== 'graphql') { | ||
const tags = route.settings.tags || []; | ||
if (route.method !== 'graphql' && tags.indexOf('graphql') === -1) { | ||
return; | ||
} | ||
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); | ||
server.plugins.graphi.resolvers[path] = resolver({ prefix, method: route.method }); | ||
}); | ||
@@ -107,0 +106,0 @@ }; |
@@ -6,2 +6,3 @@ 'use strict'; | ||
const GraphqlTools = require('graphql-tools'); | ||
const Scalars = require('scalars'); | ||
@@ -16,2 +17,3 @@ | ||
const astSchema = Graphql.buildASTSchema(parsed, { commentDescriptions: true }); | ||
internals.decorateDirectives(astSchema, parsed); | ||
@@ -31,3 +33,3 @@ for (const resolverName of Object.keys(resolvers)) { | ||
if (typeof preResolve === 'function') { | ||
fieldResolver = internals.wrapResolve(preResolve, fieldResolver); | ||
fieldResolver = internals.wrapResolve.call(preResolve, fieldResolver); | ||
} | ||
@@ -56,2 +58,4 @@ | ||
} | ||
// console.log(astSchema._typeMap['Person']._fields.firstname.type.__proto__) | ||
return astSchema; | ||
@@ -68,4 +72,60 @@ }; | ||
internals.wrapResolve = function (preResolve, resolve) { | ||
internals.decorateDirectives = function (astSchema, parsed) { | ||
for (const definition of parsed.definitions) { | ||
if (definition.kind !== 'ObjectTypeDefinition') { | ||
continue; | ||
} | ||
for (const field of definition.fields) { | ||
for (const directive of field.directives) { | ||
const scalar = internals.createScalar(directive.name.value, directive.arguments); | ||
if (!scalar) { | ||
continue; | ||
} | ||
// Set the type on the schame directly (not the parsed object) | ||
astSchema._typeMap[definition.name.value]._fields[field.name.value].type = scalar; | ||
} | ||
for (const argument of field.arguments) { | ||
for (const directive of argument.directives) { | ||
const scalar = internals.createScalar(directive.name.value, directive.arguments); | ||
if (!scalar) { | ||
continue; | ||
} | ||
const foundArg = astSchema._typeMap[definition.name.value]._fields[field.name.value].args.find((arg) => { | ||
return arg.name === argument.name.value; | ||
}); | ||
foundArg.type = scalar; | ||
} | ||
} | ||
} | ||
} | ||
}; | ||
internals.createScalar = function (name, args) { | ||
const scalarFn = Scalars[name]; | ||
if (typeof scalarFn !== 'function') { | ||
return; | ||
} | ||
const formattedArgs = {}; | ||
for (const arg of args) { | ||
let value = arg.value.value; | ||
if (arg.value.kind === 'IntValue') { | ||
value = parseInt(value, 10); | ||
} else if (arg.value.kind === 'BooleanValue') { | ||
value = Boolean(value); | ||
} | ||
formattedArgs[arg.name.value] = value; | ||
} | ||
return scalarFn(formattedArgs); | ||
}; | ||
internals.wrapResolve = function (resolve) { | ||
return (root, args, request) => { | ||
const preResolve = this; | ||
const context = preResolve(root, args, request); | ||
@@ -72,0 +132,0 @@ |
{ | ||
"name": "graphi", | ||
"version": "5.7.0", | ||
"version": "6.0.0", | ||
"description": "hapi graphql plugin", | ||
@@ -33,3 +33,2 @@ "main": "lib", | ||
"lab": "15.x.x", | ||
"scalars": "0.x.x", | ||
"wreck": "14.x.x" | ||
@@ -41,5 +40,6 @@ }, | ||
"graphql-server-module-graphiql": "1.3.x", | ||
"graphql-tools": "v3.0.0-beta.0", | ||
"lodash.merge": "4.6.x" | ||
"graphql-tools": "3.0.x", | ||
"lodash.merge": "4.6.x", | ||
"scalars": "1.x.x" | ||
} | ||
} |
# graphi | ||
hapi GraphQL server plugin | ||
hapi GraphQL server plugin with Joi scalars | ||
@@ -33,3 +33,3 @@ [![Build Status](https://secure.travis-ci.org/geek/graphi.svg)](http://travis-ci.org/geek/graphi) | ||
type Person { | ||
firstname: String! | ||
firstname: String! @JoiString(min 4) | ||
lastname: String! | ||
@@ -82,3 +82,3 @@ } | ||
You can also define resolvers as hapi routes. As a result, each resolver is able to benefit from route caching, custom auth strategies, and all of the other powerful hapi routing features. Each route should use the custom method `'graphql'` and the path should be the key name for the resolver prefixed with `/`. You can also mix and match existing resolvers with routes. | ||
You can also define resolvers as hapi routes. As a result, each resolver is able to benefit from route caching, custom auth strategies, and all of the other powerful hapi routing features. Each route should either use the custom method `'graphql'` or it should add a tag named `'graphql'` and the path should be the key name for the resolver prefixed with `/`. You can also mix and match existing resolvers with routes. | ||
@@ -110,1 +110,37 @@ ```javascript | ||
``` | ||
This enables existing RESTful APIs to be easily converted over to GraphQL resolvers: | ||
```javascript | ||
server.route({ | ||
method: 'POST', | ||
path: '/person', | ||
config: { | ||
tags: ['graphql'], | ||
handler: (request, h) => { | ||
// request.payload contains any arguments sent to the query | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
} | ||
} | ||
}); | ||
``` | ||
## Joi scalar support | ||
Any schema that is expressed with JoiType directives is converted to valid scalars. As a result, using graphi you are able to create more expressive GraphQL schema definitions. For example, if you want to allow the creation of a well formed user the schema can look like the following, resulting in validated input fields before the fields are passed to any resolvers. | ||
``` | ||
type Mutation { | ||
createUser(name: String @JoiString(min 2), email: String @JoiString(email: true, max: 128)) | ||
} | ||
``` | ||
Additionally, you can also use the Joi scalars to perform extra preprosessing or postprocessing on you data. For example, the following schema will result in `firstname` being uppercased on the response. | ||
``` | ||
type Person { | ||
firstname: String @JoiString(uppercase: true) | ||
} | ||
``` | ||
@@ -266,8 +266,11 @@ 'use strict'; | ||
server.route({ | ||
method: 'graphql', | ||
method: 'POST', | ||
path: '/createPerson', | ||
handler: (request, h) => { | ||
expect(request.payload.firstname).to.equal('billy'); | ||
expect(request.payload.lastname).to.equal('jean'); | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
config: { | ||
tags: ['graphql'], | ||
handler: (request, h) => { | ||
expect(request.payload.firstname).to.equal('billy'); | ||
expect(request.payload.lastname).to.equal('jean'); | ||
return { firstname: 'billy', lastname: 'jean' }; | ||
} | ||
} | ||
@@ -1120,31 +1123,24 @@ }); | ||
describe('makeExecutableSchema()', () => { | ||
it('converts a graphql schema into executable graphql objects', () => { | ||
const schema = ` | ||
input Someone { | ||
name: String | ||
} | ||
describe('server.registerSchema()', () => { | ||
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' }; | ||
}; | ||
interface IPerson { | ||
firstname: String | ||
} | ||
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' }; | ||
}; | ||
type Person implements IPerson { | ||
const schema1 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
lastname: String! | ||
email: String! | ||
description: People | ||
ability: Ability | ||
search: SearchResult | ||
} | ||
scalar People | ||
enum Ability { | ||
COOK | ||
PROGRAM | ||
} | ||
union SearchResult = Person | String | ||
type Query { | ||
@@ -1155,32 +1151,5 @@ person(firstname: String!): Person! | ||
const resolvers = { | ||
Query: { | ||
person: () => {} | ||
}, | ||
People: { | ||
description: () => {} | ||
}, | ||
Ability: { | ||
COOK: () => {} | ||
}, | ||
Person: { | ||
ability: () => {}, | ||
description: () => {}, | ||
search: () => {} | ||
}, | ||
IPerson: { | ||
firstname: () => {} | ||
}, | ||
Someone: { | ||
name: () => {} | ||
} | ||
}; | ||
const executable = Graphi.makeExecutableSchema({ schema, resolvers }); | ||
expect(executable instanceof Graphi.graphql.GraphQLSchema).to.be.true(); | ||
}); | ||
it('converts a graphql schema and executes preResolve first', async () => { | ||
const schema = ` | ||
const schema2 = ` | ||
type Person { | ||
id: ID! | ||
firstname: String! | ||
@@ -1190,70 +1159,37 @@ lastname: String! | ||
type Mutation { | ||
createPerson(firstname: String!, lastname: String!): Person! | ||
} | ||
type Query { | ||
person(firstname: String!): Person! | ||
people: [Person] | ||
} | ||
`; | ||
const resolvers = { | ||
Query: { | ||
person: () => { | ||
return { firstname: 'peter', lastname: 'pluck' }; | ||
} | ||
}, | ||
Person: { | ||
firstname: function (root, args, request) { | ||
expect(this.fu).to.equal('bar'); | ||
return root.firstname.toUpperCase(); | ||
}, | ||
lastname: function (root, args, request) { | ||
expect(this.fu).to.equal('bar'); | ||
return root.lastname.toUpperCase(); | ||
} | ||
} | ||
const resolvers1 = { | ||
person: getPerson, | ||
people: () => {} | ||
}; | ||
const preResolve = () => { | ||
return { fu: 'bar' }; | ||
const resolvers2 = { | ||
createPerson | ||
}; | ||
const executable = Graphi.makeExecutableSchema({ schema, resolvers, preResolve }); | ||
expect(executable instanceof Graphi.graphql.GraphQLSchema).to.be.true(); | ||
const server = Hapi.server(); | ||
await server.register({ plugin: Graphi, options: { schema: executable } }); | ||
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 payload = { query: 'query { person(firstname: "peter") { firstname lastname } }' }; | ||
const res = await server.inject({ method: 'POST', url: '/graphql', payload }); | ||
expect(res.statusCode).to.equal(200); | ||
expect(res.result.data.person.firstname).to.equal('PETER'); | ||
expect(res.result.data.person.lastname).to.equal('PLUCK'); | ||
}); | ||
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'); | ||
it('errors when resolver missing from schema', () => { | ||
const schema = ` | ||
type Person { | ||
firstname: String! | ||
lastname: String! | ||
email: String! | ||
} | ||
type Query { | ||
person(firstname: String!): Person! | ||
} | ||
`; | ||
let err; | ||
try { | ||
Graphi.makeExecutableSchema({ schema, resolvers: { Query: { human: () => {} } } }); | ||
} catch (ex) { | ||
err = ex; | ||
} | ||
expect(err).to.be.error(); | ||
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'); | ||
}); | ||
}); | ||
describe('server.registerGraph()', () => { | ||
it('will merge multiple schemas together', async () => { | ||
it('will merge new query resolvers', async () => { | ||
const getPerson = function (args, request) { | ||
@@ -1265,7 +1201,4 @@ expect(args.firstname).to.equal('billy'); | ||
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 getPeople = function (args, request) { | ||
return [{ firstname: 'billy', lastname: 'jean' }]; | ||
}; | ||
@@ -1292,6 +1225,2 @@ | ||
type Mutation { | ||
createPerson(firstname: String!, lastname: String!): Person! | ||
} | ||
type Query { | ||
@@ -1303,8 +1232,7 @@ people: [Person] | ||
const resolvers1 = { | ||
person: getPerson, | ||
people: () => {} | ||
person: getPerson | ||
}; | ||
const resolvers2 = { | ||
createPerson | ||
people: getPeople | ||
}; | ||
@@ -1317,6 +1245,6 @@ | ||
const payload1 = { query: 'mutation { createPerson(firstname: "billy", lastname: "jean") { lastname } }' }; | ||
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.createPerson.lastname).to.equal('jean'); | ||
expect(res1.result.data.people[0].lastname).to.equal('jean'); | ||
@@ -1329,3 +1257,3 @@ const payload2 = { query: 'query { person(firstname: "billy") { lastname } }' }; | ||
it('will merge new query resolvers', async () => { | ||
it('will merge new query resolvers when neither was registered with the plugin', async () => { | ||
const getPerson = function (args, request) { | ||
@@ -1374,3 +1302,4 @@ expect(args.firstname).to.equal('billy'); | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi, options: { schema: schema1, resolvers: resolvers1 } }); | ||
await server.register({ plugin: Graphi }); | ||
server.registerSchema({ schema: schema1, resolvers: resolvers1 }); | ||
server.registerSchema({ schema: schema2, resolvers: resolvers2 }); | ||
@@ -1390,3 +1319,3 @@ await server.initialize(); | ||
it('will merge new query resolvers when neither was registered with the plugin', async () => { | ||
it('will merge new query resolvers with formatters', async () => { | ||
const getPerson = function (args, request) { | ||
@@ -1407,2 +1336,3 @@ expect(args.firstname).to.equal('billy'); | ||
lastname: String! | ||
friend: Person | ||
} | ||
@@ -1420,2 +1350,3 @@ | ||
lastname: String! | ||
friend: Person | ||
} | ||
@@ -1433,3 +1364,8 @@ | ||
const resolvers2 = { | ||
people: getPeople | ||
people: getPeople, | ||
Person: { | ||
friend: (root, args, request) => { | ||
return root; | ||
} | ||
} | ||
}; | ||
@@ -1448,3 +1384,3 @@ | ||
const payload2 = { query: 'query { person(firstname: "billy") { lastname } }' }; | ||
const payload2 = { query: 'query { person(firstname: "billy") { lastname friend { firstname } } }' }; | ||
const res2 = await server.inject({ method: 'POST', url: '/graphql', payload: payload2 }); | ||
@@ -1451,0 +1387,0 @@ expect(res2.statusCode).to.equal(200); |
79570
6
9
1628
144
6
+ Addedscalars@1.x.x
+ Added@types/graphql@0.12.6(transitive)
+ Added@wry/equality@0.1.11(transitive)
+ Addedapollo-link@1.2.2(transitive)
+ Addedapollo-utilities@1.3.4(transitive)
+ Addeddeprecated-decorator@0.1.6(transitive)
+ Addedfast-json-stable-stringify@2.1.0(transitive)
+ Addedgraphql@14.0.2(transitive)
+ Addedgraphql-tools@3.0.5(transitive)
+ Addedhoek@5.0.4(transitive)
+ Addedisemail@3.2.0(transitive)
+ Addedjoi@13.6.0(transitive)
+ Addedpunycode@2.3.1(transitive)
+ Addedscalars@1.2.0(transitive)
+ Addedtopo@3.0.3(transitive)
+ Addedts-invariant@0.4.4(transitive)
+ Addedtslib@1.14.1(transitive)
+ Addeduuid@3.4.0(transitive)
+ Addedzen-observable@0.8.15(transitive)
+ Addedzen-observable-ts@0.8.21(transitive)
Updatedgraphql-tools@3.0.x