├── .editorconfig ├── .gitattributes ├── .gitignore ├── .npmignore ├── package.json ├── readme.md └── src ├── index.js └── renderGraphiQL.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [package.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | coverage 5 | lib 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | src 3 | test 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-graphql", 3 | "version": "1.0.1", 4 | "description": "Create a GraphQL HTTP server with Hapi.", 5 | "author": "Simon Degraeve ", 6 | "main": "./lib/index.js", 7 | "license": "MIT", 8 | "bugs": { 9 | "url": "https://github.com/SimonDegraeve/hapi-graphql/issues" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "http://github.com/SimonDegraeve/hapi-graphql.git" 14 | }, 15 | "keywords": [ 16 | "hapi", 17 | "graphql" 18 | ], 19 | "scripts": { 20 | "test": "eslint src", 21 | "prepublish": "rm -rf lib/* && babel src --out-dir lib" 22 | }, 23 | "devDependencies": { 24 | "babel-cli": "^6.10.1", 25 | "babel-eslint": "^6.1.0", 26 | "babel-plugin-add-module-exports": "^0.2.1", 27 | "babel-plugin-transform-runtime": "^6.9.0", 28 | "babel-preset-es2015": "^6.9.0", 29 | "babel-preset-stage-2": "^6.11.0", 30 | "babel-runtime": "^6.9.2", 31 | "eslint": "^3.0.1", 32 | "eslint-config-airbnb-base": "^4.0.0", 33 | "eslint-plugin-import": "^1.10.2", 34 | "graphql": "^0.6.0", 35 | "hapi": "^13.5.0" 36 | }, 37 | "dependencies": { 38 | "accepts": "^1.3.3", 39 | "babel-runtime": "^6.9.2", 40 | "boom": "^3.2.2", 41 | "joi": "^8.4.2" 42 | }, 43 | "peerDependencies": { 44 | "babel-runtime": "^6.9.2", 45 | "graphql": "^0.6.0" 46 | }, 47 | "eslintConfig": { 48 | "parser": "babel-eslint", 49 | "extends": [ 50 | "airbnb-base" 51 | ] 52 | }, 53 | "babel": { 54 | "presets": [ 55 | "es2015", 56 | "stage-2" 57 | ], 58 | "plugins": [ 59 | "add-module-exports", 60 | "transform-runtime" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # GraphQL Hapi Plugin 2 | 3 | Create a GraphQL HTTP server with [Hapi](http://hapijs.com). 4 | Port from [express-graphql](https://github.com/graphql/express-graphql). 5 | 6 | ```js 7 | npm install --save hapi-graphql 8 | ``` 9 | 10 | ### Example 11 | 12 | ```js 13 | import Hapi from 'hapi'; 14 | import GraphQL from 'hapi-graphql'; 15 | import {GraphQLSchema} from 'graphql'; 16 | 17 | const server = new Hapi.Server(); 18 | server.connection({ 19 | port: 3000 20 | }); 21 | 22 | const TestSchema = new GraphQLSchema({}); 23 | 24 | server.register({ 25 | register: GraphQL, 26 | options: { 27 | query: { 28 | # options, see below 29 | }, 30 | // OR 31 | // 32 | // query: (request) => ({ 33 | // # options, see below 34 | // }), 35 | route: { 36 | path: '/graphql', 37 | config: {} 38 | } 39 | } 40 | }, () => 41 | server.start(() => 42 | console.log('Server running at:', server.info.uri) 43 | ) 44 | ); 45 | ``` 46 | 47 | ### Options 48 | 49 | The `options` key of `query` accepts the following: 50 | 51 | * **`schema`**: A `GraphQLSchema` instance from [`graphql-js`][]. 52 | A `schema` *must* be provided. 53 | 54 | * **`context`**: A value to pass as the `context` to the `graphql()` 55 | function from [`graphql-js`][]. 56 | 57 | * **`rootValue`**: A value to pass as the `rootValue` to the `graphql()` 58 | function from [`graphql-js`][]. 59 | 60 | * **`pretty`**: If `true`, any JSON response will be pretty-printed. 61 | 62 | * **`formatError`**: An optional function which will be used to format any 63 | errors produced by fulfilling a GraphQL operation. If no function is 64 | provided, GraphQL's default spec-compliant [`formatError`][] function will 65 | be used. 66 | 67 | * **`validationRules`**: Optional additional validation rules queries must 68 | satisfy in addition to those defined by the GraphQL spec. 69 | 70 | * **`graphiql`**: If `true`, may present [GraphiQL][] when loaded directly 71 | from a browser (a useful tool for debugging and exploration). 72 | 73 | #### Debugging 74 | 75 | During development, it's useful to get more information from errors, such as 76 | stack traces. Providing a function to `formatError` enables this: 77 | 78 | ```js 79 | formatError: error => ({ 80 | message: error.message, 81 | locations: error.locations, 82 | stack: error.stack 83 | }) 84 | ``` 85 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Import dependencies 3 | */ 4 | import Joi from 'joi'; 5 | import Boom from 'boom'; 6 | import { Stream } from 'stream'; 7 | import { 8 | Source, 9 | parse, 10 | validate, 11 | execute, 12 | formatError, 13 | getOperationAST, 14 | specifiedRules, 15 | } from 'graphql'; 16 | import { version } from '../package.json'; 17 | import renderGraphiQL from './renderGraphiQL'; 18 | import accepts from 'accepts'; 19 | 20 | 21 | /** 22 | * Define constants 23 | */ 24 | const optionsSchema = { 25 | query: [ 26 | Joi.func(), 27 | Joi.object({ 28 | schema: Joi.object().required(), 29 | context: Joi.object(), 30 | rootValue: Joi.object(), 31 | pretty: Joi.boolean(), 32 | graphiql: Joi.boolean(), 33 | formatError: Joi.func(), 34 | validationRules: Joi.array(), 35 | }).required(), 36 | ], 37 | route: Joi.object().keys({ 38 | path: Joi.string().required(), 39 | config: Joi.object(), 40 | }).required(), 41 | }; 42 | 43 | 44 | /** 45 | * Define helper: get options from object/function 46 | */ 47 | const getOptions = async (options, request) => { 48 | // Get options 49 | const optionsData = await Promise 50 | .resolve(typeof options === 'function' ? options(request) : options); 51 | 52 | // Validate options 53 | const validation = Joi.validate(optionsData, optionsSchema.query); 54 | if (validation.error) { 55 | throw validation.error; 56 | } 57 | return validation.value; 58 | }; 59 | 60 | 61 | /** 62 | * Define helper: parse payload 63 | */ 64 | const parsePayload = async (request) => { 65 | // Read stream 66 | const result = await new Promise((resolve) => { 67 | if (request.payload instanceof Stream) { 68 | let data = ''; 69 | request.payload.on('data', (chunk) => { 70 | data += chunk; 71 | }); 72 | request.payload.on('end', () => resolve(data)); 73 | } else { 74 | resolve('{}'); 75 | } 76 | }); 77 | 78 | // Return normalized payload 79 | let formattedResult = null; 80 | if (request.mime === 'application/graphql') { 81 | formattedResult = { query: result }; 82 | } else { 83 | formattedResult = JSON.parse(result); 84 | } 85 | return formattedResult; 86 | }; 87 | 88 | 89 | /** 90 | * Define helper: get GraphQL parameters from query/payload 91 | */ 92 | const getGraphQLParams = (request, payload = {}) => { 93 | // GraphQL Query string. 94 | const query = request.query.query || payload.query; 95 | 96 | // Parse the variables if needed. 97 | let variables = request.query.variables || payload.variables; 98 | if (variables && typeof variables === 'string') { 99 | try { 100 | variables = JSON.parse(variables); 101 | } catch (error) { 102 | throw Boom.badRequest('Variables are invalid JSON.'); 103 | } 104 | } 105 | 106 | // Name of GraphQL operation to execute. 107 | const operationName = request.query.operationName || payload.operationName; 108 | 109 | // Return params 110 | return { query, variables, operationName }; 111 | }; 112 | 113 | 114 | /** 115 | * Define helper: determine if GraphiQL can be displayed. 116 | */ 117 | const canDisplayGraphiQL = (request, data) => { 118 | // If `raw` exists, GraphiQL mode is not enabled. 119 | const raw = ((request.query.raw !== undefined) || (data.raw !== undefined)); 120 | 121 | // Allowed to show GraphiQL if not requested as raw and this request 122 | // prefers HTML over JSON. 123 | const accept = accepts(request.raw.req); 124 | return !raw && accept.type(['json', 'html']) === 'html'; 125 | }; 126 | 127 | 128 | /** 129 | * Define helper: execute query and create result 130 | */ 131 | const createResult = async ({ 132 | context, 133 | operationName, 134 | query, 135 | request, 136 | rootValue, 137 | schema, 138 | showGraphiQL, 139 | validationRules, 140 | variables, 141 | }) => { 142 | // If there is no query, but GraphiQL will be displayed, do not produce 143 | // a result, otherwise return a 400: Bad Request. 144 | if (!query) { 145 | if (showGraphiQL) { 146 | return null; 147 | } 148 | throw Boom.badRequest('Must provide query string.'); 149 | } 150 | 151 | // GraphQL source. 152 | const source = new Source(query, 'GraphQL request'); 153 | 154 | // Parse source to AST, reporting any syntax error. 155 | let documentAST; 156 | try { 157 | documentAST = parse(source); 158 | } catch (syntaxError) { 159 | // Return 400: Bad Request if any syntax errors errors exist. 160 | throw Boom.badRequest('Syntax error', [syntaxError]); 161 | } 162 | 163 | // Validate AST, reporting any errors. 164 | const validationErrors = validate(schema, documentAST, validationRules); 165 | if (validationErrors.length > 0) { 166 | // Return 400: Bad Request if any validation errors exist. 167 | throw Boom.badRequest('Validation error', validationErrors); 168 | } 169 | 170 | // Only query operations are allowed on GET requests. 171 | if (request.method === 'get') { 172 | // Determine if this GET request will perform a non-query. 173 | const operationAST = getOperationAST(documentAST, operationName); 174 | if (operationAST && operationAST.operation !== 'query') { 175 | // If GraphiQL can be shown, do not perform this query, but 176 | // provide it to GraphiQL so that the requester may perform it 177 | // themselves if desired. 178 | if (showGraphiQL) { 179 | return null; 180 | } 181 | 182 | // Otherwise, report a 405: Method Not Allowed error. 183 | throw Boom.methodNotAllowed( 184 | `Can only perform a ${operationAST.operation} operation from a POST request.` 185 | ); 186 | } 187 | } 188 | 189 | // Perform the execution, reporting any errors creating the context. 190 | try { 191 | return await execute( 192 | schema, 193 | documentAST, 194 | rootValue, 195 | context, 196 | variables, 197 | operationName 198 | ); 199 | } catch (contextError) { 200 | // Return 400: Bad Request if any execution context errors exist. 201 | throw Boom.badRequest('Context error', [contextError]); 202 | } 203 | }; 204 | 205 | 206 | /** 207 | * Define handler 208 | */ 209 | const handler = (route, options = {}) => async (request, reply) => { 210 | let errorFormatter = formatError; 211 | 212 | try { 213 | // Get GraphQL options given this request. 214 | const { 215 | schema, 216 | context, 217 | rootValue, 218 | pretty, 219 | graphiql, 220 | formatError: customFormatError, 221 | validationRules: additionalValidationRules, 222 | } = await getOptions(options, request); 223 | 224 | let validationRules = specifiedRules; 225 | if (additionalValidationRules) { 226 | validationRules = validationRules.concat(additionalValidationRules); 227 | } 228 | 229 | if (customFormatError) { 230 | errorFormatter = customFormatError; 231 | } 232 | 233 | // GraphQL HTTP only supports GET and POST methods. 234 | if ((request.method !== 'get') && (request.method !== 'post')) { 235 | throw Boom.methodNotAllowed('GraphQL only supports GET and POST requests.'); 236 | } 237 | 238 | // Parse payload 239 | const payload = await parsePayload(request); 240 | 241 | // Can we show graphiQL? 242 | const showGraphiQL = graphiql && canDisplayGraphiQL(request, payload); 243 | 244 | // Get GraphQL params from the request and POST body data. 245 | const { query, variables, operationName } = getGraphQLParams(request, payload); 246 | 247 | // Create the result 248 | const result = await createResult({ 249 | context, 250 | operationName, 251 | query, 252 | request, 253 | rootValue, 254 | schema, 255 | showGraphiQL, 256 | validationRules, 257 | variables, 258 | }); 259 | 260 | // Format any encountered errors. 261 | if (result && result.errors) { 262 | result.errors = result.errors.map(errorFormatter); 263 | } 264 | 265 | // If allowed to show GraphiQL, present it instead of JSON. 266 | if (showGraphiQL) { 267 | reply(renderGraphiQL({ query, variables, operationName, result })).type('text/html'); 268 | } else { 269 | // Otherwise, present JSON directly. 270 | reply(JSON.stringify(result, null, pretty ? 2 : 0)).type('application/json'); 271 | } 272 | } catch (error) { 273 | // Return error, picking up Boom overrides 274 | const { statusCode = 500 } = error.output; 275 | const errors = error.data || [error]; 276 | reply({ errors: errors.map(errorFormatter) }).code(statusCode); 277 | } 278 | }; 279 | 280 | 281 | /** 282 | * Define handler defaults 283 | */ 284 | handler.defaults = (method) => { 285 | if (method === 'post') { 286 | return { 287 | payload: { 288 | output: 'stream', 289 | }, 290 | }; 291 | } 292 | return {}; 293 | }; 294 | 295 | 296 | /** 297 | * Define plugin 298 | */ 299 | function register(server, options = {}, next) { 300 | // Validate options 301 | const validation = Joi.validate(options, optionsSchema); 302 | if (validation.error) { 303 | throw validation.error; 304 | } 305 | const { route, query } = validation.value; 306 | 307 | // Register handler 308 | server.handler('graphql', handler); 309 | 310 | // Register route 311 | server.route({ 312 | method: ['get', 'post'], 313 | path: route.path, 314 | config: route.config, 315 | handler: { 316 | graphql: query, 317 | }, 318 | }); 319 | 320 | // Done 321 | return next(); 322 | } 323 | 324 | 325 | /** 326 | * Define plugin attributes 327 | */ 328 | register.attributes = { name: 'graphql', version }; 329 | 330 | 331 | /** 332 | * Export plugin 333 | */ 334 | export default register; 335 | -------------------------------------------------------------------------------- /src/renderGraphiQL.js: -------------------------------------------------------------------------------- 1 | // Based on: https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js 2 | 3 | /** 4 | * Copyright (c) 2015, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | */ 11 | 12 | 13 | // Current latest version of GraphiQL. 14 | const GRAPHIQL_VERSION = '0.7.1'; 15 | 16 | // Ensures string values are save to be used within a 61 | 62 | 63 | 64 | 65 | 66 | 148 | 149 | `; 150 | } 151 | --------------------------------------------------------------------------------