├── .eslintignore ├── .eslintrc.yml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .mocharc.yml ├── .nycrc.yml ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── codecov.yml ├── cspell.json ├── examples ├── index.ts ├── index_subscription.ts ├── index_subscription_legacy.ts └── schema.ts ├── integrationTests ├── README.md ├── integration-test.js ├── node │ ├── index.js │ ├── package.json │ └── test.js └── ts │ ├── index.ts │ ├── package.json │ ├── test.js │ └── tsconfig.json ├── package-lock.json ├── package.json ├── resources ├── build-npm.js ├── checkgit.sh ├── eslint-internal-rules │ ├── README.md │ ├── index.js │ ├── no-dir-import.js │ └── package.json ├── gen-changelog.js ├── load-statically-from-npm.js ├── register.js └── utils.js ├── src ├── __tests__ │ ├── http-test.ts │ └── usage-test.ts ├── index.ts ├── parseBody.ts └── renderGraphiQL.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # Copied from '.gitignore', please keep it in sync. 2 | /node_modules 3 | /coverage 4 | /npmDist 5 | 6 | # Ignore TS files inside integration test 7 | /integrationTests/ts/*.ts 8 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | sourceType: script 3 | ecmaVersion: 2020 4 | env: 5 | es6: true 6 | node: true 7 | reportUnusedDisableDirectives: true 8 | plugins: 9 | - internal-rules 10 | - node 11 | - istanbul 12 | - import 13 | settings: 14 | node: 15 | tryExtensions: ['.js', '.json', '.node', '.ts', '.d.ts'] 16 | 17 | rules: 18 | ############################################################################## 19 | # Internal rules located in 'resources/eslint-internal-rules'. 20 | # See './resources/eslint-internal-rules/README.md' 21 | ############################################################################## 22 | 23 | internal-rules/no-dir-import: error 24 | 25 | ############################################################################## 26 | # `eslint-plugin-istanbul` rule list based on `v0.1.2` 27 | # https://github.com/istanbuljs/eslint-plugin-istanbul#rules 28 | ############################################################################## 29 | 30 | istanbul/no-ignore-file: error 31 | istanbul/prefer-ignore-reason: error 32 | 33 | ############################################################################## 34 | # `eslint-plugin-node` rule list based on `v11.1.x` 35 | ############################################################################## 36 | 37 | # Possible Errors 38 | # https://github.com/mysticatea/eslint-plugin-node#possible-errors 39 | 40 | node/handle-callback-err: [error, error] 41 | node/no-callback-literal: error 42 | node/no-exports-assign: error 43 | node/no-extraneous-import: error 44 | node/no-extraneous-require: error 45 | node/no-missing-import: error 46 | node/no-missing-require: error 47 | node/no-new-require: error 48 | node/no-path-concat: error 49 | node/no-process-exit: off 50 | node/no-unpublished-bin: error 51 | node/no-unpublished-import: error 52 | node/no-unpublished-require: error 53 | node/no-unsupported-features/es-builtins: error 54 | node/no-unsupported-features/es-syntax: error 55 | node/no-unsupported-features/node-builtins: error 56 | node/process-exit-as-throw: error 57 | node/shebang: error 58 | 59 | # Best Practices 60 | # https://github.com/mysticatea/eslint-plugin-node#best-practices 61 | node/no-deprecated-api: error 62 | 63 | # Stylistic Issues 64 | # https://github.com/mysticatea/eslint-plugin-node#stylistic-issues 65 | 66 | node/callback-return: error 67 | node/exports-style: off # TODO consider 68 | node/file-extension-in-import: off # TODO consider 69 | node/global-require: error 70 | node/no-mixed-requires: error 71 | node/no-process-env: off 72 | node/no-restricted-import: off 73 | node/no-restricted-require: off 74 | node/no-sync: error 75 | node/prefer-global/buffer: error 76 | node/prefer-global/console: error 77 | node/prefer-global/process: error 78 | node/prefer-global/text-decoder: error 79 | node/prefer-global/text-encoder: error 80 | node/prefer-global/url-search-params: error 81 | node/prefer-global/url: error 82 | node/prefer-promises/dns: error 83 | node/prefer-promises/fs: error 84 | 85 | ############################################################################## 86 | # `eslint-plugin-import` rule list based on `v2.22.x` 87 | ############################################################################## 88 | 89 | # Static analysis 90 | # https://github.com/benmosher/eslint-plugin-import#static-analysis 91 | import/no-unresolved: error 92 | import/named: error 93 | import/default: error 94 | import/namespace: error 95 | import/no-restricted-paths: off 96 | import/no-absolute-path: error 97 | import/no-dynamic-require: error 98 | import/no-internal-modules: off 99 | import/no-webpack-loader-syntax: error 100 | import/no-self-import: error 101 | import/no-cycle: error 102 | import/no-useless-path-segments: error 103 | import/no-relative-parent-imports: off 104 | 105 | # Helpful warnings 106 | # https://github.com/benmosher/eslint-plugin-import#helpful-warnings 107 | import/export: error 108 | import/no-named-as-default: error 109 | import/no-named-as-default-member: error 110 | import/no-deprecated: error 111 | import/no-extraneous-dependencies: [error, { devDependencies: false }] 112 | import/no-mutable-exports: error 113 | import/no-unused-modules: error 114 | 115 | # Module systems 116 | # https://github.com/benmosher/eslint-plugin-import#module-systems 117 | import/unambiguous: error 118 | import/no-commonjs: error 119 | import/no-amd: error 120 | import/no-nodejs-modules: off 121 | 122 | # Style guide 123 | # https://github.com/benmosher/eslint-plugin-import#style-guide 124 | import/first: error 125 | import/exports-last: off 126 | import/no-duplicates: error 127 | import/no-namespace: error 128 | import/extensions: [error, never] # TODO: switch to ignorePackages 129 | import/order: [error, { newlines-between: always-and-inside-groups }] 130 | import/newline-after-import: error 131 | import/prefer-default-export: off 132 | import/max-dependencies: off 133 | import/no-unassigned-import: error 134 | import/no-named-default: error 135 | import/no-default-export: off 136 | import/no-named-export: off 137 | import/no-anonymous-default-export: error 138 | import/group-exports: off 139 | import/dynamic-import-chunkname: off 140 | 141 | ############################################################################## 142 | # ESLint builtin rules list based on `v7.13.x` 143 | ############################################################################## 144 | 145 | # Possible Errors 146 | # https://eslint.org/docs/rules/#possible-errors 147 | 148 | for-direction: error 149 | getter-return: error 150 | no-async-promise-executor: error 151 | no-await-in-loop: error 152 | no-compare-neg-zero: error 153 | no-cond-assign: error 154 | no-console: warn 155 | no-constant-condition: error 156 | no-control-regex: error 157 | no-debugger: warn 158 | no-dupe-args: error 159 | no-dupe-else-if: error 160 | no-dupe-keys: error 161 | no-duplicate-case: error 162 | no-empty: error 163 | no-empty-character-class: error 164 | no-ex-assign: error 165 | no-extra-boolean-cast: error 166 | no-func-assign: error 167 | no-import-assign: error 168 | no-inner-declarations: [error, both] 169 | no-invalid-regexp: error 170 | no-irregular-whitespace: error 171 | no-loss-of-precision: error 172 | no-misleading-character-class: error 173 | no-obj-calls: error 174 | no-promise-executor-return: error 175 | no-prototype-builtins: error 176 | no-regex-spaces: error 177 | no-setter-return: error 178 | no-sparse-arrays: error 179 | no-template-curly-in-string: error 180 | no-unreachable: error 181 | no-unreachable-loop: error 182 | no-unsafe-finally: error 183 | no-unsafe-negation: error 184 | no-useless-backreference: error 185 | require-atomic-updates: error 186 | use-isnan: error 187 | valid-typeof: error 188 | 189 | # Best Practices 190 | # https://eslint.org/docs/rules/#best-practices 191 | 192 | accessor-pairs: error 193 | array-callback-return: error 194 | block-scoped-var: error 195 | class-methods-use-this: off 196 | complexity: off 197 | consistent-return: off 198 | curly: error 199 | default-case: off 200 | default-case-last: error 201 | default-param-last: error 202 | dot-notation: error 203 | eqeqeq: [error, smart] 204 | grouped-accessor-pairs: error 205 | guard-for-in: error 206 | max-classes-per-file: off 207 | no-alert: error 208 | no-caller: error 209 | no-case-declarations: error 210 | no-constructor-return: error 211 | no-div-regex: error 212 | no-else-return: error 213 | no-empty-function: error 214 | no-empty-pattern: error 215 | no-eq-null: off 216 | no-eval: error 217 | no-extend-native: error 218 | no-extra-bind: error 219 | no-extra-label: error 220 | no-fallthrough: error 221 | no-global-assign: error 222 | no-implicit-coercion: error 223 | no-implicit-globals: off 224 | no-implied-eval: error 225 | no-invalid-this: error 226 | no-iterator: error 227 | no-labels: error 228 | no-lone-blocks: error 229 | no-loop-func: error 230 | no-magic-numbers: off 231 | no-multi-str: error 232 | no-new: error 233 | no-new-func: error 234 | no-new-wrappers: error 235 | no-octal: error 236 | no-octal-escape: error 237 | no-param-reassign: error 238 | no-proto: error 239 | no-redeclare: error 240 | no-restricted-properties: off 241 | no-return-assign: error 242 | no-return-await: error 243 | no-script-url: error 244 | no-self-assign: error 245 | no-self-compare: error 246 | no-sequences: error 247 | no-throw-literal: error 248 | no-unmodified-loop-condition: error 249 | no-unused-expressions: error 250 | no-unused-labels: error 251 | no-useless-call: error 252 | no-useless-catch: error 253 | no-useless-concat: error 254 | no-useless-escape: error 255 | no-useless-return: error 256 | no-void: error 257 | no-warning-comments: off 258 | no-with: error 259 | prefer-named-capture-group: error 260 | prefer-promise-reject-errors: error 261 | prefer-regex-literals: error 262 | radix: error 263 | require-await: error 264 | require-unicode-regexp: off 265 | vars-on-top: error 266 | yoda: [error, never, { exceptRange: true }] 267 | 268 | # Strict Mode 269 | # https://eslint.org/docs/rules/#strict-mode 270 | 271 | strict: error 272 | 273 | # Variables 274 | # https://eslint.org/docs/rules/#variables 275 | 276 | init-declarations: off 277 | no-delete-var: error 278 | no-label-var: error 279 | no-restricted-globals: off 280 | no-shadow: error 281 | no-shadow-restricted-names: error 282 | no-undef: error 283 | no-undef-init: error 284 | no-undefined: off 285 | no-unused-vars: [error, { vars: all, args: all, argsIgnorePattern: '^_' }] 286 | no-use-before-define: off 287 | 288 | # Stylistic Issues 289 | # https://eslint.org/docs/rules/#stylistic-issues 290 | 291 | camelcase: error 292 | capitalized-comments: off # maybe 293 | consistent-this: off 294 | func-name-matching: off 295 | func-names: off 296 | func-style: off 297 | id-denylist: off 298 | id-length: off 299 | id-match: [error, '^(?:_?[a-zA-Z0-9]*)|[_A-Z0-9]+$'] 300 | line-comment-position: off 301 | lines-around-comment: off 302 | lines-between-class-members: [error, always, { exceptAfterSingleLine: true }] 303 | max-depth: off 304 | max-lines: off 305 | max-lines-per-function: off 306 | max-nested-callbacks: off 307 | max-params: off 308 | max-statements: off 309 | max-statements-per-line: off 310 | multiline-comment-style: off 311 | new-cap: error 312 | no-array-constructor: error 313 | no-bitwise: off 314 | no-continue: off 315 | no-inline-comments: off 316 | no-lonely-if: error 317 | no-multi-assign: off 318 | no-negated-condition: off 319 | no-nested-ternary: off 320 | no-new-object: error 321 | no-plusplus: off 322 | no-restricted-syntax: off 323 | no-tabs: error 324 | no-ternary: off 325 | no-underscore-dangle: error 326 | no-unneeded-ternary: error 327 | one-var: [error, never] 328 | operator-assignment: error 329 | padding-line-between-statements: off 330 | prefer-exponentiation-operator: error 331 | prefer-object-spread: error 332 | quotes: [error, single, { avoidEscape: true }] 333 | sort-keys: off 334 | sort-vars: off 335 | spaced-comment: error 336 | 337 | # ECMAScript 6 338 | # https://eslint.org/docs/rules/#ecmascript-6 339 | 340 | arrow-body-style: error 341 | constructor-super: error 342 | no-class-assign: error 343 | no-const-assign: error 344 | no-dupe-class-members: error 345 | no-duplicate-imports: off # Superseded by `import/no-duplicates` 346 | no-new-symbol: error 347 | no-restricted-exports: off 348 | no-restricted-imports: off 349 | no-this-before-super: error 350 | no-useless-computed-key: error 351 | no-useless-constructor: error 352 | no-useless-rename: error 353 | no-var: error 354 | object-shorthand: error 355 | prefer-arrow-callback: error 356 | prefer-const: error 357 | prefer-destructuring: off 358 | prefer-numeric-literals: error 359 | prefer-rest-params: error 360 | prefer-spread: error 361 | prefer-template: off 362 | require-yield: error 363 | sort-imports: off 364 | symbol-description: off 365 | 366 | # Bellow rules are disabled because coflicts with Prettier, see: 367 | # https://github.com/prettier/eslint-config-prettier/blob/master/index.js 368 | array-bracket-newline: off 369 | array-bracket-spacing: off 370 | array-element-newline: off 371 | arrow-parens: off 372 | arrow-spacing: off 373 | block-spacing: off 374 | brace-style: off 375 | comma-dangle: off 376 | comma-spacing: off 377 | comma-style: off 378 | computed-property-spacing: off 379 | dot-location: off 380 | eol-last: off 381 | func-call-spacing: off 382 | function-call-argument-newline: off 383 | function-paren-newline: off 384 | generator-star-spacing: off 385 | implicit-arrow-linebreak: off 386 | indent: off 387 | jsx-quotes: off 388 | key-spacing: off 389 | keyword-spacing: off 390 | linebreak-style: off 391 | max-len: off 392 | multiline-ternary: off 393 | newline-per-chained-call: off 394 | new-parens: off 395 | no-confusing-arrow: off 396 | no-extra-parens: off 397 | no-extra-semi: off 398 | no-floating-decimal: off 399 | no-mixed-operators: off 400 | no-mixed-spaces-and-tabs: off 401 | no-multi-spaces: off 402 | no-multiple-empty-lines: off 403 | no-trailing-spaces: off 404 | no-unexpected-multiline: off 405 | no-whitespace-before-property: off 406 | nonblock-statement-body-position: off 407 | object-curly-newline: off 408 | object-curly-spacing: off 409 | object-property-newline: off 410 | one-var-declaration-per-line: off 411 | operator-linebreak: off 412 | padded-blocks: off 413 | quote-props: off 414 | rest-spread-spacing: off 415 | semi: off 416 | semi-spacing: off 417 | semi-style: off 418 | space-before-blocks: off 419 | space-before-function-paren: off 420 | space-in-parens: off 421 | space-infix-ops: off 422 | space-unary-ops: off 423 | switch-colon-spacing: off 424 | template-curly-spacing: off 425 | template-tag-spacing: off 426 | unicode-bom: off 427 | wrap-iife: off 428 | wrap-regex: off 429 | yield-star-spacing: off 430 | 431 | overrides: 432 | - files: '**/*.ts' 433 | parser: '@typescript-eslint/parser' 434 | parserOptions: 435 | sourceType: module 436 | project: ['tsconfig.json'] 437 | plugins: 438 | - '@typescript-eslint' 439 | extends: 440 | - plugin:import/typescript 441 | rules: 442 | node/no-unsupported-features/es-syntax: off 443 | 444 | ########################################################################## 445 | # `@typescript-eslint/eslint-plugin` rule list based on `v4.8.x` 446 | ########################################################################## 447 | 448 | # Supported Rules 449 | # https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules 450 | '@typescript-eslint/adjacent-overload-signatures': error 451 | '@typescript-eslint/array-type': [error, { default: generic }] 452 | '@typescript-eslint/await-thenable': error 453 | '@typescript-eslint/ban-ts-comment': [error, { 'ts-expect-error': false }] 454 | '@typescript-eslint/ban-tslint-comment': error 455 | '@typescript-eslint/ban-types': error 456 | '@typescript-eslint/class-literal-property-style': error 457 | '@typescript-eslint/consistent-indexed-object-style': off # TODO enable 458 | '@typescript-eslint/consistent-type-assertions': 459 | [error, { assertionStyle: as, objectLiteralTypeAssertions: never }] 460 | '@typescript-eslint/consistent-type-definitions': off # TODO consider 461 | '@typescript-eslint/consistent-type-imports': error 462 | '@typescript-eslint/explicit-function-return-type': off # TODO consider 463 | '@typescript-eslint/explicit-member-accessibility': off # TODO consider 464 | '@typescript-eslint/explicit-module-boundary-types': off # TODO consider 465 | '@typescript-eslint/member-ordering': off # TODO consider 466 | '@typescript-eslint/method-signature-style': error 467 | '@typescript-eslint/naming-convention': off # TODO consider 468 | '@typescript-eslint/no-base-to-string': error 469 | '@typescript-eslint/no-confusing-non-null-assertion': error 470 | '@typescript-eslint/no-confusing-void-expression': off # FIXME 471 | '@typescript-eslint/no-dynamic-delete': off 472 | '@typescript-eslint/no-empty-interface': error 473 | '@typescript-eslint/no-explicit-any': off # TODO error 474 | '@typescript-eslint/no-extra-non-null-assertion': error 475 | '@typescript-eslint/no-extraneous-class': off # TODO consider 476 | '@typescript-eslint/no-floating-promises': error 477 | '@typescript-eslint/no-for-in-array': error 478 | '@typescript-eslint/no-implicit-any-catch': error 479 | '@typescript-eslint/no-implied-eval': error 480 | '@typescript-eslint/no-inferrable-types': 481 | [error, { ignoreParameters: true, ignoreProperties: true }] 482 | '@typescript-eslint/no-misused-new': error 483 | '@typescript-eslint/no-misused-promises': error 484 | '@typescript-eslint/no-namespace': error 485 | '@typescript-eslint/no-non-null-asserted-optional-chain': error 486 | '@typescript-eslint/no-non-null-assertion': error 487 | '@typescript-eslint/no-parameter-properties': error 488 | '@typescript-eslint/no-invalid-void-type': error 489 | '@typescript-eslint/no-require-imports': error 490 | '@typescript-eslint/no-this-alias': error 491 | '@typescript-eslint/no-throw-literal': error 492 | '@typescript-eslint/no-type-alias': off # TODO consider 493 | '@typescript-eslint/no-unnecessary-boolean-literal-compare': error 494 | '@typescript-eslint/no-unnecessary-condition': error 495 | '@typescript-eslint/no-unnecessary-qualifier': error 496 | '@typescript-eslint/no-unnecessary-type-arguments': error 497 | '@typescript-eslint/no-unnecessary-type-assertion': error 498 | '@typescript-eslint/no-unnecessary-type-constraint': off # TODO consider 499 | '@typescript-eslint/no-unsafe-assignment': off # TODO consider 500 | '@typescript-eslint/no-unsafe-call': off # TODO consider 501 | '@typescript-eslint/no-unsafe-member-access': off # TODO consider 502 | '@typescript-eslint/no-unsafe-return': off # TODO consider 503 | '@typescript-eslint/no-var-requires': error 504 | '@typescript-eslint/prefer-as-const': off # TODO consider 505 | '@typescript-eslint/prefer-enum-initializers': off # TODO consider 506 | '@typescript-eslint/prefer-for-of': error 507 | '@typescript-eslint/prefer-function-type': error 508 | '@typescript-eslint/prefer-includes': error 509 | '@typescript-eslint/prefer-literal-enum-member': error 510 | '@typescript-eslint/prefer-namespace-keyword': error 511 | '@typescript-eslint/prefer-nullish-coalescing': error 512 | '@typescript-eslint/prefer-optional-chain': error 513 | '@typescript-eslint/prefer-readonly': error 514 | '@typescript-eslint/prefer-readonly-parameter-types': off # TODO consider 515 | '@typescript-eslint/prefer-reduce-type-parameter': error 516 | '@typescript-eslint/prefer-regexp-exec': error 517 | '@typescript-eslint/prefer-ts-expect-error': error 518 | '@typescript-eslint/prefer-string-starts-ends-with': error 519 | '@typescript-eslint/promise-function-async': off 520 | '@typescript-eslint/require-array-sort-compare': error 521 | '@typescript-eslint/restrict-plus-operands': 522 | [error, { checkCompoundAssignments: true }] 523 | '@typescript-eslint/restrict-template-expressions': error 524 | '@typescript-eslint/strict-boolean-expressions': error 525 | '@typescript-eslint/switch-exhaustiveness-check': error 526 | '@typescript-eslint/triple-slash-reference': error 527 | '@typescript-eslint/typedef': off 528 | '@typescript-eslint/unbound-method': off # TODO consider 529 | '@typescript-eslint/unified-signatures': error 530 | 531 | # Extension Rules 532 | # https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#extension-rules 533 | 534 | # Disable conflicting ESLint rules and enable TS-compatible ones 535 | default-param-last: off 536 | dot-notation: off 537 | lines-between-class-members: off 538 | no-array-constructor: off 539 | no-dupe-class-members: off 540 | no-empty-function: off 541 | no-invalid-this: off 542 | no-loop-func: off 543 | no-loss-of-precision: off 544 | no-redeclare: off 545 | no-shadow: off 546 | no-unused-expressions: off 547 | no-unused-vars: off 548 | no-useless-constructor: off 549 | require-await: off 550 | no-return-await: off 551 | '@typescript-eslint/default-param-last': error 552 | '@typescript-eslint/dot-notation': error 553 | '@typescript-eslint/lines-between-class-members': 554 | [error, always, { exceptAfterSingleLine: true }] 555 | '@typescript-eslint/no-array-constructor': error 556 | '@typescript-eslint/no-dupe-class-members': error 557 | '@typescript-eslint/no-empty-function': error 558 | '@typescript-eslint/no-invalid-this': error 559 | '@typescript-eslint/no-loop-func': error 560 | '@typescript-eslint/no-loss-of-precision': error 561 | '@typescript-eslint/no-redeclare': error 562 | '@typescript-eslint/no-shadow': error 563 | '@typescript-eslint/no-unused-expressions': error 564 | '@typescript-eslint/no-unused-vars': 565 | [ 566 | error, 567 | { 568 | vars: all, 569 | args: all, 570 | argsIgnorePattern: '^_', 571 | varsIgnorePattern: '^_T', 572 | }, 573 | ] 574 | '@typescript-eslint/no-useless-constructor': error 575 | '@typescript-eslint/require-await': error 576 | '@typescript-eslint/return-await': error 577 | 578 | # Disable for JS, Flow and TS 579 | '@typescript-eslint/init-declarations': off 580 | '@typescript-eslint/no-magic-numbers': off 581 | '@typescript-eslint/no-use-before-define': off 582 | '@typescript-eslint/no-duplicate-imports': off # Superseded by `import/no-duplicates` 583 | 584 | # Bellow rules are disabled because coflicts with Prettier, see: 585 | # https://github.com/prettier/eslint-config-prettier/blob/master/%40typescript-eslint.js 586 | '@typescript-eslint/quotes': off 587 | '@typescript-eslint/brace-style': off 588 | '@typescript-eslint/comma-dangle': off 589 | '@typescript-eslint/comma-spacing': off 590 | '@typescript-eslint/func-call-spacing': off 591 | '@typescript-eslint/indent': off 592 | '@typescript-eslint/keyword-spacing': off 593 | '@typescript-eslint/member-delimiter-style': off 594 | '@typescript-eslint/no-extra-parens': off 595 | '@typescript-eslint/no-extra-semi': off 596 | '@typescript-eslint/semi': off 597 | '@typescript-eslint/space-before-function-paren': off 598 | '@typescript-eslint/space-infix-ops': off 599 | '@typescript-eslint/type-annotation-spacing': off 600 | - files: ['src/**/__*__/**', 'integrationTests/**'] 601 | rules: 602 | node/no-unpublished-import: off 603 | node/no-unpublished-require: off 604 | node/no-sync: off 605 | import/no-restricted-paths: off 606 | import/no-extraneous-dependencies: [error, { devDependencies: true }] 607 | import/no-nodejs-modules: off 608 | - files: 'integrationTests/*/**' 609 | rules: 610 | node/no-missing-require: off 611 | no-console: off 612 | - files: 'resources/**' 613 | rules: 614 | node/no-unpublished-import: off 615 | node/no-unpublished-require: off 616 | node/no-missing-require: off 617 | node/no-sync: off 618 | node/global-require: off 619 | import/no-dynamic-require: off 620 | import/no-extraneous-dependencies: [error, { devDependencies: true }] 621 | import/no-nodejs-modules: off 622 | import/no-commonjs: off 623 | no-await-in-loop: off 624 | no-console: off 625 | - files: 'examples/**' 626 | rules: 627 | internal-rules/no-dir-import: off 628 | node/no-unpublished-import: off 629 | import/no-extraneous-dependencies: [error, { devDependencies: true }] 630 | no-console: off 631 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | env: 4 | NODE_VERSION_USED_FOR_DEVELOPMENT: 14 5 | jobs: 6 | lint: 7 | name: Lint source files 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repo 11 | uses: actions/checkout@v2 12 | 13 | - name: Setup Node.js 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ env.NODE_VERSION_USED_FOR_DEVELOPMENT }} 17 | 18 | - name: Cache Node.js modules 19 | uses: actions/cache@v2 20 | with: 21 | path: ~/.npm 22 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 23 | restore-keys: | 24 | ${{ runner.OS }}-node- 25 | 26 | - name: Install Dependencies 27 | run: npm ci 28 | 29 | - name: Lint ESLint 30 | run: npm run lint 31 | 32 | - name: Lint Flow 33 | run: npm run check 34 | 35 | - name: Lint Prettier 36 | run: npm run prettier:check 37 | 38 | - name: Spellcheck 39 | run: npm run check:spelling 40 | 41 | checkForCommonlyIgnoredFiles: 42 | name: Check for commonly ignored files 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Checkout repo 46 | uses: actions/checkout@v2 47 | 48 | - name: Check if commit contains files that should be ignored 49 | run: | 50 | git clone --depth 1 https://github.com/github/gitignore.git && 51 | cat gitignore/Node.gitignore $(find gitignore/Global -name "*.gitignore" | grep -v ModelSim) > all.gitignore && 52 | if [[ "$(git ls-files -iX all.gitignore)" != "" ]]; then 53 | echo "::error::Please remove these files:" 54 | git ls-files -iX all.gitignore 55 | exit 1 56 | fi 57 | 58 | integrationTests: 59 | name: Run integration tests 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Checkout repo 63 | uses: actions/checkout@v2 64 | 65 | - name: Setup Node.js 66 | uses: actions/setup-node@v1 67 | with: 68 | node-version: ${{ env.NODE_VERSION_USED_FOR_DEVELOPMENT }} 69 | 70 | # We install bunch of packages during integration tests without locking them 71 | # so we skip cache action to not pollute cache for other jobs. 72 | - name: Install Dependencies 73 | run: npm ci 74 | 75 | - name: Build NPM package 76 | run: npm run build:npm 77 | 78 | - name: Run Integration Tests 79 | run: npm run check:integrations 80 | 81 | coverage: 82 | name: Measure test coverage 83 | runs-on: ubuntu-latest 84 | steps: 85 | - name: Checkout repo 86 | uses: actions/checkout@v2 87 | 88 | - name: Setup Node.js 89 | uses: actions/setup-node@v1 90 | with: 91 | node-version: ${{ env.NODE_VERSION_USED_FOR_DEVELOPMENT }} 92 | 93 | - name: Cache Node.js modules 94 | uses: actions/cache@v2 95 | with: 96 | path: ~/.npm 97 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 98 | restore-keys: | 99 | ${{ runner.OS }}-node- 100 | 101 | - name: Install Dependencies 102 | run: npm ci 103 | 104 | - name: Run tests and measure code coverage 105 | run: npm run testonly:cover 106 | 107 | - name: Upload coverage to Codecov 108 | if: ${{ always() }} 109 | uses: codecov/codecov-action@v1 110 | with: 111 | file: ./coverage/coverage-final.json 112 | fail_ci_if_error: true 113 | 114 | test: 115 | name: Run tests on Node v${{ matrix.node_version_to_setup }} 116 | runs-on: ubuntu-latest 117 | strategy: 118 | matrix: 119 | node_version_to_setup: [10, 12, 14, 16] 120 | steps: 121 | - name: Checkout repo 122 | uses: actions/checkout@v2 123 | 124 | - name: Setup Node.js v${{ matrix.node_version_to_setup }} 125 | uses: actions/setup-node@v1 126 | with: 127 | node-version: ${{ matrix.node_version_to_setup }} 128 | 129 | - name: Cache Node.js modules 130 | uses: actions/cache@v2 131 | with: 132 | path: ~/.npm 133 | key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} 134 | restore-keys: | 135 | ${{ runner.OS }}-node- 136 | 137 | - name: Install Dependencies 138 | run: npm ci 139 | 140 | - name: Run Tests 141 | run: npm run testonly 142 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore only ignores files specific to this repository. 2 | # If you see other files generated by your OS or tools you use, consider 3 | # creating a global .gitignore file. 4 | # 5 | # https://help.github.com/articles/ignoring-files/#create-a-global-gitignore 6 | # https://www.gitignore.io/ 7 | 8 | /node_modules 9 | /coverage 10 | /npmDist 11 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | check-leaks: true 2 | require: 3 | - './resources/register.js' 4 | -------------------------------------------------------------------------------- /.nycrc.yml: -------------------------------------------------------------------------------- 1 | all: true 2 | include: 3 | - 'src/' 4 | exclude: [] 5 | clean: true 6 | temp-directory: 'coverage' 7 | report-dir: 'coverage' 8 | skip-full: true 9 | reporter: [json, html, text] 10 | check-coverage: true 11 | branches: 100 12 | lines: 100 13 | functions: 100 14 | statements: 100 15 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Copied from '.gitignore', please keep it in sync. 2 | /node_modules 3 | /coverage 4 | /npmDist 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GraphQL Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | _If you still need to use `express-graphql`, please [read the previous version of this readme](https://github.com/graphql/express-graphql/blob/8b6ffc65776aa40d9e03f554425a1dc14840b165/README.md)._ 2 | 3 | # This library is deprecated 4 | 5 | `express-graphql` was the first official reference implementation of using GraphQL with HTTP. It has existed since 2015 and was mostly unmaintained in recent years. 6 | 7 | The official [GraphQL over HTTP](https://github.com/graphql/graphql-over-http) work group is standardizing the way you transport GraphQL over HTTP and it made great progress bringing up the need for a fresh reference implementation. 8 | 9 | Please read the [GraphQL over HTTP spec](https://graphql.github.io/graphql-over-http) for detailed implementation information. 10 | 11 | ## Say hello to [`graphql-http`](https://github.com/graphql/graphql-http) 12 | 13 | [`graphql-http`](https://github.com/graphql/graphql-http) is now the GraphQL official reference implementation of the [GraphQL over HTTP spec](https://graphql.github.io/graphql-over-http). 14 | 15 | ## For users 16 | 17 | As a reference implementation, [`graphql-http`](https://github.com/graphql/graphql-http) implements exclusively the [GraphQL over HTTP spec](https://graphql.github.io/graphql-over-http/). 18 | 19 | In case you're seeking for a full-featured experience (with file uploads, @defer/@stream directives, subscriptions, etc.), you're recommended to use some of the great JavaScript GraphQL server options: 20 | 21 | - [`graphql-yoga`](https://www.the-guild.dev/graphql/yoga-server) ([compliant (0 warnings)](https://github.com/graphql/graphql-http/tree/master/implementations/graphql-yoga), [migration guide](https://www.the-guild.dev/graphql/yoga-server/v3/migration/migration-from-express-graphql)) 22 | - [`postgraphile`](https://www.graphile.org/postgraphile/) ([compliant](https://github.com/graphql/graphql-http/tree/master/implementations/postgraphile)) 23 | - [`apollo-server`](https://www.apollographql.com/docs/apollo-server/) ([compliant](https://github.com/graphql/graphql-http/tree/master/implementations/apollo-server)) 24 | - [`mercurius`](https://mercurius.dev/) ([compliant](https://github.com/graphql/graphql-http/tree/master/implementations/mercurius)) 25 | - _\*To add your JavaScript server here, please first add it to [graphql-http/implementations](https://github.com/graphql/graphql-http/tree/master/implementations/)_ 26 | 27 | ## For library authors 28 | 29 | Being the official [GraphQL over HTTP spec](https://graphql.github.io/graphql-over-http/) reference implementation, [`graphql-http`](https://github.com/graphql/graphql-http) follows the specification strictly without any additional features (like file uploads, @stream/@defer directives and subscriptions). 30 | 31 | Having said this, [`graphql-http`](https://github.com/graphql/graphql-http) is mostly aimed for library authors and simple server setups, where the requirements are exact to what the aforementioned spec offers. 32 | 33 | ### Spec compliance audit suite 34 | 35 | Suite of tests used to audit an HTTP server for [GraphQL over HTTP spec](https://graphql.github.io/graphql-over-http) compliance is [available in `graphql-http`](https://github.com/graphql/graphql-http/blob/master/src/audits/server.ts) and you can use it to check your own, or other, servers! 36 | 37 | Additionally, `graphql-http` will maintain a list of GraphQL servers in the ecosystem and share their compliance results ([see them here](https://github.com/graphql/graphql-http/tree/master/implementations)). 38 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | parsers: 6 | javascript: 7 | enable_partials: yes 8 | 9 | comment: no 10 | coverage: 11 | status: 12 | project: 13 | default: 14 | target: auto 15 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "language": "en", 3 | "ignorePaths": [ 4 | // Copied from '.gitignore', please keep it in sync. 5 | ".eslintcache", 6 | "node_modules", 7 | "coverage", 8 | "npmDist", 9 | 10 | // Excluded from spelling check 11 | "cspell.json", 12 | "package.json", 13 | "package-lock.json", 14 | "tsconfig.json" 15 | ], 16 | "words": [ 17 | "graphiql", 18 | "unfetch", 19 | "noindex", 20 | "codecov", 21 | "recognise", 22 | "serializable", 23 | "subcommand", 24 | "charsets", 25 | "downlevel", 26 | "mercurius", 27 | "postgraphile", 28 | 29 | // TODO: remove bellow words 30 | "Graphi", // GraphiQL 31 | "QL's" // GraphQL's 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /examples/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { buildSchema } from 'graphql'; 3 | 4 | import { graphqlHTTP } from '../src'; 5 | 6 | // Construct a schema, using GraphQL schema language 7 | const schema = buildSchema(` 8 | type Query { 9 | hello: String 10 | } 11 | `); 12 | 13 | // The root provides a resolver function for each API endpoint 14 | const rootValue = { 15 | hello: () => 'Hello world!', 16 | }; 17 | 18 | const app = express(); 19 | app.use( 20 | '/graphql', 21 | graphqlHTTP({ 22 | schema, 23 | rootValue, 24 | graphiql: { headerEditorEnabled: true }, 25 | }), 26 | ); 27 | app.listen(4000); 28 | console.log('Running a GraphQL API server at http://localhost:4000/graphql'); 29 | -------------------------------------------------------------------------------- /examples/index_subscription.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | 3 | import express from 'express'; 4 | import { execute, subscribe } from 'graphql'; 5 | import ws from 'ws'; 6 | import { useServer } from 'graphql-ws/lib/use/ws'; 7 | 8 | import { graphqlHTTP } from '../src'; 9 | 10 | import { schema, roots, rootValue } from './schema'; 11 | 12 | const PORT = 4000; 13 | const subscriptionEndpoint = `ws://localhost:${PORT}/subscriptions`; 14 | 15 | const app = express(); 16 | app.use( 17 | '/graphql', 18 | graphqlHTTP({ 19 | schema, 20 | rootValue, 21 | graphiql: { 22 | subscriptionEndpoint, 23 | websocketClient: 'v1', 24 | }, 25 | }), 26 | ); 27 | 28 | const server = createServer(app); 29 | 30 | const wsServer = new ws.Server({ 31 | server, 32 | path: '/subscriptions', 33 | }); 34 | 35 | server.listen(PORT, () => { 36 | useServer( 37 | { 38 | schema, 39 | roots, 40 | execute, 41 | subscribe, 42 | }, 43 | wsServer, 44 | ); 45 | console.info( 46 | `Running a GraphQL API server with subscriptions at http://localhost:${PORT}/graphql`, 47 | ); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/index_subscription_legacy.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | 3 | import express from 'express'; 4 | import { execute, subscribe } from 'graphql'; 5 | import { SubscriptionServer } from 'subscriptions-transport-ws'; 6 | 7 | import { graphqlHTTP } from '../src'; 8 | 9 | import { schema, rootValue } from './schema'; 10 | 11 | const PORT = 4000; 12 | const subscriptionEndpoint = `ws://localhost:${PORT}/subscriptions`; 13 | 14 | const app = express(); 15 | app.use( 16 | '/graphql', 17 | graphqlHTTP({ 18 | schema, 19 | rootValue, 20 | graphiql: { subscriptionEndpoint }, 21 | }), 22 | ); 23 | 24 | const ws = createServer(app); 25 | 26 | ws.listen(PORT, () => { 27 | console.log( 28 | `Running a GraphQL API server with subscriptions at http://localhost:${PORT}/graphql`, 29 | ); 30 | }); 31 | 32 | const onConnect = (_: any, __: any) => { 33 | console.log('connecting ....'); 34 | }; 35 | 36 | const onDisconnect = (_: any) => { 37 | console.log('disconnecting ...'); 38 | }; 39 | 40 | SubscriptionServer.create( 41 | { 42 | schema, 43 | rootValue, 44 | execute, 45 | subscribe, 46 | onConnect, 47 | onDisconnect, 48 | }, 49 | { 50 | server: ws, 51 | path: '/subscriptions', 52 | }, 53 | ); 54 | -------------------------------------------------------------------------------- /examples/schema.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'graphql'; 2 | 3 | function sleep(ms: number) { 4 | return new Promise((resolve) => { 5 | setTimeout(resolve, ms); 6 | }); 7 | } 8 | 9 | export const schema = buildSchema(` 10 | type Query { 11 | hello: String 12 | } 13 | type Subscription { 14 | countDown: Int 15 | } 16 | `); 17 | 18 | export const roots = { 19 | Query: { 20 | hello: () => 'Hello World!', 21 | }, 22 | subscription: { 23 | /* eslint no-await-in-loop: "off" */ 24 | 25 | countDown: async function* fiveToOne() { 26 | for (const number of [5, 4, 3, 2, 1]) { 27 | await sleep(1000); // slow down a bit so user can see the count down on GraphiQL 28 | yield { countDown: number }; 29 | } 30 | }, 31 | }, 32 | }; 33 | 34 | export const rootValue = { 35 | hello: roots.Query.hello, 36 | countDown: roots.subscription.countDown, 37 | }; 38 | -------------------------------------------------------------------------------- /integrationTests/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /integrationTests/integration-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const os = require('os'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const childProcess = require('child_process'); 7 | 8 | const { describe, it } = require('mocha'); 9 | 10 | function exec(command, options = {}) { 11 | const result = childProcess.execSync(command, { 12 | encoding: 'utf-8', 13 | ...options, 14 | }); 15 | return result != null ? result.trimEnd() : result; 16 | } 17 | 18 | describe('Integration Tests', () => { 19 | const tmpDir = path.join(os.tmpdir(), 'express-graphql-integrationTmp'); 20 | fs.rmdirSync(tmpDir, { recursive: true, force: true }); 21 | fs.mkdirSync(tmpDir); 22 | 23 | const distDir = path.resolve('./npmDist'); 24 | const archiveName = exec(`npm --quiet pack ${distDir}`, { cwd: tmpDir }); 25 | fs.renameSync( 26 | path.join(tmpDir, archiveName), 27 | path.join(tmpDir, 'express-graphql.tgz'), 28 | ); 29 | 30 | function testOnNodeProject(projectName) { 31 | exec(`cp -R ${path.join(__dirname, projectName)} ${tmpDir}`); 32 | 33 | const cwd = path.join(tmpDir, projectName); 34 | exec('npm --quiet install', { cwd, stdio: 'inherit' }); 35 | exec('npm --quiet test', { cwd, stdio: 'inherit' }); 36 | } 37 | 38 | it('Should compile with all supported TS versions', () => { 39 | testOnNodeProject('ts'); 40 | }).timeout(40000); 41 | 42 | it('Should work on all supported node versions', () => { 43 | testOnNodeProject('node'); 44 | }).timeout(40000); 45 | }); 46 | -------------------------------------------------------------------------------- /integrationTests/node/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | 5 | const { buildSchema } = require('graphql'); 6 | 7 | const { graphqlHTTP } = require('express-graphql'); 8 | 9 | const schema = buildSchema('type Query { hello: String }'); 10 | 11 | const middleware = graphqlHTTP({ 12 | graphiql: true, 13 | schema, 14 | rootValue: { hello: 'world' }, 15 | }); 16 | 17 | assert(typeof middleware === 'function'); 18 | 19 | const request = { 20 | url: 'http://example.com', 21 | method: 'GET', 22 | headers: {}, 23 | body: { 24 | query: '{ hello }', 25 | }, 26 | }; 27 | 28 | const response = { 29 | headers: {}, 30 | setHeader(name, value) { 31 | this.headers[name] = value; 32 | }, 33 | text: null, 34 | end(buffer) { 35 | this.text = buffer.toString(); 36 | }, 37 | }; 38 | 39 | middleware(request, response).then(() => { 40 | assert.deepStrictEqual(response.headers, { 41 | 'Content-Length': '26', 42 | 'Content-Type': 'application/json; charset=utf-8', 43 | }); 44 | assert.deepStrictEqual(response.text, '{"data":{"hello":"world"}}'); 45 | }); 46 | -------------------------------------------------------------------------------- /integrationTests/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "node test.js" 5 | }, 6 | "dependencies": { 7 | "express-graphql": "file:../express-graphql.tgz", 8 | "graphql": "14.7.0", 9 | "node-10": "npm:node@10.x.x", 10 | "node-12": "npm:node@12.x.x", 11 | "node-14": "npm:node@14.x.x", 12 | "node-16": "npm:node@16.x.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integrationTests/node/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const childProcess = require('child_process'); 5 | 6 | const { dependencies } = require('./package.json'); 7 | 8 | const nodeVersions = Object.keys(dependencies) 9 | .filter((pkg) => pkg.startsWith('node-')) 10 | .sort((a, b) => b.localeCompare(a)); 11 | 12 | for (const version of nodeVersions) { 13 | console.log(`Testing on ${version} ...`); 14 | 15 | const nodePath = path.join(__dirname, 'node_modules', version, 'bin/node'); 16 | childProcess.execSync(nodePath + ' index.js', { stdio: 'inherit' }); 17 | } 18 | -------------------------------------------------------------------------------- /integrationTests/ts/index.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'graphql'; 2 | 3 | // eslint-disable-next-line import/no-unresolved, node/no-missing-import 4 | import { graphqlHTTP, RequestInfo } from 'express-graphql'; 5 | 6 | const schema = buildSchema('type Query { hello: String }'); 7 | 8 | const validationRules = [ 9 | () => ({ Field: () => false }), 10 | () => ({ Variable: () => true }), 11 | ]; 12 | 13 | graphqlHTTP({ 14 | graphiql: true, 15 | schema, 16 | customFormatErrorFn: (error: Error) => ({ 17 | message: error.message, 18 | }), 19 | validationRules, 20 | extensions: () => ({ 21 | key: 'value', 22 | key2: 'value', 23 | }), 24 | }); 25 | 26 | graphqlHTTP((request) => ({ 27 | graphiql: true, 28 | schema, 29 | context: request.headers, 30 | validationRules, 31 | })); 32 | 33 | graphqlHTTP(async (request) => ({ 34 | graphiql: true, 35 | schema: await Promise.resolve(schema), 36 | context: request.headers, 37 | extensions: (_args: RequestInfo) => ({}), 38 | validationRules, 39 | })); 40 | -------------------------------------------------------------------------------- /integrationTests/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "test": "node test.js" 5 | }, 6 | "dependencies": { 7 | "@types/node": "14.0.13", 8 | "express-graphql": "file:../express-graphql.tgz", 9 | "graphql": "14.7.0", 10 | "typescript-3.4": "npm:typescript@3.4.x", 11 | "typescript-3.5": "npm:typescript@3.5.x", 12 | "typescript-3.6": "npm:typescript@3.6.x", 13 | "typescript-3.7": "npm:typescript@3.7.x", 14 | "typescript-3.8": "npm:typescript@3.8.x", 15 | "typescript-3.9": "npm:typescript@3.9.x", 16 | "typescript-4.0": "npm:typescript@4.0.x", 17 | "typescript-4.1": "npm:typescript@4.1.x", 18 | "typescript-4.2": "npm:typescript@4.2.x", 19 | "typescript-4.3": "npm:typescript@4.3.x" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integrationTests/ts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const childProcess = require('child_process'); 5 | 6 | const { dependencies } = require('./package.json'); 7 | 8 | const tsVersions = Object.keys(dependencies) 9 | .filter((pkg) => pkg.startsWith('typescript-')) 10 | .sort((a, b) => b.localeCompare(a)); 11 | 12 | for (const version of tsVersions) { 13 | console.log(`Testing on ${version} ...`); 14 | 15 | const tscPath = path.join(__dirname, 'node_modules', version, 'bin/tsc'); 16 | childProcess.execSync(tscPath, { stdio: 'inherit' }); 17 | } 18 | -------------------------------------------------------------------------------- /integrationTests/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es6", "esnext.asynciterable"], 5 | "strict": true, 6 | "noEmit": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-graphql", 3 | "version": "0.12.0", 4 | "description": "Production ready GraphQL HTTP middleware.", 5 | "license": "MIT", 6 | "private": true, 7 | "main": "index.js", 8 | "types": "index.d.ts", 9 | "typesVersions": { 10 | "<3.8": { 11 | "*": [ 12 | "ts3.4/*" 13 | ] 14 | } 15 | }, 16 | "sideEffects": false, 17 | "homepage": "https://github.com/graphql/express-graphql", 18 | "bugs": { 19 | "url": "https://github.com/graphql/express-graphql/issues" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/graphql/express-graphql.git" 24 | }, 25 | "keywords": [ 26 | "express", 27 | "restify", 28 | "connect", 29 | "http", 30 | "graphql", 31 | "middleware", 32 | "api" 33 | ], 34 | "engines": { 35 | "node": ">= 10.x" 36 | }, 37 | "scripts": { 38 | "preversion": ". ./resources/checkgit.sh && npm ci", 39 | "version": "npm test", 40 | "changelog": "node resources/gen-changelog.js", 41 | "test": "npm run lint && npm run check && npm run testonly:cover && npm run prettier:check && npm run check:spelling && npm run build:npm && npm run check:integrations", 42 | "lint": "eslint .", 43 | "check": "tsc --noEmit", 44 | "testonly": "mocha src/**/__tests__/**/*.ts", 45 | "testonly:cover": "nyc npm run testonly", 46 | "prettier": "prettier --write --list-different .", 47 | "prettier:check": "prettier --check .", 48 | "check:spelling": "cspell '**/*'", 49 | "check:integrations": "mocha --full-trace integrationTests/*-test.js", 50 | "build:npm": "node resources/build-npm.js", 51 | "start": "node -r ./resources/register.js examples/index.ts", 52 | "start:subscription": "node -r ./resources/register.js examples/index_subscription.ts", 53 | "start:subscription_legacy": "node -r ./resources/register.js examples/index_subscription_legacy.ts" 54 | }, 55 | "dependencies": { 56 | "accepts": "^1.3.7", 57 | "content-type": "^1.0.4", 58 | "get-stream": "^6.0.0", 59 | "http-errors": "1.8.0" 60 | }, 61 | "devDependencies": { 62 | "@graphiql/toolkit": "^0.1.0", 63 | "@types/accepts": "1.3.5", 64 | "@types/body-parser": "1.19.0", 65 | "@types/chai": "4.2.14", 66 | "@types/connect": "3.4.33", 67 | "@types/content-type": "1.1.3", 68 | "@types/express": "4.17.9", 69 | "@types/http-errors": "1.8.0", 70 | "@types/mocha": "8.0.4", 71 | "@types/multer": "1.4.4", 72 | "@types/node": "14.14.9", 73 | "@types/restify": "8.4.2", 74 | "@types/sinon": "9.0.8", 75 | "@types/supertest": "2.0.10", 76 | "@types/ws": "5.1.2", 77 | "@typescript-eslint/eslint-plugin": "4.8.1", 78 | "@typescript-eslint/parser": "4.8.1", 79 | "body-parser": "1.19.0", 80 | "chai": "4.2.0", 81 | "connect": "3.7.0", 82 | "cspell": "4.2.2", 83 | "downlevel-dts": "0.7.0", 84 | "eslint": "7.13.0", 85 | "eslint-plugin-import": "2.22.1", 86 | "eslint-plugin-internal-rules": "file:./resources/eslint-internal-rules", 87 | "eslint-plugin-istanbul": "0.1.2", 88 | "eslint-plugin-node": "11.1.0", 89 | "express": "4.17.1", 90 | "graphiql": "^1.4.7", 91 | "graphiql-subscriptions-fetcher": "0.0.2", 92 | "graphql": "15.4.0", 93 | "graphql-ws": "4.1.2", 94 | "mocha": "8.2.1", 95 | "multer": "1.4.2", 96 | "nyc": "15.1.0", 97 | "prettier": "2.2.0", 98 | "promise-polyfill": "8.2.0", 99 | "react": "16.14.0", 100 | "react-dom": "16.14.0", 101 | "restify": "8.5.1", 102 | "sinon": "9.2.1", 103 | "subscriptions-transport-ws": "0.9.18", 104 | "supertest": "6.0.1", 105 | "ts-node": "9.0.0", 106 | "typescript": "4.1.2", 107 | "unfetch": "4.2.0", 108 | "ws": "5.2.2" 109 | }, 110 | "peerDependencies": { 111 | "graphql": "^14.7.0 || ^15.3.0" 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /resources/build-npm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const assert = require('assert'); 6 | 7 | const ts = require('typescript'); 8 | const { main: downlevel } = require('downlevel-dts'); 9 | 10 | const tsConfig = require('../tsconfig.json'); 11 | 12 | const { 13 | transformLoadFileStaticallyFromNPM, 14 | } = require('./load-statically-from-npm'); 15 | const { rmdirRecursive, readdirRecursive, showDirStats } = require('./utils'); 16 | 17 | if (require.main === module) { 18 | rmdirRecursive('./npmDist'); 19 | fs.mkdirSync('./npmDist'); 20 | 21 | const srcFiles = readdirRecursive('./src', { ignoreDir: /^__.*__$/ }); 22 | const { options } = ts.convertCompilerOptionsFromJson( 23 | { ...tsConfig.compilerOptions, outDir: 'npmDist' }, 24 | process.cwd(), 25 | ); 26 | const program = ts.createProgram({ 27 | rootNames: srcFiles.map((filepath) => path.join('./src', filepath)), 28 | options, 29 | }); 30 | program.emit(undefined, undefined, undefined, undefined, { 31 | after: [transformLoadFileStaticallyFromNPM], 32 | }); 33 | downlevel('./npmDist', './npmDist/ts3.4', '3.4.0'); 34 | 35 | fs.copyFileSync('./LICENSE', './npmDist/LICENSE'); 36 | fs.copyFileSync('./README.md', './npmDist/README.md'); 37 | 38 | // Should be done as the last step so only valid packages can be published 39 | const packageJSON = buildPackageJSON(); 40 | fs.writeFileSync( 41 | './npmDist/package.json', 42 | JSON.stringify(packageJSON, null, 2), 43 | ); 44 | 45 | showDirStats('./npmDist'); 46 | } 47 | 48 | function buildPackageJSON() { 49 | const packageJSON = require('../package.json'); 50 | delete packageJSON.private; 51 | delete packageJSON.scripts; 52 | delete packageJSON.devDependencies; 53 | 54 | const { version } = packageJSON; 55 | const versionMatch = /^\d+\.\d+\.\d+-?(?.*)?$/.exec(version); 56 | if (!versionMatch) { 57 | throw new Error('Version does not match semver spec: ' + version); 58 | } 59 | 60 | const { preReleaseTag } = versionMatch.groups; 61 | 62 | if (preReleaseTag != null) { 63 | const [tag] = preReleaseTag.split('.'); 64 | assert( 65 | tag.startsWith('experimental-') || ['alpha', 'beta', 'rc'].includes(tag), 66 | `"${tag}" tag is supported.`, 67 | ); 68 | 69 | assert(!packageJSON.publishConfig, 'Can not override "publishConfig".'); 70 | packageJSON.publishConfig = { tag: tag || 'latest' }; 71 | } 72 | 73 | return packageJSON; 74 | } 75 | -------------------------------------------------------------------------------- /resources/checkgit.sh: -------------------------------------------------------------------------------- 1 | # Exit immediately if any subcommand terminated 2 | trap "exit 1" ERR 3 | 4 | # 5 | # This script determines if current git state is the up to date master. If so 6 | # it exits normally. If not it prompts for an explicit continue. This script 7 | # intends to protect from versioning for NPM without first pushing changes 8 | # and including any changes on master. 9 | # 10 | 11 | # First fetch to ensure git is up to date. Fail-fast if this fails. 12 | git fetch; 13 | if [[ $? -ne 0 ]]; then exit 1; fi; 14 | 15 | # Extract useful information. 16 | GIT_BRANCH=$(git branch -v 2> /dev/null | sed '/^[^*]/d'); 17 | GIT_BRANCH_NAME=$(echo "$GIT_BRANCH" | sed 's/* \([A-Za-z0-9_\-]*\).*/\1/'); 18 | GIT_BRANCH_SYNC=$(echo "$GIT_BRANCH" | sed 's/* [^[]*.\([^]]*\).*/\1/'); 19 | 20 | # Check if main is checked out 21 | if [ "$GIT_BRANCH_NAME" != "main" ]; then 22 | read -p "Git not on main but $GIT_BRANCH_NAME. Continue? (y|N) " yn; 23 | if [ "$yn" != "y" ]; then exit 1; fi; 24 | fi; 25 | 26 | # Check if branch is synced with remote 27 | if [ "$GIT_BRANCH_SYNC" != "" ]; then 28 | read -p "Git not up to date but $GIT_BRANCH_SYNC. Continue? (y|N) " yn; 29 | if [ "$yn" != "y" ]; then exit 1; fi; 30 | fi; 31 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/README.md: -------------------------------------------------------------------------------- 1 | # Custom ESLint Rules 2 | 3 | This is a dummy npm package that allows us to treat it as an `eslint-plugin-graphql-internal`. 4 | It's not actually published, nor are the rules here useful for users of graphql. 5 | 6 | **If you modify this rule, you must re-run `npm install` for it to take effect.** 7 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | rules: { 5 | 'no-dir-import': require('./no-dir-import'), 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/no-dir-import.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | module.exports = function (context) { 7 | return { 8 | ImportDeclaration: checkImportPath, 9 | ExportNamedDeclaration: checkImportPath, 10 | }; 11 | 12 | function checkImportPath(node) { 13 | const { source } = node; 14 | 15 | // bail if the declaration doesn't have a source, e.g. "export { foo };" 16 | if (!source) { 17 | return; 18 | } 19 | 20 | const importPath = source.value; 21 | if (importPath.startsWith('./') || importPath.startsWith('../')) { 22 | const baseDir = path.dirname(context.getFilename()); 23 | const resolvedPath = path.resolve(baseDir, importPath); 24 | 25 | if ( 26 | fs.existsSync(resolvedPath) && 27 | fs.statSync(resolvedPath).isDirectory() 28 | ) { 29 | context.report({ 30 | node: source, 31 | message: 'It is not allowed to import from directory', 32 | }); 33 | } 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-graphql-internal", 3 | "version": "0.0.0" 4 | } 5 | -------------------------------------------------------------------------------- /resources/gen-changelog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const util = require('util'); 4 | const https = require('https'); 5 | 6 | const packageJSON = require('../package.json'); 7 | 8 | const { exec } = require('./utils'); 9 | 10 | const graphqlRequest = util.promisify(graphqlRequestImpl); 11 | const labelsConfig = { 12 | 'PR: breaking change 💥': { 13 | section: 'Breaking Change 💥', 14 | }, 15 | 'PR: feature 🚀': { 16 | section: 'New Feature 🚀', 17 | }, 18 | 'PR: bug fix 🐞': { 19 | section: 'Bug Fix 🐞', 20 | }, 21 | 'PR: docs 📝': { 22 | section: 'Docs 📝', 23 | fold: true, 24 | }, 25 | 'PR: polish 💅': { 26 | section: 'Polish 💅', 27 | fold: true, 28 | }, 29 | 'PR: internal 🏠': { 30 | section: 'Internal 🏠', 31 | fold: true, 32 | }, 33 | 'PR: dependency 📦': { 34 | section: 'Dependency 📦', 35 | fold: true, 36 | }, 37 | }; 38 | const { GH_TOKEN } = process.env; 39 | 40 | if (!GH_TOKEN) { 41 | console.error('Must provide GH_TOKEN as environment variable!'); 42 | process.exit(1); 43 | } 44 | 45 | if (!packageJSON.repository || typeof packageJSON.repository.url !== 'string') { 46 | console.error('package.json is missing repository.url string!'); 47 | process.exit(1); 48 | } 49 | 50 | const repoURLMatch = /https:\/\/github.com\/(?[^/]+)\/(?[^/]+).git/.exec( 51 | packageJSON.repository.url, 52 | ); 53 | if (repoURLMatch == null) { 54 | console.error('Cannot extract organization and repo name from repo URL!'); 55 | process.exit(1); 56 | } 57 | const { githubOrg, githubRepo } = repoURLMatch.groups; 58 | 59 | getChangeLog() 60 | .then((changelog) => process.stdout.write(changelog)) 61 | .catch((error) => { 62 | console.error(error); 63 | process.exit(1); 64 | }); 65 | 66 | function getChangeLog() { 67 | const { version } = packageJSON; 68 | 69 | let tag = null; 70 | let commitsList = exec(`git rev-list --reverse v${version}..`); 71 | if (commitsList === '') { 72 | const parentPackageJSON = exec('git cat-file blob HEAD~1:package.json'); 73 | const parentVersion = JSON.parse(parentPackageJSON).version; 74 | commitsList = exec(`git rev-list --reverse v${parentVersion}..HEAD~1`); 75 | tag = `v${version}`; 76 | } 77 | 78 | const date = exec('git log -1 --format=%cd --date=short'); 79 | return getCommitsInfo(commitsList.split('\n')) 80 | .then((commitsInfo) => getPRsInfo(commitsInfoToPRs(commitsInfo))) 81 | .then((prsInfo) => genChangeLog(tag, date, prsInfo)); 82 | } 83 | 84 | function genChangeLog(tag, date, allPRs) { 85 | const byLabel = {}; 86 | const committersByLogin = {}; 87 | 88 | for (const pr of allPRs) { 89 | const labels = pr.labels.nodes 90 | .map((label) => label.name) 91 | .filter((label) => label.startsWith('PR: ')); 92 | 93 | if (labels.length === 0) { 94 | throw new Error(`PR is missing label. See ${pr.url}`); 95 | } 96 | if (labels.length > 1) { 97 | throw new Error( 98 | `PR has conflicting labels: ${labels.join('\n')}\nSee ${pr.url}`, 99 | ); 100 | } 101 | 102 | const label = labels[0]; 103 | if (!labelsConfig[label]) { 104 | throw new Error(`Unknown label: ${label}. See ${pr.url}`); 105 | } 106 | byLabel[label] = byLabel[label] || []; 107 | byLabel[label].push(pr); 108 | committersByLogin[pr.author.login] = pr.author; 109 | } 110 | 111 | let changelog = `## ${tag || 'Unreleased'} (${date})\n`; 112 | for (const [label, config] of Object.entries(labelsConfig)) { 113 | const prs = byLabel[label]; 114 | if (prs) { 115 | const shouldFold = config.fold && prs.length > 1; 116 | 117 | changelog += `\n#### ${config.section}\n`; 118 | if (shouldFold) { 119 | changelog += '
\n'; 120 | changelog += ` ${prs.length} PRs were merged \n\n`; 121 | } 122 | 123 | for (const pr of prs) { 124 | const { number, url, author } = pr; 125 | changelog += `* [#${number}](${url}) ${pr.title} ([@${author.login}](${author.url}))\n`; 126 | } 127 | 128 | if (shouldFold) { 129 | changelog += '
\n'; 130 | } 131 | } 132 | } 133 | 134 | const committers = Object.values(committersByLogin).sort((a, b) => 135 | (a.name || a.login).localeCompare(b.name || b.login), 136 | ); 137 | changelog += `\n#### Committers: ${committers.length}\n`; 138 | for (const committer of committers) { 139 | changelog += `* ${committer.name}([@${committer.login}](${committer.url}))\n`; 140 | } 141 | 142 | return changelog; 143 | } 144 | 145 | function graphqlRequestImpl(query, variables, cb) { 146 | const resultCB = typeof variables === 'function' ? variables : cb; 147 | 148 | const req = https.request('https://api.github.com/graphql', { 149 | method: 'POST', 150 | headers: { 151 | Authorization: 'bearer ' + GH_TOKEN, 152 | 'Content-Type': 'application/json', 153 | 'User-Agent': 'gen-changelog', 154 | }, 155 | }); 156 | 157 | req.on('response', (res) => { 158 | let responseBody = ''; 159 | 160 | res.setEncoding('utf8'); 161 | res.on('data', (d) => (responseBody += d)); 162 | res.on('error', (error) => resultCB(error)); 163 | 164 | res.on('end', () => { 165 | if (res.statusCode !== 200) { 166 | return resultCB( 167 | new Error( 168 | `GitHub responded with ${res.statusCode}: ${res.statusMessage}\n` + 169 | responseBody, 170 | ), 171 | ); 172 | } 173 | 174 | let json; 175 | try { 176 | json = JSON.parse(responseBody); 177 | } catch (error) { 178 | return resultCB(error); 179 | } 180 | 181 | if (json.errors) { 182 | return resultCB( 183 | new Error('Errors: ' + JSON.stringify(json.errors, null, 2)), 184 | ); 185 | } 186 | 187 | resultCB(undefined, json.data); 188 | }); 189 | }); 190 | 191 | req.on('error', (error) => resultCB(error)); 192 | req.write(JSON.stringify({ query, variables })); 193 | req.end(); 194 | } 195 | 196 | async function batchCommitInfo(commits) { 197 | let commitsSubQuery = ''; 198 | for (const oid of commits) { 199 | commitsSubQuery += ` 200 | commit_${oid}: object(oid: "${oid}") { 201 | ... on Commit { 202 | oid 203 | message 204 | associatedPullRequests(first: 10) { 205 | nodes { 206 | number 207 | repository { 208 | nameWithOwner 209 | } 210 | } 211 | } 212 | } 213 | } 214 | `; 215 | } 216 | 217 | const response = await graphqlRequest(` 218 | { 219 | repository(owner: "${githubOrg}", name: "${githubRepo}") { 220 | ${commitsSubQuery} 221 | } 222 | } 223 | `); 224 | 225 | const commitsInfo = []; 226 | for (const oid of commits) { 227 | commitsInfo.push(response.repository['commit_' + oid]); 228 | } 229 | return commitsInfo; 230 | } 231 | 232 | async function batchPRInfo(prs) { 233 | let prsSubQuery = ''; 234 | for (const number of prs) { 235 | prsSubQuery += ` 236 | pr_${number}: pullRequest(number: ${number}) { 237 | number 238 | title 239 | url 240 | author { 241 | login 242 | url 243 | ... on User { 244 | name 245 | } 246 | } 247 | labels(first: 10) { 248 | nodes { 249 | name 250 | } 251 | } 252 | } 253 | `; 254 | } 255 | 256 | const response = await graphqlRequest(` 257 | { 258 | repository(owner: "${githubOrg}", name: "${githubRepo}") { 259 | ${prsSubQuery} 260 | } 261 | } 262 | `); 263 | 264 | const prsInfo = []; 265 | for (const number of prs) { 266 | prsInfo.push(response.repository['pr_' + number]); 267 | } 268 | return prsInfo; 269 | } 270 | 271 | function commitsInfoToPRs(commits) { 272 | const prs = {}; 273 | for (const commit of commits) { 274 | const associatedPRs = commit.associatedPullRequests.nodes.filter( 275 | (pr) => pr.repository.nameWithOwner === `${githubOrg}/${githubRepo}`, 276 | ); 277 | if (associatedPRs.length === 0) { 278 | const match = / \(#(?[0-9]+)\)$/m.exec(commit.message); 279 | if (match) { 280 | prs[parseInt(match.groups.prNumber, 10)] = true; 281 | continue; 282 | } 283 | throw new Error( 284 | `Commit ${commit.oid} has no associated PR: ${commit.message}`, 285 | ); 286 | } 287 | if (associatedPRs.length > 1) { 288 | throw new Error( 289 | `Commit ${commit.oid} is associated with multiple PRs: ${commit.message}`, 290 | ); 291 | } 292 | 293 | prs[associatedPRs[0].number] = true; 294 | } 295 | 296 | return Object.keys(prs); 297 | } 298 | 299 | async function getPRsInfo(commits) { 300 | // Split pr into batches of 50 to prevent timeouts 301 | const prInfoPromises = []; 302 | for (let i = 0; i < commits.length; i += 50) { 303 | const batch = commits.slice(i, i + 50); 304 | prInfoPromises.push(batchPRInfo(batch)); 305 | } 306 | 307 | return (await Promise.all(prInfoPromises)).flat(); 308 | } 309 | 310 | async function getCommitsInfo(commits) { 311 | // Split commits into batches of 50 to prevent timeouts 312 | const commitInfoPromises = []; 313 | for (let i = 0; i < commits.length; i += 50) { 314 | const batch = commits.slice(i, i + 50); 315 | commitInfoPromises.push(batchCommitInfo(batch)); 316 | } 317 | 318 | return (await Promise.all(commitInfoPromises)).flat(); 319 | } 320 | -------------------------------------------------------------------------------- /resources/load-statically-from-npm.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | 5 | const ts = require('typescript'); 6 | 7 | /** 8 | * Transforms: 9 | * 10 | * loadFileStaticallyFromNPM() 11 | * 12 | * to: 13 | * 14 | * "" 15 | */ 16 | module.exports.transformLoadFileStaticallyFromNPM = function (context) { 17 | return function visit(node) { 18 | if (ts.isCallExpression(node)) { 19 | if ( 20 | ts.isIdentifier(node.expression) && 21 | node.expression.text === 'loadFileStaticallyFromNPM' 22 | ) { 23 | const npmPath = node.arguments[0].text; 24 | const filePath = require.resolve(npmPath); 25 | const content = fs.readFileSync(filePath, 'utf-8'); 26 | return ts.createStringLiteral(content); 27 | } 28 | } 29 | return ts.visitEachChild(node, visit, context); 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /resources/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | transformLoadFileStaticallyFromNPM, 5 | } = require('./load-statically-from-npm'); 6 | 7 | require('ts-node').register({ 8 | logError: true, 9 | transformers: () => ({ 10 | after: [transformLoadFileStaticallyFromNPM], 11 | }), 12 | }); 13 | -------------------------------------------------------------------------------- /resources/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const util = require('util'); 5 | const path = require('path'); 6 | const childProcess = require('child_process'); 7 | 8 | function exec(command, options) { 9 | const output = childProcess.execSync(command, { 10 | maxBuffer: 10 * 1024 * 1024, // 10MB 11 | encoding: 'utf-8', 12 | ...options, 13 | }); 14 | return removeTrailingNewLine(output); 15 | } 16 | 17 | const childProcessExec = util.promisify(childProcess.exec); 18 | async function execAsync(command, options) { 19 | const output = await childProcessExec(command, { 20 | maxBuffer: 10 * 1024 * 1024, // 10MB 21 | encoding: 'utf-8', 22 | ...options, 23 | }); 24 | return removeTrailingNewLine(output.stdout); 25 | } 26 | 27 | function removeTrailingNewLine(str) { 28 | if (str == null) { 29 | return str; 30 | } 31 | 32 | return str.split('\n').slice(0, -1).join('\n'); 33 | } 34 | 35 | function rmdirRecursive(dirPath) { 36 | if (fs.existsSync(dirPath)) { 37 | for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) { 38 | const fullPath = path.join(dirPath, dirent.name); 39 | if (dirent.isDirectory()) { 40 | rmdirRecursive(fullPath); 41 | } else { 42 | fs.unlinkSync(fullPath); 43 | } 44 | } 45 | fs.rmdirSync(dirPath); 46 | } 47 | } 48 | 49 | function readdirRecursive(dirPath, opts = {}) { 50 | const { ignoreDir } = opts; 51 | const result = []; 52 | for (const dirent of fs.readdirSync(dirPath, { withFileTypes: true })) { 53 | const name = dirent.name; 54 | if (!dirent.isDirectory()) { 55 | result.push(dirent.name); 56 | continue; 57 | } 58 | 59 | if (ignoreDir && ignoreDir.test(name)) { 60 | continue; 61 | } 62 | const list = readdirRecursive(path.join(dirPath, name), opts).map((f) => 63 | path.join(name, f), 64 | ); 65 | result.push(...list); 66 | } 67 | return result; 68 | } 69 | 70 | function showDirStats(dirPath) { 71 | const fileTypes = {}; 72 | let totalSize = 0; 73 | 74 | for (const filepath of readdirRecursive(dirPath)) { 75 | const name = filepath.split(path.sep).pop(); 76 | const [base, ...splitExt] = name.split('.'); 77 | const ext = splitExt.join('.'); 78 | 79 | const filetype = ext ? '*.' + ext : base; 80 | fileTypes[filetype] = fileTypes[filetype] || { filepaths: [], size: 0 }; 81 | 82 | const { size } = fs.lstatSync(path.join(dirPath, filepath)); 83 | totalSize += size; 84 | fileTypes[filetype].size += size; 85 | fileTypes[filetype].filepaths.push(filepath); 86 | } 87 | 88 | let stats = []; 89 | for (const [filetype, typeStats] of Object.entries(fileTypes)) { 90 | const numFiles = typeStats.filepaths.length; 91 | 92 | if (numFiles > 1) { 93 | stats.push([filetype + ' x' + numFiles, typeStats.size]); 94 | } else { 95 | stats.push([typeStats.filepaths[0], typeStats.size]); 96 | } 97 | } 98 | stats.sort((a, b) => b[1] - a[1]); 99 | stats = stats.map(([type, size]) => [type, (size / 1024).toFixed(2) + ' KB']); 100 | 101 | const typeMaxLength = Math.max(...stats.map((x) => x[0].length)); 102 | const sizeMaxLength = Math.max(...stats.map((x) => x[1].length)); 103 | for (const [type, size] of stats) { 104 | console.log( 105 | type.padStart(typeMaxLength) + ' | ' + size.padStart(sizeMaxLength), 106 | ); 107 | } 108 | 109 | console.log('-'.repeat(typeMaxLength + 3 + sizeMaxLength)); 110 | const totalMB = (totalSize / 1024 / 1024).toFixed(2) + ' MB'; 111 | console.log( 112 | 'Total'.padStart(typeMaxLength) + ' | ' + totalMB.padStart(sizeMaxLength), 113 | ); 114 | } 115 | 116 | module.exports = { 117 | exec, 118 | execAsync, 119 | rmdirRecursive, 120 | readdirRecursive, 121 | showDirStats, 122 | }; 123 | -------------------------------------------------------------------------------- /src/__tests__/http-test.ts: -------------------------------------------------------------------------------- 1 | import zlib from 'zlib'; 2 | 3 | import type { Server as Restify } from 'restify'; 4 | import connect from 'connect'; 5 | import express from 'express'; 6 | import supertest from 'supertest'; 7 | import bodyParser from 'body-parser'; 8 | 9 | import type { ASTVisitor, ValidationContext } from 'graphql'; 10 | import sinon from 'sinon'; 11 | import multer from 'multer'; // cSpell:words mimetype originalname 12 | import { expect } from 'chai'; 13 | import { describe, it } from 'mocha'; 14 | import { 15 | Source, 16 | GraphQLError, 17 | GraphQLString, 18 | GraphQLNonNull, 19 | GraphQLObjectType, 20 | GraphQLSchema, 21 | parse, 22 | execute, 23 | validate, 24 | buildSchema, 25 | } from 'graphql'; 26 | 27 | import { graphqlHTTP } from '../index'; 28 | 29 | type Middleware = (req: any, res: any, next: () => void) => unknown; 30 | type Server = () => { 31 | request: () => supertest.SuperTest; 32 | use: (middleware: Middleware) => unknown; 33 | get: (path: string, middleware: Middleware) => unknown; 34 | post: (path: string, middleware: Middleware) => unknown; 35 | put: (path: string, middleware: Middleware) => unknown; 36 | }; 37 | 38 | const QueryRootType = new GraphQLObjectType({ 39 | name: 'QueryRoot', 40 | fields: { 41 | test: { 42 | type: GraphQLString, 43 | args: { 44 | who: { type: GraphQLString }, 45 | }, 46 | resolve: (_root, args: { who?: string }) => 47 | 'Hello ' + (args.who ?? 'World'), 48 | }, 49 | thrower: { 50 | type: GraphQLString, 51 | resolve() { 52 | throw new Error('Throws!'); 53 | }, 54 | }, 55 | }, 56 | }); 57 | 58 | const TestSchema = new GraphQLSchema({ 59 | query: QueryRootType, 60 | mutation: new GraphQLObjectType({ 61 | name: 'MutationRoot', 62 | fields: { 63 | writeTest: { 64 | type: QueryRootType, 65 | resolve: () => ({}), 66 | }, 67 | }, 68 | }), 69 | }); 70 | 71 | function stringifyURLParams(urlParams?: { [param: string]: string }): string { 72 | return new URLSearchParams(urlParams).toString(); 73 | } 74 | 75 | function urlString(urlParams?: { [param: string]: string }): string { 76 | let string = '/graphql'; 77 | if (urlParams) { 78 | string += '?' + stringifyURLParams(urlParams); 79 | } 80 | return string; 81 | } 82 | 83 | describe('GraphQL-HTTP tests for connect', () => { 84 | runTests(() => { 85 | const app = connect(); 86 | 87 | /* istanbul ignore next Error handler added only for debugging failed tests */ 88 | app.on('error', (error) => { 89 | // eslint-disable-next-line no-console 90 | console.warn('App encountered an error:', error); 91 | }); 92 | 93 | return { 94 | request: () => supertest(app), 95 | use: app.use.bind(app), 96 | // Connect only likes using app.use. 97 | get: app.use.bind(app), 98 | put: app.use.bind(app), 99 | post: app.use.bind(app), 100 | }; 101 | }); 102 | }); 103 | 104 | describe('GraphQL-HTTP tests for express', () => { 105 | runTests(() => { 106 | const app = express(); 107 | 108 | // This ensures consistent tests, as express defaults json spacing to 0 only in "production" mode. 109 | app.set('json spaces', 0); 110 | 111 | /* istanbul ignore next Error handler added only for debugging failed tests */ 112 | app.on('error', (error) => { 113 | // eslint-disable-next-line no-console 114 | console.warn('App encountered an error:', error); 115 | }); 116 | 117 | return { 118 | request: () => supertest(app), 119 | use: app.use.bind(app), 120 | get: app.get.bind(app), 121 | put: app.put.bind(app), 122 | post: app.post.bind(app), 123 | }; 124 | }); 125 | }); 126 | 127 | describe('GraphQL-HTTP tests for restify', () => { 128 | runTests(() => { 129 | // We're lazily loading the restify module so as to avoid issues caused by it patching the 130 | // native IncomingMessage and ServerResponse classes. See https://github.com/restify/node-restify/issues/1540 131 | // Using require instead of import here to avoid using Promises. 132 | // eslint-disable-next-line @typescript-eslint/no-var-requires,@typescript-eslint/no-require-imports,node/global-require 133 | const restify = require('restify'); 134 | const app: Restify = restify.createServer(); 135 | 136 | /* istanbul ignore next Error handler added only for debugging failed tests */ 137 | app.on('error', (error) => { 138 | // eslint-disable-next-line no-console 139 | console.warn('App encountered an error:', error); 140 | }); 141 | 142 | return { 143 | request: () => supertest(app), 144 | use: app.use.bind(app), 145 | get: app.get.bind(app), 146 | put: app.put.bind(app), 147 | post: app.post.bind(app), 148 | }; 149 | }); 150 | }); 151 | 152 | function runTests(server: Server) { 153 | describe('GET functionality', () => { 154 | it('allows GET with query param', async () => { 155 | const app = server(); 156 | 157 | app.get( 158 | urlString(), 159 | graphqlHTTP({ 160 | schema: TestSchema, 161 | }), 162 | ); 163 | 164 | const response = await app.request().get( 165 | urlString({ 166 | query: '{ test }', 167 | }), 168 | ); 169 | 170 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 171 | }); 172 | 173 | it('allows GET with variable values', async () => { 174 | const app = server(); 175 | 176 | app.get( 177 | urlString(), 178 | graphqlHTTP({ 179 | schema: TestSchema, 180 | }), 181 | ); 182 | 183 | const response = await app.request().get( 184 | urlString({ 185 | query: 'query helloWho($who: String){ test(who: $who) }', 186 | variables: JSON.stringify({ who: 'Dolly' }), 187 | }), 188 | ); 189 | 190 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 191 | }); 192 | 193 | it('allows GET with operation name', async () => { 194 | const app = server(); 195 | 196 | app.get( 197 | urlString(), 198 | graphqlHTTP(() => ({ 199 | schema: TestSchema, 200 | })), 201 | ); 202 | 203 | const response = await app.request().get( 204 | urlString({ 205 | query: ` 206 | query helloYou { test(who: "You"), ...shared } 207 | query helloWorld { test(who: "World"), ...shared } 208 | query helloDolly { test(who: "Dolly"), ...shared } 209 | fragment shared on QueryRoot { 210 | shared: test(who: "Everyone") 211 | } 212 | `, 213 | operationName: 'helloWorld', 214 | }), 215 | ); 216 | 217 | expect(JSON.parse(response.text)).to.deep.equal({ 218 | data: { 219 | test: 'Hello World', 220 | shared: 'Hello Everyone', 221 | }, 222 | }); 223 | }); 224 | 225 | it('reports validation errors', async () => { 226 | const app = server(); 227 | 228 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 229 | 230 | const response = await app.request().get( 231 | urlString({ 232 | query: '{ test, unknownOne, unknownTwo }', 233 | }), 234 | ); 235 | 236 | expect(response.status).to.equal(400); 237 | expect(JSON.parse(response.text)).to.deep.equal({ 238 | errors: [ 239 | { 240 | message: 'Cannot query field "unknownOne" on type "QueryRoot".', 241 | locations: [{ line: 1, column: 9 }], 242 | }, 243 | { 244 | message: 'Cannot query field "unknownTwo" on type "QueryRoot".', 245 | locations: [{ line: 1, column: 21 }], 246 | }, 247 | ], 248 | }); 249 | }); 250 | 251 | it('errors when missing operation name', async () => { 252 | const app = server(); 253 | 254 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 255 | 256 | const response = await app.request().get( 257 | urlString({ 258 | query: ` 259 | query TestQuery { test } 260 | mutation TestMutation { writeTest { test } } 261 | `, 262 | }), 263 | ); 264 | 265 | expect(response.status).to.equal(500); 266 | expect(JSON.parse(response.text)).to.deep.equal({ 267 | errors: [ 268 | { 269 | message: 270 | 'Must provide operation name if query contains multiple operations.', 271 | }, 272 | ], 273 | }); 274 | }); 275 | 276 | it('errors when sending a mutation via GET', async () => { 277 | const app = server(); 278 | 279 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 280 | 281 | const response = await app.request().get( 282 | urlString({ 283 | query: 'mutation TestMutation { writeTest { test } }', 284 | }), 285 | ); 286 | 287 | expect(response.status).to.equal(405); 288 | expect(JSON.parse(response.text)).to.deep.equal({ 289 | errors: [ 290 | { 291 | message: 292 | 'Can only perform a mutation operation from a POST request.', 293 | }, 294 | ], 295 | }); 296 | }); 297 | 298 | it('errors when selecting a mutation within a GET', async () => { 299 | const app = server(); 300 | 301 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 302 | 303 | const response = await app.request().get( 304 | urlString({ 305 | operationName: 'TestMutation', 306 | query: ` 307 | query TestQuery { test } 308 | mutation TestMutation { writeTest { test } } 309 | `, 310 | }), 311 | ); 312 | 313 | expect(response.status).to.equal(405); 314 | expect(JSON.parse(response.text)).to.deep.equal({ 315 | errors: [ 316 | { 317 | message: 318 | 'Can only perform a mutation operation from a POST request.', 319 | }, 320 | ], 321 | }); 322 | }); 323 | 324 | it('allows a mutation to exist within a GET', async () => { 325 | const app = server(); 326 | 327 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 328 | 329 | const response = await app.request().get( 330 | urlString({ 331 | operationName: 'TestQuery', 332 | query: ` 333 | mutation TestMutation { writeTest { test } } 334 | query TestQuery { test } 335 | `, 336 | }), 337 | ); 338 | 339 | expect(response.status).to.equal(200); 340 | expect(JSON.parse(response.text)).to.deep.equal({ 341 | data: { 342 | test: 'Hello World', 343 | }, 344 | }); 345 | }); 346 | 347 | it('allows async resolvers', async () => { 348 | const schema = new GraphQLSchema({ 349 | query: new GraphQLObjectType({ 350 | name: 'Query', 351 | fields: { 352 | foo: { 353 | type: GraphQLString, 354 | resolve: () => Promise.resolve('bar'), 355 | }, 356 | }, 357 | }), 358 | }); 359 | const app = server(); 360 | 361 | app.get(urlString(), graphqlHTTP({ schema })); 362 | 363 | const response = await app.request().get( 364 | urlString({ 365 | query: '{ foo }', 366 | }), 367 | ); 368 | 369 | expect(response.status).to.equal(200); 370 | expect(JSON.parse(response.text)).to.deep.equal({ 371 | data: { foo: 'bar' }, 372 | }); 373 | }); 374 | 375 | it('allows passing in a context', async () => { 376 | const schema = new GraphQLSchema({ 377 | query: new GraphQLObjectType({ 378 | name: 'Query', 379 | fields: { 380 | test: { 381 | type: GraphQLString, 382 | resolve: (_obj, _args, context) => context, 383 | }, 384 | }, 385 | }), 386 | }); 387 | const app = server(); 388 | 389 | app.get( 390 | urlString(), 391 | graphqlHTTP({ 392 | schema, 393 | context: 'testValue', 394 | }), 395 | ); 396 | 397 | const response = await app.request().get( 398 | urlString({ 399 | query: '{ test }', 400 | }), 401 | ); 402 | 403 | expect(response.status).to.equal(200); 404 | expect(JSON.parse(response.text)).to.deep.equal({ 405 | data: { 406 | test: 'testValue', 407 | }, 408 | }); 409 | }); 410 | 411 | it('allows passing in a fieldResolver', async () => { 412 | const schema = buildSchema(` 413 | type Query { 414 | test: String 415 | } 416 | `); 417 | const app = server(); 418 | 419 | app.get( 420 | urlString(), 421 | graphqlHTTP({ 422 | schema, 423 | fieldResolver: () => 'fieldResolver data', 424 | }), 425 | ); 426 | 427 | const response = await app.request().get( 428 | urlString({ 429 | query: '{ test }', 430 | }), 431 | ); 432 | 433 | expect(response.status).to.equal(200); 434 | expect(JSON.parse(response.text)).to.deep.equal({ 435 | data: { 436 | test: 'fieldResolver data', 437 | }, 438 | }); 439 | }); 440 | 441 | it('allows passing in a typeResolver', async () => { 442 | const schema = buildSchema(` 443 | type Foo { 444 | foo: String 445 | } 446 | 447 | type Bar { 448 | bar: String 449 | } 450 | 451 | union UnionType = Foo | Bar 452 | 453 | type Query { 454 | test: UnionType 455 | } 456 | `); 457 | const app = server(); 458 | 459 | app.get( 460 | urlString(), 461 | graphqlHTTP({ 462 | schema, 463 | rootValue: { test: {} }, 464 | typeResolver: () => 'Bar', 465 | }), 466 | ); 467 | 468 | const response = await app.request().get( 469 | urlString({ 470 | query: '{ test { __typename } }', 471 | }), 472 | ); 473 | 474 | expect(response.status).to.equal(200); 475 | expect(JSON.parse(response.text)).to.deep.equal({ 476 | data: { 477 | test: { __typename: 'Bar' }, 478 | }, 479 | }); 480 | }); 481 | 482 | it('uses request as context by default', async () => { 483 | const schema = new GraphQLSchema({ 484 | query: new GraphQLObjectType({ 485 | name: 'Query', 486 | fields: { 487 | test: { 488 | type: GraphQLString, 489 | resolve: (_obj, _args, context) => context.foo, 490 | }, 491 | }, 492 | }), 493 | }); 494 | const app = server(); 495 | 496 | // Middleware that adds req.foo to every request 497 | app.use((req, _res, next) => { 498 | req.foo = 'bar'; 499 | next(); 500 | }); 501 | 502 | app.get(urlString(), graphqlHTTP({ schema })); 503 | 504 | const response = await app.request().get( 505 | urlString({ 506 | query: '{ test }', 507 | }), 508 | ); 509 | 510 | expect(response.status).to.equal(200); 511 | expect(JSON.parse(response.text)).to.deep.equal({ 512 | data: { 513 | test: 'bar', 514 | }, 515 | }); 516 | }); 517 | 518 | it('allows returning an options Promise', async () => { 519 | const app = server(); 520 | 521 | app.get( 522 | urlString(), 523 | graphqlHTTP(() => 524 | Promise.resolve({ 525 | schema: TestSchema, 526 | }), 527 | ), 528 | ); 529 | 530 | const response = await app.request().get( 531 | urlString({ 532 | query: '{ test }', 533 | }), 534 | ); 535 | 536 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 537 | }); 538 | 539 | it('provides an options function with arguments', async () => { 540 | const app = server(); 541 | 542 | let seenRequest; 543 | let seenResponse; 544 | let seenParams; 545 | 546 | app.get( 547 | urlString(), 548 | graphqlHTTP((req, res, params) => { 549 | seenRequest = req; 550 | seenResponse = res; 551 | seenParams = params; 552 | return { schema: TestSchema }; 553 | }), 554 | ); 555 | 556 | const response = await app.request().get( 557 | urlString({ 558 | query: '{ test }', 559 | }), 560 | ); 561 | 562 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 563 | 564 | expect(seenRequest).to.not.equal(undefined); 565 | expect(seenResponse).to.not.equal(undefined); 566 | expect(seenParams).to.deep.equal({ 567 | query: '{ test }', 568 | operationName: null, 569 | variables: null, 570 | raw: false, 571 | }); 572 | }); 573 | 574 | it('catches errors thrown from options function', async () => { 575 | const app = server(); 576 | 577 | app.get( 578 | urlString(), 579 | graphqlHTTP(() => { 580 | throw new Error('I did something wrong'); 581 | }), 582 | ); 583 | 584 | const response = await app.request().get( 585 | urlString({ 586 | query: '{ test }', 587 | }), 588 | ); 589 | 590 | expect(response.status).to.equal(500); 591 | expect(response.text).to.equal( 592 | '{"errors":[{"message":"I did something wrong"}]}', 593 | ); 594 | }); 595 | }); 596 | 597 | describe('POST functionality', () => { 598 | it('allows POST with JSON encoding', async () => { 599 | const app = server(); 600 | 601 | app.post( 602 | urlString(), 603 | graphqlHTTP({ 604 | schema: TestSchema, 605 | }), 606 | ); 607 | 608 | const response = await app 609 | .request() 610 | .post(urlString()) 611 | .send({ query: '{ test }' }); 612 | 613 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 614 | }); 615 | 616 | it('allows sending a mutation via POST', async () => { 617 | const app = server(); 618 | 619 | app.post(urlString(), graphqlHTTP({ schema: TestSchema })); 620 | 621 | const response = await app 622 | .request() 623 | .post(urlString()) 624 | .send({ query: 'mutation TestMutation { writeTest { test } }' }); 625 | 626 | expect(response.status).to.equal(200); 627 | expect(response.text).to.equal( 628 | '{"data":{"writeTest":{"test":"Hello World"}}}', 629 | ); 630 | }); 631 | 632 | it('allows POST with url encoding', async () => { 633 | const app = server(); 634 | 635 | app.post( 636 | urlString(), 637 | graphqlHTTP({ 638 | schema: TestSchema, 639 | }), 640 | ); 641 | 642 | const response = await app 643 | .request() 644 | .post(urlString()) 645 | .send(stringifyURLParams({ query: '{ test }' })); 646 | 647 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 648 | }); 649 | 650 | it('supports POST JSON query with string variables', async () => { 651 | const app = server(); 652 | 653 | app.post( 654 | urlString(), 655 | graphqlHTTP({ 656 | schema: TestSchema, 657 | }), 658 | ); 659 | 660 | const response = await app 661 | .request() 662 | .post(urlString()) 663 | .send({ 664 | query: 'query helloWho($who: String){ test(who: $who) }', 665 | variables: JSON.stringify({ who: 'Dolly' }), 666 | }); 667 | 668 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 669 | }); 670 | 671 | it('supports POST JSON query with JSON variables', async () => { 672 | const app = server(); 673 | 674 | app.post( 675 | urlString(), 676 | graphqlHTTP({ 677 | schema: TestSchema, 678 | }), 679 | ); 680 | 681 | const response = await app 682 | .request() 683 | .post(urlString()) 684 | .send({ 685 | query: 'query helloWho($who: String){ test(who: $who) }', 686 | variables: { who: 'Dolly' }, 687 | }); 688 | 689 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 690 | }); 691 | 692 | it('supports POST url encoded query with string variables', async () => { 693 | const app = server(); 694 | 695 | app.post( 696 | urlString(), 697 | graphqlHTTP({ 698 | schema: TestSchema, 699 | }), 700 | ); 701 | 702 | const response = await app 703 | .request() 704 | .post(urlString()) 705 | .send( 706 | stringifyURLParams({ 707 | query: 'query helloWho($who: String){ test(who: $who) }', 708 | variables: JSON.stringify({ who: 'Dolly' }), 709 | }), 710 | ); 711 | 712 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 713 | }); 714 | 715 | it('supports POST JSON query with GET variable values', async () => { 716 | const app = server(); 717 | 718 | app.post( 719 | urlString(), 720 | graphqlHTTP({ 721 | schema: TestSchema, 722 | }), 723 | ); 724 | 725 | const response = await app 726 | .request() 727 | .post( 728 | urlString({ 729 | variables: JSON.stringify({ who: 'Dolly' }), 730 | }), 731 | ) 732 | .send({ query: 'query helloWho($who: String){ test(who: $who) }' }); 733 | 734 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 735 | }); 736 | 737 | it('supports POST url encoded query with GET variable values', async () => { 738 | const app = server(); 739 | 740 | app.post( 741 | urlString(), 742 | graphqlHTTP({ 743 | schema: TestSchema, 744 | }), 745 | ); 746 | 747 | const response = await app 748 | .request() 749 | .post( 750 | urlString({ 751 | variables: JSON.stringify({ who: 'Dolly' }), 752 | }), 753 | ) 754 | .send( 755 | stringifyURLParams({ 756 | query: 'query helloWho($who: String){ test(who: $who) }', 757 | }), 758 | ); 759 | 760 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 761 | }); 762 | 763 | it('supports POST raw text query with GET variable values', async () => { 764 | const app = server(); 765 | 766 | app.post( 767 | urlString(), 768 | graphqlHTTP({ 769 | schema: TestSchema, 770 | }), 771 | ); 772 | 773 | const response = await app 774 | .request() 775 | .post( 776 | urlString({ 777 | variables: JSON.stringify({ who: 'Dolly' }), 778 | }), 779 | ) 780 | .set('Content-Type', 'application/graphql') 781 | .send('query helloWho($who: String){ test(who: $who) }'); 782 | 783 | expect(response.text).to.equal('{"data":{"test":"Hello Dolly"}}'); 784 | }); 785 | 786 | it('allows POST with operation name', async () => { 787 | const app = server(); 788 | 789 | app.post( 790 | urlString(), 791 | graphqlHTTP(() => ({ 792 | schema: TestSchema, 793 | })), 794 | ); 795 | 796 | const response = await app.request().post(urlString()).send({ 797 | query: ` 798 | query helloYou { test(who: "You"), ...shared } 799 | query helloWorld { test(who: "World"), ...shared } 800 | query helloDolly { test(who: "Dolly"), ...shared } 801 | fragment shared on QueryRoot { 802 | shared: test(who: "Everyone") 803 | } 804 | `, 805 | operationName: 'helloWorld', 806 | }); 807 | 808 | expect(JSON.parse(response.text)).to.deep.equal({ 809 | data: { 810 | test: 'Hello World', 811 | shared: 'Hello Everyone', 812 | }, 813 | }); 814 | }); 815 | 816 | it('allows POST with GET operation name', async () => { 817 | const app = server(); 818 | 819 | app.post( 820 | urlString(), 821 | graphqlHTTP(() => ({ 822 | schema: TestSchema, 823 | })), 824 | ); 825 | 826 | const response = await app 827 | .request() 828 | .post( 829 | urlString({ 830 | operationName: 'helloWorld', 831 | }), 832 | ) 833 | .set('Content-Type', 'application/graphql').send(` 834 | query helloYou { test(who: "You"), ...shared } 835 | query helloWorld { test(who: "World"), ...shared } 836 | query helloDolly { test(who: "Dolly"), ...shared } 837 | fragment shared on QueryRoot { 838 | shared: test(who: "Everyone") 839 | } 840 | `); 841 | 842 | expect(JSON.parse(response.text)).to.deep.equal({ 843 | data: { 844 | test: 'Hello World', 845 | shared: 'Hello Everyone', 846 | }, 847 | }); 848 | }); 849 | 850 | it('allows other UTF charsets', async () => { 851 | const app = server(); 852 | 853 | app.post( 854 | urlString(), 855 | graphqlHTTP(() => ({ 856 | schema: TestSchema, 857 | })), 858 | ); 859 | 860 | const req = app 861 | .request() 862 | .post(urlString()) 863 | .set('Content-Type', 'application/json') 864 | .set('Content-Encoding', 'gzip'); 865 | 866 | req.write(zlib.gzipSync('{ "query": "{ test }" }')); 867 | 868 | const response = await req; 869 | expect(JSON.parse(response.text)).to.deep.equal({ 870 | data: { 871 | test: 'Hello World', 872 | }, 873 | }); 874 | }); 875 | 876 | it('allows deflated POST bodies', async () => { 877 | const app = server(); 878 | 879 | app.post( 880 | urlString(), 881 | graphqlHTTP(() => ({ 882 | schema: TestSchema, 883 | })), 884 | ); 885 | 886 | const req = app 887 | .request() 888 | .post(urlString()) 889 | .set('Content-Type', 'application/json') 890 | .set('Content-Encoding', 'deflate'); 891 | 892 | req.write(zlib.deflateSync('{ "query": "{ test }" }')); 893 | 894 | const response = await req; 895 | expect(JSON.parse(response.text)).to.deep.equal({ 896 | data: { 897 | test: 'Hello World', 898 | }, 899 | }); 900 | }); 901 | 902 | it('allows for pre-parsed POST bodies', async () => { 903 | // Note: this is not the only way to handle file uploads with GraphQL, 904 | // but it is terse and illustrative of using express-graphql and multer 905 | // together. 906 | 907 | // A simple schema which includes a mutation. 908 | const UploadedFileType = new GraphQLObjectType({ 909 | name: 'UploadedFile', 910 | fields: { 911 | originalname: { type: GraphQLString }, 912 | mimetype: { type: GraphQLString }, 913 | }, 914 | }); 915 | 916 | const TestMutationSchema = new GraphQLSchema({ 917 | query: new GraphQLObjectType({ 918 | name: 'QueryRoot', 919 | fields: { 920 | test: { type: GraphQLString }, 921 | }, 922 | }), 923 | mutation: new GraphQLObjectType({ 924 | name: 'MutationRoot', 925 | fields: { 926 | uploadFile: { 927 | type: UploadedFileType, 928 | resolve(rootValue) { 929 | // For this test demo, we're just returning the uploaded 930 | // file directly, but presumably you might return a Promise 931 | // to go store the file somewhere first. 932 | return rootValue.request.file; 933 | }, 934 | }, 935 | }, 936 | }), 937 | }); 938 | 939 | const app = server(); 940 | 941 | // Multer provides multipart form data parsing. 942 | const storage = multer.memoryStorage(); 943 | app.use(multer({ storage }).single('file')); 944 | 945 | // Providing the request as part of `rootValue` allows it to 946 | // be accessible from within Schema resolve functions. 947 | app.post( 948 | urlString(), 949 | graphqlHTTP((req) => ({ 950 | schema: TestMutationSchema, 951 | rootValue: { request: req }, 952 | })), 953 | ); 954 | 955 | const response = await app 956 | .request() 957 | .post(urlString()) 958 | .field( 959 | 'query', 960 | `mutation TestMutation { 961 | uploadFile { originalname, mimetype } 962 | }`, 963 | ) 964 | .attach('file', Buffer.from('test'), 'test.txt'); 965 | 966 | expect(JSON.parse(response.text)).to.deep.equal({ 967 | data: { 968 | uploadFile: { 969 | originalname: 'test.txt', 970 | mimetype: 'text/plain', 971 | }, 972 | }, 973 | }); 974 | }); 975 | 976 | it('allows for pre-parsed POST using application/graphql', async () => { 977 | const app = server(); 978 | app.use(bodyParser.text({ type: 'application/graphql' })); 979 | 980 | app.post(urlString(), graphqlHTTP({ schema: TestSchema })); 981 | 982 | const req = app 983 | .request() 984 | .post(urlString()) 985 | .set('Content-Type', 'application/graphql'); 986 | req.write(Buffer.from('{ test(who: "World") }')); 987 | const response = await req; 988 | 989 | expect(JSON.parse(response.text)).to.deep.equal({ 990 | data: { 991 | test: 'Hello World', 992 | }, 993 | }); 994 | }); 995 | 996 | it('does not accept unknown pre-parsed POST string', async () => { 997 | const app = server(); 998 | app.use(bodyParser.text({ type: '*/*' })); 999 | 1000 | app.post(urlString(), graphqlHTTP({ schema: TestSchema })); 1001 | 1002 | const req = app.request().post(urlString()); 1003 | req.write(Buffer.from('{ test(who: "World") }')); 1004 | const response = await req; 1005 | 1006 | expect(response.status).to.equal(400); 1007 | expect(JSON.parse(response.text)).to.deep.equal({ 1008 | errors: [{ message: 'Must provide query string.' }], 1009 | }); 1010 | }); 1011 | 1012 | it('does not accept unknown pre-parsed POST raw Buffer', async () => { 1013 | const app = server(); 1014 | app.use(bodyParser.raw({ type: '*/*' })); 1015 | 1016 | app.post(urlString(), graphqlHTTP({ schema: TestSchema })); 1017 | 1018 | const req = app 1019 | .request() 1020 | .post(urlString()) 1021 | .set('Content-Type', 'application/graphql'); 1022 | req.write(Buffer.from('{ test(who: "World") }')); 1023 | const response = await req; 1024 | 1025 | expect(response.status).to.equal(400); 1026 | expect(JSON.parse(response.text)).to.deep.equal({ 1027 | errors: [{ message: 'Must provide query string.' }], 1028 | }); 1029 | }); 1030 | }); 1031 | 1032 | describe('Pretty printing', () => { 1033 | it('supports pretty printing', async () => { 1034 | const app = server(); 1035 | 1036 | app.get( 1037 | urlString(), 1038 | graphqlHTTP({ 1039 | schema: TestSchema, 1040 | pretty: true, 1041 | }), 1042 | ); 1043 | 1044 | const response = await app.request().get( 1045 | urlString({ 1046 | query: '{ test }', 1047 | }), 1048 | ); 1049 | 1050 | expect(response.text).to.equal( 1051 | [ 1052 | // Pretty printed JSON 1053 | '{', 1054 | ' "data": {', 1055 | ' "test": "Hello World"', 1056 | ' }', 1057 | '}', 1058 | ].join('\n'), 1059 | ); 1060 | }); 1061 | 1062 | it('supports pretty printing configured by request', async () => { 1063 | const app = server(); 1064 | let pretty: boolean | undefined; 1065 | 1066 | app.get( 1067 | urlString(), 1068 | graphqlHTTP(() => ({ 1069 | schema: TestSchema, 1070 | pretty, 1071 | })), 1072 | ); 1073 | 1074 | pretty = undefined; 1075 | const defaultResponse = await app.request().get( 1076 | urlString({ 1077 | query: '{ test }', 1078 | }), 1079 | ); 1080 | 1081 | expect(defaultResponse.text).to.equal('{"data":{"test":"Hello World"}}'); 1082 | 1083 | pretty = true; 1084 | const prettyResponse = await app.request().get( 1085 | urlString({ 1086 | query: '{ test }', 1087 | pretty: '1', 1088 | }), 1089 | ); 1090 | 1091 | expect(prettyResponse.text).to.equal( 1092 | [ 1093 | // Pretty printed JSON 1094 | '{', 1095 | ' "data": {', 1096 | ' "test": "Hello World"', 1097 | ' }', 1098 | '}', 1099 | ].join('\n'), 1100 | ); 1101 | 1102 | pretty = false; 1103 | const unprettyResponse = await app.request().get( 1104 | urlString({ 1105 | query: '{ test }', 1106 | pretty: '0', 1107 | }), 1108 | ); 1109 | 1110 | expect(unprettyResponse.text).to.equal('{"data":{"test":"Hello World"}}'); 1111 | }); 1112 | }); 1113 | 1114 | describe('Error handling functionality', () => { 1115 | it('handles field errors caught by GraphQL', async () => { 1116 | const app = server(); 1117 | 1118 | app.get( 1119 | urlString(), 1120 | graphqlHTTP({ 1121 | schema: TestSchema, 1122 | }), 1123 | ); 1124 | 1125 | const response = await app.request().get( 1126 | urlString({ 1127 | query: '{ thrower }', 1128 | }), 1129 | ); 1130 | 1131 | expect(response.status).to.equal(200); 1132 | expect(JSON.parse(response.text)).to.deep.equal({ 1133 | data: { thrower: null }, 1134 | errors: [ 1135 | { 1136 | message: 'Throws!', 1137 | locations: [{ line: 1, column: 3 }], 1138 | path: ['thrower'], 1139 | }, 1140 | ], 1141 | }); 1142 | }); 1143 | 1144 | it('handles query errors from non-null top field errors', async () => { 1145 | const schema = new GraphQLSchema({ 1146 | query: new GraphQLObjectType({ 1147 | name: 'Query', 1148 | fields: { 1149 | test: { 1150 | type: new GraphQLNonNull(GraphQLString), 1151 | resolve() { 1152 | throw new Error('Throws!'); 1153 | }, 1154 | }, 1155 | }, 1156 | }), 1157 | }); 1158 | const app = server(); 1159 | 1160 | app.get(urlString(), graphqlHTTP({ schema })); 1161 | 1162 | const response = await app.request().get( 1163 | urlString({ 1164 | query: '{ test }', 1165 | }), 1166 | ); 1167 | 1168 | expect(response.status).to.equal(500); 1169 | expect(JSON.parse(response.text)).to.deep.equal({ 1170 | data: null, 1171 | errors: [ 1172 | { 1173 | message: 'Throws!', 1174 | locations: [{ line: 1, column: 3 }], 1175 | path: ['test'], 1176 | }, 1177 | ], 1178 | }); 1179 | }); 1180 | 1181 | it('allows for custom error formatting to sanitize GraphQL errors', async () => { 1182 | const app = server(); 1183 | 1184 | app.get( 1185 | urlString(), 1186 | graphqlHTTP({ 1187 | schema: TestSchema, 1188 | customFormatErrorFn(error) { 1189 | return { 1190 | message: 1191 | `Custom ${error.constructor.name} format: ` + error.message, 1192 | }; 1193 | }, 1194 | }), 1195 | ); 1196 | 1197 | const response = await app.request().get( 1198 | urlString({ 1199 | query: '{ thrower }', 1200 | }), 1201 | ); 1202 | 1203 | expect(response.status).to.equal(200); 1204 | expect(JSON.parse(response.text)).to.deep.equal({ 1205 | data: { thrower: null }, 1206 | errors: [ 1207 | { 1208 | message: 'Custom GraphQLError format: Throws!', 1209 | }, 1210 | ], 1211 | }); 1212 | }); 1213 | 1214 | it('allows for custom error formatting to sanitize HTTP errors', async () => { 1215 | const app = server(); 1216 | 1217 | app.get( 1218 | urlString(), 1219 | graphqlHTTP({ 1220 | schema: TestSchema, 1221 | customFormatErrorFn(error) { 1222 | return { 1223 | message: 1224 | `Custom ${error.constructor.name} format: ` + error.message, 1225 | }; 1226 | }, 1227 | }), 1228 | ); 1229 | 1230 | const response = await app.request().get(urlString()); 1231 | 1232 | expect(response.status).to.equal(400); 1233 | expect(JSON.parse(response.text)).to.deep.equal({ 1234 | errors: [ 1235 | { 1236 | message: 'Custom GraphQLError format: Must provide query string.', 1237 | }, 1238 | ], 1239 | }); 1240 | }); 1241 | 1242 | it('allows for custom error formatting to elaborate', async () => { 1243 | const app = server(); 1244 | 1245 | app.get( 1246 | urlString(), 1247 | graphqlHTTP({ 1248 | schema: TestSchema, 1249 | customFormatErrorFn(error) { 1250 | return { 1251 | message: error.message, 1252 | locations: error.locations, 1253 | stack: 'Stack trace', 1254 | }; 1255 | }, 1256 | }), 1257 | ); 1258 | 1259 | const response = await app.request().get( 1260 | urlString({ 1261 | query: '{ thrower }', 1262 | }), 1263 | ); 1264 | 1265 | expect(response.status).to.equal(200); 1266 | expect(JSON.parse(response.text)).to.deep.equal({ 1267 | data: { thrower: null }, 1268 | errors: [ 1269 | { 1270 | message: 'Throws!', 1271 | locations: [{ line: 1, column: 3 }], 1272 | stack: 'Stack trace', 1273 | }, 1274 | ], 1275 | }); 1276 | }); 1277 | 1278 | it('handles syntax errors caught by GraphQL', async () => { 1279 | const app = server(); 1280 | 1281 | app.get( 1282 | urlString(), 1283 | graphqlHTTP({ 1284 | schema: TestSchema, 1285 | }), 1286 | ); 1287 | 1288 | const response = await app.request().get( 1289 | urlString({ 1290 | query: 'syntax_error', 1291 | }), 1292 | ); 1293 | 1294 | expect(response.status).to.equal(400); 1295 | expect(JSON.parse(response.text)).to.deep.equal({ 1296 | errors: [ 1297 | { 1298 | message: 'Syntax Error: Unexpected Name "syntax_error".', 1299 | locations: [{ line: 1, column: 1 }], 1300 | }, 1301 | ], 1302 | }); 1303 | }); 1304 | 1305 | it('handles errors caused by a lack of query', async () => { 1306 | const app = server(); 1307 | 1308 | app.get( 1309 | urlString(), 1310 | graphqlHTTP({ 1311 | schema: TestSchema, 1312 | }), 1313 | ); 1314 | 1315 | const response = await app.request().get(urlString()); 1316 | 1317 | expect(response.status).to.equal(400); 1318 | expect(JSON.parse(response.text)).to.deep.equal({ 1319 | errors: [{ message: 'Must provide query string.' }], 1320 | }); 1321 | }); 1322 | 1323 | it('handles invalid JSON bodies', async () => { 1324 | const app = server(); 1325 | 1326 | app.post( 1327 | urlString(), 1328 | graphqlHTTP({ 1329 | schema: TestSchema, 1330 | }), 1331 | ); 1332 | 1333 | const response = await app 1334 | .request() 1335 | .post(urlString()) 1336 | .set('Content-Type', 'application/json') 1337 | .send('[]'); 1338 | 1339 | expect(response.status).to.equal(400); 1340 | expect(JSON.parse(response.text)).to.deep.equal({ 1341 | errors: [{ message: 'POST body sent invalid JSON.' }], 1342 | }); 1343 | }); 1344 | 1345 | it('handles incomplete JSON bodies', async () => { 1346 | const app = server(); 1347 | 1348 | app.post( 1349 | urlString(), 1350 | graphqlHTTP({ 1351 | schema: TestSchema, 1352 | }), 1353 | ); 1354 | 1355 | const response = await app 1356 | .request() 1357 | .post(urlString()) 1358 | .set('Content-Type', 'application/json') 1359 | .send('{"query":'); 1360 | 1361 | expect(response.status).to.equal(400); 1362 | expect(JSON.parse(response.text)).to.deep.equal({ 1363 | errors: [{ message: 'POST body sent invalid JSON.' }], 1364 | }); 1365 | }); 1366 | 1367 | it('handles plain POST text', async () => { 1368 | const app = server(); 1369 | 1370 | app.post( 1371 | urlString(), 1372 | graphqlHTTP({ 1373 | schema: TestSchema, 1374 | }), 1375 | ); 1376 | 1377 | const response = await app 1378 | .request() 1379 | .post( 1380 | urlString({ 1381 | variables: JSON.stringify({ who: 'Dolly' }), 1382 | }), 1383 | ) 1384 | .set('Content-Type', 'text/plain') 1385 | .send('query helloWho($who: String){ test(who: $who) }'); 1386 | 1387 | expect(response.status).to.equal(400); 1388 | expect(JSON.parse(response.text)).to.deep.equal({ 1389 | errors: [{ message: 'Must provide query string.' }], 1390 | }); 1391 | }); 1392 | 1393 | it('handles unsupported charset', async () => { 1394 | const app = server(); 1395 | 1396 | app.post( 1397 | urlString(), 1398 | graphqlHTTP(() => ({ 1399 | schema: TestSchema, 1400 | })), 1401 | ); 1402 | 1403 | const response = await app 1404 | .request() 1405 | .post(urlString()) 1406 | .set('Content-Type', 'application/graphql; charset=ascii') 1407 | .send('{ test(who: "World") }'); 1408 | 1409 | expect(response.status).to.equal(415); 1410 | expect(JSON.parse(response.text)).to.deep.equal({ 1411 | errors: [{ message: 'Unsupported charset "ASCII".' }], 1412 | }); 1413 | }); 1414 | 1415 | it('handles unsupported utf charset', async () => { 1416 | const app = server(); 1417 | 1418 | app.post( 1419 | urlString(), 1420 | graphqlHTTP(() => ({ 1421 | schema: TestSchema, 1422 | })), 1423 | ); 1424 | 1425 | const response = await app 1426 | .request() 1427 | .post(urlString()) 1428 | .set('Content-Type', 'application/graphql; charset=utf-53') 1429 | .send('{ test(who: "World") }'); 1430 | 1431 | expect(response.status).to.equal(415); 1432 | expect(JSON.parse(response.text)).to.deep.equal({ 1433 | errors: [{ message: 'Unsupported charset "UTF-53".' }], 1434 | }); 1435 | }); 1436 | 1437 | it('handles unknown encoding', async () => { 1438 | const app = server(); 1439 | 1440 | app.post( 1441 | urlString(), 1442 | graphqlHTTP(() => ({ 1443 | schema: TestSchema, 1444 | })), 1445 | ); 1446 | 1447 | const response = await app 1448 | .request() 1449 | .post(urlString()) 1450 | .set('Content-Encoding', 'garbage') 1451 | .send('!@#$%^*(&^$%#@'); 1452 | 1453 | expect(response.status).to.equal(415); 1454 | expect(JSON.parse(response.text)).to.deep.equal({ 1455 | errors: [{ message: 'Unsupported content-encoding "garbage".' }], 1456 | }); 1457 | }); 1458 | 1459 | it('handles invalid body', async () => { 1460 | const app = server(); 1461 | 1462 | app.post( 1463 | urlString(), 1464 | graphqlHTTP(() => ({ 1465 | schema: TestSchema, 1466 | })), 1467 | ); 1468 | 1469 | const response = await app 1470 | .request() 1471 | .post(urlString()) 1472 | .set('Content-Type', 'application/json') 1473 | .send(`{ "query": "{ ${new Array(102400).fill('test').join('')} }" }`); 1474 | 1475 | expect(response.status).to.equal(413); 1476 | expect(JSON.parse(response.text)).to.deep.equal({ 1477 | errors: [{ message: 'Invalid body: request entity too large.' }], 1478 | }); 1479 | }); 1480 | 1481 | it('handles poorly formed variables', async () => { 1482 | const app = server(); 1483 | 1484 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 1485 | 1486 | const response = await app.request().get( 1487 | urlString({ 1488 | variables: 'who:You', 1489 | query: 'query helloWho($who: String){ test(who: $who) }', 1490 | }), 1491 | ); 1492 | 1493 | expect(response.status).to.equal(400); 1494 | expect(JSON.parse(response.text)).to.deep.equal({ 1495 | errors: [{ message: 'Variables are invalid JSON.' }], 1496 | }); 1497 | }); 1498 | 1499 | it('`formatError` is deprecated', async () => { 1500 | const app = server(); 1501 | 1502 | app.get( 1503 | urlString(), 1504 | graphqlHTTP({ 1505 | schema: TestSchema, 1506 | formatError(error) { 1507 | return { message: 'Custom error format: ' + error.message }; 1508 | }, 1509 | }), 1510 | ); 1511 | 1512 | const spy = sinon.spy(console, 'warn'); 1513 | 1514 | const response = await app.request().get( 1515 | urlString({ 1516 | variables: 'who:You', 1517 | query: 'query helloWho($who: String){ test(who: $who) }', 1518 | }), 1519 | ); 1520 | 1521 | expect( 1522 | spy.calledWith( 1523 | '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.', 1524 | ), 1525 | ); 1526 | expect(response.status).to.equal(400); 1527 | expect(JSON.parse(response.text)).to.deep.equal({ 1528 | errors: [ 1529 | { 1530 | message: 'Custom error format: Variables are invalid JSON.', 1531 | }, 1532 | ], 1533 | }); 1534 | 1535 | spy.restore(); 1536 | }); 1537 | 1538 | it('allows for custom error formatting of poorly formed requests', async () => { 1539 | const app = server(); 1540 | 1541 | app.get( 1542 | urlString(), 1543 | graphqlHTTP({ 1544 | schema: TestSchema, 1545 | customFormatErrorFn(error) { 1546 | return { message: 'Custom error format: ' + error.message }; 1547 | }, 1548 | }), 1549 | ); 1550 | 1551 | const response = await app.request().get( 1552 | urlString({ 1553 | variables: 'who:You', 1554 | query: 'query helloWho($who: String){ test(who: $who) }', 1555 | }), 1556 | ); 1557 | 1558 | expect(response.status).to.equal(400); 1559 | expect(JSON.parse(response.text)).to.deep.equal({ 1560 | errors: [ 1561 | { 1562 | message: 'Custom error format: Variables are invalid JSON.', 1563 | }, 1564 | ], 1565 | }); 1566 | }); 1567 | 1568 | it('allows disabling prettifying poorly formed requests', async () => { 1569 | const app = server(); 1570 | 1571 | app.get( 1572 | urlString(), 1573 | graphqlHTTP({ 1574 | schema: TestSchema, 1575 | pretty: false, 1576 | }), 1577 | ); 1578 | 1579 | const response = await app.request().get( 1580 | urlString({ 1581 | variables: 'who:You', 1582 | query: 'query helloWho($who: String){ test(who: $who) }', 1583 | }), 1584 | ); 1585 | 1586 | expect(response.status).to.equal(400); 1587 | expect(response.text).to.equal( 1588 | '{"errors":[{"message":"Variables are invalid JSON."}]}', 1589 | ); 1590 | }); 1591 | 1592 | it('handles invalid variables', async () => { 1593 | const app = server(); 1594 | 1595 | app.post( 1596 | urlString(), 1597 | graphqlHTTP({ 1598 | schema: TestSchema, 1599 | }), 1600 | ); 1601 | 1602 | const response = await app 1603 | .request() 1604 | .post(urlString()) 1605 | .send({ 1606 | query: 'query helloWho($who: String){ test(who: $who) }', 1607 | variables: { who: ['John', 'Jane'] }, 1608 | }); 1609 | 1610 | expect(response.status).to.equal(500); 1611 | expect(JSON.parse(response.text)).to.deep.equal({ 1612 | errors: [ 1613 | { 1614 | locations: [{ column: 16, line: 1 }], 1615 | message: 1616 | 'Variable "$who" got invalid value ["John", "Jane"]; String cannot represent a non string value: ["John", "Jane"]', 1617 | }, 1618 | ], 1619 | }); 1620 | }); 1621 | 1622 | it('handles unsupported HTTP methods', async () => { 1623 | const app = server(); 1624 | 1625 | app.put(urlString(), graphqlHTTP({ schema: TestSchema })); 1626 | 1627 | const response = await app 1628 | .request() 1629 | .put(urlString({ query: '{ test }' })); 1630 | 1631 | expect(response.status).to.equal(405); 1632 | expect(response.get('allow')).to.equal('GET, POST'); 1633 | expect(JSON.parse(response.text)).to.deep.equal({ 1634 | errors: [{ message: 'GraphQL only supports GET and POST requests.' }], 1635 | }); 1636 | }); 1637 | }); 1638 | 1639 | describe('Built-in GraphiQL support', () => { 1640 | it('does not render GraphiQL if no opt-in', async () => { 1641 | const app = server(); 1642 | 1643 | app.get(urlString(), graphqlHTTP({ schema: TestSchema })); 1644 | 1645 | const response = await app 1646 | .request() 1647 | .get(urlString({ query: '{ test }' })) 1648 | .set('Accept', 'text/html'); 1649 | 1650 | expect(response.status).to.equal(200); 1651 | expect(response.type).to.equal('application/json'); 1652 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1653 | }); 1654 | 1655 | it('presents GraphiQL when accepting HTML', async () => { 1656 | const app = server(); 1657 | 1658 | app.get( 1659 | urlString(), 1660 | graphqlHTTP({ 1661 | schema: TestSchema, 1662 | graphiql: true, 1663 | }), 1664 | ); 1665 | 1666 | const response = await app 1667 | .request() 1668 | .get(urlString({ query: '{ test }' })) 1669 | .set('Accept', 'text/html'); 1670 | 1671 | expect(response.status).to.equal(200); 1672 | expect(response.type).to.equal('text/html'); 1673 | expect(response.text).to.include('{ test }'); 1674 | expect(response.text).to.include('graphiql.min.js'); 1675 | }); 1676 | 1677 | it('contains a default query within GraphiQL', async () => { 1678 | const app = server(); 1679 | 1680 | app.get( 1681 | urlString(), 1682 | graphqlHTTP({ 1683 | schema: TestSchema, 1684 | graphiql: { defaultQuery: 'query testDefaultQuery { hello }' }, 1685 | }), 1686 | ); 1687 | 1688 | const response = await app 1689 | .request() 1690 | .get(urlString()) 1691 | .set('Accept', 'text/html'); 1692 | 1693 | expect(response.status).to.equal(200); 1694 | expect(response.type).to.equal('text/html'); 1695 | expect(response.text).to.include( 1696 | 'defaultQuery: "query testDefaultQuery { hello }"', 1697 | ); 1698 | }); 1699 | 1700 | it('contains a pre-run response within GraphiQL', async () => { 1701 | const app = server(); 1702 | 1703 | app.get( 1704 | urlString(), 1705 | graphqlHTTP({ 1706 | schema: TestSchema, 1707 | graphiql: true, 1708 | }), 1709 | ); 1710 | 1711 | const response = await app 1712 | .request() 1713 | .get(urlString({ query: '{ test }' })) 1714 | .set('Accept', 'text/html'); 1715 | 1716 | expect(response.status).to.equal(200); 1717 | expect(response.type).to.equal('text/html'); 1718 | expect(response.text).to.include( 1719 | 'response: ' + 1720 | JSON.stringify( 1721 | JSON.stringify({ data: { test: 'Hello World' } }, null, 2), 1722 | ), 1723 | ); 1724 | }); 1725 | 1726 | it('contains a pre-run operation name within GraphiQL', async () => { 1727 | const app = server(); 1728 | 1729 | app.get( 1730 | urlString(), 1731 | graphqlHTTP({ 1732 | schema: TestSchema, 1733 | graphiql: true, 1734 | }), 1735 | ); 1736 | 1737 | const response = await app 1738 | .request() 1739 | .get( 1740 | urlString({ 1741 | query: 'query A{a:test} query B{b:test}', 1742 | operationName: 'B', 1743 | }), 1744 | ) 1745 | .set('Accept', 'text/html'); 1746 | 1747 | expect(response.status).to.equal(200); 1748 | expect(response.type).to.equal('text/html'); 1749 | expect(response.text).to.include( 1750 | 'response: ' + 1751 | JSON.stringify( 1752 | JSON.stringify({ data: { b: 'Hello World' } }, null, 2), 1753 | ), 1754 | ); 1755 | expect(response.text).to.include('operationName: "B"'); 1756 | }); 1757 | 1758 | it('escapes HTML in queries within GraphiQL', async () => { 1759 | const app = server(); 1760 | 1761 | app.get( 1762 | urlString(), 1763 | graphqlHTTP({ 1764 | schema: TestSchema, 1765 | graphiql: true, 1766 | }), 1767 | ); 1768 | 1769 | const response = await app 1770 | .request() 1771 | .get(urlString({ query: '' })) 1772 | .set('Accept', 'text/html'); 1773 | 1774 | expect(response.status).to.equal(400); 1775 | expect(response.type).to.equal('text/html'); 1776 | expect(response.text).to.not.include( 1777 | '', 1778 | ); 1779 | }); 1780 | 1781 | it('escapes HTML in variables within GraphiQL', async () => { 1782 | const app = server(); 1783 | 1784 | app.get( 1785 | urlString(), 1786 | graphqlHTTP({ 1787 | schema: TestSchema, 1788 | graphiql: true, 1789 | }), 1790 | ); 1791 | 1792 | const response = await app 1793 | .request() 1794 | .get( 1795 | urlString({ 1796 | query: 'query helloWho($who: String) { test(who: $who) }', 1797 | variables: JSON.stringify({ 1798 | who: '', 1799 | }), 1800 | }), 1801 | ) 1802 | .set('Accept', 'text/html'); 1803 | 1804 | expect(response.status).to.equal(200); 1805 | expect(response.type).to.equal('text/html'); 1806 | expect(response.text).to.not.include( 1807 | '', 1808 | ); 1809 | }); 1810 | 1811 | it('renders provided variables within GraphiQL', async () => { 1812 | const app = server(); 1813 | 1814 | app.get( 1815 | urlString(), 1816 | graphqlHTTP({ 1817 | schema: TestSchema, 1818 | graphiql: true, 1819 | }), 1820 | ); 1821 | 1822 | const response = await app 1823 | .request() 1824 | .get( 1825 | urlString({ 1826 | query: 'query helloWho($who: String) { test(who: $who) }', 1827 | variables: JSON.stringify({ who: 'Dolly' }), 1828 | }), 1829 | ) 1830 | .set('Accept', 'text/html'); 1831 | 1832 | expect(response.status).to.equal(200); 1833 | expect(response.type).to.equal('text/html'); 1834 | expect(response.text).to.include( 1835 | 'variables: ' + 1836 | JSON.stringify(JSON.stringify({ who: 'Dolly' }, null, 2)), 1837 | ); 1838 | }); 1839 | 1840 | it('accepts an empty query when rendering GraphiQL', async () => { 1841 | const app = server(); 1842 | 1843 | app.get( 1844 | urlString(), 1845 | graphqlHTTP({ 1846 | schema: TestSchema, 1847 | graphiql: true, 1848 | }), 1849 | ); 1850 | 1851 | const response = await app 1852 | .request() 1853 | .get(urlString()) 1854 | .set('Accept', 'text/html'); 1855 | 1856 | expect(response.status).to.equal(200); 1857 | expect(response.type).to.equal('text/html'); 1858 | expect(response.text).to.include('response: undefined'); 1859 | }); 1860 | 1861 | it('accepts a mutation query when rendering GraphiQL - does not execute it', async () => { 1862 | const app = server(); 1863 | 1864 | app.get( 1865 | urlString(), 1866 | graphqlHTTP({ 1867 | schema: TestSchema, 1868 | graphiql: true, 1869 | }), 1870 | ); 1871 | 1872 | const response = await app 1873 | .request() 1874 | .get( 1875 | urlString({ 1876 | query: 'mutation TestMutation { writeTest { test } }', 1877 | }), 1878 | ) 1879 | .set('Accept', 'text/html'); 1880 | 1881 | expect(response.status).to.equal(200); 1882 | expect(response.type).to.equal('text/html'); 1883 | expect(response.text).to.include( 1884 | 'query: "mutation TestMutation { writeTest { test } }"', 1885 | ); 1886 | expect(response.text).to.include('response: undefined'); 1887 | }); 1888 | 1889 | it('returns HTML if preferred', async () => { 1890 | const app = server(); 1891 | 1892 | app.get( 1893 | urlString(), 1894 | graphqlHTTP({ 1895 | schema: TestSchema, 1896 | graphiql: true, 1897 | }), 1898 | ); 1899 | 1900 | const response = await app 1901 | .request() 1902 | .get(urlString({ query: '{ test }' })) 1903 | .set('Accept', 'text/html,application/json'); 1904 | 1905 | expect(response.status).to.equal(200); 1906 | expect(response.type).to.equal('text/html'); 1907 | expect(response.text).to.include('graphiql.min.js'); 1908 | }); 1909 | 1910 | it('returns JSON if preferred', async () => { 1911 | const app = server(); 1912 | 1913 | app.get( 1914 | urlString(), 1915 | graphqlHTTP({ 1916 | schema: TestSchema, 1917 | graphiql: true, 1918 | }), 1919 | ); 1920 | 1921 | const response = await app 1922 | .request() 1923 | .get(urlString({ query: '{ test }' })) 1924 | .set('Accept', 'application/json,text/html'); 1925 | 1926 | expect(response.status).to.equal(200); 1927 | expect(response.type).to.equal('application/json'); 1928 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1929 | }); 1930 | 1931 | it('prefers JSON if unknown accept', async () => { 1932 | const app = server(); 1933 | 1934 | app.get( 1935 | urlString(), 1936 | graphqlHTTP({ 1937 | schema: TestSchema, 1938 | graphiql: true, 1939 | }), 1940 | ); 1941 | 1942 | const response = await app 1943 | .request() 1944 | .get(urlString({ query: '{ test }' })) 1945 | .set('Accept', 'unknown'); 1946 | 1947 | expect(response.status).to.equal(200); 1948 | expect(response.type).to.equal('application/json'); 1949 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1950 | }); 1951 | 1952 | it('prefers JSON if explicitly requested raw response', async () => { 1953 | const app = server(); 1954 | 1955 | app.get( 1956 | urlString(), 1957 | graphqlHTTP({ 1958 | schema: TestSchema, 1959 | graphiql: true, 1960 | }), 1961 | ); 1962 | 1963 | const response = await app 1964 | .request() 1965 | .get(urlString({ query: '{ test }', raw: '' })) 1966 | .set('Accept', 'text/html'); 1967 | 1968 | expect(response.status).to.equal(200); 1969 | expect(response.type).to.equal('application/json'); 1970 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 1971 | }); 1972 | 1973 | it('contains subscriptionEndpoint within GraphiQL', async () => { 1974 | const app = server(); 1975 | 1976 | app.get( 1977 | urlString(), 1978 | graphqlHTTP({ 1979 | schema: TestSchema, 1980 | graphiql: { subscriptionEndpoint: 'ws://localhost' }, 1981 | }), 1982 | ); 1983 | 1984 | const response = await app 1985 | .request() 1986 | .get(urlString()) 1987 | .set('Accept', 'text/html'); 1988 | 1989 | expect(response.status).to.equal(200); 1990 | expect(response.type).to.equal('text/html'); 1991 | // should contain the function to make fetcher for subscription or non-subscription 1992 | expect(response.text).to.include('makeFetcher'); 1993 | // should contain subscriptions-transport-ws browser client 1994 | expect(response.text).to.include('SubscriptionsTransportWs'); 1995 | 1996 | // should contain the subscriptionEndpoint url 1997 | expect(response.text).to.include('ws:\\/\\/localhost'); 1998 | }); 1999 | 2000 | it('contains subscriptionEndpoint within GraphiQL with websocketClient option', async () => { 2001 | const app = server(); 2002 | 2003 | app.get( 2004 | urlString(), 2005 | graphqlHTTP({ 2006 | schema: TestSchema, 2007 | graphiql: { 2008 | subscriptionEndpoint: 'ws://localhost', 2009 | websocketClient: 'v1', 2010 | }, 2011 | }), 2012 | ); 2013 | 2014 | const response = await app 2015 | .request() 2016 | .get(urlString()) 2017 | .set('Accept', 'text/html'); 2018 | 2019 | expect(response.status).to.equal(200); 2020 | expect(response.type).to.equal('text/html'); 2021 | // should contain graphql-ws browser client 2022 | expect(response.text).to.include('graphql-transport-ws'); 2023 | 2024 | // should contain the subscriptionEndpoint url 2025 | expect(response.text).to.include('ws:\\/\\/localhost'); 2026 | }); 2027 | }); 2028 | 2029 | describe('Custom validate function', () => { 2030 | it('returns data', async () => { 2031 | const app = server(); 2032 | 2033 | app.get( 2034 | urlString(), 2035 | graphqlHTTP({ 2036 | schema: TestSchema, 2037 | customValidateFn(schema, documentAST, validationRules) { 2038 | return validate(schema, documentAST, validationRules); 2039 | }, 2040 | }), 2041 | ); 2042 | 2043 | const response = await app 2044 | .request() 2045 | .get(urlString({ query: '{ test }', raw: '' })) 2046 | .set('Accept', 'text/html'); 2047 | 2048 | expect(response.status).to.equal(200); 2049 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 2050 | }); 2051 | 2052 | it('returns validation errors', async () => { 2053 | const app = server(); 2054 | 2055 | app.get( 2056 | urlString(), 2057 | graphqlHTTP({ 2058 | schema: TestSchema, 2059 | customValidateFn(schema, documentAST, validationRules) { 2060 | const errors = validate(schema, documentAST, validationRules); 2061 | 2062 | return [new GraphQLError(`custom error ${errors.length}`)]; 2063 | }, 2064 | }), 2065 | ); 2066 | 2067 | const response = await app.request().get( 2068 | urlString({ 2069 | query: '{ thrower }', 2070 | }), 2071 | ); 2072 | 2073 | expect(response.status).to.equal(400); 2074 | expect(JSON.parse(response.text)).to.deep.equal({ 2075 | errors: [ 2076 | { 2077 | message: 'custom error 0', 2078 | }, 2079 | ], 2080 | }); 2081 | }); 2082 | }); 2083 | 2084 | describe('Custom validation rules', () => { 2085 | const AlwaysInvalidRule = function ( 2086 | context: ValidationContext, 2087 | ): ASTVisitor { 2088 | return { 2089 | Document() { 2090 | context.reportError( 2091 | new GraphQLError('AlwaysInvalidRule was really invalid!'), 2092 | ); 2093 | }, 2094 | }; 2095 | }; 2096 | 2097 | it('does not execute a query if the custom validation does not pass', async () => { 2098 | const app = server(); 2099 | 2100 | app.get( 2101 | urlString(), 2102 | graphqlHTTP({ 2103 | schema: TestSchema, 2104 | validationRules: [AlwaysInvalidRule], 2105 | pretty: true, 2106 | }), 2107 | ); 2108 | 2109 | const response = await app.request().get( 2110 | urlString({ 2111 | query: '{ thrower }', 2112 | }), 2113 | ); 2114 | 2115 | expect(response.status).to.equal(400); 2116 | expect(JSON.parse(response.text)).to.deep.equal({ 2117 | errors: [ 2118 | { 2119 | message: 'AlwaysInvalidRule was really invalid!', 2120 | }, 2121 | ], 2122 | }); 2123 | }); 2124 | }); 2125 | 2126 | describe('Custom execute function', () => { 2127 | it('allows to replace the default execute function', async () => { 2128 | const app = server(); 2129 | 2130 | let seenExecuteArgs; 2131 | 2132 | app.get( 2133 | urlString(), 2134 | graphqlHTTP(() => ({ 2135 | schema: TestSchema, 2136 | async customExecuteFn(args) { 2137 | seenExecuteArgs = args; 2138 | const result = await Promise.resolve(execute(args)); 2139 | return { 2140 | ...result, 2141 | data: { 2142 | ...result.data, 2143 | test2: 'Modification', 2144 | }, 2145 | }; 2146 | }, 2147 | })), 2148 | ); 2149 | 2150 | const response = await app 2151 | .request() 2152 | .get(urlString({ query: '{ test }' })); 2153 | 2154 | expect(response.text).to.equal( 2155 | '{"data":{"test":"Hello World","test2":"Modification"}}', 2156 | ); 2157 | expect(seenExecuteArgs).to.not.equal(undefined); 2158 | }); 2159 | 2160 | it('catches errors thrown from custom execute function', async () => { 2161 | const app = server(); 2162 | 2163 | app.get( 2164 | urlString(), 2165 | graphqlHTTP(() => ({ 2166 | schema: TestSchema, 2167 | customExecuteFn() { 2168 | throw new Error('I did something wrong'); 2169 | }, 2170 | })), 2171 | ); 2172 | 2173 | const response = await app 2174 | .request() 2175 | .get(urlString({ query: '{ test }' })); 2176 | 2177 | expect(response.status).to.equal(400); 2178 | expect(response.text).to.equal( 2179 | '{"errors":[{"message":"I did something wrong"}]}', 2180 | ); 2181 | }); 2182 | }); 2183 | 2184 | describe('Custom parse function', () => { 2185 | it('can replace default parse functionality', async () => { 2186 | const app = server(); 2187 | 2188 | let seenParseArgs; 2189 | 2190 | app.get( 2191 | urlString(), 2192 | graphqlHTTP(() => ({ 2193 | schema: TestSchema, 2194 | customParseFn(args) { 2195 | seenParseArgs = args; 2196 | return parse(new Source('{ test }', 'Custom parse function')); 2197 | }, 2198 | })), 2199 | ); 2200 | 2201 | const response = await app.request().get(urlString({ query: '----' })); 2202 | 2203 | expect(response.status).to.equal(200); 2204 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 2205 | expect(seenParseArgs).property('body', '----'); 2206 | }); 2207 | 2208 | it('can throw errors', async () => { 2209 | const app = server(); 2210 | 2211 | app.get( 2212 | urlString(), 2213 | graphqlHTTP(() => ({ 2214 | schema: TestSchema, 2215 | customParseFn() { 2216 | throw new GraphQLError('my custom parse error'); 2217 | }, 2218 | })), 2219 | ); 2220 | 2221 | const response = await app.request().get(urlString({ query: '----' })); 2222 | 2223 | expect(response.status).to.equal(400); 2224 | expect(response.text).to.equal( 2225 | '{"errors":[{"message":"my custom parse error"}]}', 2226 | ); 2227 | }); 2228 | }); 2229 | 2230 | describe('Custom result extensions', () => { 2231 | it('allows adding extensions', async () => { 2232 | const app = server(); 2233 | 2234 | app.get( 2235 | urlString(), 2236 | graphqlHTTP(() => ({ 2237 | schema: TestSchema, 2238 | context: { foo: 'bar' }, 2239 | extensions({ context }) { 2240 | return { contextValue: JSON.stringify(context) }; 2241 | }, 2242 | })), 2243 | ); 2244 | 2245 | const response = await app 2246 | .request() 2247 | .get(urlString({ query: '{ test }', raw: '' })) 2248 | .set('Accept', 'text/html'); 2249 | 2250 | expect(response.status).to.equal(200); 2251 | expect(response.type).to.equal('application/json'); 2252 | expect(response.text).to.equal( 2253 | '{"data":{"test":"Hello World"},"extensions":{"contextValue":"{\\"foo\\":\\"bar\\"}"}}', 2254 | ); 2255 | }); 2256 | 2257 | it('gives extensions access to the initial GraphQL result', async () => { 2258 | const app = server(); 2259 | 2260 | app.get( 2261 | urlString(), 2262 | graphqlHTTP({ 2263 | schema: TestSchema, 2264 | customFormatErrorFn: () => ({ 2265 | message: 'Some generic error message.', 2266 | }), 2267 | extensions({ result }) { 2268 | return { preservedResult: { ...result } }; 2269 | }, 2270 | }), 2271 | ); 2272 | 2273 | const response = await app.request().get( 2274 | urlString({ 2275 | query: '{ thrower }', 2276 | }), 2277 | ); 2278 | 2279 | expect(response.status).to.equal(200); 2280 | expect(JSON.parse(response.text)).to.deep.equal({ 2281 | data: { thrower: null }, 2282 | errors: [{ message: 'Some generic error message.' }], 2283 | extensions: { 2284 | preservedResult: { 2285 | data: { thrower: null }, 2286 | errors: [ 2287 | { 2288 | message: 'Throws!', 2289 | locations: [{ line: 1, column: 3 }], 2290 | path: ['thrower'], 2291 | }, 2292 | ], 2293 | }, 2294 | }, 2295 | }); 2296 | }); 2297 | 2298 | it('allows the extensions function to be async', async () => { 2299 | const app = server(); 2300 | 2301 | app.get( 2302 | urlString(), 2303 | graphqlHTTP({ 2304 | schema: TestSchema, 2305 | extensions() { 2306 | // Note: you can return arbitrary Promises here! 2307 | return Promise.resolve({ eventually: 42 }); 2308 | }, 2309 | }), 2310 | ); 2311 | 2312 | const response = await app 2313 | .request() 2314 | .get(urlString({ query: '{ test }', raw: '' })) 2315 | .set('Accept', 'text/html'); 2316 | 2317 | expect(response.status).to.equal(200); 2318 | expect(response.type).to.equal('application/json'); 2319 | expect(response.text).to.equal( 2320 | '{"data":{"test":"Hello World"},"extensions":{"eventually":42}}', 2321 | ); 2322 | }); 2323 | 2324 | it('does nothing if extensions function does not return an object', async () => { 2325 | const app = server(); 2326 | 2327 | app.get( 2328 | urlString(), 2329 | graphqlHTTP(() => ({ 2330 | schema: TestSchema, 2331 | context: { foo: 'bar' }, 2332 | extensions: () => undefined, 2333 | })), 2334 | ); 2335 | 2336 | const response = await app 2337 | .request() 2338 | .get(urlString({ query: '{ test }', raw: '' })) 2339 | .set('Accept', 'text/html'); 2340 | 2341 | expect(response.status).to.equal(200); 2342 | expect(response.type).to.equal('application/json'); 2343 | expect(response.text).to.equal('{"data":{"test":"Hello World"}}'); 2344 | }); 2345 | }); 2346 | } 2347 | -------------------------------------------------------------------------------- /src/__tests__/usage-test.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import request from 'supertest'; 3 | import { expect } from 'chai'; 4 | import { describe, it } from 'mocha'; 5 | import { GraphQLSchema } from 'graphql'; 6 | 7 | import { graphqlHTTP } from '../index'; 8 | 9 | describe('Useful errors when incorrectly used', () => { 10 | it('requires an option factory function', () => { 11 | expect(() => { 12 | // @ts-expect-error 13 | graphqlHTTP(); 14 | }).to.throw('GraphQL middleware requires options.'); 15 | }); 16 | 17 | it('requires option factory function to return object', async () => { 18 | const app = express(); 19 | 20 | app.use( 21 | '/graphql', 22 | // @ts-expect-error 23 | graphqlHTTP(() => null), 24 | ); 25 | 26 | const response = await request(app).get('/graphql?query={test}'); 27 | 28 | expect(response.status).to.equal(500); 29 | expect(JSON.parse(response.text)).to.deep.equal({ 30 | errors: [ 31 | { 32 | message: 33 | 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.', 34 | }, 35 | ], 36 | }); 37 | }); 38 | 39 | it('requires option factory function to return object or promise of object', async () => { 40 | const app = express(); 41 | 42 | app.use( 43 | '/graphql', 44 | // @ts-expect-error 45 | graphqlHTTP(() => Promise.resolve(null)), 46 | ); 47 | 48 | const response = await request(app).get('/graphql?query={test}'); 49 | 50 | expect(response.status).to.equal(500); 51 | expect(JSON.parse(response.text)).to.deep.equal({ 52 | errors: [ 53 | { 54 | message: 55 | 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.', 56 | }, 57 | ], 58 | }); 59 | }); 60 | 61 | it('requires option factory function to return object with schema', async () => { 62 | const app = express(); 63 | 64 | app.use( 65 | '/graphql', 66 | // @ts-expect-error 67 | graphqlHTTP(() => ({})), 68 | ); 69 | 70 | const response = await request(app).get('/graphql?query={test}'); 71 | 72 | expect(response.status).to.equal(500); 73 | expect(JSON.parse(response.text)).to.deep.equal({ 74 | errors: [ 75 | { message: 'GraphQL middleware options must contain a schema.' }, 76 | ], 77 | }); 78 | }); 79 | 80 | it('requires option factory function to return object or promise of object with schema', async () => { 81 | const app = express(); 82 | 83 | app.use( 84 | '/graphql', 85 | // @ts-expect-error 86 | graphqlHTTP(() => Promise.resolve({})), 87 | ); 88 | 89 | const response = await request(app).get('/graphql?query={test}'); 90 | 91 | expect(response.status).to.equal(500); 92 | expect(JSON.parse(response.text)).to.deep.equal({ 93 | errors: [ 94 | { message: 'GraphQL middleware options must contain a schema.' }, 95 | ], 96 | }); 97 | }); 98 | 99 | it('validates schema before executing request', async () => { 100 | // @ts-expect-error 101 | const schema = new GraphQLSchema({ directives: [null] }); 102 | 103 | const app = express(); 104 | 105 | app.use( 106 | '/graphql', 107 | graphqlHTTP(() => Promise.resolve({ schema })), 108 | ); 109 | 110 | const response = await request(app).get('/graphql?query={test}'); 111 | 112 | expect(response.status).to.equal(500); 113 | expect(JSON.parse(response.text)).to.deep.equal({ 114 | errors: [ 115 | { message: 'Query root type must be provided.' }, 116 | { message: 'Expected directive but got: null.' }, 117 | ], 118 | }); 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'http'; 2 | 3 | import type { 4 | DocumentNode, 5 | ValidationRule, 6 | ExecutionArgs, 7 | ExecutionResult, 8 | FormattedExecutionResult, 9 | GraphQLSchema, 10 | GraphQLFieldResolver, 11 | GraphQLTypeResolver, 12 | GraphQLFormattedError, 13 | } from 'graphql'; 14 | import accepts from 'accepts'; 15 | import httpError from 'http-errors'; 16 | import { 17 | Source, 18 | GraphQLError, 19 | parse, 20 | validate, 21 | execute, 22 | formatError, 23 | validateSchema, 24 | getOperationAST, 25 | specifiedRules, 26 | } from 'graphql'; 27 | 28 | import type { GraphiQLOptions, GraphiQLData } from './renderGraphiQL'; 29 | import { parseBody } from './parseBody'; 30 | import { renderGraphiQL } from './renderGraphiQL'; 31 | 32 | // `url` is always defined for IncomingMessage coming from http.Server 33 | type Request = IncomingMessage & { url: string }; 34 | 35 | type Response = ServerResponse & { json?: (data: unknown) => void }; 36 | type MaybePromise = Promise | T; 37 | 38 | /** 39 | * Used to configure the graphqlHTTP middleware by providing a schema 40 | * and other configuration options. 41 | * 42 | * Options can be provided as an Object, a Promise for an Object, or a Function 43 | * that returns an Object or a Promise for an Object. 44 | */ 45 | export type Options = 46 | | (( 47 | request: Request, 48 | response: Response, 49 | params?: GraphQLParams, 50 | ) => MaybePromise) 51 | | MaybePromise; 52 | 53 | export interface OptionsData { 54 | /** 55 | * A GraphQL schema from `graphql-js`. 56 | */ 57 | schema: GraphQLSchema; 58 | 59 | /** 60 | * A value to pass as the `contextValue` to the `execute` function. 61 | */ 62 | context?: unknown; 63 | 64 | /** 65 | * An object to pass as the `rootValue` to the `execute` function. 66 | */ 67 | rootValue?: unknown; 68 | 69 | /** 70 | * A boolean to configure whether the output should be pretty-printed. 71 | */ 72 | pretty?: boolean; 73 | 74 | /** 75 | * An optional array of validation rules that will be applied on the document 76 | * in addition to those defined by the GraphQL spec. 77 | */ 78 | validationRules?: ReadonlyArray; 79 | 80 | /** 81 | * An optional function which will be used to validate instead of default `validate` 82 | * from `graphql-js`. 83 | */ 84 | customValidateFn?: ( 85 | schema: GraphQLSchema, 86 | documentAST: DocumentNode, 87 | rules: ReadonlyArray, 88 | ) => ReadonlyArray; 89 | 90 | /** 91 | * An optional function which will be used to execute instead of default `execute` 92 | * from `graphql-js`. 93 | */ 94 | customExecuteFn?: (args: ExecutionArgs) => MaybePromise; 95 | 96 | /** 97 | * An optional function which will be used to format any errors produced by 98 | * fulfilling a GraphQL operation. If no function is provided, GraphQL's 99 | * default spec-compliant `formatError` function will be used. 100 | */ 101 | customFormatErrorFn?: (error: GraphQLError) => GraphQLFormattedError; 102 | 103 | /** 104 | * An optional function which will be used to create a document instead of 105 | * the default `parse` from `graphql-js`. 106 | */ 107 | customParseFn?: (source: Source) => DocumentNode; 108 | 109 | /** 110 | * @deprecated `formatError` is deprecated and replaced by `customFormatErrorFn`. 111 | * It will be removed in version 1.0.0. 112 | */ 113 | formatError?: (error: GraphQLError) => GraphQLFormattedError; 114 | 115 | /** 116 | * An optional function for adding additional metadata to the GraphQL response 117 | * as a key-value object. The result will be added to "extensions" field in 118 | * the resulting JSON. This is often a useful place to add development time 119 | * info such as the runtime of a query or the amount of resources consumed. 120 | * 121 | * Information about the request is provided to be used. 122 | * 123 | * This function may be async. 124 | */ 125 | extensions?: ( 126 | info: RequestInfo, 127 | ) => MaybePromise; 128 | 129 | /** 130 | * A boolean to optionally enable GraphiQL mode. 131 | * Alternatively, instead of `true` you can pass in an options object. 132 | */ 133 | graphiql?: boolean | GraphiQLOptions; 134 | 135 | /** 136 | * A resolver function to use when one is not provided by the schema. 137 | * If not provided, the default field resolver is used (which looks for a 138 | * value or method on the source value with the field's name). 139 | */ 140 | fieldResolver?: GraphQLFieldResolver; 141 | 142 | /** 143 | * A type resolver function to use when none is provided by the schema. 144 | * If not provided, the default type resolver is used (which looks for a 145 | * `__typename` field or alternatively calls the `isTypeOf` method). 146 | */ 147 | typeResolver?: GraphQLTypeResolver; 148 | } 149 | 150 | /** 151 | * All information about a GraphQL request. 152 | */ 153 | export interface RequestInfo { 154 | /** 155 | * The parsed GraphQL document. 156 | */ 157 | document: DocumentNode; 158 | 159 | /** 160 | * The variable values used at runtime. 161 | */ 162 | variables: { readonly [name: string]: unknown } | null; 163 | 164 | /** 165 | * The (optional) operation name requested. 166 | */ 167 | operationName: string | null; 168 | 169 | /** 170 | * The result of executing the operation. 171 | */ 172 | result: FormattedExecutionResult; 173 | 174 | /** 175 | * The value passed as the `contextValue` to the `execute` function. 176 | */ 177 | context?: unknown; 178 | } 179 | 180 | type Middleware = (request: Request, response: Response) => Promise; 181 | 182 | /** 183 | * Middleware for express; takes an options object or function as input to 184 | * configure behavior, and returns an express middleware. 185 | */ 186 | export function graphqlHTTP(options: Options): Middleware { 187 | devAssertIsNonNullable(options, 'GraphQL middleware requires options.'); 188 | 189 | return async function graphqlMiddleware( 190 | request: Request, 191 | response: Response, 192 | ): Promise { 193 | // Higher scoped variables are referred to at various stages in the asynchronous state machine below. 194 | let params: GraphQLParams | undefined; 195 | let showGraphiQL = false; 196 | let graphiqlOptions: GraphiQLOptions | undefined; 197 | let formatErrorFn = formatError; 198 | let pretty = false; 199 | let result: ExecutionResult; 200 | 201 | try { 202 | // Parse the Request to get GraphQL request parameters. 203 | try { 204 | params = await getGraphQLParams(request); 205 | } catch (error: unknown) { 206 | // When we failed to parse the GraphQL parameters, we still need to get 207 | // the options object, so make an options call to resolve just that. 208 | const optionsData = await resolveOptions(); 209 | pretty = optionsData.pretty ?? false; 210 | formatErrorFn = 211 | optionsData.customFormatErrorFn ?? 212 | optionsData.formatError ?? 213 | formatErrorFn; 214 | throw error; 215 | } 216 | 217 | // Then, resolve the Options to get OptionsData. 218 | const optionsData = await resolveOptions(params); 219 | 220 | // Collect information from the options data object. 221 | const schema = optionsData.schema; 222 | const rootValue = optionsData.rootValue; 223 | const validationRules = optionsData.validationRules ?? []; 224 | const fieldResolver = optionsData.fieldResolver; 225 | const typeResolver = optionsData.typeResolver; 226 | const graphiql = optionsData.graphiql ?? false; 227 | const extensionsFn = optionsData.extensions; 228 | const context = optionsData.context ?? request; 229 | const parseFn = optionsData.customParseFn ?? parse; 230 | const executeFn = optionsData.customExecuteFn ?? execute; 231 | const validateFn = optionsData.customValidateFn ?? validate; 232 | 233 | pretty = optionsData.pretty ?? false; 234 | formatErrorFn = 235 | optionsData.customFormatErrorFn ?? 236 | optionsData.formatError ?? 237 | formatErrorFn; 238 | 239 | devAssertIsObject( 240 | schema, 241 | 'GraphQL middleware options must contain a schema.', 242 | ); 243 | 244 | // GraphQL HTTP only supports GET and POST methods. 245 | if (request.method !== 'GET' && request.method !== 'POST') { 246 | throw httpError(405, 'GraphQL only supports GET and POST requests.', { 247 | headers: { Allow: 'GET, POST' }, 248 | }); 249 | } 250 | 251 | // Get GraphQL params from the request and POST body data. 252 | const { query, variables, operationName } = params; 253 | showGraphiQL = canDisplayGraphiQL(request, params) && graphiql !== false; 254 | if (typeof graphiql !== 'boolean') { 255 | graphiqlOptions = graphiql; 256 | } 257 | 258 | // If there is no query, but GraphiQL will be displayed, do not produce 259 | // a result, otherwise return a 400: Bad Request. 260 | if (query == null) { 261 | if (showGraphiQL) { 262 | return respondWithGraphiQL(response, graphiqlOptions); 263 | } 264 | throw httpError(400, 'Must provide query string.'); 265 | } 266 | 267 | // Validate Schema 268 | const schemaValidationErrors = validateSchema(schema); 269 | if (schemaValidationErrors.length > 0) { 270 | // Return 500: Internal Server Error if invalid schema. 271 | throw httpError(500, 'GraphQL schema validation error.', { 272 | graphqlErrors: schemaValidationErrors, 273 | }); 274 | } 275 | 276 | // Parse source to AST, reporting any syntax error. 277 | let documentAST: DocumentNode; 278 | try { 279 | documentAST = parseFn(new Source(query, 'GraphQL request')); 280 | } catch (syntaxError: unknown) { 281 | // Return 400: Bad Request if any syntax errors exist. 282 | throw httpError(400, 'GraphQL syntax error.', { 283 | graphqlErrors: [syntaxError], 284 | }); 285 | } 286 | 287 | // Validate AST, reporting any errors. 288 | const validationErrors = validateFn(schema, documentAST, [ 289 | ...specifiedRules, 290 | ...validationRules, 291 | ]); 292 | 293 | if (validationErrors.length > 0) { 294 | // Return 400: Bad Request if any validation errors exist. 295 | throw httpError(400, 'GraphQL validation error.', { 296 | graphqlErrors: validationErrors, 297 | }); 298 | } 299 | 300 | // Only query operations are allowed on GET requests. 301 | if (request.method === 'GET') { 302 | // Determine if this GET request will perform a non-query. 303 | const operationAST = getOperationAST(documentAST, operationName); 304 | if (operationAST && operationAST.operation !== 'query') { 305 | // If GraphiQL can be shown, do not perform this query, but 306 | // provide it to GraphiQL so that the requester may perform it 307 | // themselves if desired. 308 | if (showGraphiQL) { 309 | return respondWithGraphiQL(response, graphiqlOptions, params); 310 | } 311 | 312 | // Otherwise, report a 405: Method Not Allowed error. 313 | throw httpError( 314 | 405, 315 | `Can only perform a ${operationAST.operation} operation from a POST request.`, 316 | { headers: { Allow: 'POST' } }, 317 | ); 318 | } 319 | } 320 | 321 | // Perform the execution, reporting any errors creating the context. 322 | try { 323 | result = await executeFn({ 324 | schema, 325 | document: documentAST, 326 | rootValue, 327 | contextValue: context, 328 | variableValues: variables, 329 | operationName, 330 | fieldResolver, 331 | typeResolver, 332 | }); 333 | } catch (contextError: unknown) { 334 | // Return 400: Bad Request if any execution context errors exist. 335 | throw httpError(400, 'GraphQL execution context error.', { 336 | graphqlErrors: [contextError], 337 | }); 338 | } 339 | 340 | // Collect and apply any metadata extensions if a function was provided. 341 | // https://graphql.github.io/graphql-spec/#sec-Response-Format 342 | if (extensionsFn) { 343 | const extensions = await extensionsFn({ 344 | document: documentAST, 345 | variables, 346 | operationName, 347 | result, 348 | context, 349 | }); 350 | 351 | if (extensions != null) { 352 | result = { ...result, extensions }; 353 | } 354 | } 355 | } catch (rawError: unknown) { 356 | // If an error was caught, report the httpError status, or 500. 357 | const error = httpError( 358 | 500, 359 | /* istanbul ignore next: Thrown by underlying library. */ 360 | rawError instanceof Error ? rawError : String(rawError), 361 | ); 362 | 363 | response.statusCode = error.status; 364 | 365 | const { headers } = error; 366 | if (headers != null) { 367 | for (const [key, value] of Object.entries(headers)) { 368 | response.setHeader(key, String(value)); 369 | } 370 | } 371 | 372 | if (error.graphqlErrors == null) { 373 | const graphqlError = new GraphQLError( 374 | error.message, 375 | undefined, 376 | undefined, 377 | undefined, 378 | undefined, 379 | error, 380 | ); 381 | result = { data: undefined, errors: [graphqlError] }; 382 | } else { 383 | result = { data: undefined, errors: error.graphqlErrors }; 384 | } 385 | } 386 | 387 | // If no data was included in the result, that indicates a runtime query 388 | // error, indicate as such with a generic status code. 389 | // Note: Information about the error itself will still be contained in 390 | // the resulting JSON payload. 391 | // https://graphql.github.io/graphql-spec/#sec-Data 392 | if (response.statusCode === 200 && result.data == null) { 393 | response.statusCode = 500; 394 | } 395 | 396 | // Format any encountered errors. 397 | const formattedResult: FormattedExecutionResult = { 398 | ...result, 399 | errors: result.errors?.map(formatErrorFn), 400 | }; 401 | 402 | // If allowed to show GraphiQL, present it instead of JSON. 403 | if (showGraphiQL) { 404 | return respondWithGraphiQL( 405 | response, 406 | graphiqlOptions, 407 | params, 408 | formattedResult, 409 | ); 410 | } 411 | 412 | // If "pretty" JSON isn't requested, and the server provides a 413 | // response.json method (express), use that directly. 414 | // Otherwise use the simplified sendResponse method. 415 | if (!pretty && typeof response.json === 'function') { 416 | response.json(formattedResult); 417 | } else { 418 | const payload = JSON.stringify(formattedResult, null, pretty ? 2 : 0); 419 | sendResponse(response, 'application/json', payload); 420 | } 421 | 422 | async function resolveOptions( 423 | requestParams?: GraphQLParams, 424 | ): Promise { 425 | const optionsResult = await Promise.resolve( 426 | typeof options === 'function' 427 | ? options(request, response, requestParams) 428 | : options, 429 | ); 430 | 431 | devAssertIsObject( 432 | optionsResult, 433 | 'GraphQL middleware option function must return an options object or a promise which will be resolved to an options object.', 434 | ); 435 | 436 | if (optionsResult.formatError) { 437 | // eslint-disable-next-line no-console 438 | console.warn( 439 | '`formatError` is deprecated and replaced by `customFormatErrorFn`. It will be removed in version 1.0.0.', 440 | ); 441 | } 442 | 443 | return optionsResult; 444 | } 445 | }; 446 | } 447 | 448 | function respondWithGraphiQL( 449 | response: Response, 450 | options?: GraphiQLOptions, 451 | params?: GraphQLParams, 452 | result?: FormattedExecutionResult, 453 | ): void { 454 | const data: GraphiQLData = { 455 | query: params?.query, 456 | variables: params?.variables, 457 | operationName: params?.operationName, 458 | result, 459 | }; 460 | const payload = renderGraphiQL(data, options); 461 | return sendResponse(response, 'text/html', payload); 462 | } 463 | 464 | export interface GraphQLParams { 465 | query: string | null; 466 | variables: { readonly [name: string]: unknown } | null; 467 | operationName: string | null; 468 | raw: boolean; 469 | } 470 | 471 | /** 472 | * Provided a "Request" provided by express or connect (typically a node style 473 | * HTTPClientRequest), Promise the GraphQL request parameters. 474 | */ 475 | export async function getGraphQLParams( 476 | request: Request, 477 | ): Promise { 478 | const urlData = new URLSearchParams(request.url.split('?')[1]); 479 | const bodyData = await parseBody(request); 480 | 481 | // GraphQL Query string. 482 | let query = urlData.get('query') ?? (bodyData.query as string | null); 483 | if (typeof query !== 'string') { 484 | query = null; 485 | } 486 | 487 | // Parse the variables if needed. 488 | let variables = (urlData.get('variables') ?? bodyData.variables) as { 489 | readonly [name: string]: unknown; 490 | } | null; 491 | if (typeof variables === 'string') { 492 | try { 493 | variables = JSON.parse(variables); 494 | } catch { 495 | throw httpError(400, 'Variables are invalid JSON.'); 496 | } 497 | } else if (typeof variables !== 'object') { 498 | variables = null; 499 | } 500 | 501 | // Name of GraphQL operation to execute. 502 | let operationName = 503 | urlData.get('operationName') ?? (bodyData.operationName as string | null); 504 | if (typeof operationName !== 'string') { 505 | operationName = null; 506 | } 507 | 508 | const raw = urlData.get('raw') != null || bodyData.raw !== undefined; 509 | 510 | return { query, variables, operationName, raw }; 511 | } 512 | 513 | /** 514 | * Helper function to determine if GraphiQL can be displayed. 515 | */ 516 | function canDisplayGraphiQL(request: Request, params: GraphQLParams): boolean { 517 | // If `raw` false, GraphiQL mode is not enabled. 518 | // Allowed to show GraphiQL if not requested as raw and this request prefers HTML over JSON. 519 | return !params.raw && accepts(request).types(['json', 'html']) === 'html'; 520 | } 521 | 522 | /** 523 | * Helper function for sending a response using only the core Node server APIs. 524 | */ 525 | function sendResponse(response: Response, type: string, data: string): void { 526 | const chunk = Buffer.from(data, 'utf8'); 527 | response.setHeader('Content-Type', type + '; charset=utf-8'); 528 | response.setHeader('Content-Length', String(chunk.length)); 529 | response.end(chunk); 530 | } 531 | 532 | function devAssertIsObject(value: unknown, message: string): void { 533 | devAssert(value != null && typeof value === 'object', message); 534 | } 535 | 536 | function devAssertIsNonNullable(value: unknown, message: string): void { 537 | devAssert(value != null, message); 538 | } 539 | 540 | function devAssert(condition: unknown, message: string): void { 541 | const booleanCondition = Boolean(condition); 542 | if (!booleanCondition) { 543 | throw new TypeError(message); 544 | } 545 | } 546 | -------------------------------------------------------------------------------- /src/parseBody.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage } from 'http'; 2 | import type { Inflate, Gunzip } from 'zlib'; 3 | import zlib from 'zlib'; 4 | import querystring from 'querystring'; 5 | 6 | import getStream, { MaxBufferError } from 'get-stream'; 7 | import httpError from 'http-errors'; 8 | import contentType from 'content-type'; 9 | import type { ParsedMediaType } from 'content-type'; 10 | 11 | type Request = IncomingMessage & { body?: unknown }; 12 | 13 | /** 14 | * Provided a "Request" provided by express or connect (typically a node style 15 | * HTTPClientRequest), Promise the body data contained. 16 | */ 17 | export async function parseBody( 18 | req: Request, 19 | ): Promise<{ [param: string]: unknown }> { 20 | const { body } = req; 21 | 22 | // If express has already parsed a body as a keyed object, use it. 23 | if (typeof body === 'object' && !(body instanceof Buffer)) { 24 | return body as { [param: string]: unknown }; 25 | } 26 | 27 | // Skip requests without content types. 28 | if (req.headers['content-type'] === undefined) { 29 | return {}; 30 | } 31 | 32 | const typeInfo = contentType.parse(req); 33 | 34 | // If express has already parsed a body as a string, and the content-type 35 | // was application/graphql, parse the string body. 36 | if (typeof body === 'string' && typeInfo.type === 'application/graphql') { 37 | return { query: body }; 38 | } 39 | 40 | // Already parsed body we didn't recognise? Parse nothing. 41 | if (body != null) { 42 | return {}; 43 | } 44 | 45 | const rawBody = await readBody(req, typeInfo); 46 | // Use the correct body parser based on Content-Type header. 47 | switch (typeInfo.type) { 48 | case 'application/graphql': 49 | return { query: rawBody }; 50 | case 'application/json': 51 | if (jsonObjRegex.test(rawBody)) { 52 | try { 53 | return JSON.parse(rawBody); 54 | } catch { 55 | // Do nothing 56 | } 57 | } 58 | throw httpError(400, 'POST body sent invalid JSON.'); 59 | case 'application/x-www-form-urlencoded': 60 | return querystring.parse(rawBody); 61 | } 62 | 63 | // If no Content-Type header matches, parse nothing. 64 | return {}; 65 | } 66 | 67 | /** 68 | * RegExp to match an Object-opening brace "{" as the first non-space 69 | * in a string. Allowed whitespace is defined in RFC 7159: 70 | * 71 | * ' ' Space 72 | * '\t' Horizontal tab 73 | * '\n' Line feed or New line 74 | * '\r' Carriage return 75 | */ 76 | const jsonObjRegex = /^[ \t\n\r]*\{/; 77 | 78 | // Read and parse a request body. 79 | async function readBody( 80 | req: Request, 81 | typeInfo: ParsedMediaType, 82 | ): Promise { 83 | const charset = typeInfo.parameters.charset?.toLowerCase() ?? 'utf-8'; 84 | 85 | // Assert charset encoding per JSON RFC 7159 sec 8.1 86 | if (charset !== 'utf8' && charset !== 'utf-8' && charset !== 'utf16le') { 87 | throw httpError(415, `Unsupported charset "${charset.toUpperCase()}".`); 88 | } 89 | 90 | // Get content-encoding (e.g. gzip) 91 | const contentEncoding = req.headers['content-encoding']; 92 | const encoding = 93 | typeof contentEncoding === 'string' 94 | ? contentEncoding.toLowerCase() 95 | : 'identity'; 96 | const maxBuffer = 100 * 1024; // 100kb 97 | const stream = decompressed(req, encoding); 98 | 99 | // Read body from stream. 100 | try { 101 | const buffer = await getStream.buffer(stream, { maxBuffer }); 102 | return buffer.toString(charset); 103 | } catch (rawError: unknown) { 104 | /* istanbul ignore else: Thrown by underlying library. */ 105 | if (rawError instanceof MaxBufferError) { 106 | throw httpError(413, 'Invalid body: request entity too large.'); 107 | } else { 108 | const message = 109 | rawError instanceof Error ? rawError.message : String(rawError); 110 | throw httpError(400, `Invalid body: ${message}.`); 111 | } 112 | } 113 | } 114 | 115 | // Return a decompressed stream, given an encoding. 116 | function decompressed( 117 | req: Request, 118 | encoding: string, 119 | ): Request | Inflate | Gunzip { 120 | switch (encoding) { 121 | case 'identity': 122 | return req; 123 | case 'deflate': 124 | return req.pipe(zlib.createInflate()); 125 | case 'gzip': 126 | return req.pipe(zlib.createGunzip()); 127 | } 128 | throw httpError(415, `Unsupported content-encoding "${encoding}".`); 129 | } 130 | -------------------------------------------------------------------------------- /src/renderGraphiQL.ts: -------------------------------------------------------------------------------- 1 | import type { FormattedExecutionResult } from 'graphql'; 2 | 3 | export interface GraphiQLData { 4 | query?: string | null; 5 | variables?: { readonly [name: string]: unknown } | null; 6 | operationName?: string | null; 7 | result?: FormattedExecutionResult; 8 | } 9 | 10 | export interface GraphiQLOptions { 11 | /** 12 | * An optional GraphQL string to use when no query is provided and no stored 13 | * query exists from a previous session. If undefined is provided, GraphiQL 14 | * will use its own default query. 15 | */ 16 | defaultQuery?: string; 17 | 18 | /** 19 | * An optional boolean which enables the header editor when true. 20 | * Defaults to false. 21 | */ 22 | headerEditorEnabled?: boolean; 23 | 24 | /** 25 | * An optional boolean which enables headers to be saved to local 26 | * storage when true. 27 | * Defaults to false. 28 | */ 29 | shouldPersistHeaders?: boolean; 30 | 31 | /** 32 | * An optional string of initial state for the header editor. Only makes 33 | * sense if headerEditorEnabled is true. 34 | * Defaults to empty. 35 | */ 36 | headers?: string; 37 | 38 | /** 39 | * A websocket endpoint for subscription 40 | */ 41 | subscriptionEndpoint?: string; 42 | 43 | /** 44 | * websocket client option for subscription, defaults to v0 45 | * v0: subscriptions-transport-ws 46 | * v1: graphql-ws 47 | */ 48 | websocketClient?: string; 49 | } 50 | 51 | // Ensures string values are safe to be used within a 92 | 97 | `; 98 | } else { 99 | subscriptionScripts = ` 100 | 105 | 110 | 115 | `; 116 | } 117 | } 118 | 119 | return ` 127 | 128 | 129 | 130 | 131 | GraphiQL 132 | 133 | 134 | 135 | 144 | 148 | 152 | 156 | 160 | 164 | 168 | ${subscriptionScripts} 169 | 170 | 171 |
Loading...
172 | 287 | 288 | `; 289 | } 290 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["integrationTests/ts/**/*"], 3 | "compilerOptions": { 4 | "declaration": true, 5 | "isolatedModules": true, 6 | "lib": ["es2018"], 7 | "module": "commonjs", 8 | "target": "es2018", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "importsNotUsedAsValues": "error", 19 | "newLine": "LF", 20 | "preserveConstEnums": true 21 | } 22 | } 23 | --------------------------------------------------------------------------------