Comparing version 6.3.1 to 7.0.0
@@ -16,3 +16,4 @@ 'use strict'; | ||
authStrategy: false, | ||
graphiAuthStrategy: false | ||
graphiAuthStrategy: false, | ||
formatError: null | ||
} | ||
@@ -126,3 +127,5 @@ }; | ||
let span; | ||
/* $lab:coverage:off$ */ | ||
if (parentSpan && request.server.tracer) { | ||
/* $lab:coverage:on$ */ | ||
span = request.server.tracer.startSpan('graphql_request', { childOf: parentSpan.context() }); | ||
@@ -167,2 +170,7 @@ span.log({ event: 'onGraphQL', payload: request.payload, info: request.info }); | ||
if (result.errors) { | ||
const formatError = request.server.plugins.graphi.settings.formatError; | ||
if (typeof formatError === 'function') { | ||
result.errors = result.errors.map(formatError); | ||
} | ||
if (span) { | ||
@@ -169,0 +177,0 @@ span.log({ event: 'error', method: 'graphql.execute', error: result.errors }); |
@@ -6,3 +6,2 @@ 'use strict'; | ||
const GraphqlTools = require('graphql-tools'); | ||
const Scalars = require('scalars'); | ||
@@ -17,3 +16,2 @@ | ||
const astSchema = Graphql.buildASTSchema(parsed, { commentDescriptions: true, assumeValidSDL: true }); | ||
internals.decorateDirectives(astSchema, parsed); | ||
@@ -107,57 +105,2 @@ for (const resolverName of Object.keys(resolvers)) { | ||
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) { | ||
@@ -164,0 +107,0 @@ return (root, args, request) => { |
{ | ||
"name": "graphi", | ||
"version": "6.3.1", | ||
"version": "7.0.0", | ||
"description": "hapi graphql plugin", | ||
@@ -31,6 +31,6 @@ "main": "lib", | ||
"code": "5.x.x", | ||
"hapi": "17.x.x", | ||
"hapi": "18.x.x", | ||
"hapi-auth-bearer-token": "6.x.x", | ||
"lab": "17.x.x", | ||
"nes": "9.0.x", | ||
"lab": "18.x.x", | ||
"nes": "10.0.x", | ||
"opentracing": "0.14.3", | ||
@@ -42,8 +42,7 @@ "traci": "1.4.0", | ||
"boom": "7.x.x", | ||
"graphql": "14.0.x", | ||
"graphql": "14.1.x", | ||
"apollo-server-module-graphiql": "1.4.x", | ||
"graphql-tools": "4.0.x", | ||
"lodash.merge": "4.6.x", | ||
"scalars": "1.x.x" | ||
"lodash.merge": "4.6.x" | ||
} | ||
} |
# graphi | ||
hapi GraphQL server plugin with Joi scalars | ||
hapi GraphQL server plugin | ||
@@ -15,2 +15,3 @@ [![Build Status](https://secure.travis-ci.org/geek/graphi.svg)](http://travis-ci.org/geek/graphi) | ||
- `graphiAuthStrategy` - (optional) Authentication strategy to apply to `/graphiql` route. Default is `false`. | ||
- `formatError` - (optional) Function that receives a [GraphQLError](https://github.com/graphql/graphql-js/blob/271e23e13ec093e7ffb844e7ffaf340ab92f053e/src/error/GraphQLError.js) as its only argument and returns a custom error object, which is returned to the client. | ||
@@ -35,3 +36,3 @@ ## API | ||
type Person { | ||
firstname: String! @JoiString(min 4) | ||
firstname: String! | ||
lastname: String! | ||
@@ -67,3 +68,3 @@ } | ||
args: { | ||
firstname: { type: new Scalars.JoiString({ min: [2, 'utf8'], max: 10 }) } | ||
firstname: { type: GraphQLString } | ||
}, | ||
@@ -157,21 +158,1 @@ resolve: (root, { firstname }, request) => { | ||
At the moment clients are required to use a nes compatible library and to subscribe to events using the `client.subscribe` function. The path that clients should use depends on the message, but in the previous example this would be `'/personCreated/peter'`. | ||
## 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) | ||
} | ||
``` | ||
@@ -11,3 +11,2 @@ 'use strict'; | ||
const { MockTracer } = require('opentracing'); | ||
const Scalars = require('scalars'); | ||
const Traci = require('traci'); | ||
@@ -20,3 +19,2 @@ const Wreck = require('wreck'); | ||
const { GraphQLObjectType, GraphQLSchema, GraphQLString } = GraphQL; | ||
const lab = exports.lab = Lab.script(); | ||
@@ -75,30 +73,2 @@ const describe = lab.describe; | ||
it('will handle graphql GET requests GraphQL instance schema', async () => { | ||
const schema = new GraphQLSchema({ | ||
query: new GraphQLObjectType({ | ||
name: 'RootQueryType', | ||
fields: { | ||
person: { | ||
type: GraphQLString, | ||
args: { | ||
firstname: { type: new Scalars.JoiString({ min: [2, 'utf8'], max: 10 }) } | ||
}, | ||
resolve: (root, { firstname }, request) => { | ||
return firstname; | ||
} | ||
} | ||
} | ||
}) | ||
}); | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi, options: { schema } }); | ||
await server.initialize(); | ||
const url = '/graphql?query=' + encodeURIComponent('{ person(firstname: "tom")}'); | ||
const res = await server.inject({ method: 'GET', url }); | ||
expect(res.statusCode).to.equal(200); | ||
expect(res.result.data.person).to.equal('tom'); | ||
}); | ||
it('will handle graphql POST requests with query', async () => { | ||
@@ -566,3 +536,3 @@ const schema = ` | ||
args: { firstname: { type: GraphQL.GraphQLString } }, | ||
resolve: (root, args) => { | ||
resolve: (rootValue, args) => { | ||
expect(args.firstname).to.equal('billy'); | ||
@@ -925,2 +895,43 @@ return 'jean'; | ||
it('allows errors to be formatted', async () => { | ||
const schema = ` | ||
type Person { | ||
firstname: String! | ||
lastname: String! | ||
} | ||
type Query { | ||
person: Person! | ||
} | ||
`; | ||
const getPerson = function (args, request) { | ||
const error = new Error('my silly error'); | ||
error.id = 'my id'; | ||
throw error; | ||
}; | ||
const resolvers = { | ||
person: getPerson | ||
}; | ||
const formatError = function (error) { | ||
expect(error.originalError.message).to.equal('my silly error'); | ||
expect(error.originalError.id).to.equal('my id'); | ||
error.originalError.custom = 'field'; | ||
return error.originalError; | ||
}; | ||
const server = Hapi.server(); | ||
await server.register({ plugin: Graphi, options: { schema, resolvers, formatError } }); | ||
await server.initialize(); | ||
const payload = { query: 'query { person { lastname } }' }; | ||
const res = await server.inject({ method: 'POST', url: '/graphql', payload }); | ||
expect(res.statusCode).to.equal(200); | ||
expect(res.result.errors[0].message).to.equal('my silly error'); | ||
expect(res.result.errors[0].id).to.equal('my id'); | ||
expect(res.result.errors[0].custom).to.equal('field'); | ||
}); | ||
it('will log result with errors property', async () => { | ||
@@ -1749,4 +1760,4 @@ const schema = ` | ||
Person: { | ||
friend: (root, args, request) => { | ||
return root; | ||
friend: (rootValue, args, request) => { | ||
return rootValue; | ||
} | ||
@@ -1753,0 +1764,0 @@ } |
@@ -80,75 +80,2 @@ 'use strict'; | ||
it('converts a graphql schema with joi scalars', async () => { | ||
const schema = ` | ||
type Person { | ||
firstname: String @JoiString(min: 2, max: 100, normalize: "NFC", uppercase: true) | ||
lastname: String | ||
email: String! | ||
} | ||
type Query { | ||
person(personname: String @JoiString(max: 5)): Person! | ||
} | ||
`; | ||
const resolvers = { | ||
Query: { | ||
person: (root, { personname }) => { | ||
return { firstname: personname, lastname: 'pluck' }; | ||
} | ||
} | ||
}; | ||
const executable = Utils.makeExecutableSchema({ schema, resolvers }); | ||
expect(executable instanceof GraphQL.GraphQLSchema).to.be.true(); | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi, options: { schema: executable } }); | ||
await server.initialize(); | ||
const payload = { query: 'query { person(personname: "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'); | ||
}); | ||
it('converts a graphql schema with joi scalars and validation errors in query', async () => { | ||
const schema = ` | ||
type Person { | ||
firstname: String @JoiString(min: 2, max: 100, trim: true) | ||
lastname: String | ||
email: String! | ||
} | ||
type Query { | ||
person(personname: String @JoiString(max: 4)): Person! | ||
} | ||
`; | ||
const resolvers = { | ||
Query: { | ||
person: (root, { personname }) => { | ||
expect(personname).to.be.error(); | ||
expect(personname.message).to.contain('length must be less than or equal to 4 characters'); | ||
return { lastname: 'pluck' }; | ||
} | ||
} | ||
}; | ||
const executable = Utils.makeExecutableSchema({ schema, resolvers }); | ||
expect(executable instanceof GraphQL.GraphQLSchema).to.be.true(); | ||
const server = Hapi.server({ debug: { request: ['error'] } }); | ||
await server.register({ plugin: Graphi, options: { schema: executable } }); | ||
await server.initialize(); | ||
const payload = { query: 'query { person(personname: "peter") { lastname } }' }; | ||
const res = await server.inject({ method: 'POST', url: '/graphql', payload }); | ||
expect(res.statusCode).to.equal(200); | ||
expect(res.result.data.person.lastname).to.equal('pluck'); | ||
}); | ||
it('only converts valid Joi directives', async () => { | ||
@@ -155,0 +82,0 @@ const schema = ` |
5
90984
1918
155
+ Addedgraphql@14.1.1(transitive)
- Removedscalars@1.x.x
- Removedgraphql@14.0.2(transitive)
- Removedhoek@5.0.4(transitive)
- Removedisemail@3.2.0(transitive)
- Removedjoi@13.6.0(transitive)
- Removedpunycode@2.3.1(transitive)
- Removedscalars@1.2.0(transitive)
- Removedtopo@3.0.3(transitive)
Updatedgraphql@14.1.x