├── .flowconfig ├── .gitignore ├── resources ├── mocha-bootload.js ├── prepublish.sh ├── pretty.js ├── interfaces │ └── koa.js ├── travis_after_all.py └── watch.js ├── src ├── __tests__ │ ├── helpers │ │ └── koa-multer.js │ ├── usage-test.js │ └── http-test.js ├── renderGraphiQL.js └── index.js ├── .travis.yml ├── LICENSE ├── PATENTS ├── package.json ├── .eslintrc ├── flow-typed └── npm │ └── express_v4.x.x.js └── README.md /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /dist/.* 3 | .*/node_modules/flow-bin/.* 4 | .*/node_modules/y18n/.* 5 | 6 | [libs] 7 | ./resources/interfaces 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | *.iml 4 | .*.haste_cache.* 5 | .DS_Store 6 | .idea 7 | npm-debug.log 8 | 9 | dist 10 | node_modules 11 | coverage 12 | -------------------------------------------------------------------------------- /resources/mocha-bootload.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | 3 | require('babel-register')({ 4 | plugins: ['transform-async-to-generator', 'transform-runtime'], 5 | }); 6 | 7 | process.on('unhandledRejection', function(error) { 8 | console.error('Unhandled Promise Rejection:'); 9 | console.error((error && error.stack) || error); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/helpers/koa-multer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable callback-return */ 2 | 3 | import thenify from 'thenify'; 4 | import multer from 'multer'; 5 | 6 | export default function multerWrapper(options) { 7 | const upload = multer(options); 8 | const _single = upload.single.bind(upload); 9 | upload.single = function(param) { 10 | return async function(ctx, next) { 11 | const thenified = thenify(_single(param)); 12 | await thenified(ctx.req, ctx.res); 13 | await next(); 14 | }; 15 | }; 16 | return upload; 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | # https://github.com/nodejs/LTS 6 | node_js: 7 | - "stable" 8 | - "9" 9 | - "8" 10 | 11 | git: 12 | depth: 5 13 | 14 | cache: yarn 15 | 16 | before_install: 17 | - npm config set spin false --global 18 | 19 | script: 20 | - if [[ "$TRAVIS_JOB_NUMBER" == *.1 ]]; then npm run lint && npm run check && npm run cover:lcov; else npm run testonly; fi 21 | 22 | #after_failure: 23 | # - (cd resources; python travis_after_all.py) 24 | 25 | after_success: 26 | # - (cd resources; python travis_after_all.py) 27 | # - export $(cat resources/.to_export_back) 28 | - if [[ "$TRAVIS_JOB_NUMBER" == *.1 ]]; then cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js; fi 29 | -------------------------------------------------------------------------------- /resources/prepublish.sh: -------------------------------------------------------------------------------- 1 | # Because of a long-running npm issue (https://github.com/npm/npm/issues/3059) 2 | # prepublish runs after `npm install` and `npm pack`. 3 | # In order to only run prepublish before `npm publish`, we have to check argv. 4 | if node -e "process.exit(($npm_config_argv).original[0].indexOf('pu') === 0)"; then 5 | exit 0; 6 | fi 7 | 8 | # Publishing to NPM is currently supported by Travis CI, which ensures that all 9 | # tests pass first and the deployed module contains the correct file struture. 10 | # In order to prevent inadvertently circumventing this, we ensure that a CI 11 | # environment exists before continuing. 12 | # if [ "$CI" != true ]; then 13 | # echo "\n\n\n \033[101;30m Only Travis CI can publish to NPM. \033[0m" 1>&2; 14 | # echo " Ensure git is left is a good state by backing out any commits and deleting any tags." 1>&2; 15 | # echo " Then read CONTRIBUTING.md to learn how to publish to NPM.\n\n\n" 1>&2; 16 | # exit 1; 17 | # fi; 18 | 19 | # Build before Travis CI publishes to NPM 20 | npm run build; 21 | -------------------------------------------------------------------------------- /resources/pretty.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { spawnSync } = require('child_process'); 4 | const { join } = require('path'); 5 | 6 | const INVERSE = '\x1b[7m'; 7 | const RESET = '\x1b[0m'; 8 | const YELLOW = '\x1b[33m'; 9 | 10 | const options = ['--single-quote', '--trailing-comma=all']; 11 | const glob = '{resources,src}/**/*.js'; 12 | const root = join(__dirname, '..'); 13 | const executable = join(root, 'node_modules', '.bin', 'prettier'); 14 | 15 | const check = process.argv.indexOf('--check') !== -1; 16 | const mode = check ? '--list-different' : '--write'; 17 | process.chdir(root); 18 | 19 | const { stdout, stderr, status, error } = spawnSync(executable, [ 20 | ...options, 21 | mode, 22 | glob, 23 | ]); 24 | const out = stdout.toString().trim(); 25 | const err = stdout.toString().trim(); 26 | 27 | function print(message) { 28 | if (message) { 29 | process.stdout.write(message + '\n'); 30 | } 31 | } 32 | 33 | if (status) { 34 | print(out); 35 | print(err); 36 | if (check) { 37 | print(`\n${YELLOW}The files listed above are not correctly formatted.`); 38 | print(`Try: ${INVERSE} npm run pretty ${RESET}`); 39 | } 40 | } 41 | if (error) { 42 | print('error', error); 43 | } 44 | process.exit(status != null ? status : 1); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD License 2 | 3 | For GraphQL software 4 | 5 | Copyright (c) 2015, Facebook, Inc. All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, 8 | are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name Facebook nor the names of its contributors may be used to 18 | endorse or promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 25 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 26 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 27 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 28 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 30 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /PATENTS: -------------------------------------------------------------------------------- 1 | Additional Grant of Patent Rights Version 2 2 | 3 | "Software" means the GraphQL software distributed by Facebook, Inc. 4 | 5 | Facebook, Inc. (“Facebook”) hereby grants to each recipient of the Software (“you”) a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (subject to the termination provision below) license under any Necessary Claims, to make, have made, use, sell, offer to sell, import, and otherwise transfer the Software. For avoidance of doubt, no license is granted under Facebook’s rights in any patent claims that are infringed by (i) modifications to the Software made by you or any third party or (ii) the Software in combination with any software or other technology. 6 | 7 | The license granted hereunder will terminate, automatically and without notice, if you (or any of your subsidiaries, corporate affiliates or agents) initiate directly or indirectly, or take a direct financial interest in, any Patent Assertion: (i) against Facebook or any of its subsidiaries or corporate affiliates, (ii) against any party if such Patent Assertion arises in whole or in part from any software, technology, product or service of Facebook or any of its subsidiaries or corporate affiliates, or (iii) against any party relating to the Software. Notwithstanding the foregoing, if Facebook or any of its subsidiaries or corporate affiliates files a lawsuit alleging patent infringement against you in the first instance, and you respond by filing a patent infringement counterclaim in that lawsuit against that party that is unrelated to the Software, the license granted hereunder will not terminate under section (i) of this paragraph due to such counterclaim. 8 | 9 | A “Necessary Claim” is a claim of a patent owned by Facebook that is necessarily infringed by the Software standing alone. 10 | 11 | A “Patent Assertion” is any lawsuit or other action alleging direct, indirect, or contributory infringement or inducement to infringe any patent, including a cross-claim or counterclaim. 12 | -------------------------------------------------------------------------------- /src/__tests__/usage-test.js: -------------------------------------------------------------------------------- 1 | // 80+ char lines are useful in describe/it, so ignore in this file. 2 | /* eslint-disable max-len */ 3 | 4 | import { expect } from 'chai'; 5 | import { describe, it } from 'mocha'; 6 | import request from 'supertest'; 7 | import Koa from 'koa'; 8 | import mount from 'koa-mount'; 9 | import graphqlHTTP from '..'; 10 | 11 | describe('Useful errors when incorrectly used', () => { 12 | it('requires an option factory function', () => { 13 | expect(() => { 14 | graphqlHTTP(); 15 | }).to.throw('GraphQL middleware requires options.'); 16 | }); 17 | 18 | it('requires option factory function to return object', async () => { 19 | const app = new Koa(); 20 | 21 | app.use(mount('/graphql', graphqlHTTP(() => null))); 22 | 23 | const response = await request(app.listen()).get('/graphql?query={test}'); 24 | 25 | expect(response.status).to.equal(500); 26 | expect(JSON.parse(response.text)).to.deep.equal({ 27 | errors: [ 28 | { 29 | message: 30 | 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.', 31 | }, 32 | ], 33 | }); 34 | }); 35 | 36 | it('requires option factory function to return object or promise of object', async () => { 37 | const app = new Koa(); 38 | 39 | app.use(mount('/graphql', graphqlHTTP(() => Promise.resolve(null)))); 40 | 41 | const response = await request(app.listen()).get('/graphql?query={test}'); 42 | 43 | expect(response.status).to.equal(500); 44 | expect(JSON.parse(response.text)).to.deep.equal({ 45 | errors: [ 46 | { 47 | message: 48 | 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.', 49 | }, 50 | ], 51 | }); 52 | }); 53 | 54 | it('requires option factory function to return object with schema', async () => { 55 | const app = new Koa(); 56 | 57 | app.use(mount('/graphql', graphqlHTTP(() => ({})))); 58 | 59 | const response = await request(app.listen()).get('/graphql?query={test}'); 60 | 61 | expect(response.status).to.equal(500); 62 | expect(JSON.parse(response.text)).to.deep.equal({ 63 | errors: [ 64 | { message: 'GraphQL middleware options must contain a schema.' }, 65 | ], 66 | }); 67 | }); 68 | 69 | it('requires option factory function to return object or promise of object with schema', async () => { 70 | const app = new Koa(); 71 | 72 | app.use(mount('/graphql', graphqlHTTP(() => Promise.resolve({})))); 73 | 74 | const response = await request(app.listen()).get('/graphql?query={test}'); 75 | 76 | expect(response.status).to.equal(500); 77 | expect(JSON.parse(response.text)).to.deep.equal({ 78 | errors: [ 79 | { message: 'GraphQL middleware options must contain a schema.' }, 80 | ], 81 | }); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /resources/interfaces/koa.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* Flow declarations for koa requests and responses */ 3 | /* eslint-disable no-unused-vars */ 4 | declare class Context { 5 | // Request 6 | accepts: (type: string | Array) => ?string, 7 | acceptsEncodings: ( 8 | encodings: string | Array, 9 | ) => string | Array, 10 | acceptsCharsets: (charsets: string | Array) => string | Array, 11 | acceptsLanguages: (langs: string | Array) => string | Array, 12 | get: (field: string) => ?string, 13 | is: (types: string | Array) => string | boolean | null, 14 | querystring: string, 15 | idempotent: boolean, 16 | socket: net$Socket, 17 | search: string, 18 | method: string, 19 | query: Object, 20 | path: string, 21 | url: string, 22 | origin: string, 23 | href: string, 24 | subdomains: Array, 25 | protocol: string, 26 | host: string, 27 | hostname: string, 28 | header: Object, 29 | headers: Object, 30 | secure: boolean, 31 | stale: boolean, 32 | fresh: boolean, 33 | ips: Array, 34 | ip: string, 35 | 36 | // Response 37 | attachment: (filename: string) => void, 38 | redirect: (url: string, alt: string) => void, 39 | remove: (field: string) => void, 40 | vary: string, 41 | set: (field: string | Object | Array, val: string) => void, 42 | append: (field: string, val: string | Array) => void, 43 | status: number, 44 | message: string, 45 | body: mixed, 46 | length: number, 47 | type: string, 48 | lastModified: string | Date, 49 | etag: string, 50 | headerSent: boolean, 51 | writable: boolean, 52 | } 53 | 54 | declare class Request { 55 | header: Object, 56 | headers: Object, 57 | url: string, 58 | origin: string, 59 | href: string, 60 | method: string, 61 | path: string, 62 | query: Object, 63 | querystring: string, 64 | search: string, 65 | host: string, 66 | hostname: string, 67 | fresh: boolean, 68 | stale: boolean, 69 | idempotent: boolean, 70 | socket: net$Socket, 71 | charset: string, 72 | length: Number, 73 | protocol: string, 74 | secure: boolean, 75 | ip: string, 76 | ips: Array, 77 | subdomains: Array, 78 | accepts: (type: string | Array) => ?string, 79 | acceptsEncodings: ( 80 | encodings: string | Array, 81 | ) => string | Array, 82 | acceptsCharsets: (charsets: string | Array) => string | Array, 83 | acceptsLanguages: (langs: string | Array) => string | Array, 84 | is: (types: string | Array) => string | boolean | null, 85 | type: string, 86 | get: (field: string) => ?string, 87 | inspect: () => ?Object, 88 | toJSON: () => Object, 89 | body: mixed, 90 | } 91 | 92 | declare class Response { 93 | socket: () => net$Socket, 94 | header: Object, 95 | headers: Object, 96 | status: number, 97 | message: string, 98 | body: mixed, 99 | length: number, 100 | headerSent: boolean, 101 | vary: string, 102 | redirect: (url: string, alt: string) => void, 103 | attachment: (filename: string) => void, 104 | type: string, 105 | lastModified: string | Date, 106 | etag: string, 107 | is: (types: string | Array) => string | boolean, 108 | get: (field: string) => string, 109 | set: (field: string | Object | Array, val: string) => void, 110 | append: (field: string, val: string | Array) => void, 111 | remove: (field: string) => void, 112 | writable: boolean, 113 | inspect: () => ?Object, 114 | toJSON: () => Object, 115 | } 116 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datacamp/koa-graphql", 3 | "version": "0.9.0", 4 | "description": "Production ready GraphQL Koa middleware.", 5 | "contributors": [ 6 | "Lee Byron (http://leebyron.com/)", 7 | "Daniel Schafer ", 8 | "C.T. Lin " 9 | ], 10 | "license": "BSD-3-Clause", 11 | "bugs": { 12 | "url": "https://github.com/chentsulin/koa-graphql/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/chentsulin/koa-graphql.git" 17 | }, 18 | "keywords": [ 19 | "koa", 20 | "http", 21 | "graphql", 22 | "middleware", 23 | "api" 24 | ], 25 | "main": "dist/index.js", 26 | "directories": { 27 | "lib": "./dist" 28 | }, 29 | "files": [ 30 | "dist", 31 | "README.md", 32 | "LICENSE", 33 | "PATENTS" 34 | ], 35 | "options": { 36 | "mocha": "--harmony --exit --require resources/mocha-bootload src/**/__tests__/**/*.js" 37 | }, 38 | "babel": { 39 | "presets": [ 40 | "es2015" 41 | ], 42 | "plugins": [ 43 | "transform-class-properties", 44 | "transform-flow-strip-types", 45 | [ 46 | "transform-runtime", 47 | { 48 | "polyfill": false, 49 | "regenerator": true 50 | } 51 | ] 52 | ] 53 | }, 54 | "scripts": { 55 | "prepublish": ". ./resources/prepublish.sh", 56 | "test": "npm run lint && npm run pretty-check && npm run check && npm run testonly", 57 | "testonly": "mocha $npm_package_options_mocha", 58 | "lint": "eslint src", 59 | "check": "flow check", 60 | "build": "rm -rf dist/* && babel src --ignore __tests__ --out-dir dist -b regenerator && npm run build:flow", 61 | "build:flow": "find ./src -name '*.js' -not -path '*/__tests__*' | while read filepath; do cp $filepath `echo $filepath | sed 's/\\/src\\//\\/dist\\//g'`.flow; done", 62 | "watch": "node resources/watch.js", 63 | "cover": "babel-node node_modules/.bin/isparta cover --root src --report html node_modules/.bin/_mocha -- $npm_package_options_mocha", 64 | "cover:lcov": "babel-node node_modules/.bin/isparta cover --root src --report lcovonly node_modules/.bin/_mocha -- $npm_package_options_mocha", 65 | "pretty": "node resources/pretty.js", 66 | "pretty-check": "node resources/pretty.js --check", 67 | "preversion": "npm test" 68 | }, 69 | "dependencies": { 70 | "babel-runtime": "^6.25.0", 71 | "express-graphql": "^0.6.11", 72 | "http-errors": "^1.6.2", 73 | "thenify": "^3.3.0" 74 | }, 75 | "devDependencies": { 76 | "babel-cli": "^6.26.0", 77 | "babel-eslint": "^8.2.1", 78 | "babel-plugin-transform-async-to-generator": "^6.24.1", 79 | "babel-plugin-transform-class-properties": "^6.24.1", 80 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 81 | "babel-plugin-transform-runtime": "^6.23.0", 82 | "babel-preset-es2015": "^6.24.1", 83 | "babel-register": "^6.26.0", 84 | "chai": "^4.1.2", 85 | "co-body": "^5.1.1", 86 | "coveralls": "^3.0.0", 87 | "eslint": "^4.17.0", 88 | "eslint-plugin-flowtype": "^2.43.0", 89 | "flow-bin": "^0.65.0", 90 | "graphql": "^0.13.0", 91 | "isparta": "^4.0.0", 92 | "koa": "^2.4.1", 93 | "koa-mount": "^3.0.0", 94 | "koa-session": "^5.8.1", 95 | "mocha": "^5.0.0", 96 | "multer": "^1.3.0", 97 | "prettier": "^1.3.1", 98 | "raw-body": "^2.3.2", 99 | "sane": "^2.4.1", 100 | "supertest": "^3.0.0" 101 | }, 102 | "peerDependencies": { 103 | "graphql": "^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /resources/travis_after_all.py: -------------------------------------------------------------------------------- 1 | """ 2 | https://github.com/dmakhno/travis_after_all/blob/master/travis_after_all.py 3 | """ 4 | 5 | import os 6 | import json 7 | import time 8 | import logging 9 | 10 | try: 11 | import urllib.request as urllib2 12 | except ImportError: 13 | import urllib2 14 | 15 | log = logging.getLogger("travis.leader") 16 | log.addHandler(logging.StreamHandler()) 17 | log.setLevel(logging.INFO) 18 | 19 | TRAVIS_JOB_NUMBER = 'TRAVIS_JOB_NUMBER' 20 | TRAVIS_BUILD_ID = 'TRAVIS_BUILD_ID' 21 | POLLING_INTERVAL = 'LEADER_POLLING_INTERVAL' 22 | 23 | build_id = os.getenv(TRAVIS_BUILD_ID) 24 | polling_interval = int(os.getenv(POLLING_INTERVAL, '5')) 25 | 26 | #assume, first job is the leader 27 | is_leader = lambda job_number: job_number.endswith('.1') 28 | 29 | if not os.getenv(TRAVIS_JOB_NUMBER): 30 | # seems even for builds with only one job, this won't get here 31 | log.fatal("Don't use defining leader for build without matrix") 32 | exit(1) 33 | elif is_leader(os.getenv(TRAVIS_JOB_NUMBER)): 34 | log.info("This is a leader") 35 | else: 36 | #since python is subprocess, env variables are exported back via file 37 | with open(".to_export_back", "w") as export_var: 38 | export_var.write("BUILD_MINION=YES") 39 | log.info("This is a minion") 40 | exit(0) 41 | 42 | 43 | class MatrixElement(object): 44 | def __init__(self, json_raw): 45 | self.is_finished = json_raw['finished_at'] is not None 46 | self.is_succeeded = json_raw['result'] == 0 47 | self.number = json_raw['number'] 48 | self.is_leader = is_leader(self.number) 49 | 50 | 51 | def matrix_snapshot(): 52 | """ 53 | :return: Matrix List 54 | """ 55 | response = urllib2.build_opener().open("https://api.travis-ci.org/builds/{0}".format(build_id)).read() 56 | raw_json = json.loads(response) 57 | matrix_without_leader = [MatrixElement(element) for element in raw_json["matrix"]] 58 | return matrix_without_leader 59 | 60 | 61 | def wait_others_to_finish(): 62 | def others_finished(): 63 | """ 64 | Dumps others to finish 65 | Leader cannot finish, it is working now 66 | :return: tuple(True or False, List of not finished jobs) 67 | """ 68 | snapshot = matrix_snapshot() 69 | finished = [el.is_finished for el in snapshot if not el.is_leader] 70 | return reduce(lambda a, b: a and b, finished), [el.number for el in snapshot if 71 | not el.is_leader and not el.is_finished] 72 | 73 | while True: 74 | finished, waiting_list = others_finished() 75 | if finished: break 76 | log.info("Leader waits for minions {0}...".format(waiting_list)) # just in case do not get "silence timeout" 77 | time.sleep(polling_interval) 78 | 79 | 80 | try: 81 | wait_others_to_finish() 82 | 83 | final_snapshot = matrix_snapshot() 84 | log.info("Final Results: {0}".format([(e.number, e.is_succeeded) for e in final_snapshot])) 85 | 86 | BUILD_AGGREGATE_STATUS = 'BUILD_AGGREGATE_STATUS' 87 | others_snapshot = [el for el in final_snapshot if not el.is_leader] 88 | if reduce(lambda a, b: a and b, [e.is_succeeded for e in others_snapshot]): 89 | os.environ[BUILD_AGGREGATE_STATUS] = "others_succeeded" 90 | elif reduce(lambda a, b: a and b, [not e.is_succeeded for e in others_snapshot]): 91 | log.error("Others Failed") 92 | os.environ[BUILD_AGGREGATE_STATUS] = "others_failed" 93 | else: 94 | log.warn("Others Unknown") 95 | os.environ[BUILD_AGGREGATE_STATUS] = "unknown" 96 | #since python is subprocess, env variables are exported back via file 97 | with open(".to_export_back", "w") as export_var: 98 | export_var.write("BUILD_LEADER=YES {0}={1}".format(BUILD_AGGREGATE_STATUS, os.environ[BUILD_AGGREGATE_STATUS])) 99 | 100 | except Exception as e: 101 | log.fatal(e) 102 | -------------------------------------------------------------------------------- /src/renderGraphiQL.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | type GraphiQLData = { 4 | query: ?string, 5 | variables: ?{ [param: string]: mixed }, 6 | operationName: ?string, 7 | result?: mixed, 8 | }; 9 | 10 | // Current latest version of GraphiQL. 11 | const GRAPHIQL_VERSION = '0.11.3'; 12 | 13 | // Ensures string values are safe to be used within a 59 | 60 | 61 | 62 | 63 | 64 | 153 | 154 | `; 155 | } 156 | -------------------------------------------------------------------------------- /resources/watch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | /* eslint-disable no-console */ 10 | 11 | const sane = require('sane'); 12 | const { resolve: resolvePath } = require('path'); 13 | const { spawn } = require('child_process'); 14 | const flowBinPath = require('flow-bin'); 15 | 16 | process.env.PATH += ':./node_modules/.bin'; 17 | 18 | const cmd = resolvePath(__dirname); 19 | const srcDir = resolvePath(cmd, '../src'); 20 | 21 | function exec(command, options) { 22 | return new Promise(function(resolve, reject) { 23 | const child = spawn(command, options, { 24 | cmd, 25 | env: process.env, 26 | stdio: 'inherit', 27 | }); 28 | child.on('exit', function(code) { 29 | if (code === 0) { 30 | resolve(true); 31 | } else { 32 | reject(new Error('Error code: ' + code)); 33 | } 34 | }); 35 | }); 36 | } 37 | 38 | const flowServer = spawn(flowBinPath, ['server'], { 39 | cmd, 40 | env: process.env, 41 | }); 42 | 43 | const watcher = sane(srcDir, { glob: ['**/*.js', '**/*.graphql'] }) 44 | .on('ready', startWatch) 45 | .on('add', changeFile) 46 | .on('delete', deleteFile) 47 | .on('change', changeFile); 48 | 49 | process.on('SIGINT', function() { 50 | watcher.close(); 51 | flowServer.kill(); 52 | console.log(CLEARLINE + yellow(invert('stopped watching'))); 53 | process.exit(); 54 | }); 55 | 56 | let isChecking; 57 | let needsCheck; 58 | let toCheck = {}; 59 | let timeout; 60 | 61 | function startWatch() { 62 | process.stdout.write(CLEARSCREEN + green(invert('watching...'))); 63 | } 64 | 65 | function changeFile(filepath, root, stat) { 66 | if (!stat.isDirectory()) { 67 | toCheck[filepath] = true; 68 | debouncedCheck(); 69 | } 70 | } 71 | 72 | function deleteFile(filepath) { 73 | delete toCheck[filepath]; 74 | debouncedCheck(); 75 | } 76 | 77 | function debouncedCheck() { 78 | needsCheck = true; 79 | clearTimeout(timeout); 80 | timeout = setTimeout(guardedCheck, 250); 81 | } 82 | 83 | function guardedCheck() { 84 | if (isChecking || !needsCheck) { 85 | return; 86 | } 87 | isChecking = true; 88 | const filepaths = Object.keys(toCheck); 89 | toCheck = {}; 90 | needsCheck = false; 91 | checkFiles(filepaths).then(() => { 92 | isChecking = false; 93 | process.nextTick(guardedCheck); 94 | }); 95 | } 96 | 97 | function checkFiles(filepaths) { 98 | console.log('\u001b[2J'); 99 | 100 | return parseFiles(filepaths) 101 | .then(() => runTests(filepaths)) 102 | .then(testSuccess => 103 | lintFiles(filepaths).then(lintSuccess => 104 | typecheckStatus().then( 105 | typecheckSuccess => testSuccess && lintSuccess && typecheckSuccess, 106 | ), 107 | ), 108 | ) 109 | .catch(() => false) 110 | .then(success => { 111 | process.stdout.write( 112 | '\n' + (success ? '' : '\x07') + green(invert('watching...')), 113 | ); 114 | }); 115 | } 116 | 117 | // Checking steps 118 | 119 | function parseFiles(filepaths) { 120 | console.log('Checking Syntax'); 121 | 122 | return Promise.all( 123 | filepaths.map(filepath => { 124 | if (isJS(filepath) && !isTest(filepath)) { 125 | return exec('babel', [ 126 | '--optional', 127 | 'runtime', 128 | '--out-file', 129 | '/dev/null', 130 | srcPath(filepath), 131 | ]); 132 | } 133 | }), 134 | ); 135 | } 136 | 137 | function runTests(filepaths) { 138 | console.log('\nRunning Tests'); 139 | 140 | return exec( 141 | 'mocha', 142 | ['--reporter', 'progress', '--require', 'resources/mocha-bootload'].concat( 143 | allTests(filepaths) 144 | ? filepaths.map(srcPath) 145 | : ['src/**/__tests__/**/*.js'], 146 | ), 147 | ).catch(() => false); 148 | } 149 | 150 | function lintFiles(filepaths) { 151 | console.log('Linting Code\n'); 152 | 153 | return filepaths.reduce( 154 | (prev, filepath) => 155 | prev.then(prevSuccess => { 156 | if (isJS(filepath)) { 157 | process.stdout.write(' ' + filepath + ' ...'); 158 | return exec('eslint', [srcPath(filepath)]) 159 | .catch(() => false) 160 | .then(success => { 161 | const msg = 162 | CLEARLINE + ' ' + (success ? CHECK : X) + ' ' + filepath; 163 | console.log(msg); 164 | return prevSuccess && success; 165 | }); 166 | } 167 | return prevSuccess; 168 | }), 169 | Promise.resolve(true), 170 | ); 171 | } 172 | 173 | function typecheckStatus() { 174 | console.log('\nType Checking\n'); 175 | return exec(flowBinPath, ['status']).catch(() => false); 176 | } 177 | 178 | // Filepath 179 | 180 | function srcPath(filepath) { 181 | return resolvePath(srcDir, filepath); 182 | } 183 | 184 | // Predicates 185 | 186 | function isJS(filepath) { 187 | return filepath.indexOf('.js') === filepath.length - 3; 188 | } 189 | 190 | function allTests(filepaths) { 191 | return filepaths.length > 0 && filepaths.every(isTest); 192 | } 193 | 194 | function isTest(filepath) { 195 | return isJS(filepath) && filepath.indexOf('__tests__/') !== -1; 196 | } 197 | 198 | // Print helpers 199 | 200 | const CLEARSCREEN = '\u001b[2J'; 201 | const CLEARLINE = '\r\x1B[K'; 202 | const CHECK = green('\u2713'); 203 | const X = red('\u2718'); 204 | 205 | function invert(str) { 206 | return `\u001b[7m ${str} \u001b[27m`; 207 | } 208 | 209 | function red(str) { 210 | return `\x1B[K\u001b[1m\u001b[31m${str}\u001b[39m\u001b[22m`; 211 | } 212 | 213 | function green(str) { 214 | return `\x1B[K\u001b[1m\u001b[32m${str}\u001b[39m\u001b[22m`; 215 | } 216 | 217 | function yellow(str) { 218 | return `\x1B[K\u001b[1m\u001b[33m${str}\u001b[39m\u001b[22m`; 219 | } 220 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | 4 | "plugins": [ 5 | "flowtype" 6 | ], 7 | 8 | "env": { 9 | "es6": true, 10 | "node": true 11 | }, 12 | 13 | "parserOptions": { 14 | "arrowFunctions": true, 15 | "binaryLiterals": true, 16 | "blockBindings": true, 17 | "classes": true, 18 | "defaultParams": true, 19 | "destructuring": true, 20 | "experimentalObjectRestSpread": true, 21 | "forOf": true, 22 | "generators": true, 23 | "globalReturn": true, 24 | "jsx": true, 25 | "modules": true, 26 | "objectLiteralComputedProperties": true, 27 | "objectLiteralDuplicateProperties": true, 28 | "objectLiteralShorthandMethods": true, 29 | "objectLiteralShorthandProperties": true, 30 | "octalLiterals": true, 31 | "regexUFlag": true, 32 | "regexYFlag": true, 33 | "restParams": true, 34 | "spread": true, 35 | "superInFunctions": true, 36 | "templateStrings": true, 37 | "unicodeCodePointEscapes": true 38 | }, 39 | 40 | "rules": { 41 | "flowtype/space-after-type-colon": [2, "always"], 42 | "flowtype/space-before-type-colon": [2, "never"], 43 | "flowtype/space-before-generic-bracket": [2, "never"], 44 | "flowtype/union-intersection-spacing": [2, "always"], 45 | "flowtype/no-weak-types": [2, {"any": false}], 46 | "flowtype/define-flow-type": 2, 47 | "flowtype/use-flow-type": 2, 48 | "flowtype/semi": 2, 49 | 50 | "arrow-parens": [2, "as-needed"], 51 | "arrow-spacing": 2, 52 | "block-scoped-var": 0, 53 | "brace-style": [2, "1tbs", {"allowSingleLine": true}], 54 | "callback-return": 2, 55 | "camelcase": [2, {"properties": "always"}], 56 | "comma-dangle": 0, 57 | "comma-spacing": 0, 58 | "comma-style": [2, "last"], 59 | "complexity": 0, 60 | "computed-property-spacing": [2, "never"], 61 | "consistent-return": 0, 62 | "consistent-this": 0, 63 | "curly": [2, "all"], 64 | "default-case": 0, 65 | "dot-location": [2, "property"], 66 | "dot-notation": 0, 67 | "eol-last": 2, 68 | "eqeqeq": 2, 69 | "func-names": 0, 70 | "func-style": 0, 71 | "generator-star-spacing": [2, {"before": true, "after": false}], 72 | "guard-for-in": 2, 73 | "handle-callback-err": [2, "error"], 74 | "id-length": 0, 75 | "id-match": [2, "^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$"], 76 | "indent": [2, 2, {"SwitchCase": 1}], 77 | "init-declarations": 0, 78 | "key-spacing": [2, {"beforeColon": false, "afterColon": true}], 79 | "keyword-spacing": 2, 80 | "linebreak-style": 2, 81 | "lines-around-comment": 0, 82 | "max-depth": 0, 83 | "max-nested-callbacks": 0, 84 | "max-params": 0, 85 | "max-statements": 0, 86 | "new-cap": 0, 87 | "new-parens": 2, 88 | "newline-after-var": 0, 89 | "no-alert": 2, 90 | "no-array-constructor": 2, 91 | "no-await-in-loop": 2, 92 | "no-bitwise": 0, 93 | "no-caller": 2, 94 | "no-catch-shadow": 0, 95 | "no-class-assign": 2, 96 | "no-cond-assign": 2, 97 | "no-console": 1, 98 | "no-const-assign": 2, 99 | "no-constant-condition": 2, 100 | "no-continue": 0, 101 | "no-control-regex": 0, 102 | "no-debugger": 1, 103 | "no-delete-var": 2, 104 | "no-div-regex": 2, 105 | "no-dupe-args": 2, 106 | "no-dupe-keys": 2, 107 | "no-duplicate-case": 2, 108 | "no-else-return": 2, 109 | "no-empty": 2, 110 | "no-empty-character-class": 2, 111 | "no-eq-null": 0, 112 | "no-eval": 2, 113 | "no-ex-assign": 2, 114 | "no-extend-native": 2, 115 | "no-extra-bind": 2, 116 | "no-extra-boolean-cast": 2, 117 | "no-extra-parens": 0, 118 | "no-extra-semi": 2, 119 | "no-fallthrough": 2, 120 | "no-floating-decimal": 2, 121 | "no-func-assign": 2, 122 | "no-implicit-coercion": 2, 123 | "no-implied-eval": 2, 124 | "no-inline-comments": 0, 125 | "no-inner-declarations": [2, "functions"], 126 | "no-invalid-regexp": 2, 127 | "no-invalid-this": 0, 128 | "no-irregular-whitespace": 2, 129 | "no-iterator": 2, 130 | "no-label-var": 2, 131 | "no-labels": [2, {"allowLoop": true}], 132 | "no-lone-blocks": 2, 133 | "no-lonely-if": 2, 134 | "no-loop-func": 0, 135 | "no-mixed-requires": [2, true], 136 | "no-mixed-spaces-and-tabs": 2, 137 | "no-multi-spaces": 2, 138 | "no-multi-str": 2, 139 | "no-multiple-empty-lines": 0, 140 | "no-native-reassign": 0, 141 | "no-negated-in-lhs": 2, 142 | "no-nested-ternary": 0, 143 | "no-new": 2, 144 | "no-new-func": 0, 145 | "no-new-object": 2, 146 | "no-new-require": 2, 147 | "no-new-wrappers": 2, 148 | "no-obj-calls": 2, 149 | "no-octal": 2, 150 | "no-octal-escape": 2, 151 | "no-param-reassign": 2, 152 | "no-path-concat": 2, 153 | "no-plusplus": 0, 154 | "no-process-env": 0, 155 | "no-process-exit": 0, 156 | "no-proto": 2, 157 | "no-redeclare": 2, 158 | "no-regex-spaces": 2, 159 | "no-restricted-modules": 0, 160 | "no-return-assign": 2, 161 | "no-script-url": 2, 162 | "no-self-compare": 0, 163 | "no-sequences": 0, 164 | "no-shadow": 2, 165 | "no-shadow-restricted-names": 2, 166 | "no-spaced-func": 2, 167 | "no-sparse-arrays": 2, 168 | "no-sync": 2, 169 | "no-ternary": 0, 170 | "no-this-before-super": 2, 171 | "no-throw-literal": 2, 172 | "no-trailing-spaces": 2, 173 | "no-undef": 2, 174 | "no-undef-init": 2, 175 | "no-undefined": 0, 176 | "no-underscore-dangle": 0, 177 | "no-unexpected-multiline": 2, 178 | "no-unneeded-ternary": 2, 179 | "no-unreachable": 2, 180 | "no-unused-expressions": 2, 181 | "no-unused-vars": [2, {"vars": "all", "args": "after-used"}], 182 | "no-use-before-define": 0, 183 | "no-useless-call": 2, 184 | "no-useless-escape": 2, 185 | "no-useless-return": 2, 186 | "no-var": 2, 187 | "no-void": 2, 188 | "no-warning-comments": 0, 189 | "no-with": 2, 190 | "object-curly-spacing": [0, "always"], 191 | "object-shorthand": [2, "always"], 192 | "one-var": [2, "never"], 193 | "operator-assignment": [2, "always"], 194 | "padded-blocks": 0, 195 | "prefer-const": 2, 196 | "prefer-reflect": 0, 197 | "prefer-spread": 0, 198 | "quote-props": [2, "as-needed", {"numbers": true}], 199 | "quotes": [2, "single"], 200 | "radix": 2, 201 | "require-yield": 2, 202 | "semi": [2, "always"], 203 | "semi-spacing": [2, {"before": false, "after": true}], 204 | "sort-vars": 0, 205 | "space-before-blocks": [2, "always"], 206 | "space-in-parens": 0, 207 | "space-infix-ops": [2, {"int32Hint": false}], 208 | "space-unary-ops": [2, {"words": true, "nonwords": false}], 209 | "spaced-comment": [2, "always"], 210 | "strict": 0, 211 | "use-isnan": 2, 212 | "valid-jsdoc": 0, 213 | "valid-typeof": 2, 214 | "vars-on-top": 0, 215 | "wrap-iife": 2, 216 | "wrap-regex": 0, 217 | "yoda": [2, "never", {"exceptRange": true}] 218 | }, 219 | globals: { 220 | Generator: true, 221 | http$IncomingMessage: true 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /flow-typed/npm/express_v4.x.x.js: -------------------------------------------------------------------------------- 1 | // flow-typed signature: 1e4577f4ead8bf5f0adbdaa1a243c60a 2 | // flow-typed version: ef2fdb0770/express_v4.x.x/flow_>=v0.32.x 3 | 4 | import type { Server } from 'http'; 5 | import type { Socket } from 'net'; 6 | 7 | declare type express$RouterOptions = { 8 | caseSensitive?: boolean, 9 | mergeParams?: boolean, 10 | strict?: boolean 11 | }; 12 | 13 | declare class express$RequestResponseBase { 14 | app: express$Application; 15 | get(field: string): string | void; 16 | } 17 | 18 | declare type express$RequestParams = { 19 | [param: string]: string 20 | } 21 | 22 | declare class express$Request extends http$IncomingMessage mixins express$RequestResponseBase { 23 | baseUrl: string; 24 | body: any; 25 | cookies: {[cookie: string]: string}; 26 | connection: Socket; 27 | fresh: boolean; 28 | hostname: string; 29 | ip: string; 30 | ips: Array; 31 | method: string; 32 | originalUrl: string; 33 | params: express$RequestParams; 34 | path: string; 35 | protocol: 'https' | 'http'; 36 | query: {[name: string]: string}; 37 | route: string; 38 | secure: boolean; 39 | signedCookies: {[signedCookie: string]: string}; 40 | stale: boolean; 41 | subdomains: Array; 42 | xhr: boolean; 43 | accepts(types: string): string | false; 44 | accepts(types: Array): string | false; 45 | acceptsCharsets(...charsets: Array): string | false; 46 | acceptsEncodings(...encoding: Array): string | false; 47 | acceptsLanguages(...lang: Array): string | false; 48 | header(field: string): string | void; 49 | is(type: string): boolean; 50 | param(name: string, defaultValue?: string): string | void; 51 | } 52 | 53 | declare type express$CookieOptions = { 54 | domain?: string, 55 | encode?: (value: string) => string, 56 | expires?: Date, 57 | httpOnly?: boolean, 58 | maxAge?: number, 59 | path?: string, 60 | secure?: boolean, 61 | signed?: boolean 62 | }; 63 | 64 | declare type express$RenderCallback = (err: Error | null, html?: string) => mixed; 65 | 66 | declare type express$SendFileOptions = { 67 | maxAge?: number, 68 | root?: string, 69 | lastModified?: boolean, 70 | headers?: {[name: string]: string}, 71 | dotfiles?: 'allow' | 'deny' | 'ignore' 72 | }; 73 | 74 | declare class express$Response extends http$ServerResponse mixins express$RequestResponseBase { 75 | headersSent: boolean; 76 | locals: {[name: string]: mixed}; 77 | append(field: string, value?: string): this; 78 | attachment(filename?: string): this; 79 | cookie(name: string, value: string, options?: express$CookieOptions): this; 80 | clearCookie(name: string, options?: express$CookieOptions): this; 81 | download(path: string, filename?: string, callback?: (err?: ?Error) => void): this; 82 | format(typesObject: {[type: string]: Function}): this; 83 | json(body?: mixed): this; 84 | jsonp(body?: mixed): this; 85 | links(links: {[name: string]: string}): this; 86 | location(path: string): this; 87 | redirect(url: string, ...args: Array): this; 88 | redirect(status: number, url: string, ...args: Array): this; 89 | render(view: string, locals?: {[name: string]: mixed}, callback?: express$RenderCallback): this; 90 | send(body?: mixed): this; 91 | sendFile(path: string, options?: express$SendFileOptions, callback?: (err?: ?Error) => mixed): this; 92 | sendStatus(statusCode: number): this; 93 | header(field: string, value?: string): this; 94 | header(headers: {[name: string]: string}): this; 95 | set(field: string, value?: string|string[]): this; 96 | set(headers: {[name: string]: string}): this; 97 | status(statusCode: number): this; 98 | type(type: string): this; 99 | vary(field: string): this; 100 | req: express$Request; 101 | } 102 | 103 | declare type express$NextFunction = (err?: ?Error | 'route') => mixed; 104 | declare type express$Middleware = 105 | ((req: express$Request, res: express$Response, next: express$NextFunction) => mixed) | 106 | ((error: ?Error, req: express$Request, res: express$Response, next: express$NextFunction) => mixed); 107 | declare interface express$RouteMethodType { 108 | (middleware: express$Middleware): T; 109 | (...middleware: Array): T; 110 | (path: string|RegExp|string[], ...middleware: Array): T; 111 | } 112 | declare class express$Route { 113 | all: express$RouteMethodType; 114 | get: express$RouteMethodType; 115 | post: express$RouteMethodType; 116 | put: express$RouteMethodType; 117 | head: express$RouteMethodType; 118 | delete: express$RouteMethodType; 119 | options: express$RouteMethodType; 120 | trace: express$RouteMethodType; 121 | copy: express$RouteMethodType; 122 | lock: express$RouteMethodType; 123 | mkcol: express$RouteMethodType; 124 | move: express$RouteMethodType; 125 | purge: express$RouteMethodType; 126 | propfind: express$RouteMethodType; 127 | proppatch: express$RouteMethodType; 128 | unlock: express$RouteMethodType; 129 | report: express$RouteMethodType; 130 | mkactivity: express$RouteMethodType; 131 | checkout: express$RouteMethodType; 132 | merge: express$RouteMethodType; 133 | 134 | // @TODO Missing 'm-search' but get flow illegal name error. 135 | 136 | notify: express$RouteMethodType; 137 | subscribe: express$RouteMethodType; 138 | unsubscribe: express$RouteMethodType; 139 | patch: express$RouteMethodType; 140 | search: express$RouteMethodType; 141 | connect: express$RouteMethodType; 142 | } 143 | 144 | declare class express$Router extends express$Route { 145 | constructor(options?: express$RouterOptions): void; 146 | route(path: string): express$Route; 147 | static (options?: express$RouterOptions): express$Router; 148 | use(middleware: express$Middleware): this; 149 | use(...middleware: Array): this; 150 | use(path: string|RegExp|string[], ...middleware: Array): this; 151 | use(path: string, router: express$Router): this; 152 | handle(req: http$IncomingMessage, res: http$ServerResponse, next: express$NextFunction): void; 153 | 154 | // Can't use regular callable signature syntax due to https://github.com/facebook/flow/issues/3084 155 | $call: (req: http$IncomingMessage, res: http$ServerResponse, next?: ?express$NextFunction) => void; 156 | } 157 | 158 | declare class express$Application extends express$Router mixins events$EventEmitter { 159 | constructor(): void; 160 | locals: {[name: string]: mixed}; 161 | mountpath: string; 162 | listen(port: number, hostname?: string, backlog?: number, callback?: (err?: ?Error) => mixed): Server; 163 | listen(port: number, hostname?: string, callback?: (err?: ?Error) => mixed): Server; 164 | listen(port: number, callback?: (err?: ?Error) => mixed): Server; 165 | listen(path: string, callback?: (err?: ?Error) => mixed): Server; 166 | listen(handle: Object, callback?: (err?: ?Error) => mixed): Server; 167 | disable(name: string): void; 168 | disabled(name: string): boolean; 169 | enable(name: string): express$Application; 170 | enabled(name: string): boolean; 171 | engine(name: string, callback: Function): void; 172 | /** 173 | * Mixed will not be taken as a value option. Issue around using the GET http method name and the get for settings. 174 | */ 175 | // get(name: string): mixed; 176 | set(name: string, value: mixed): mixed; 177 | render(name: string, optionsOrFunction: {[name: string]: mixed}, callback: express$RenderCallback): void; 178 | handle(req: http$IncomingMessage, res: http$ServerResponse, next?: ?express$NextFunction): void; 179 | } 180 | 181 | declare module 'express' { 182 | declare export type RouterOptions = express$RouterOptions; 183 | declare export type CookieOptions = express$CookieOptions; 184 | declare export type Middleware = express$Middleware; 185 | declare export type NextFunction = express$NextFunction; 186 | declare export type RequestParams = express$RequestParams; 187 | declare export type $Response = express$Response; 188 | declare export type $Request = express$Request; 189 | declare export type $Application = express$Application; 190 | 191 | declare module.exports: { 192 | (): express$Application, // If you try to call like a function, it will use this signature 193 | static: (root: string, options?: Object) => express$Middleware, // `static` property on the function 194 | Router: typeof express$Router, // `Router` property on the function 195 | }; 196 | } 197 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | Source, 5 | parse, 6 | validate, 7 | execute, 8 | formatError, 9 | getOperationAST, 10 | specifiedRules, 11 | } from 'graphql'; 12 | import expressGraphQL from 'express-graphql'; 13 | import httpError from 'http-errors'; 14 | 15 | import { renderGraphiQL } from './renderGraphiQL'; 16 | 17 | import type { GraphQLError, GraphQLSchema } from 'graphql'; 18 | import type { Context, Request, Response } from 'koa'; 19 | import type { GraphQLParams, RequestInfo } from 'express-graphql'; 20 | 21 | const { getGraphQLParams } = expressGraphQL; 22 | 23 | /** 24 | * Used to configure the graphqlHTTP middleware by providing a schema 25 | * and other configuration options. 26 | * 27 | * Options can be provided as an Object, a Promise for an Object, or a Function 28 | * that returns an Object or a Promise for an Object. 29 | */ 30 | export type Options = 31 | | ((request: Request, response: Response, ctx: Context) => OptionsResult) 32 | | OptionsResult; 33 | export type OptionsResult = OptionsData | Promise; 34 | export type OptionsData = { 35 | /** 36 | * A GraphQL schema from graphql-js. 37 | */ 38 | schema: GraphQLSchema, 39 | 40 | /** 41 | * A value to pass as the context to the graphql() function. 42 | */ 43 | context?: ?mixed, 44 | 45 | /** 46 | * An object to pass as the rootValue to the graphql() function. 47 | */ 48 | rootValue?: ?mixed, 49 | 50 | /** 51 | * A boolean to configure whether the output should be pretty-printed. 52 | */ 53 | pretty?: ?boolean, 54 | 55 | /** 56 | * An optional function which will be used to format any errors produced by 57 | * fulfilling a GraphQL operation. If no function is provided, GraphQL's 58 | * default spec-compliant `formatError` function will be used. 59 | */ 60 | formatError?: ?(error: GraphQLError, context?: ?any) => mixed, 61 | 62 | /** 63 | * An optional array of validation rules that will be applied on the document 64 | * in additional to those defined by the GraphQL spec. 65 | */ 66 | validationRules?: ?Array, 67 | 68 | /** 69 | * An optional function for adding additional metadata to the GraphQL response 70 | * as a key-value object. The result will be added to "extensions" field in 71 | * the resulting JSON. This is often a useful place to add development time 72 | * info such as the runtime of a query or the amount of resources consumed. 73 | * 74 | * Information about the request is provided to be used. 75 | * 76 | * This function may be async. 77 | */ 78 | extensions?: ?(info: RequestInfo) => { [key: string]: mixed }, 79 | 80 | /** 81 | * A boolean to optionally enable GraphiQL mode. 82 | */ 83 | graphiql?: ?boolean, 84 | }; 85 | 86 | type Middleware = (ctx: Context) => Promise; 87 | 88 | /** 89 | * Middleware for express; takes an options object or function as input to 90 | * configure behavior, and returns an express middleware. 91 | */ 92 | module.exports = graphqlHTTP; 93 | function graphqlHTTP(options: Options): Middleware { 94 | if (!options) { 95 | throw new Error('GraphQL middleware requires options.'); 96 | } 97 | 98 | return async function middleware(ctx): Promise { 99 | const req = ctx.req; 100 | const request = ctx.request; 101 | const response = ctx.response; 102 | 103 | // Higher scoped variables are referred to at various stages in the 104 | // asynchronous state machine below. 105 | let schema; 106 | let context; 107 | let rootValue; 108 | let pretty; 109 | let graphiql; 110 | let formatErrorFn; 111 | let extensionsFn; 112 | let showGraphiQL; 113 | let query; 114 | let documentAST; 115 | let variables; 116 | let operationName; 117 | let validationRules; 118 | 119 | let result; 120 | 121 | try { 122 | // Promises are used as a mechanism for capturing any thrown errors during 123 | // the asynchronous process below. 124 | 125 | // Resolve the Options to get OptionsData. 126 | const optionsData = await (typeof options === 'function' 127 | ? options(request, response, ctx) 128 | : options); 129 | 130 | // Assert that optionsData is in fact an Object. 131 | if (!optionsData || typeof optionsData !== 'object') { 132 | throw new Error( 133 | 'GraphQL middleware option function must return an options object ' + 134 | 'or a promise which will be resolved to an options object.', 135 | ); 136 | } 137 | 138 | // Assert that schema is required. 139 | if (!optionsData.schema) { 140 | throw new Error('GraphQL middleware options must contain a schema.'); 141 | } 142 | 143 | // Collect information from the options data object. 144 | schema = optionsData.schema; 145 | context = optionsData.context || ctx; 146 | rootValue = optionsData.rootValue; 147 | pretty = optionsData.pretty; 148 | graphiql = optionsData.graphiql; 149 | formatErrorFn = optionsData.formatError; 150 | extensionsFn = optionsData.extensions; 151 | 152 | validationRules = specifiedRules; 153 | if (optionsData.validationRules) { 154 | validationRules = validationRules.concat(optionsData.validationRules); 155 | } 156 | 157 | // GraphQL HTTP only supports GET and POST methods. 158 | if (request.method !== 'GET' && request.method !== 'POST') { 159 | response.set('Allow', 'GET, POST'); 160 | throw httpError(405, 'GraphQL only supports GET and POST requests.'); 161 | } 162 | 163 | // Use request.body when req.body is undefined. 164 | req.body = req.body || request.body; 165 | 166 | // Parse the Request to get GraphQL request parameters. 167 | const params: GraphQLParams = await getGraphQLParams(req); 168 | 169 | // Get GraphQL params from the request and POST body data. 170 | query = params.query; 171 | variables = params.variables; 172 | operationName = params.operationName; 173 | showGraphiQL = graphiql && canDisplayGraphiQL(request, params); 174 | 175 | result = await new Promise(resolve => { 176 | // If there is no query, but GraphiQL will be displayed, do not produce 177 | // a result, otherwise return a 400: Bad Request. 178 | if (!query) { 179 | if (showGraphiQL) { 180 | return resolve(null); 181 | } 182 | throw httpError(400, 'Must provide query string.'); 183 | } 184 | 185 | // GraphQL source. 186 | const source = new Source(query, 'GraphQL request'); 187 | 188 | // Parse source to AST, reporting any syntax error. 189 | try { 190 | documentAST = parse(source); 191 | } catch (syntaxError) { 192 | // Return 400: Bad Request if any syntax errors errors exist. 193 | response.status = 400; 194 | return resolve({ errors: [syntaxError] }); 195 | } 196 | 197 | // Validate AST, reporting any errors. 198 | const validationErrors = validate(schema, documentAST, validationRules); 199 | if (validationErrors.length > 0) { 200 | // Return 400: Bad Request if any validation errors exist. 201 | response.status = 400; 202 | return resolve({ errors: validationErrors }); 203 | } 204 | 205 | // Only query operations are allowed on GET requests. 206 | if (request.method === 'GET') { 207 | // Determine if this GET request will perform a non-query. 208 | const operationAST = getOperationAST(documentAST, operationName); 209 | if (operationAST && operationAST.operation !== 'query') { 210 | // If GraphiQL can be shown, do not perform this query, but 211 | // provide it to GraphiQL so that the requester may perform it 212 | // themselves if desired. 213 | if (showGraphiQL) { 214 | return resolve(null); 215 | } 216 | 217 | // Otherwise, report a 405: Method Not Allowed error. 218 | response.set('Allow', 'POST'); 219 | throw httpError( 220 | 405, 221 | `Can only perform a ${operationAST.operation} operation ` + 222 | 'from a POST request.', 223 | ); 224 | } 225 | } 226 | 227 | // Perform the execution, reporting any errors creating the context. 228 | try { 229 | resolve( 230 | execute( 231 | schema, 232 | documentAST, 233 | rootValue, 234 | context, 235 | variables, 236 | operationName, 237 | ), 238 | ); 239 | } catch (contextError) { 240 | // Return 400: Bad Request if any execution context errors exist. 241 | response.status = 400; 242 | resolve({ errors: [contextError] }); 243 | } 244 | }); 245 | 246 | // Collect and apply any metadata extensions if a function was provided. 247 | // http://facebook.github.io/graphql/#sec-Response-Format 248 | if (result && extensionsFn) { 249 | result = await Promise.resolve( 250 | extensionsFn({ 251 | document: documentAST, 252 | variables, 253 | operationName, 254 | result, 255 | }), 256 | ).then(extensions => { 257 | if (extensions && typeof extensions === 'object') { 258 | (result: any).extensions = extensions; 259 | } 260 | return result; 261 | }); 262 | } 263 | } catch (error) { 264 | // If an error was caught, report the httpError status, or 500. 265 | response.status = error.status || 500; 266 | result = { errors: [error] }; 267 | } 268 | 269 | // If no data was included in the result, that indicates a runtime query 270 | // error, indicate as such with a generic status code. 271 | // Note: Information about the error itself will still be contained in 272 | // the resulting JSON payload. 273 | // http://facebook.github.io/graphql/#sec-Data 274 | if (result && result.data === null) { 275 | // $FlowFixMe 276 | const errorWithStatus = result.errors.find(error => error.originalError.status !== undefined); 277 | if (errorWithStatus !== undefined) { 278 | // $FlowFixMe 279 | response.status = errorWithStatus.originalError.status; 280 | } else { 281 | response.status = 500; 282 | } 283 | } 284 | // Format any encountered errors. 285 | if (result && result.errors) { 286 | (result: any).errors = result.errors.map( 287 | err => (formatErrorFn ? formatErrorFn(err, context) : formatError(err)), 288 | ); 289 | } 290 | 291 | // If allowed to show GraphiQL, present it instead of JSON. 292 | if (showGraphiQL) { 293 | const payload = renderGraphiQL({ 294 | query, 295 | variables, 296 | operationName, 297 | result, 298 | }); 299 | response.type = 'text/html'; 300 | response.body = payload; 301 | } else { 302 | // Otherwise, present JSON directly. 303 | const payload = pretty ? JSON.stringify(result, null, 2) : result; 304 | response.type = 'application/json'; 305 | response.body = payload; 306 | } 307 | }; 308 | } 309 | 310 | /** 311 | * Helper function to determine if GraphiQL can be displayed. 312 | */ 313 | function canDisplayGraphiQL(request: Request, params: GraphQLParams): boolean { 314 | // If `raw` exists, GraphiQL mode is not enabled. 315 | // Allowed to show GraphiQL if not requested as raw and this request 316 | // prefers HTML over JSON. 317 | return !params.raw && request.accepts(['json', 'html']) === 'html'; 318 | } 319 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Koa Middleware 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build Status][travis-image]][travis-url] 5 | [![Test coverage][coveralls-image]][coveralls-url] 6 | [![Dependency Status][david_img]][david_site] 7 | 8 | Create a GraphQL HTTP server with [Koa](http://koajs.com/). 9 | 10 | Port from [express-graphql](https://github.com/graphql/express-graphql) 11 | 12 | ## Installation 13 | 14 | ``` 15 | npm install --save koa-graphql 16 | ``` 17 | 18 | ## Usage 19 | 20 | Mount `koa-graphql` as a route handler: 21 | 22 | ```js 23 | const Koa = require('koa'); 24 | const mount = require('koa-mount'); 25 | const graphqlHTTP = require('koa-graphql'); 26 | 27 | const app = new Koa(); 28 | 29 | app.use(mount('/graphql', graphqlHTTP({ 30 | schema: MyGraphQLSchema, 31 | graphiql: true 32 | }))); 33 | 34 | app.listen(4000); 35 | ``` 36 | 37 | With koa-router@7 38 | 39 | ```js 40 | const Koa = require('koa'); 41 | const Router = require('koa-router'); // koa-router@7.x 42 | const graphqlHTTP = require('koa-graphql'); 43 | 44 | const app = new Koa(); 45 | const router = new Router(); 46 | 47 | router.all('/graphql', graphqlHTTP({ 48 | schema: MyGraphQLSchema, 49 | graphiql: true 50 | })); 51 | 52 | app.use(router.routes()).use(router.allowedMethods()); 53 | ``` 54 | 55 | For Koa 1, use [koa-convert](https://github.com/koajs/convert) to convert the middleware: 56 | 57 | ```js 58 | const koa = require('koa'); 59 | const mount = require('koa-mount'); // koa-mount@1.x 60 | const convert = require('koa-convert'); 61 | const graphqlHTTP = require('koa-graphql'); 62 | 63 | const app = koa(); 64 | 65 | app.use(mount('/graphql', convert.back(graphqlHTTP({ 66 | schema: MyGraphQLSchema, 67 | graphiql: true 68 | })))); 69 | ``` 70 | 71 | > NOTE: Below is a copy from express-graphql's README. In this time I implemented almost same api, but it may be changed as time goes on. 72 | 73 | ## Options 74 | 75 | The `graphqlHTTP` function accepts the following options: 76 | 77 | * **`schema`**: A `GraphQLSchema` instance from [`graphql-js`][]. 78 | A `schema` *must* be provided. 79 | 80 | * **`graphiql`**: If `true`, presents [GraphiQL][] when the route with a 81 | `/graphiql` appended is loaded in a browser. We recommend that you set 82 | `graphiql` to `true` when your app is in development, because it's 83 | quite useful. You may or may not want it in production. 84 | 85 | * **`rootValue`**: A value to pass as the `rootValue` to the `graphql()` 86 | function from [`graphql-js/src/execute.js`](https://github.com/graphql/graphql-js/blob/master/src/execution/execute.js#L121). 87 | 88 | * **`context`**: A value to pass as the `context` to the `graphql()` 89 | function from [`graphql-js/src/execute.js`](https://github.com/graphql/graphql-js/blob/master/src/execution/execute.js#L122). If `context` is not provided, the 90 | `ctx` object is passed as the context. 91 | 92 | * **`pretty`**: If `true`, any JSON response will be pretty-printed. 93 | 94 | * **`formatError`**: An optional function which will be used to format any 95 | errors produced by fulfilling a GraphQL operation. If no function is 96 | provided, GraphQL's default spec-compliant [`formatError`][] function will be used. 97 | 98 | * **`extensions`**: An optional function for adding additional metadata to the 99 | GraphQL response as a key-value object. The result will be added to 100 | `"extensions"` field in the resulting JSON. This is often a useful place to 101 | add development time metadata such as the runtime of a query or the amount 102 | of resources consumed. This may be an async function. The function is 103 | give one object as an argument: `{ document, variables, operationName, result }`. 104 | 105 | * **`validationRules`**: Optional additional validation rules queries must 106 | satisfy in addition to those defined by the GraphQL spec. 107 | 108 | 109 | ## HTTP Usage 110 | 111 | Once installed at a path, `koa-graphql` will accept requests with 112 | the parameters: 113 | 114 | * **`query`**: A string GraphQL document to be executed. 115 | 116 | * **`variables`**: The runtime values to use for any GraphQL query variables 117 | as a JSON object. 118 | 119 | * **`operationName`**: If the provided `query` contains multiple named 120 | operations, this specifies which operation should be executed. If not 121 | provided, a 400 error will be returned if the `query` contains multiple 122 | named operations. 123 | 124 | * **`raw`**: If the `graphiql` option is enabled and the `raw` parameter is 125 | provided raw JSON will always be returned instead of GraphiQL even when 126 | loaded from a browser. 127 | 128 | GraphQL will first look for each parameter in the URL's query-string: 129 | 130 | ``` 131 | /graphql?query=query+getUser($id:ID){user(id:$id){name}}&variables={"id":"4"} 132 | ``` 133 | 134 | If not found in the query-string, it will look in the POST request body. 135 | 136 | If a previous middleware has already parsed the POST body, the `request.body` 137 | value will be used. Use [`multer`][] or a similar middleware to add support 138 | for `multipart/form-data` content, which may be useful for GraphQL mutations 139 | involving uploading files. See an [example using multer](https://github.com/chentsulin/koa-graphql/blob/e1a98f3548203a3c41fedf3d4267846785480d28/src/__tests__/http-test.js#L664-L732). 140 | 141 | If the POST body has not yet been parsed, koa-graphql will interpret it 142 | depending on the provided *Content-Type* header. 143 | 144 | * **`application/json`**: the POST body will be parsed as a JSON 145 | object of parameters. 146 | 147 | * **`application/x-www-form-urlencoded`**: this POST body will be 148 | parsed as a url-encoded string of key-value pairs. 149 | 150 | * **`application/graphql`**: The POST body will be parsed as GraphQL 151 | query string, which provides the `query` parameter. 152 | 153 | ## Combining with Other koa Middleware 154 | 155 | By default, the koa request is passed as the GraphQL `context`. 156 | Since most koa middleware operates by adding extra data to the 157 | request object, this means you can use most koa middleware just by inserting it before `graphqlHTTP` is mounted. This covers scenarios such as authenticating the user, handling file uploads, or mounting GraphQL on a dynamic endpoint. 158 | 159 | This example uses [`koa-session`][] to provide GraphQL with the currently logged-in session. 160 | 161 | ```js 162 | const Koa = require('koa'); 163 | const mount = require('koa-mount'); 164 | const session = require('koa-session'); 165 | const graphqlHTTP = require('koa-graphql'); 166 | 167 | const app = new Koa(); 168 | app.keys = [ 'some secret hurr' ]; 169 | app.use(session(app)); 170 | app.use(function *(next) { 171 | this.session.id = 'me'; 172 | yield next; 173 | }); 174 | 175 | app.use(mount('/graphql', graphqlHTTP({ 176 | schema: MySessionAwareGraphQLSchema, 177 | graphiql: true 178 | }))); 179 | ``` 180 | 181 | Then in your type definitions, you can access the ctx via the third "context" argument in your `resolve` function: 182 | 183 | ```js 184 | new GraphQLObjectType({ 185 | name: 'MyType', 186 | fields: { 187 | myField: { 188 | type: GraphQLString, 189 | resolve(parentValue, args, ctx) { 190 | // use `ctx.session` here 191 | } 192 | } 193 | } 194 | }); 195 | ``` 196 | 197 | 198 | ## Providing Extensions 199 | 200 | The GraphQL response allows for adding additional information in a response to 201 | a GraphQL query via a field in the response called `"extensions"`. This is added 202 | by providing an `extensions` function when using `graphqlHTTP`. The function 203 | must return a JSON-serializable Object. 204 | 205 | When called, this is provided an argument which you can use to get information 206 | about the GraphQL request: 207 | 208 | `{ document, variables, operationName, result }` 209 | 210 | This example illustrates adding the amount of time consumed by running the 211 | provided query, which could perhaps be used by your development tools. 212 | 213 | ```js 214 | const graphqlHTTP = require('koa-graphql'); 215 | 216 | const app = new Koa(); 217 | 218 | app.keys = [ 'some secret hurr' ]; 219 | app.use(session(app)); 220 | 221 | app.use(mount('/graphql', graphqlHTTP(request => { 222 | const startTime = Date.now(); 223 | return { 224 | schema: MyGraphQLSchema, 225 | graphiql: true, 226 | extensions({ document, variables, operationName, result }) { 227 | return { runTime: Date.now() - startTime }; 228 | } 229 | }; 230 | }))); 231 | ``` 232 | 233 | When querying this endpoint, it would include this information in the result, 234 | for example: 235 | 236 | ```js 237 | { 238 | "data": { ... } 239 | "extensions": { 240 | "runTime": 135 241 | } 242 | } 243 | ``` 244 | 245 | 246 | ## Additional Validation Rules 247 | 248 | GraphQL's [validation phase](https://facebook.github.io/graphql/#sec-Validation) checks the query to ensure that it can be successfully executed against the schema. The `validationRules` option allows for additional rules to be run during this phase. Rules are applied to each node in an AST representing the query using the Visitor pattern. 249 | 250 | A validation rule is a function which returns a visitor for one or more node Types. Below is an example of a validation preventing the specific fieldname `metadata` from being queried. For more examples see the [`specifiedRules`](https://github.com/graphql/graphql-js/tree/master/src/validation/rules) in the [graphql-js](https://github.com/graphql/graphql-js) package. 251 | 252 | ```js 253 | import { GraphQLError } from 'graphql'; 254 | 255 | export function DisallowMetadataQueries(context) { 256 | return { 257 | Field(node) { 258 | const fieldName = node.name.value; 259 | 260 | if (fieldName === "metadata") { 261 | context.reportError( 262 | new GraphQLError( 263 | `Validation: Requesting the field ${fieldName} is not allowed`, 264 | ), 265 | ); 266 | } 267 | } 268 | }; 269 | } 270 | ``` 271 | 272 | ## Debugging Tips 273 | 274 | During development, it's useful to get more information from errors, such as 275 | stack traces. Providing a function to `formatError` enables this: 276 | 277 | ```js 278 | formatError: (error, ctx) => ({ 279 | message: error.message, 280 | locations: error.locations, 281 | stack: error.stack, 282 | path: error.path 283 | }) 284 | ``` 285 | 286 | 287 | ### Examples 288 | 289 | - [koa-graphql-relay-example](https://github.com/chentsulin/koa-graphql-relay-example) 290 | - [tests](https://github.com/chentsulin/koa-graphql/blob/master/src/__tests__/http-test.js) 291 | 292 | 293 | ### Other relevant projects 294 | 295 | Please checkout [awesome-graphql](https://github.com/chentsulin/awesome-graphql). 296 | 297 | ### Contributing 298 | 299 | Welcome pull requests! 300 | 301 | ### License 302 | 303 | BSD-3-Clause 304 | 305 | [`graphql-js`]: https://github.com/graphql/graphql-js 306 | [`formatError`]: https://github.com/graphql/graphql-js/blob/master/src/error/formatError.js 307 | [GraphiQL]: https://github.com/graphql/graphiql 308 | [`multer`]: https://github.com/expressjs/multer 309 | [`koa-session`]: https://github.com/koajs/session 310 | [npm-image]: https://img.shields.io/npm/v/koa-graphql.svg?style=flat-square 311 | [npm-url]: https://npmjs.org/package/koa-graphql 312 | [travis-image]: https://travis-ci.org/chentsulin/koa-graphql.svg?branch=master 313 | [travis-url]: https://travis-ci.org/chentsulin/koa-graphql 314 | [coveralls-image]: https://coveralls.io/repos/chentsulin/koa-graphql/badge.svg?branch=master&service=github 315 | [coveralls-url]: https://coveralls.io/github/chentsulin/koa-graphql?branch=master 316 | [david_img]: https://david-dm.org/chentsulin/koa-graphql.svg 317 | [david_site]: https://david-dm.org/chentsulin/koa-graphql 318 | -------------------------------------------------------------------------------- /src/__tests__/http-test.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // 80+ char lines are useful in describe/it, so ignore in this file. 4 | /* eslint-disable max-len */ 5 | /* eslint-disable callback-return */ 6 | 7 | import { expect } from 'chai'; 8 | import { describe, it } from 'mocha'; 9 | import { stringify } from 'querystring'; 10 | import zlib from 'zlib'; 11 | import multer from 'multer'; 12 | import multerWrapper from './helpers/koa-multer'; 13 | import request from 'supertest'; 14 | import Koa from 'koa'; 15 | import mount from 'koa-mount'; 16 | import session from 'koa-session'; 17 | import parse from 'co-body'; 18 | import getRawBody from 'raw-body'; 19 | 20 | import { 21 | GraphQLSchema, 22 | GraphQLObjectType, 23 | GraphQLNonNull, 24 | GraphQLString, 25 | GraphQLError, 26 | BREAK, 27 | } from 'graphql'; 28 | import graphqlHTTP from '../'; 29 | 30 | const QueryRootType = new GraphQLObjectType({ 31 | name: 'QueryRoot', 32 | fields: { 33 | test: { 34 | type: GraphQLString, 35 | args: { 36 | who: { 37 | type: GraphQLString, 38 | }, 39 | }, 40 | resolve: (root, { who }) => 'Hello ' + ((who: any) || 'World'), 41 | }, 42 | nonNullThrower: { 43 | type: new GraphQLNonNull(GraphQLString), 44 | resolve: () => { 45 | throw new Error('Throws!'); 46 | }, 47 | }, 48 | thrower: { 49 | type: GraphQLString, 50 | resolve: () => { 51 | throw new Error('Throws!'); 52 | }, 53 | }, 54 | context: { 55 | type: GraphQLString, 56 | resolve: (obj, args, context) => context, 57 | }, 58 | contextDotFoo: { 59 | type: GraphQLString, 60 | resolve: (obj, args, context) => { 61 | return (context: any).foo; 62 | }, 63 | }, 64 | }, 65 | }); 66 | 67 | const TestSchema = new GraphQLSchema({ 68 | query: QueryRootType, 69 | mutation: new GraphQLObjectType({ 70 | name: 'MutationRoot', 71 | fields: { 72 | writeTest: { 73 | type: QueryRootType, 74 | resolve: () => ({}), 75 | }, 76 | }, 77 | }), 78 | }); 79 | 80 | function urlString(urlParams?: ?{ [param: string]: mixed }) { 81 | let string = '/graphql'; 82 | if (urlParams) { 83 | string += '?' + stringify(urlParams); 84 | } 85 | return string; 86 | } 87 | 88 | function promiseTo(fn) { 89 | return new Promise((resolve, reject) => { 90 | fn((error, result) => (error ? reject(error) : resolve(result))); 91 | }); 92 | } 93 | 94 | describe('test harness', () => { 95 | it('resolves callback promises', async () => { 96 | const resolveValue = {}; 97 | const result = await promiseTo(cb => cb(null, resolveValue)); 98 | expect(result).to.equal(resolveValue); 99 | }); 100 | 101 | it('rejects callback promises with errors', async () => { 102 | const rejectError = new Error(); 103 | let caught; 104 | try { 105 | await promiseTo(cb => cb(rejectError)); 106 | } catch (error) { 107 | caught = error; 108 | } 109 | expect(caught).to.equal(rejectError); 110 | }); 111 | }); 112 | 113 | function server() { 114 | const app = new Koa(); 115 | app.on('error', error => { 116 | // eslint-disable-next-line no-console 117 | console.log('App encountered an error:', error); 118 | }); 119 | return app; 120 | } 121 | 122 | describe('GraphQL-HTTP tests', () => { 123 | describe('GET functionality', () => { 124 | it('allows GET with query param', async () => { 125 | const app = server(); 126 | 127 | app.use( 128 | mount( 129 | urlString(), 130 | graphqlHTTP({ 131 | schema: TestSchema, 132 | }), 133 | ), 134 | ); 135 | 136 | const response = await request(app.listen()).get( 137 | urlString({ 138 | query: '{test}', 139 | }), 140 | ); 141 | 142 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 143 | }); 144 | 145 | it('allows GET with variable values', async () => { 146 | const app = server(); 147 | 148 | app.use( 149 | mount( 150 | urlString(), 151 | graphqlHTTP({ 152 | schema: TestSchema, 153 | }), 154 | ), 155 | ); 156 | 157 | const response = await request(app.listen()).get( 158 | urlString({ 159 | query: 'query helloWho($who: String){ test(who: $who) }', 160 | variables: JSON.stringify({ who: 'Dolly' }), 161 | }), 162 | ); 163 | 164 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 165 | }); 166 | 167 | it('allows GET with operation name', async () => { 168 | const app = server(); 169 | 170 | app.use( 171 | mount( 172 | urlString(), 173 | graphqlHTTP({ 174 | schema: TestSchema, 175 | }), 176 | ), 177 | ); 178 | 179 | const response = await request(app.listen()).get( 180 | urlString({ 181 | query: ` 182 | query helloYou { test(who: "You"), ...shared } 183 | query helloWorld { test(who: "World"), ...shared } 184 | query helloDolly { test(who: "Dolly"), ...shared } 185 | fragment shared on QueryRoot { 186 | shared: test(who: "Everyone") 187 | } 188 | `, 189 | operationName: 'helloWorld', 190 | }), 191 | ); 192 | 193 | expect(JSON.parse(response.text)).to.deep.equal({ 194 | data: { 195 | test: 'Hello World', 196 | shared: 'Hello Everyone', 197 | }, 198 | }); 199 | }); 200 | 201 | it('Reports validation errors', async () => { 202 | const app = server(); 203 | 204 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 205 | 206 | const response = await request(app.listen()).get( 207 | urlString({ 208 | query: '{ test, unknownOne, unknownTwo }', 209 | }), 210 | ); 211 | 212 | expect(response.status).to.equal(400); 213 | expect(JSON.parse(response.text)).to.deep.equal({ 214 | errors: [ 215 | { 216 | message: 'Cannot query field "unknownOne" on type "QueryRoot".', 217 | locations: [{ line: 1, column: 9 }], 218 | }, 219 | { 220 | message: 'Cannot query field "unknownTwo" on type "QueryRoot".', 221 | locations: [{ line: 1, column: 21 }], 222 | }, 223 | ], 224 | }); 225 | }); 226 | 227 | it('Errors when missing operation name', async () => { 228 | const app = server(); 229 | 230 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 231 | 232 | const response = await request(app.listen()).get( 233 | urlString({ 234 | query: ` 235 | query TestQuery { test } 236 | mutation TestMutation { writeTest { test } } 237 | `, 238 | }), 239 | ); 240 | 241 | expect(response.status).to.equal(200); 242 | expect(JSON.parse(response.text)).to.deep.equal({ 243 | errors: [ 244 | { 245 | message: 246 | 'Must provide operation name if query contains multiple operations.', 247 | }, 248 | ], 249 | }); 250 | }); 251 | 252 | it('Errors when sending a mutation via GET', async () => { 253 | const app = server(); 254 | 255 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 256 | 257 | const response = await request(app.listen()).get( 258 | urlString({ 259 | query: 'mutation TestMutation { writeTest { test } }', 260 | }), 261 | ); 262 | 263 | expect(response.status).to.equal(405); 264 | expect(JSON.parse(response.text)).to.deep.equal({ 265 | errors: [ 266 | { 267 | message: 268 | 'Can only perform a mutation operation from a POST request.', 269 | }, 270 | ], 271 | }); 272 | }); 273 | 274 | it('Errors when selecting a mutation within a GET', async () => { 275 | const app = server(); 276 | 277 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 278 | 279 | const response = await request(app.listen()).get( 280 | urlString({ 281 | operationName: 'TestMutation', 282 | query: ` 283 | query TestQuery { test } 284 | mutation TestMutation { writeTest { test } } 285 | `, 286 | }), 287 | ); 288 | 289 | expect(response.status).to.equal(405); 290 | expect(JSON.parse(response.text)).to.deep.equal({ 291 | errors: [ 292 | { 293 | message: 294 | 'Can only perform a mutation operation from a POST request.', 295 | }, 296 | ], 297 | }); 298 | }); 299 | 300 | it('Allows a mutation to exist within a GET', async () => { 301 | const app = server(); 302 | 303 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 304 | 305 | const response = await request(app.listen()).get( 306 | urlString({ 307 | operationName: 'TestQuery', 308 | query: ` 309 | mutation TestMutation { writeTest { test } } 310 | query TestQuery { test } 311 | `, 312 | }), 313 | ); 314 | 315 | expect(response.status).to.equal(200); 316 | expect(JSON.parse(response.text)).to.deep.equal({ 317 | data: { 318 | test: 'Hello World', 319 | }, 320 | }); 321 | }); 322 | 323 | it('Allows passing in a context', async () => { 324 | const app = server(); 325 | 326 | app.use( 327 | mount( 328 | urlString(), 329 | graphqlHTTP({ 330 | schema: TestSchema, 331 | context: 'testValue', 332 | }), 333 | ), 334 | ); 335 | 336 | const response = await request(app.listen()).get( 337 | urlString({ 338 | operationName: 'TestQuery', 339 | query: ` 340 | query TestQuery { context } 341 | `, 342 | }), 343 | ); 344 | 345 | expect(response.status).to.equal(200); 346 | expect(JSON.parse(response.text)).to.deep.equal({ 347 | data: { 348 | context: 'testValue', 349 | }, 350 | }); 351 | }); 352 | 353 | it('Uses ctx as context by default', async () => { 354 | const app = server(); 355 | 356 | // Middleware that adds ctx.foo to every request 357 | app.use((ctx, next) => { 358 | ctx.foo = 'bar'; 359 | return next(); 360 | }); 361 | 362 | app.use( 363 | mount( 364 | urlString(), 365 | graphqlHTTP({ 366 | schema: TestSchema, 367 | }), 368 | ), 369 | ); 370 | 371 | const response = await request(app.listen()).get( 372 | urlString({ 373 | operationName: 'TestQuery', 374 | query: ` 375 | query TestQuery { contextDotFoo } 376 | `, 377 | }), 378 | ); 379 | 380 | expect(response.status).to.equal(200); 381 | expect(JSON.parse(response.text)).to.deep.equal({ 382 | data: { 383 | contextDotFoo: 'bar', 384 | }, 385 | }); 386 | }); 387 | 388 | it('Allows returning an options Promise', async () => { 389 | const app = server(); 390 | 391 | app.use( 392 | mount( 393 | urlString(), 394 | graphqlHTTP(() => 395 | Promise.resolve({ 396 | schema: TestSchema, 397 | }), 398 | ), 399 | ), 400 | ); 401 | 402 | const response = await request(app.listen()).get( 403 | urlString({ 404 | query: '{test}', 405 | }), 406 | ); 407 | 408 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 409 | }); 410 | 411 | it('Catches errors thrown from options function', async () => { 412 | const app = server(); 413 | 414 | app.use( 415 | mount( 416 | urlString(), 417 | graphqlHTTP(() => { 418 | throw new Error('I did something wrong'); 419 | }), 420 | ), 421 | ); 422 | 423 | const response = await request(app.listen()).get( 424 | urlString({ 425 | query: '{test}', 426 | }), 427 | ); 428 | 429 | expect(response.status).to.equal(500); 430 | expect(response.text).to.equal( 431 | '{"errors":[{"message":"I did something wrong"}]}', 432 | ); 433 | }); 434 | }); 435 | 436 | describe('POST functionality', () => { 437 | it('allows POST with JSON encoding', async () => { 438 | const app = server(); 439 | 440 | app.use( 441 | mount( 442 | urlString(), 443 | graphqlHTTP({ 444 | schema: TestSchema, 445 | }), 446 | ), 447 | ); 448 | 449 | const response = await request(app.listen()) 450 | .post(urlString()) 451 | .send({ query: '{test}' }); 452 | 453 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 454 | }); 455 | 456 | it('Allows sending a mutation via POST', async () => { 457 | const app = server(); 458 | 459 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 460 | 461 | const response = await request(app.listen()) 462 | .post(urlString()) 463 | .send({ query: 'mutation TestMutation { writeTest { test } }' }); 464 | 465 | expect(response.status).to.equal(200); 466 | expect(response.text).to.equal( 467 | '{"data":{"writeTest":{"test":"Hello World"}}}', 468 | ); 469 | }); 470 | 471 | it('allows POST with url encoding', async () => { 472 | const app = server(); 473 | 474 | app.use( 475 | mount( 476 | urlString(), 477 | graphqlHTTP({ 478 | schema: TestSchema, 479 | }), 480 | ), 481 | ); 482 | 483 | const response = await request(app.listen()) 484 | .post(urlString()) 485 | .send(stringify({ query: '{test}' })); 486 | 487 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 488 | }); 489 | 490 | it('supports POST JSON query with string variables', async () => { 491 | const app = server(); 492 | 493 | app.use( 494 | mount( 495 | urlString(), 496 | graphqlHTTP({ 497 | schema: TestSchema, 498 | }), 499 | ), 500 | ); 501 | 502 | const response = await request(app.listen()).post(urlString()).send({ 503 | query: 'query helloWho($who: String){ test(who: $who) }', 504 | variables: JSON.stringify({ who: 'Dolly' }), 505 | }); 506 | 507 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 508 | }); 509 | 510 | it('supports POST JSON query with JSON variables', async () => { 511 | const app = server(); 512 | 513 | app.use( 514 | mount( 515 | urlString(), 516 | graphqlHTTP({ 517 | schema: TestSchema, 518 | }), 519 | ), 520 | ); 521 | 522 | const response = await request(app.listen()).post(urlString()).send({ 523 | query: 'query helloWho($who: String){ test(who: $who) }', 524 | variables: { who: 'Dolly' }, 525 | }); 526 | 527 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 528 | }); 529 | 530 | it('supports POST url encoded query with string variables', async () => { 531 | const app = server(); 532 | 533 | app.use( 534 | mount( 535 | urlString(), 536 | graphqlHTTP({ 537 | schema: TestSchema, 538 | }), 539 | ), 540 | ); 541 | 542 | const response = await request(app.listen()).post(urlString()).send( 543 | stringify({ 544 | query: 'query helloWho($who: String){ test(who: $who) }', 545 | variables: JSON.stringify({ who: 'Dolly' }), 546 | }), 547 | ); 548 | 549 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 550 | }); 551 | 552 | it('supports POST JSON query with GET variable values', async () => { 553 | const app = server(); 554 | 555 | app.use( 556 | mount( 557 | urlString(), 558 | graphqlHTTP({ 559 | schema: TestSchema, 560 | }), 561 | ), 562 | ); 563 | 564 | const response = await request(app.listen()) 565 | .post( 566 | urlString({ 567 | variables: JSON.stringify({ who: 'Dolly' }), 568 | }), 569 | ) 570 | .send({ query: 'query helloWho($who: String){ test(who: $who) }' }); 571 | 572 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 573 | }); 574 | 575 | it('supports POST url encoded query with GET variable values', async () => { 576 | const app = server(); 577 | 578 | app.use( 579 | mount( 580 | urlString(), 581 | graphqlHTTP({ 582 | schema: TestSchema, 583 | }), 584 | ), 585 | ); 586 | 587 | const response = await request(app.listen()) 588 | .post( 589 | urlString({ 590 | variables: JSON.stringify({ who: 'Dolly' }), 591 | }), 592 | ) 593 | .send( 594 | stringify({ 595 | query: 'query helloWho($who: String){ test(who: $who) }', 596 | }), 597 | ); 598 | 599 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 600 | }); 601 | 602 | it('supports POST raw text query with GET variable values', async () => { 603 | const app = server(); 604 | 605 | app.use( 606 | mount( 607 | urlString(), 608 | graphqlHTTP({ 609 | schema: TestSchema, 610 | }), 611 | ), 612 | ); 613 | 614 | const response = await request(app.listen()) 615 | .post( 616 | urlString({ 617 | variables: JSON.stringify({ who: 'Dolly' }), 618 | }), 619 | ) 620 | .set('Content-Type', 'application/graphql') 621 | .send('query helloWho($who: String){ test(who: $who) }'); 622 | 623 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 624 | }); 625 | 626 | it('allows POST with operation name', async () => { 627 | const app = server(); 628 | 629 | app.use( 630 | mount( 631 | urlString(), 632 | graphqlHTTP({ 633 | schema: TestSchema, 634 | }), 635 | ), 636 | ); 637 | 638 | const response = await request(app.listen()).post(urlString()).send({ 639 | query: ` 640 | query helloYou { test(who: "You"), ...shared } 641 | query helloWorld { test(who: "World"), ...shared } 642 | query helloDolly { test(who: "Dolly"), ...shared } 643 | fragment shared on QueryRoot { 644 | shared: test(who: "Everyone") 645 | } 646 | `, 647 | operationName: 'helloWorld', 648 | }); 649 | 650 | expect(JSON.parse(response.text)).to.deep.equal({ 651 | data: { 652 | test: 'Hello World', 653 | shared: 'Hello Everyone', 654 | }, 655 | }); 656 | }); 657 | 658 | it('allows POST with GET operation name', async () => { 659 | const app = server(); 660 | 661 | app.use( 662 | mount( 663 | urlString(), 664 | graphqlHTTP({ 665 | schema: TestSchema, 666 | }), 667 | ), 668 | ); 669 | 670 | const response = await request(app.listen()) 671 | .post( 672 | urlString({ 673 | operationName: 'helloWorld', 674 | }), 675 | ) 676 | .set('Content-Type', 'application/graphql').send(` 677 | query helloYou { test(who: "You"), ...shared } 678 | query helloWorld { test(who: "World"), ...shared } 679 | query helloDolly { test(who: "Dolly"), ...shared } 680 | fragment shared on QueryRoot { 681 | shared: test(who: "Everyone") 682 | } 683 | `); 684 | 685 | expect(JSON.parse(response.text)).to.deep.equal({ 686 | data: { 687 | test: 'Hello World', 688 | shared: 'Hello Everyone', 689 | }, 690 | }); 691 | }); 692 | 693 | it('allows other UTF charsets', async () => { 694 | const app = server(); 695 | 696 | app.use( 697 | mount( 698 | urlString(), 699 | graphqlHTTP({ 700 | schema: TestSchema, 701 | }), 702 | ), 703 | ); 704 | 705 | const req = request(app.listen()) 706 | .post(urlString()) 707 | .set('Content-Type', 'application/graphql; charset=utf-16'); 708 | req.write(new Buffer('{ test(who: "World") }', 'utf16le')); 709 | const response = await req; 710 | 711 | expect(JSON.parse(response.text)).to.deep.equal({ 712 | data: { 713 | test: 'Hello World', 714 | }, 715 | }); 716 | }); 717 | 718 | it('allows gzipped POST bodies', async () => { 719 | const app = server(); 720 | 721 | app.use( 722 | mount( 723 | urlString(), 724 | graphqlHTTP({ 725 | schema: TestSchema, 726 | }), 727 | ), 728 | ); 729 | 730 | const data = { query: '{ test(who: "World") }' }; 731 | const json = JSON.stringify(data); 732 | const gzippedJson = await promiseTo(cb => zlib.gzip(json, cb)); 733 | 734 | const req = request(app.listen()) 735 | .post(urlString()) 736 | .set('Content-Type', 'application/json') 737 | .set('Content-Encoding', 'gzip'); 738 | req.write(gzippedJson); 739 | const response = await req; 740 | 741 | expect(JSON.parse(response.text)).to.deep.equal({ 742 | data: { 743 | test: 'Hello World', 744 | }, 745 | }); 746 | }); 747 | 748 | it('allows deflated POST bodies', async () => { 749 | const app = server(); 750 | 751 | app.use( 752 | mount( 753 | urlString(), 754 | graphqlHTTP({ 755 | schema: TestSchema, 756 | }), 757 | ), 758 | ); 759 | 760 | const data = { query: '{ test(who: "World") }' }; 761 | const json = JSON.stringify(data); 762 | const deflatedJson = await promiseTo(cb => zlib.deflate(json, cb)); 763 | 764 | const req = request(app.listen()) 765 | .post(urlString()) 766 | .set('Content-Type', 'application/json') 767 | .set('Content-Encoding', 'deflate'); 768 | req.write(deflatedJson); 769 | const response = await req; 770 | 771 | expect(JSON.parse(response.text)).to.deep.equal({ 772 | data: { 773 | test: 'Hello World', 774 | }, 775 | }); 776 | }); 777 | 778 | // should replace multer with koa middleware 779 | it('allows for pre-parsed POST bodies', async () => { 780 | // Note: this is not the only way to handle file uploads with GraphQL, 781 | // but it is terse and illustrative of using koa-graphql and multer 782 | // together. 783 | 784 | // A simple schema which includes a mutation. 785 | const UploadedFileType = new GraphQLObjectType({ 786 | name: 'UploadedFile', 787 | fields: { 788 | originalname: { type: GraphQLString }, 789 | mimetype: { type: GraphQLString }, 790 | }, 791 | }); 792 | 793 | const TestMutationSchema = new GraphQLSchema({ 794 | query: new GraphQLObjectType({ 795 | name: 'QueryRoot', 796 | fields: { 797 | test: { type: GraphQLString }, 798 | }, 799 | }), 800 | mutation: new GraphQLObjectType({ 801 | name: 'MutationRoot', 802 | fields: { 803 | uploadFile: { 804 | type: UploadedFileType, 805 | resolve(rootValue) { 806 | // For this test demo, we're just returning the uploaded 807 | // file directly, but presumably you might return a Promise 808 | // to go store the file somewhere first. 809 | return rootValue.request.file; 810 | }, 811 | }, 812 | }, 813 | }), 814 | }); 815 | 816 | const app = server(); 817 | 818 | // Multer provides multipart form data parsing. 819 | const storage = multer.memoryStorage(); 820 | app.use(mount(urlString(), multerWrapper({ storage }).single('file'))); 821 | 822 | // Providing the request as part of `rootValue` allows it to 823 | // be accessible from within Schema resolve functions. 824 | app.use( 825 | mount( 826 | urlString(), 827 | graphqlHTTP((req, ctx) => { 828 | expect(ctx.req.file.originalname).to.equal('http-test.js'); 829 | return { 830 | schema: TestMutationSchema, 831 | rootValue: { request: ctx.req }, 832 | }; 833 | }), 834 | ), 835 | ); 836 | 837 | const response = await request(app.listen()) 838 | .post(urlString()) 839 | .field( 840 | 'query', 841 | `mutation TestMutation { 842 | uploadFile { originalname, mimetype } 843 | }`, 844 | ) 845 | .attach('file', __filename); 846 | 847 | expect(JSON.parse(response.text)).to.deep.equal({ 848 | data: { 849 | uploadFile: { 850 | originalname: 'http-test.js', 851 | mimetype: 'application/javascript', 852 | }, 853 | }, 854 | }); 855 | }); 856 | 857 | it('allows for pre-parsed POST using application/graphql', async () => { 858 | const app = server(); 859 | app.use(async function(ctx, next) { 860 | if (ctx.is('application/graphql')) { 861 | ctx.request.body = await parse.text(ctx); 862 | } 863 | await next(); 864 | }); 865 | 866 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 867 | 868 | const req = request(app.listen()) 869 | .post(urlString()) 870 | .set('Content-Type', 'application/graphql'); 871 | req.write(new Buffer('{ test(who: "World") }')); 872 | const response = await req; 873 | 874 | expect(JSON.parse(response.text)).to.deep.equal({ 875 | data: { 876 | test: 'Hello World', 877 | }, 878 | }); 879 | }); 880 | 881 | it('does not accept unknown pre-parsed POST string', async () => { 882 | const app = server(); 883 | app.use(async function(ctx, next) { 884 | if (ctx.is('*/*')) { 885 | ctx.request.body = await parse.text(ctx); 886 | } 887 | await next(); 888 | }); 889 | 890 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 891 | 892 | const req = request(app.listen()).post(urlString()); 893 | req.write(new Buffer('{ test(who: "World") }')); 894 | const response = await req; 895 | 896 | expect(response.status).to.equal(400); 897 | expect(JSON.parse(response.text)).to.deep.equal({ 898 | errors: [{ message: 'Must provide query string.' }], 899 | }); 900 | }); 901 | 902 | it('does not accept unknown pre-parsed POST raw Buffer', async () => { 903 | const app = server(); 904 | app.use(async function(ctx, next) { 905 | if (ctx.is('*/*')) { 906 | const req = ctx.req; 907 | ctx.request.body = await getRawBody(req, { 908 | length: req.headers['content-length'], 909 | limit: '1mb', 910 | encoding: null, 911 | }); 912 | } 913 | await next(); 914 | }); 915 | 916 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 917 | 918 | const req = request(app.listen()) 919 | .post(urlString()) 920 | .set('Content-Type', 'application/graphql'); 921 | req.write(new Buffer('{ test(who: "World") }')); 922 | const response = await req; 923 | 924 | expect(response.status).to.equal(400); 925 | expect(JSON.parse(response.text)).to.deep.equal({ 926 | errors: [{ message: 'Must provide query string.' }], 927 | }); 928 | }); 929 | }); 930 | 931 | describe('Pretty printing', () => { 932 | it('supports pretty printing', async () => { 933 | const app = server(); 934 | 935 | app.use( 936 | mount( 937 | urlString(), 938 | graphqlHTTP({ 939 | schema: TestSchema, 940 | pretty: true, 941 | }), 942 | ), 943 | ); 944 | 945 | const response = await request(app.listen()).get( 946 | urlString({ 947 | query: '{test}', 948 | }), 949 | ); 950 | 951 | expect(response.text).to.equal( 952 | '{\n' + ' "data": {\n' + ' "test": "Hello World"\n' + ' }\n' + '}', 953 | ); 954 | }); 955 | 956 | it('supports pretty printing configured by request', async () => { 957 | const app = server(); 958 | 959 | app.use( 960 | mount( 961 | urlString(), 962 | graphqlHTTP(req => { 963 | return { 964 | schema: TestSchema, 965 | pretty: req.query.pretty === '1', 966 | }; 967 | }), 968 | ), 969 | ); 970 | 971 | const defaultResponse = await request(app.listen()).get( 972 | urlString({ 973 | query: '{test}', 974 | }), 975 | ); 976 | 977 | expect(defaultResponse.text).to.equal('{"data":{"test":"Hello World"}}'); 978 | 979 | const prettyResponse = await request(app.listen()).get( 980 | urlString({ 981 | query: '{test}', 982 | pretty: 1, 983 | }), 984 | ); 985 | 986 | expect(prettyResponse.text).to.equal( 987 | '{\n' + ' "data": {\n' + ' "test": "Hello World"\n' + ' }\n' + '}', 988 | ); 989 | 990 | const unprettyResponse = await request(app.listen()).get( 991 | urlString({ 992 | query: '{test}', 993 | pretty: 0, 994 | }), 995 | ); 996 | 997 | expect(unprettyResponse.text).to.equal('{"data":{"test":"Hello World"}}'); 998 | }); 999 | }); 1000 | 1001 | it('will send request, response and context when using thunk', async () => { 1002 | const app = server(); 1003 | 1004 | let hasRequest = false; 1005 | let hasResponse = false; 1006 | let hasContext = false; 1007 | 1008 | app.use( 1009 | mount( 1010 | urlString(), 1011 | graphqlHTTP((reqest, response, context) => { 1012 | if (request) { 1013 | hasRequest = true; 1014 | } 1015 | if (response) { 1016 | hasResponse = true; 1017 | } 1018 | if (context) { 1019 | hasContext = true; 1020 | } 1021 | return { schema: TestSchema }; 1022 | }), 1023 | ), 1024 | ); 1025 | 1026 | await request(app.listen()).get(urlString({ query: '{test}' })); 1027 | 1028 | expect(hasRequest).to.equal(true); 1029 | expect(hasResponse).to.equal(true); 1030 | expect(hasContext).to.equal(true); 1031 | }); 1032 | 1033 | describe('Error handling functionality', () => { 1034 | it('handles field errors caught by GraphQL', async () => { 1035 | const app = server(); 1036 | 1037 | app.use( 1038 | mount( 1039 | urlString(), 1040 | graphqlHTTP({ 1041 | schema: TestSchema, 1042 | }), 1043 | ), 1044 | ); 1045 | 1046 | const response = await request(app.listen()).get( 1047 | urlString({ 1048 | query: '{thrower}', 1049 | }), 1050 | ); 1051 | 1052 | expect(response.status).to.equal(200); 1053 | expect(JSON.parse(response.text)).to.deep.equal({ 1054 | data: { thrower: null }, 1055 | errors: [ 1056 | { 1057 | message: 'Throws!', 1058 | locations: [{ line: 1, column: 2 }], 1059 | path: ['thrower'], 1060 | }, 1061 | ], 1062 | }); 1063 | }); 1064 | 1065 | it('handles query errors from non-null top field errors', async () => { 1066 | const app = server(); 1067 | 1068 | app.use( 1069 | mount( 1070 | urlString(), 1071 | graphqlHTTP({ 1072 | schema: TestSchema, 1073 | }), 1074 | ), 1075 | ); 1076 | 1077 | const response = await request(app.listen()).get( 1078 | urlString({ 1079 | query: '{nonNullThrower}', 1080 | }), 1081 | ); 1082 | 1083 | expect(response.status).to.equal(500); 1084 | expect(JSON.parse(response.text)).to.deep.equal({ 1085 | data: null, 1086 | errors: [ 1087 | { 1088 | message: 'Throws!', 1089 | locations: [{ line: 1, column: 2 }], 1090 | path: ['nonNullThrower'], 1091 | }, 1092 | ], 1093 | }); 1094 | }); 1095 | 1096 | it('allows for custom error formatting to sanitize', async () => { 1097 | const app = server(); 1098 | 1099 | app.use( 1100 | mount( 1101 | urlString(), 1102 | graphqlHTTP({ 1103 | schema: TestSchema, 1104 | formatError(error) { 1105 | return { message: 'Custom error format: ' + error.message }; 1106 | }, 1107 | }), 1108 | ), 1109 | ); 1110 | 1111 | const response = await request(app.listen()).get( 1112 | urlString({ 1113 | query: '{thrower}', 1114 | }), 1115 | ); 1116 | 1117 | expect(response.status).to.equal(200); 1118 | expect(JSON.parse(response.text)).to.deep.equal({ 1119 | data: { thrower: null }, 1120 | errors: [ 1121 | { 1122 | message: 'Custom error format: Throws!', 1123 | }, 1124 | ], 1125 | }); 1126 | }); 1127 | 1128 | it('allows for custom error formatting to elaborate', async () => { 1129 | const app = server(); 1130 | 1131 | app.use( 1132 | mount( 1133 | urlString(), 1134 | graphqlHTTP({ 1135 | schema: TestSchema, 1136 | formatError(error) { 1137 | return { 1138 | message: error.message, 1139 | locations: error.locations, 1140 | stack: 'Stack trace', 1141 | }; 1142 | }, 1143 | }), 1144 | ), 1145 | ); 1146 | 1147 | const response = await request(app.listen()).get( 1148 | urlString({ 1149 | query: '{thrower}', 1150 | }), 1151 | ); 1152 | 1153 | expect(response.status).to.equal(200); 1154 | expect(JSON.parse(response.text)).to.deep.equal({ 1155 | data: { thrower: null }, 1156 | errors: [ 1157 | { 1158 | message: 'Throws!', 1159 | locations: [{ line: 1, column: 2 }], 1160 | stack: 'Stack trace', 1161 | }, 1162 | ], 1163 | }); 1164 | }); 1165 | 1166 | it('handles syntax errors caught by GraphQL', async () => { 1167 | const app = server(); 1168 | 1169 | app.use( 1170 | mount( 1171 | urlString(), 1172 | graphqlHTTP({ 1173 | schema: TestSchema, 1174 | }), 1175 | ), 1176 | ); 1177 | 1178 | const response = await request(app.listen()).get( 1179 | urlString({ 1180 | query: 'syntaxerror', 1181 | }), 1182 | ); 1183 | 1184 | expect(response.status).to.equal(400); 1185 | expect(JSON.parse(response.text)).to.deep.equal({ 1186 | errors: [ 1187 | { 1188 | message: 'Syntax Error: Unexpected Name "syntaxerror"', 1189 | locations: [{ line: 1, column: 1 }], 1190 | }, 1191 | ], 1192 | }); 1193 | }); 1194 | 1195 | it('handles errors caused by a lack of query', async () => { 1196 | const app = server(); 1197 | 1198 | app.use( 1199 | mount( 1200 | urlString(), 1201 | graphqlHTTP({ 1202 | schema: TestSchema, 1203 | }), 1204 | ), 1205 | ); 1206 | 1207 | const response = await request(app.listen()).get(urlString()); 1208 | 1209 | expect(response.status).to.equal(400); 1210 | expect(JSON.parse(response.text)).to.deep.equal({ 1211 | errors: [{ message: 'Must provide query string.' }], 1212 | }); 1213 | }); 1214 | 1215 | it('handles invalid JSON bodies', async () => { 1216 | const app = server(); 1217 | 1218 | app.use( 1219 | mount( 1220 | urlString(), 1221 | graphqlHTTP({ 1222 | schema: TestSchema, 1223 | }), 1224 | ), 1225 | ); 1226 | 1227 | const response = await request(app.listen()) 1228 | .post(urlString()) 1229 | .set('Content-Type', 'application/json') 1230 | .send('[]'); 1231 | 1232 | expect(response.status).to.equal(400); 1233 | expect(JSON.parse(response.text)).to.deep.equal({ 1234 | errors: [{ message: 'POST body sent invalid JSON.' }], 1235 | }); 1236 | }); 1237 | 1238 | it('handles incomplete JSON bodies', async () => { 1239 | const app = server(); 1240 | 1241 | app.use( 1242 | mount( 1243 | urlString(), 1244 | graphqlHTTP({ 1245 | schema: TestSchema, 1246 | }), 1247 | ), 1248 | ); 1249 | 1250 | const response = await request(app.listen()) 1251 | .post(urlString()) 1252 | .set('Content-Type', 'application/json') 1253 | .send('{"query":'); 1254 | 1255 | expect(response.status).to.equal(400); 1256 | expect(JSON.parse(response.text)).to.deep.equal({ 1257 | errors: [{ message: 'POST body sent invalid JSON.' }], 1258 | }); 1259 | }); 1260 | 1261 | it('handles plain POST text', async () => { 1262 | const app = server(); 1263 | 1264 | app.use( 1265 | mount( 1266 | urlString(), 1267 | graphqlHTTP({ 1268 | schema: TestSchema, 1269 | }), 1270 | ), 1271 | ); 1272 | 1273 | const response = await request(app.listen()) 1274 | .post( 1275 | urlString({ 1276 | variables: JSON.stringify({ who: 'Dolly' }), 1277 | }), 1278 | ) 1279 | .set('Content-Type', 'text/plain') 1280 | .send('query helloWho($who: String){ test(who: $who) }'); 1281 | 1282 | expect(response.status).to.equal(400); 1283 | expect(JSON.parse(response.text)).to.deep.equal({ 1284 | errors: [{ message: 'Must provide query string.' }], 1285 | }); 1286 | }); 1287 | 1288 | it('handles unsupported charset', async () => { 1289 | const app = server(); 1290 | 1291 | app.use( 1292 | mount( 1293 | urlString(), 1294 | graphqlHTTP({ 1295 | schema: TestSchema, 1296 | }), 1297 | ), 1298 | ); 1299 | 1300 | const response = await request(app.listen()) 1301 | .post(urlString()) 1302 | .set('Content-Type', 'application/graphql; charset=ascii') 1303 | .send('{ test(who: "World") }'); 1304 | 1305 | expect(response.status).to.equal(415); 1306 | expect(JSON.parse(response.text)).to.deep.equal({ 1307 | errors: [{ message: 'Unsupported charset "ASCII".' }], 1308 | }); 1309 | }); 1310 | 1311 | it('handles unsupported utf charset', async () => { 1312 | const app = server(); 1313 | 1314 | app.use( 1315 | mount( 1316 | urlString(), 1317 | graphqlHTTP({ 1318 | schema: TestSchema, 1319 | }), 1320 | ), 1321 | ); 1322 | 1323 | const response = await request(app.listen()) 1324 | .post(urlString()) 1325 | .set('Content-Type', 'application/graphql; charset=utf-53') 1326 | .send('{ test(who: "World") }'); 1327 | 1328 | expect(response.status).to.equal(415); 1329 | expect(JSON.parse(response.text)).to.deep.equal({ 1330 | errors: [{ message: 'Unsupported charset "UTF-53".' }], 1331 | }); 1332 | }); 1333 | 1334 | it('handles unknown encoding', async () => { 1335 | const app = server(); 1336 | 1337 | app.use( 1338 | mount( 1339 | urlString(), 1340 | graphqlHTTP({ 1341 | schema: TestSchema, 1342 | }), 1343 | ), 1344 | ); 1345 | 1346 | const response = await request(app.listen()) 1347 | .post(urlString()) 1348 | .set('Content-Encoding', 'garbage') 1349 | .send('!@#$%^*(&^$%#@'); 1350 | 1351 | expect(response.status).to.equal(415); 1352 | expect(JSON.parse(response.text)).to.deep.equal({ 1353 | errors: [{ message: 'Unsupported content-encoding "garbage".' }], 1354 | }); 1355 | }); 1356 | 1357 | it('handles poorly formed variables', async () => { 1358 | const app = server(); 1359 | 1360 | app.use( 1361 | mount( 1362 | urlString(), 1363 | graphqlHTTP({ 1364 | schema: TestSchema, 1365 | }), 1366 | ), 1367 | ); 1368 | 1369 | const response = await request(app.listen()).get( 1370 | urlString({ 1371 | variables: 'who:You', 1372 | query: 'query helloWho($who: String){ test(who: $who) }', 1373 | }), 1374 | ); 1375 | 1376 | expect(response.status).to.equal(400); 1377 | expect(JSON.parse(response.text)).to.deep.equal({ 1378 | errors: [{ message: 'Variables are invalid JSON.' }], 1379 | }); 1380 | }); 1381 | 1382 | it('handles unsupported HTTP methods', async () => { 1383 | const app = server(); 1384 | 1385 | app.use( 1386 | mount( 1387 | urlString(), 1388 | graphqlHTTP({ 1389 | schema: TestSchema, 1390 | }), 1391 | ), 1392 | ); 1393 | 1394 | const response = await request(app.listen()).put( 1395 | urlString({ query: '{test}' }), 1396 | ); 1397 | 1398 | expect(response.status).to.equal(405); 1399 | expect(response.headers.allow).to.equal('GET, POST'); 1400 | expect(JSON.parse(response.text)).to.deep.equal({ 1401 | errors: [{ message: 'GraphQL only supports GET and POST requests.' }], 1402 | }); 1403 | }); 1404 | }); 1405 | 1406 | describe('Built-in GraphiQL support', () => { 1407 | it('does not renders GraphiQL if no opt-in', async () => { 1408 | const app = server(); 1409 | 1410 | app.use(mount(urlString(), graphqlHTTP({ schema: TestSchema }))); 1411 | 1412 | const response = await request(app.listen()) 1413 | .get(urlString({ query: '{test}' })) 1414 | .set('Accept', 'text/html'); 1415 | 1416 | expect(response.status).to.equal(200); 1417 | expect(response.type).to.equal('application/json'); 1418 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1419 | }); 1420 | 1421 | it('presents GraphiQL when accepting HTML', async () => { 1422 | const app = server(); 1423 | 1424 | app.use( 1425 | mount( 1426 | urlString(), 1427 | graphqlHTTP({ 1428 | schema: TestSchema, 1429 | graphiql: true, 1430 | }), 1431 | ), 1432 | ); 1433 | 1434 | const response = await request(app.listen()) 1435 | .get(urlString({ query: '{test}' })) 1436 | .set('Accept', 'text/html'); 1437 | 1438 | expect(response.status).to.equal(200); 1439 | expect(response.type).to.equal('text/html'); 1440 | expect(response.text).to.include('graphiql.min.js'); 1441 | }); 1442 | 1443 | it('contains a pre-run response within GraphiQL', async () => { 1444 | const app = server(); 1445 | 1446 | app.use( 1447 | mount( 1448 | urlString(), 1449 | graphqlHTTP({ 1450 | schema: TestSchema, 1451 | graphiql: true, 1452 | }), 1453 | ), 1454 | ); 1455 | 1456 | const response = await request(app.listen()) 1457 | .get(urlString({ query: '{test}' })) 1458 | .set('Accept', 'text/html'); 1459 | 1460 | expect(response.status).to.equal(200); 1461 | expect(response.type).to.equal('text/html'); 1462 | expect(response.text).to.include( 1463 | 'response: ' + 1464 | JSON.stringify( 1465 | JSON.stringify({ data: { test: 'Hello World' } }, null, 2), 1466 | ), 1467 | ); 1468 | }); 1469 | 1470 | it('contains a pre-run operation name within GraphiQL', async () => { 1471 | const app = server(); 1472 | 1473 | app.use( 1474 | mount( 1475 | urlString(), 1476 | graphqlHTTP({ 1477 | schema: TestSchema, 1478 | graphiql: true, 1479 | }), 1480 | ), 1481 | ); 1482 | 1483 | const response = await request(app.listen()) 1484 | .get( 1485 | urlString({ 1486 | query: 'query A{a:test} query B{b:test}', 1487 | operationName: 'B', 1488 | }), 1489 | ) 1490 | .set('Accept', 'text/html'); 1491 | 1492 | expect(response.status).to.equal(200); 1493 | expect(response.type).to.equal('text/html'); 1494 | expect(response.text).to.include( 1495 | 'response: ' + 1496 | JSON.stringify( 1497 | JSON.stringify({ data: { b: 'Hello World' } }, null, 2), 1498 | ), 1499 | ); 1500 | expect(response.text).to.include('operationName: "B"'); 1501 | }); 1502 | 1503 | it('escapes HTML in queries within GraphiQL', async () => { 1504 | const app = server(); 1505 | 1506 | app.use( 1507 | mount( 1508 | urlString(), 1509 | graphqlHTTP({ 1510 | schema: TestSchema, 1511 | graphiql: true, 1512 | }), 1513 | ), 1514 | ); 1515 | 1516 | const response = await request(app.listen()) 1517 | .get(urlString({ query: '' })) 1518 | .set('Accept', 'text/html'); 1519 | 1520 | expect(response.status).to.equal(400); 1521 | expect(response.type).to.equal('text/html'); 1522 | expect(response.text).to.not.include( 1523 | '', 1524 | ); 1525 | }); 1526 | 1527 | it('escapes HTML in variables within GraphiQL', async () => { 1528 | const app = server(); 1529 | 1530 | app.use( 1531 | mount( 1532 | urlString(), 1533 | graphqlHTTP({ 1534 | schema: TestSchema, 1535 | graphiql: true, 1536 | }), 1537 | ), 1538 | ); 1539 | 1540 | const response = await request(app.listen()) 1541 | .get( 1542 | urlString({ 1543 | query: 'query helloWho($who: String) { test(who: $who) }', 1544 | variables: JSON.stringify({ 1545 | who: '', 1546 | }), 1547 | }), 1548 | ) 1549 | .set('Accept', 'text/html'); 1550 | 1551 | expect(response.status).to.equal(200); 1552 | expect(response.type).to.equal('text/html'); 1553 | expect(response.text).to.not.include( 1554 | '', 1555 | ); 1556 | }); 1557 | 1558 | it('GraphiQL renders provided variables', async () => { 1559 | const app = server(); 1560 | 1561 | app.use( 1562 | mount( 1563 | urlString(), 1564 | graphqlHTTP({ 1565 | schema: TestSchema, 1566 | graphiql: true, 1567 | }), 1568 | ), 1569 | ); 1570 | 1571 | const response = await request(app.listen()) 1572 | .get( 1573 | urlString({ 1574 | query: 'query helloWho($who: String) { test(who: $who) }', 1575 | variables: JSON.stringify({ who: 'Dolly' }), 1576 | }), 1577 | ) 1578 | .set('Accept', 'text/html'); 1579 | 1580 | expect(response.status).to.equal(200); 1581 | expect(response.type).to.equal('text/html'); 1582 | expect(response.text).to.include( 1583 | 'variables: ' + 1584 | JSON.stringify(JSON.stringify({ who: 'Dolly' }, null, 2)), 1585 | ); 1586 | }); 1587 | 1588 | it('GraphiQL accepts an empty query', async () => { 1589 | const app = server(); 1590 | 1591 | app.use( 1592 | mount( 1593 | urlString(), 1594 | graphqlHTTP({ 1595 | schema: TestSchema, 1596 | graphiql: true, 1597 | }), 1598 | ), 1599 | ); 1600 | 1601 | const response = await request(app.listen()) 1602 | .get(urlString()) 1603 | .set('Accept', 'text/html'); 1604 | 1605 | expect(response.status).to.equal(200); 1606 | expect(response.type).to.equal('text/html'); 1607 | expect(response.text).to.include('response: undefined'); 1608 | }); 1609 | 1610 | it('GraphiQL accepts a mutation query - does not execute it', async () => { 1611 | const app = server(); 1612 | 1613 | app.use( 1614 | mount( 1615 | urlString(), 1616 | graphqlHTTP({ 1617 | schema: TestSchema, 1618 | graphiql: true, 1619 | }), 1620 | ), 1621 | ); 1622 | 1623 | const response = await request(app.listen()) 1624 | .get( 1625 | urlString({ 1626 | query: 'mutation TestMutation { writeTest { test } }', 1627 | }), 1628 | ) 1629 | .set('Accept', 'text/html'); 1630 | 1631 | expect(response.status).to.equal(200); 1632 | expect(response.type).to.equal('text/html'); 1633 | expect(response.text).to.include( 1634 | 'query: "mutation TestMutation { writeTest { test } }"', 1635 | ); 1636 | expect(response.text).to.include('response: undefined'); 1637 | }); 1638 | 1639 | it('returns HTML if preferred', async () => { 1640 | const app = server(); 1641 | 1642 | app.use( 1643 | mount( 1644 | urlString(), 1645 | graphqlHTTP({ 1646 | schema: TestSchema, 1647 | graphiql: true, 1648 | }), 1649 | ), 1650 | ); 1651 | 1652 | const response = await request(app.listen()) 1653 | .get(urlString({ query: '{test}' })) 1654 | .set('Accept', 'text/html,application/json'); 1655 | 1656 | expect(response.status).to.equal(200); 1657 | expect(response.type).to.equal('text/html'); 1658 | expect(response.text).to.include('{test}'); 1659 | expect(response.text).to.include('graphiql.min.js'); 1660 | }); 1661 | 1662 | it('returns JSON if preferred', async () => { 1663 | const app = server(); 1664 | 1665 | app.use( 1666 | mount( 1667 | urlString(), 1668 | graphqlHTTP({ 1669 | schema: TestSchema, 1670 | graphiql: true, 1671 | }), 1672 | ), 1673 | ); 1674 | 1675 | const response = await request(app.listen()) 1676 | .get(urlString({ query: '{test}' })) 1677 | .set('Accept', 'application/json,text/html'); 1678 | 1679 | expect(response.status).to.equal(200); 1680 | expect(response.type).to.equal('application/json'); 1681 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1682 | }); 1683 | 1684 | it('prefers JSON if unknown accept', async () => { 1685 | const app = server(); 1686 | 1687 | app.use( 1688 | mount( 1689 | urlString(), 1690 | graphqlHTTP({ 1691 | schema: TestSchema, 1692 | graphiql: true, 1693 | }), 1694 | ), 1695 | ); 1696 | 1697 | const response = await request(app.listen()) 1698 | .get(urlString({ query: '{test}' })) 1699 | .set('Accept', 'unknown'); 1700 | 1701 | expect(response.status).to.equal(200); 1702 | expect(response.type).to.equal('application/json'); 1703 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1704 | }); 1705 | 1706 | it('prefers JSON if explicitly requested raw response', async () => { 1707 | const app = server(); 1708 | 1709 | app.use( 1710 | mount( 1711 | urlString(), 1712 | graphqlHTTP({ 1713 | schema: TestSchema, 1714 | graphiql: true, 1715 | }), 1716 | ), 1717 | ); 1718 | 1719 | const response = await request(app.listen()) 1720 | .get(urlString({ query: '{test}', raw: '' })) 1721 | .set('Accept', 'text/html'); 1722 | 1723 | expect(response.status).to.equal(200); 1724 | expect(response.type).to.equal('application/json'); 1725 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1726 | }); 1727 | }); 1728 | 1729 | describe('Custom validation rules', () => { 1730 | const AlwaysInvalidRule = function(context) { 1731 | return { 1732 | enter() { 1733 | context.reportError( 1734 | new GraphQLError('AlwaysInvalidRule was really invalid!'), 1735 | ); 1736 | return BREAK; 1737 | }, 1738 | }; 1739 | }; 1740 | 1741 | it('Do not execute a query if it do not pass the custom validation.', async () => { 1742 | const app = server(); 1743 | 1744 | app.use( 1745 | mount( 1746 | urlString(), 1747 | graphqlHTTP({ 1748 | schema: TestSchema, 1749 | validationRules: [AlwaysInvalidRule], 1750 | pretty: true, 1751 | }), 1752 | ), 1753 | ); 1754 | 1755 | const response = await request(app.listen()).get( 1756 | urlString({ 1757 | query: '{thrower}', 1758 | }), 1759 | ); 1760 | 1761 | expect(response.status).to.equal(400); 1762 | expect(JSON.parse(response.text)).to.deep.equal({ 1763 | errors: [ 1764 | { 1765 | message: 'AlwaysInvalidRule was really invalid!', 1766 | }, 1767 | ], 1768 | }); 1769 | }); 1770 | }); 1771 | 1772 | describe('Session support', () => { 1773 | it('supports koa-session', async () => { 1774 | const SessionAwareGraphQLSchema = new GraphQLSchema({ 1775 | query: new GraphQLObjectType({ 1776 | name: 'MyType', 1777 | fields: { 1778 | myField: { 1779 | type: GraphQLString, 1780 | resolve(parentValue, _, sess) { 1781 | return (sess: any).id; 1782 | }, 1783 | }, 1784 | }, 1785 | }), 1786 | }); 1787 | const app = server(); 1788 | app.keys = ['some secret hurr']; 1789 | app.use(session(app)); 1790 | app.use(async function(ctx, next) { 1791 | ctx.session.id = 'me'; 1792 | await next(); 1793 | }); 1794 | 1795 | app.use( 1796 | mount( 1797 | '/graphql', 1798 | graphqlHTTP((req, res, ctx) => ({ 1799 | schema: SessionAwareGraphQLSchema, 1800 | context: (ctx: any).session, 1801 | })), 1802 | ), 1803 | ); 1804 | 1805 | const response = await request(app.listen()).get( 1806 | urlString({ 1807 | query: '{myField}', 1808 | }), 1809 | ); 1810 | 1811 | expect(response.text).to.equal('{"data":{"myField":"me"}}'); 1812 | }); 1813 | }); 1814 | 1815 | describe('Custom result extensions', () => { 1816 | it('allows for adding extensions', async () => { 1817 | const app = server(); 1818 | 1819 | app.use( 1820 | mount( 1821 | urlString(), 1822 | graphqlHTTP(() => { 1823 | const startTime = 1000000000; /* Date.now(); */ 1824 | return { 1825 | schema: TestSchema, 1826 | extensions() { 1827 | return { runTime: 1000000010 /* Date.now() */ - startTime }; 1828 | }, 1829 | }; 1830 | }), 1831 | ), 1832 | ); 1833 | 1834 | const response = await request(app.listen()) 1835 | .get(urlString({ query: '{test}', raw: '' })) 1836 | .set('Accept', 'text/html'); 1837 | 1838 | expect(response.status).to.equal(200); 1839 | expect(response.type).to.equal('application/json'); 1840 | expect(response.text).to.equal( 1841 | '{"data":{"test":"Hello World"},"extensions":{"runTime":10}}', 1842 | ); 1843 | }); 1844 | 1845 | it('extensions have access to initial GraphQL result', async () => { 1846 | const app = server(); 1847 | 1848 | app.use( 1849 | mount( 1850 | urlString(), 1851 | graphqlHTTP({ 1852 | schema: TestSchema, 1853 | formatError: () => null, 1854 | extensions({ result }) { 1855 | return { preservedErrors: (result: any).errors }; 1856 | }, 1857 | }), 1858 | ), 1859 | ); 1860 | 1861 | const response = await request(app.listen()).get( 1862 | urlString({ 1863 | query: '{thrower}', 1864 | }), 1865 | ); 1866 | 1867 | expect(response.status).to.equal(200); 1868 | expect(JSON.parse(response.text)).to.deep.equal({ 1869 | data: { thrower: null }, 1870 | errors: [null], 1871 | extensions: { 1872 | preservedErrors: [ 1873 | { 1874 | message: 'Throws!', 1875 | locations: [{ line: 1, column: 2 }], 1876 | path: ['thrower'], 1877 | }, 1878 | ], 1879 | }, 1880 | }); 1881 | }); 1882 | 1883 | it('extension function may be async', async () => { 1884 | const app = server(); 1885 | 1886 | app.use( 1887 | mount( 1888 | urlString(), 1889 | graphqlHTTP({ 1890 | schema: TestSchema, 1891 | async extensions() { 1892 | // Note: you can await arbitrary things here! 1893 | return { eventually: 42 }; 1894 | }, 1895 | }), 1896 | ), 1897 | ); 1898 | 1899 | const response = await request(app.listen()) 1900 | .get(urlString({ query: '{test}', raw: '' })) 1901 | .set('Accept', 'text/html'); 1902 | 1903 | expect(response.status).to.equal(200); 1904 | expect(response.type).to.equal('application/json'); 1905 | expect(response.text).to.equal( 1906 | '{"data":{"test":"Hello World"},"extensions":{"eventually":42}}', 1907 | ); 1908 | }); 1909 | }); 1910 | }); 1911 | --------------------------------------------------------------------------------