├── .node-version ├── website ├── static │ ├── .nojekyll │ ├── favicon.ico │ └── img │ │ └── logo.svg ├── docs │ └── tutorials │ │ ├── hello.png │ │ ├── express-graphql.md │ │ ├── index.md │ │ ├── basic-types.md │ │ ├── running-an-express-graphql-server.md │ │ └── authentication-and-express-middleware.md ├── .babelrc.json ├── src │ ├── pages │ │ ├── index.module.css │ │ └── index.jsx │ └── css │ │ └── custom.css └── sidebars.js ├── integrationTests ├── README.md ├── webpack │ ├── webpack.config.json │ ├── entry.js │ ├── package.json │ └── test.js ├── ts │ ├── tsconfig.json │ ├── internalImports-test.ts │ ├── package.json │ ├── test.js │ ├── basic-test.ts │ ├── extensions-test.ts │ └── TypedQueryDocumentNode-test.ts ├── node │ ├── package.json │ ├── test.js │ └── index.js └── integration-test.ts ├── .prettierrc ├── src ├── jsutils │ ├── PromiseOrValue.ts │ ├── Maybe.ts │ ├── identityFunc.ts │ ├── devAssert.ts │ ├── capitalize.ts │ ├── isPromise.ts │ ├── invariant.ts │ ├── README.md │ ├── printPathArray.ts │ ├── isObjectLike.ts │ ├── ObjMap.ts │ ├── isAsyncIterable.ts │ ├── groupBy.ts │ ├── AccumulatorMap.ts │ ├── __tests__ │ │ ├── invariant-test.ts │ │ ├── identityFunc-test.ts │ │ ├── capitalize-test.ts │ │ ├── isObjectLike-test.ts │ │ ├── Path-test.ts │ │ ├── AccumulatorMap-test.ts │ │ ├── didYouMean-test.ts │ │ ├── isAsyncIterable-test.ts │ │ ├── toObjMap-test.ts │ │ ├── suggestionList-test.ts │ │ ├── instanceOf-test.ts │ │ └── isIterableObject-test.ts │ ├── mapValue.ts │ ├── toObjMap.ts │ ├── toError.ts │ ├── promiseForObject.ts │ ├── Path.ts │ ├── keyValMap.ts │ ├── formatList.ts │ ├── isIterableObject.ts │ ├── promiseReduce.ts │ ├── memoize3.ts │ ├── didYouMean.ts │ ├── keyMap.ts │ ├── naturalCompare.ts │ └── instanceOf.ts ├── __testUtils__ │ ├── resolveOnNextTick.ts │ ├── inspectStr.ts │ ├── __tests__ │ │ ├── inspectStr-test.ts │ │ ├── resolveOnNextTick-test.ts │ │ └── genFuzzStrings-test.ts │ ├── genFuzzStrings.ts │ ├── dedent.ts │ ├── kitchenSinkQuery.ts │ └── expectJSON.ts ├── type │ ├── README.md │ ├── assertName.ts │ └── __tests__ │ │ └── assertName-test.ts ├── error │ ├── README.md │ ├── index.ts │ ├── syntaxError.ts │ ├── locatedError.ts │ └── __tests__ │ │ └── locatedError-test.ts ├── language │ ├── README.md │ ├── tokenKind.ts │ ├── directiveLocation.ts │ ├── location.ts │ ├── __tests__ │ │ ├── source-test.ts │ │ └── blockString-fuzz.ts │ ├── characterClasses.ts │ ├── source.ts │ ├── kinds.ts │ └── printString.ts ├── validation │ ├── README.md │ ├── rules │ │ ├── KnownFragmentNamesRule.ts │ │ ├── UniqueFragmentNamesRule.ts │ │ ├── LoneAnonymousOperationRule.ts │ │ ├── LoneSchemaDefinitionRule.ts │ │ ├── UniqueVariableNamesRule.ts │ │ ├── ExecutableDefinitionsRule.ts │ │ ├── UniqueOperationNamesRule.ts │ │ ├── VariablesAreInputTypesRule.ts │ │ ├── custom │ │ │ └── NoSchemaIntrospectionCustomRule.ts │ │ ├── UniqueDirectiveNamesRule.ts │ │ ├── UniqueArgumentNamesRule.ts │ │ ├── NoUndefinedVariablesRule.ts │ │ ├── UniqueInputFieldNamesRule.ts │ │ ├── UniqueTypeNamesRule.ts │ │ ├── ScalarLeafsRule.ts │ │ ├── NoUnusedVariablesRule.ts │ │ ├── NoUnusedFragmentsRule.ts │ │ ├── FragmentsOnCompositeTypesRule.ts │ │ ├── UniqueOperationTypesRule.ts │ │ └── UniqueEnumValueNamesRule.ts │ └── __tests__ │ │ ├── ValidationContext-test.ts │ │ ├── VariablesAreInputTypesRule-test.ts │ │ ├── UniqueVariableNamesRule-test.ts │ │ ├── KnownFragmentNamesRule-test.ts │ │ ├── ExecutableDefinitionsRule-test.ts │ │ └── LoneAnonymousOperationRule-test.ts ├── execution │ ├── README.md │ ├── index.ts │ ├── __tests__ │ │ ├── simplePubSub-test.ts │ │ └── simplePubSub.ts │ └── mapAsyncIterator.ts ├── utilities │ ├── README.md │ ├── concatAST.ts │ ├── typedQueryDocumentNode.ts │ ├── __tests__ │ │ ├── concatAST-test.ts │ │ ├── sortValueNode-test.ts │ │ ├── introspectionFromSchema-test.ts │ │ ├── getOperationAST-test.ts │ │ └── valueFromASTUntyped-test.ts │ ├── getOperationAST.ts │ ├── sortValueNode.ts │ ├── introspectionFromSchema.ts │ ├── valueFromASTUntyped.ts │ └── typeFromAST.ts ├── version.ts ├── README.md └── __tests__ │ └── version-test.ts ├── .mocharc.yml ├── resources ├── eslint-internal-rules │ ├── package.json │ ├── README.md │ ├── index.js │ ├── only-ascii.js │ ├── no-dir-import.js │ └── require-to-string-tag.js ├── gen-version.ts ├── inline-invariant.ts ├── build-deno.ts ├── checkgit.sh └── add-extension-to-import-paths.ts ├── .babelrc-deno.json ├── .babelrc.json ├── .prettierignore ├── .eslintignore ├── benchmark ├── fixtures.js ├── parser-benchmark.js ├── buildClientSchema-benchmark.js ├── validateSDL-benchmark.js ├── buildASTSchema-benchmark.js ├── GraphQLSchema-benchmark.js ├── visit-benchmark.js ├── visitInParallel-benchmark.js ├── validateGQL-benchmark.js ├── introspectionFromSchema-benchmark.js ├── list-sync-benchmark.js ├── list-async-benchmark.js └── validateInvalidGQL-benchmark.js ├── .babelrc-npm.json ├── .github ├── workflows │ ├── pull_request_opened.yml │ ├── push.yml │ ├── mutation-testing.yml │ ├── pull_request.yml │ ├── deploy-artifact-as-branch.yml │ └── cmd-run-benchmark.yml └── ISSUE_TEMPLATE.md ├── stryker.conf.json ├── .gitignore ├── tsconfig.json ├── .c8rc.json ├── LICENSE ├── cspell.yml └── docs-old ├── APIReference-Execution.md └── APIReference-Validation.md /.node-version: -------------------------------------------------------------------------------- 1 | v18 2 | -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /integrationTests/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /src/jsutils/PromiseOrValue.ts: -------------------------------------------------------------------------------- 1 | export type PromiseOrValue = Promise | T; 2 | -------------------------------------------------------------------------------- /website/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeidHeidari/graphql-js/HEAD/website/static/favicon.ico -------------------------------------------------------------------------------- /website/docs/tutorials/hello.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoeidHeidari/graphql-js/HEAD/website/docs/tutorials/hello.png -------------------------------------------------------------------------------- /src/__testUtils__/resolveOnNextTick.ts: -------------------------------------------------------------------------------- 1 | export function resolveOnNextTick(): Promise { 2 | return Promise.resolve(undefined); 3 | } 4 | -------------------------------------------------------------------------------- /src/jsutils/Maybe.ts: -------------------------------------------------------------------------------- 1 | /** Conveniently represents flow's "Maybe" type https://flow.org/en/docs/types/maybe/ */ 2 | export type Maybe = null | undefined | T; 3 | -------------------------------------------------------------------------------- /src/jsutils/identityFunc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the first argument it receives. 3 | */ 4 | export function identityFunc(x: T): T { 5 | return x; 6 | } 7 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | fail-zero: true 2 | throw-deprecation: true 3 | check-leaks: true 4 | require: 5 | - 'ts-node/register/transpile-only' 6 | extension: 7 | - 'ts' 8 | -------------------------------------------------------------------------------- /src/jsutils/devAssert.ts: -------------------------------------------------------------------------------- 1 | export function devAssert(condition: boolean, message: string): void { 2 | if (!condition) { 3 | throw new Error(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-graphql-internal", 3 | "version": "0.0.0", 4 | "private": true, 5 | "engines": { 6 | "node": ">= 14.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.babelrc-deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "@babel/plugin-syntax-typescript", 4 | ["./resources/add-extension-to-import-paths", { "extension": "ts" }], 5 | "./resources/inline-invariant" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["@babel/plugin-transform-typescript"], 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { "bugfixes": true, "targets": { "node": "current" } } 7 | ] 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Copied from '.gitignore', please keep it in sync. 2 | /diff-npm-package.html 3 | /.eslintcache 4 | /.docusaurus 5 | /.stryker-tmp 6 | /node_modules 7 | /reports 8 | /npmDist 9 | /denoDist 10 | /websiteDist 11 | -------------------------------------------------------------------------------- /integrationTests/webpack/webpack.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "production", 3 | "entry": "./entry.js", 4 | "output": { 5 | "filename": "main.cjs", 6 | "library": { 7 | "type": "commonjs2" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/type/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Type System 2 | 3 | The `graphql/type` module is responsible for defining GraphQL types and schema. 4 | 5 | ```js 6 | import { ... } from 'graphql/type'; // ES6 7 | var GraphQLType = require('graphql/type'); // CommonJS 8 | ``` 9 | -------------------------------------------------------------------------------- /integrationTests/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": ["es2019", "es2020.promise", "es2020.bigint", "es2020.string"], 5 | "strict": true, 6 | "noEmit": true, 7 | "types": [] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/jsutils/capitalize.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts the first character of string to upper case and the remaining to lower case. 3 | */ 4 | export function capitalize(str: string): string { 5 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); 6 | } 7 | -------------------------------------------------------------------------------- /src/error/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Errors 2 | 3 | The `graphql/error` module is responsible for creating and formatting 4 | GraphQL errors. 5 | 6 | ```js 7 | import { ... } from 'graphql/error'; // ES6 8 | var GraphQLError = require('graphql/error'); // CommonJS 9 | ``` 10 | -------------------------------------------------------------------------------- /src/jsutils/isPromise.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the value acts like a Promise, i.e. has a "then" function, 3 | * otherwise returns false. 4 | */ 5 | export function isPromise(value: any): value is Promise { 6 | return typeof value?.then === 'function'; 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Copied from '.gitignore', please keep it in sync. 2 | /.eslintcache 3 | /.docusaurus 4 | /.stryker-tmp 5 | /node_modules 6 | /reports 7 | /npmDist 8 | /denoDist 9 | /websiteDist 10 | 11 | # Ignore TS files inside integration test 12 | /integrationTests/ts/*.ts 13 | -------------------------------------------------------------------------------- /src/jsutils/invariant.ts: -------------------------------------------------------------------------------- 1 | export function invariant( 2 | condition: boolean, 3 | message?: string, 4 | ): asserts condition { 5 | if (!condition) { 6 | throw new Error( 7 | message != null ? message : 'Unexpected invariant triggered.', 8 | ); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /integrationTests/webpack/entry.js: -------------------------------------------------------------------------------- 1 | import { buildSchema, graphqlSync } from 'graphql'; 2 | 3 | const schema = buildSchema('type Query { hello: String }'); 4 | 5 | export const result = graphqlSync({ 6 | schema, 7 | source: '{ hello }', 8 | rootValue: { hello: 'world' }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/language/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Language 2 | 3 | The `graphql/language` module is responsible for parsing and operating on the 4 | GraphQL language. 5 | 6 | ```js 7 | import { ... } from 'graphql/language'; // ES6 8 | var GraphQLLanguage = require('graphql/language'); // CommonJS 9 | ``` 10 | -------------------------------------------------------------------------------- /integrationTests/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "graphql-js should work on all supported node versions", 4 | "type": "module", 5 | "scripts": { 6 | "test": "node test.js" 7 | }, 8 | "dependencies": { 9 | "graphql": "file:../graphql.tgz" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/validation/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Validation 2 | 3 | The `graphql/validation` module fulfills the Validation phase of fulfilling a 4 | GraphQL result. 5 | 6 | ```js 7 | import { validate } from 'graphql/validation'; // ES6 8 | var GraphQLValidator = require('graphql/validation'); // CommonJS 9 | ``` 10 | -------------------------------------------------------------------------------- /website/.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@docusaurus/core/lib/babel/preset"], 3 | "browserslist": { 4 | "production": ["last 2 versions"], 5 | "development": [ 6 | "last 1 chrome version", 7 | "last 1 firefox version", 8 | "last 1 safari version" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/error/index.ts: -------------------------------------------------------------------------------- 1 | export { GraphQLError } from './GraphQLError'; 2 | export type { 3 | GraphQLErrorOptions, 4 | GraphQLFormattedError, 5 | GraphQLErrorExtensions, 6 | } from './GraphQLError'; 7 | 8 | export { syntaxError } from './syntaxError'; 9 | 10 | export { locatedError } from './locatedError'; 11 | -------------------------------------------------------------------------------- /src/execution/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Execution 2 | 3 | The `graphql/execution` module is responsible for the execution phase of 4 | fulfilling a GraphQL request. 5 | 6 | ```js 7 | import { execute } from 'graphql/execution'; // ES6 8 | var GraphQLExecution = require('graphql/execution'); // CommonJS 9 | ``` 10 | -------------------------------------------------------------------------------- /src/jsutils/README.md: -------------------------------------------------------------------------------- 1 | ## JavaScript Utils 2 | 3 | This directory contains dependency-free JavaScript utility functions used 4 | throughout the codebase. 5 | 6 | Each utility should belong in its own file and be the default export. 7 | 8 | These functions are not part of the module interface and are subject to change. 9 | -------------------------------------------------------------------------------- /src/jsutils/printPathArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a string describing the path. 3 | */ 4 | export function printPathArray(path: ReadonlyArray): string { 5 | return path 6 | .map((key) => 7 | typeof key === 'number' ? '[' + key.toString() + ']' : '.' + key, 8 | ) 9 | .join(''); 10 | } 11 | -------------------------------------------------------------------------------- /src/utilities/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL Utilities 2 | 3 | The `graphql/utilities` module contains common useful computations to use with 4 | the GraphQL language and type objects. 5 | 6 | ```js 7 | import { ... } from 'graphql/utilities'; // ES6 8 | var GraphQLUtilities = require('graphql/utilities'); // CommonJS 9 | ``` 10 | -------------------------------------------------------------------------------- /benchmark/fixtures.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export const bigSchemaSDL = fs.readFileSync( 4 | new URL('github-schema.graphql', import.meta.url), 5 | 'utf8', 6 | ); 7 | 8 | export const bigSchemaIntrospectionResult = JSON.parse( 9 | fs.readFileSync(new URL('github-schema.json', import.meta.url), 'utf8'), 10 | ); 11 | -------------------------------------------------------------------------------- /.babelrc-npm.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false, "targets": { "node": "14" } }] 4 | ], 5 | "plugins": [ 6 | ["./resources/add-extension-to-import-paths.js", { "extension": "js" }], 7 | "@babel/plugin-transform-typescript", 8 | "./resources/inline-invariant.js" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/jsutils/isObjectLike.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Return true if `value` is object-like. A value is object-like if it's not 3 | * `null` and has a `typeof` result of "object". 4 | */ 5 | export function isObjectLike( 6 | value: unknown, 7 | ): value is { [key: string]: unknown } { 8 | return typeof value == 'object' && value !== null; 9 | } 10 | -------------------------------------------------------------------------------- /src/jsutils/ObjMap.ts: -------------------------------------------------------------------------------- 1 | export interface ObjMap { 2 | [key: string]: T; 3 | } 4 | 5 | export type ObjMapLike = ObjMap | { [key: string]: T }; 6 | 7 | export interface ReadOnlyObjMap { 8 | readonly [key: string]: T; 9 | } 10 | 11 | export type ReadOnlyObjMapLike = 12 | | ReadOnlyObjMap 13 | | { readonly [key: string]: T }; 14 | -------------------------------------------------------------------------------- /integrationTests/webpack/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "graphql-js should be compatible with Webpack", 4 | "type": "module", 5 | "scripts": { 6 | "test": "webpack && node test.js" 7 | }, 8 | "dependencies": { 9 | "graphql": "file:../graphql.tgz", 10 | "webpack": "5.x.x", 11 | "webpack-cli": "4.x.x" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /integrationTests/ts/internalImports-test.ts: -------------------------------------------------------------------------------- 1 | import type { NameNode } from 'graphql/language'; 2 | 3 | // Parser class is internal API so so any changes to it are never considered breaking changes. 4 | // We just want to test that we are able to import it. 5 | import { Parser } from 'graphql/language/parser'; 6 | 7 | const parser = new Parser('foo'); 8 | const ast: NameNode = parser.parseName(); 9 | -------------------------------------------------------------------------------- /src/jsutils/isAsyncIterable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the provided object implements the AsyncIterator protocol via 3 | * implementing a `Symbol.asyncIterator` method. 4 | */ 5 | export function isAsyncIterable( 6 | maybeAsyncIterable: any, 7 | ): maybeAsyncIterable is AsyncIterable { 8 | return typeof maybeAsyncIterable?.[Symbol.asyncIterator] === 'function'; 9 | } 10 | -------------------------------------------------------------------------------- /integrationTests/webpack/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'assert'; 2 | 3 | // eslint-disable-next-line import/no-unresolved, node/no-missing-import 4 | import mainCJS from './dist/main.cjs'; 5 | 6 | assert.deepStrictEqual(mainCJS.result, { 7 | data: { 8 | __proto__: null, 9 | hello: 'world', 10 | }, 11 | }); 12 | console.log('Test script: Got correct result from Webpack bundle!'); 13 | -------------------------------------------------------------------------------- /benchmark/parser-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { getIntrospectionQuery } from 'graphql/utilities/getIntrospectionQuery.js'; 3 | 4 | const introspectionQuery = getIntrospectionQuery(); 5 | 6 | export const benchmark = { 7 | name: 'Parse introspection query', 8 | count: 1000, 9 | measure() { 10 | parse(introspectionQuery); 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /benchmark/buildClientSchema-benchmark.js: -------------------------------------------------------------------------------- 1 | import { buildClientSchema } from 'graphql/utilities/buildClientSchema.js'; 2 | 3 | import { bigSchemaIntrospectionResult } from './fixtures.js'; 4 | 5 | export const benchmark = { 6 | name: 'Build Schema from Introspection', 7 | count: 10, 8 | measure() { 9 | buildClientSchema(bigSchemaIntrospectionResult.data, { assumeValid: true }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /benchmark/validateSDL-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { validateSDL } from 'graphql/validation/validate.js'; 3 | 4 | import { bigSchemaSDL } from './fixtures.js'; 5 | 6 | const sdlAST = parse(bigSchemaSDL); 7 | 8 | export const benchmark = { 9 | name: 'Validate SDL Document', 10 | count: 10, 11 | measure() { 12 | validateSDL(sdlAST); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const onlyASCII = require('./only-ascii.js'); 4 | const noDirImport = require('./no-dir-import.js'); 5 | const requireToStringTag = require('./require-to-string-tag.js'); 6 | 7 | module.exports = { 8 | rules: { 9 | 'only-ascii': onlyASCII, 10 | 'no-dir-import': noDirImport, 11 | 'require-to-string-tag': requireToStringTag, 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /benchmark/buildASTSchema-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { buildASTSchema } from 'graphql/utilities/buildASTSchema.js'; 3 | 4 | import { bigSchemaSDL } from './fixtures.js'; 5 | 6 | const schemaAST = parse(bigSchemaSDL); 7 | 8 | export const benchmark = { 9 | name: 'Build Schema from AST', 10 | count: 10, 11 | measure() { 12 | buildASTSchema(schemaAST, { assumeValid: true }); 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/__testUtils__/inspectStr.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../jsutils/Maybe'; 2 | 3 | /** 4 | * Special inspect function to produce readable string literal for error messages in tests 5 | */ 6 | export function inspectStr(str: Maybe): string { 7 | if (str == null) { 8 | return 'null'; 9 | } 10 | return JSON.stringify(str) 11 | .replace(/^"|"$/g, '`') 12 | .replace(/\\"/g, '"') 13 | .replace(/\\\\/g, '\\'); 14 | } 15 | -------------------------------------------------------------------------------- /src/jsutils/groupBy.ts: -------------------------------------------------------------------------------- 1 | import { AccumulatorMap } from './AccumulatorMap'; 2 | 3 | /** 4 | * Groups array items into a Map, given a function to produce grouping key. 5 | */ 6 | export function groupBy( 7 | list: ReadonlyArray, 8 | keyFn: (item: T) => K, 9 | ): Map> { 10 | const result = new AccumulatorMap(); 11 | for (const item of list) { 12 | result.add(keyFn(item), item); 13 | } 14 | return result; 15 | } 16 | -------------------------------------------------------------------------------- /src/jsutils/AccumulatorMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ES6 Map with additional `add` method to accumulate items. 3 | */ 4 | export class AccumulatorMap extends Map> { 5 | get [Symbol.toStringTag]() { 6 | return 'AccumulatorMap'; 7 | } 8 | 9 | add(key: K, item: T): void { 10 | const group = this.get(key); 11 | if (group === undefined) { 12 | this.set(key, [item]); 13 | } else { 14 | group.push(item); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_opened.yml: -------------------------------------------------------------------------------- 1 | name: PullRequestOpened 2 | on: 3 | pull_request: 4 | types: [opened] 5 | jobs: 6 | save-github-event: 7 | name: "Save `github.event` as an artifact to use in subsequent 'workflow_run' actions" 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Upload event.json 11 | uses: actions/upload-artifact@v3 12 | with: 13 | name: event.json 14 | path: ${{ github.event_path }} 15 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/invariant-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { invariant } from '../invariant'; 5 | 6 | describe('invariant', () => { 7 | it('throws on false conditions', () => { 8 | expect(() => invariant(false, 'Oops!')).to.throw('Oops!'); 9 | }); 10 | 11 | it('use default error message', () => { 12 | expect(() => invariant(false)).to.throw('Unexpected invariant triggered.'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /stryker.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "warnings": { "slow": false }, 3 | "packageManager": "npm", 4 | "coverageAnalysis": "perTest", 5 | "mutate": ["src/**/*.ts", "!src/**/__tests__/**/*.ts"], 6 | "buildCommand": "tsc --outDir dist --noEmit false", 7 | "checkers": ["typescript"], 8 | "tsconfigFile": "tsconfig.json", 9 | "testRunner": "mocha", 10 | "mochaOptions": { 11 | "spec": ["dist/src/**/*-test.js"] 12 | }, 13 | "reporters": ["html", "progress", "dashboard"] 14 | } 15 | -------------------------------------------------------------------------------- /.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 | /diff-npm-package.html 9 | /.eslintcache 10 | /.cspellcache 11 | /.docusaurus 12 | /.stryker-tmp 13 | /node_modules 14 | /reports 15 | /npmDist 16 | /denoDist 17 | /websiteDist 18 | -------------------------------------------------------------------------------- /integrationTests/ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "graphql-js should compile with all supported TS versions", 4 | "type": "module", 5 | "scripts": { 6 | "test": "node test.js" 7 | }, 8 | "dependencies": { 9 | "graphql": "file:../graphql.tgz", 10 | "typescript-4.4": "npm:typescript@4.4.x", 11 | "typescript-4.5": "npm:typescript@4.5.x", 12 | "typescript-4.6": "npm:typescript@4.6.x", 13 | "typescript-4.7": "npm:typescript@4.7.x" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/execution/index.ts: -------------------------------------------------------------------------------- 1 | export { pathToArray as responsePathAsArray } from '../jsutils/Path'; 2 | 3 | export { 4 | createSourceEventStream, 5 | execute, 6 | executeSync, 7 | defaultFieldResolver, 8 | defaultTypeResolver, 9 | subscribe, 10 | } from './execute'; 11 | 12 | export type { 13 | ExecutionArgs, 14 | ExecutionResult, 15 | FormattedExecutionResult, 16 | } from './execute'; 17 | 18 | export { 19 | getArgumentValues, 20 | getVariableValues, 21 | getDirectiveValues, 22 | } from './values'; 23 | -------------------------------------------------------------------------------- /src/jsutils/mapValue.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap, ReadOnlyObjMap } from './ObjMap'; 2 | 3 | /** 4 | * Creates an object map with the same keys as `map` and values generated by 5 | * running each value of `map` thru `fn`. 6 | */ 7 | export function mapValue( 8 | map: ReadOnlyObjMap, 9 | fn: (value: T, key: string) => V, 10 | ): ObjMap { 11 | const result = Object.create(null); 12 | 13 | for (const key of Object.keys(map)) { 14 | result[key] = fn(map[key], key); 15 | } 16 | return result; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*", 4 | "integrationTests/*", 5 | "resources/*", 6 | "benchmark/benchmark.ts" 7 | ], 8 | "compilerOptions": { 9 | "lib": ["es2020"], 10 | "target": "es2020", 11 | "module": "commonjs", 12 | "moduleResolution": "node", 13 | "strict": true, 14 | "useUnknownInCatchVariables": false, 15 | "noEmit": true, 16 | "isolatedModules": true, 17 | "importsNotUsedAsValues": "error", 18 | "forceConsistentCasingInFileNames": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /benchmark/GraphQLSchema-benchmark.js: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql/type/schema.js'; 2 | import { buildClientSchema } from 'graphql/utilities/buildClientSchema.js'; 3 | 4 | import { bigSchemaIntrospectionResult } from './fixtures.js'; 5 | 6 | const bigSchema = buildClientSchema(bigSchemaIntrospectionResult.data); 7 | 8 | export const benchmark = { 9 | name: 'Recreate a GraphQLSchema', 10 | count: 40, 11 | measure() { 12 | // eslint-disable-next-line no-new 13 | new GraphQLSchema(bigSchema.toConfig()); 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /benchmark/visit-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { visit } from 'graphql/language/visitor.js'; 3 | 4 | import { bigSchemaSDL } from './fixtures.js'; 5 | 6 | const documentAST = parse(bigSchemaSDL); 7 | 8 | const visitor = { 9 | enter() { 10 | /* do nothing */ 11 | }, 12 | leave() { 13 | /* do nothing */ 14 | }, 15 | }; 16 | 17 | export const benchmark = { 18 | name: 'Visit all AST nodes', 19 | count: 10, 20 | measure() { 21 | visit(documentAST, visitor); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /.c8rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "include": ["src/"], 4 | "exclude": [ 5 | "src/**/index.ts", 6 | "src/**/*-fuzz.ts", 7 | "src/jsutils/Maybe.ts", 8 | "src/jsutils/ObjMap.ts", 9 | "src/jsutils/PromiseOrValue.ts", 10 | "src/utilities/typedQueryDocumentNode.ts" 11 | ], 12 | "clean": true, 13 | "report-dir": "reports/coverage", 14 | "skip-full": true, 15 | "reporter": ["html", "text"], 16 | "check-coverage": true, 17 | "branches": 100, 18 | "lines": 100, 19 | "functions": 100, 20 | "statements": 100 21 | } 22 | -------------------------------------------------------------------------------- /src/jsutils/toObjMap.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from './Maybe'; 2 | import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from './ObjMap'; 3 | 4 | export function toObjMap( 5 | obj: Maybe>, 6 | ): ReadOnlyObjMap { 7 | if (obj == null) { 8 | return Object.create(null); 9 | } 10 | 11 | if (Object.getPrototypeOf(obj) === null) { 12 | return obj; 13 | } 14 | 15 | const map = Object.create(null); 16 | for (const [key, value] of Object.entries(obj)) { 17 | map[key] = value; 18 | } 19 | return map; 20 | } 21 | -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | 3 | /** 4 | * CSS files with the .module.css suffix will be treated as CSS modules 5 | * and scoped locally. 6 | */ 7 | 8 | .heroBanner { 9 | padding: 4rem 0; 10 | text-align: center; 11 | position: relative; 12 | overflow: hidden; 13 | } 14 | 15 | @media screen and (max-width: 966px) { 16 | .heroBanner { 17 | padding: 2rem; 18 | } 19 | } 20 | 21 | .buttons { 22 | display: flex; 23 | align-items: center; 24 | justify-content: center; 25 | } 26 | -------------------------------------------------------------------------------- /src/error/syntaxError.ts: -------------------------------------------------------------------------------- 1 | import type { Source } from '../language/source'; 2 | 3 | import { GraphQLError } from './GraphQLError'; 4 | 5 | /** 6 | * Produces a GraphQLError representing a syntax error, containing useful 7 | * descriptive information about the syntax error's position in the source. 8 | */ 9 | export function syntaxError( 10 | source: Source, 11 | position: number, 12 | description: string, 13 | ): GraphQLError { 14 | return new GraphQLError(`Syntax Error: ${description}`, { 15 | source, 16 | positions: [position], 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/version.ts: -------------------------------------------------------------------------------- 1 | // Note: This file is autogenerated using "resources/gen-version.js" script and 2 | // automatically updated by "npm version" command. 3 | 4 | /** 5 | * A string containing the version of the GraphQL.js library 6 | */ 7 | export const version = '17.0.0-alpha.1' as string; 8 | 9 | /** 10 | * An object containing the components of the GraphQL.js version string 11 | */ 12 | export const versionInfo = Object.freeze({ 13 | major: 17 as number, 14 | minor: 0 as number, 15 | patch: 0 as number, 16 | preReleaseTag: 'alpha.1' as string | null, 17 | }); 18 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/identityFunc-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc'; 5 | 6 | describe('identityFunc', () => { 7 | it('returns the first argument it receives', () => { 8 | // @ts-expect-error (Expects an argument) 9 | expect(identityFunc()).to.equal(undefined); 10 | expect(identityFunc(undefined)).to.equal(undefined); 11 | expect(identityFunc(null)).to.equal(null); 12 | 13 | const obj = {}; 14 | expect(identityFunc(obj)).to.equal(obj); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /website/sidebars.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | tutorialSidebar: [ 5 | 'tutorials/index', 6 | 'tutorials/running-an-express-graphql-server', 7 | 'tutorials/graphql-clients', 8 | 'tutorials/basic-types', 9 | 'tutorials/passing-arguments', 10 | 'tutorials/object-types', 11 | 'tutorials/mutations-and-input-types', 12 | 'tutorials/authentication-and-express-middleware', 13 | { 14 | type: 'category', 15 | label: 'Advanced', 16 | items: ['tutorials/constructing-types'], 17 | }, 18 | 'tutorials/express-graphql', 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /benchmark/visitInParallel-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { visit, visitInParallel } from 'graphql/language/visitor.js'; 3 | 4 | import { bigSchemaSDL } from './fixtures.js'; 5 | 6 | const documentAST = parse(bigSchemaSDL); 7 | 8 | const visitors = new Array(50).fill({ 9 | enter() { 10 | /* do nothing */ 11 | }, 12 | leave() { 13 | /* do nothing */ 14 | }, 15 | }); 16 | 17 | export const benchmark = { 18 | name: 'Visit all AST nodes in parallel', 19 | count: 10, 20 | measure() { 21 | visit(documentAST, visitInParallel(visitors)); 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /integrationTests/ts/test.js: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | const { dependencies } = JSON.parse(fs.readFileSync('./package.json', 'utf-8')); 6 | 7 | const tsVersions = Object.keys(dependencies) 8 | .filter((pkg) => pkg.startsWith('typescript-')) 9 | .sort((a, b) => b.localeCompare(a)); 10 | 11 | for (const version of tsVersions) { 12 | console.log(`Testing on ${version} ...`); 13 | childProcess.execSync(tscPath(version), { stdio: 'inherit' }); 14 | } 15 | 16 | function tscPath(version) { 17 | return path.join('node_modules', version, 'bin', 'tsc'); 18 | } 19 | -------------------------------------------------------------------------------- /src/jsutils/toError.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from './inspect'; 2 | 3 | /** 4 | * Sometimes a non-error is thrown, wrap it as an Error instance to ensure a consistent Error interface. 5 | */ 6 | export function toError(thrownValue: unknown): Error { 7 | return thrownValue instanceof Error 8 | ? thrownValue 9 | : new NonErrorThrown(thrownValue); 10 | } 11 | 12 | class NonErrorThrown extends Error { 13 | thrownValue: unknown; 14 | 15 | constructor(thrownValue: unknown) { 16 | super('Unexpected error value: ' + inspect(thrownValue)); 17 | this.name = 'NonErrorThrown'; 18 | this.thrownValue = thrownValue; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/language/tokenKind.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An exported enum describing the different kinds of tokens that the 3 | * lexer emits. 4 | */ 5 | export enum TokenKind { 6 | SOF = '', 7 | EOF = '', 8 | BANG = '!', 9 | DOLLAR = '$', 10 | AMP = '&', 11 | PAREN_L = '(', 12 | PAREN_R = ')', 13 | SPREAD = '...', 14 | COLON = ':', 15 | EQUALS = '=', 16 | AT = '@', 17 | BRACKET_L = '[', 18 | BRACKET_R = ']', 19 | BRACE_L = '{', 20 | PIPE = '|', 21 | BRACE_R = '}', 22 | NAME = 'Name', 23 | INT = 'Int', 24 | FLOAT = 'Float', 25 | STRING = 'String', 26 | BLOCK_STRING = 'BlockString', 27 | COMMENT = 'Comment', 28 | } 29 | -------------------------------------------------------------------------------- /benchmark/validateGQL-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; 3 | import { getIntrospectionQuery } from 'graphql/utilities/getIntrospectionQuery.js'; 4 | import { validate } from 'graphql/validation/validate.js'; 5 | 6 | import { bigSchemaSDL } from './fixtures.js'; 7 | 8 | const schema = buildSchema(bigSchemaSDL, { assumeValid: true }); 9 | const queryAST = parse(getIntrospectionQuery()); 10 | 11 | export const benchmark = { 12 | name: 'Validate Introspection Query', 13 | count: 50, 14 | measure() { 15 | validate(schema, queryAST); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /benchmark/introspectionFromSchema-benchmark.js: -------------------------------------------------------------------------------- 1 | import { executeSync } from 'graphql/execution/execute.js'; 2 | import { parse } from 'graphql/language/parser.js'; 3 | import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; 4 | import { getIntrospectionQuery } from 'graphql/utilities/getIntrospectionQuery.js'; 5 | 6 | import { bigSchemaSDL } from './fixtures.js'; 7 | 8 | const schema = buildSchema(bigSchemaSDL, { assumeValid: true }); 9 | const document = parse(getIntrospectionQuery()); 10 | 11 | export const benchmark = { 12 | name: 'Execute Introspection Query', 13 | count: 20, 14 | measure() { 15 | executeSync({ schema, document }); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /integrationTests/node/test.js: -------------------------------------------------------------------------------- 1 | import childProcess from 'child_process'; 2 | import fs from 'fs'; 3 | 4 | const graphqlPackageJSON = JSON.parse( 5 | fs.readFileSync('./node_modules/graphql/package.json', 'utf-8'), 6 | ); 7 | 8 | const nodeVersions = graphqlPackageJSON.engines.node 9 | .split(' || ') 10 | .map((version) => version.replace(/^(\^|>=)/, '')) 11 | .sort((a, b) => b.localeCompare(a)); 12 | 13 | for (const version of nodeVersions) { 14 | console.log(`Testing on node@${version} ...`); 15 | 16 | childProcess.execSync( 17 | `docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app node:${version}-slim node ./index.js`, 18 | { stdio: 'inherit' }, 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/utilities/concatAST.ts: -------------------------------------------------------------------------------- 1 | import type { DefinitionNode, DocumentNode } from '../language/ast'; 2 | import { Kind } from '../language/kinds'; 3 | 4 | /** 5 | * Provided a collection of ASTs, presumably each from different files, 6 | * concatenate the ASTs together into batched AST, useful for validating many 7 | * GraphQL source files which together represent one conceptual application. 8 | */ 9 | export function concatAST( 10 | documents: ReadonlyArray, 11 | ): DocumentNode { 12 | const definitions: Array = []; 13 | for (const doc of documents) { 14 | definitions.push(...doc.definitions); 15 | } 16 | return { kind: Kind.DOCUMENT, definitions }; 17 | } 18 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/inspectStr-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { inspectStr } from '../inspectStr'; 5 | 6 | describe('inspectStr', () => { 7 | it('handles null and undefined values', () => { 8 | expect(inspectStr(null)).to.equal('null'); 9 | expect(inspectStr(undefined)).to.equal('null'); 10 | }); 11 | 12 | it('correctly print various strings', () => { 13 | expect(inspectStr('')).to.equal('``'); 14 | expect(inspectStr('a')).to.equal('`a`'); 15 | expect(inspectStr('"')).to.equal('`"`'); 16 | expect(inspectStr("'")).to.equal("`'`"); 17 | expect(inspectStr('\\"')).to.equal('`\\"`'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/resolveOnNextTick-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { resolveOnNextTick } from '../resolveOnNextTick'; 5 | 6 | describe('resolveOnNextTick', () => { 7 | it('resolves promise on the next tick', async () => { 8 | const output = []; 9 | 10 | const promise1 = resolveOnNextTick().then(() => { 11 | output.push('second'); 12 | }); 13 | const promise2 = resolveOnNextTick().then(() => { 14 | output.push('third'); 15 | }); 16 | output.push('first'); 17 | 18 | await Promise.all([promise1, promise2]); 19 | expect(output).to.deep.equal(['first', 'second', 'third']); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /integrationTests/node/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable simple-import-sort/imports */ 2 | import assert from 'assert'; 3 | import { readFileSync } from 'fs'; 4 | 5 | import { graphqlSync } from 'graphql'; 6 | import { buildSchema } from 'graphql/utilities'; 7 | import { version } from 'graphql/version'; 8 | 9 | assert.deepStrictEqual( 10 | version, 11 | JSON.parse(readFileSync('./node_modules/graphql/package.json')).version, 12 | ); 13 | 14 | const schema = buildSchema('type Query { hello: String }'); 15 | 16 | const result = graphqlSync({ 17 | schema, 18 | source: '{ hello }', 19 | rootValue: { hello: 'world' }, 20 | }); 21 | 22 | assert.deepStrictEqual(result, { 23 | data: { 24 | __proto__: null, 25 | hello: 'world', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /src/jsutils/promiseForObject.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap } from './ObjMap'; 2 | 3 | /** 4 | * This function transforms a JS object `ObjMap>` into 5 | * a `Promise>` 6 | * 7 | * This is akin to bluebird's `Promise.props`, but implemented only using 8 | * `Promise.all` so it will work with any implementation of ES6 promises. 9 | */ 10 | export function promiseForObject( 11 | object: ObjMap>, 12 | ): Promise> { 13 | return Promise.all(Object.values(object)).then((resolvedValues) => { 14 | const resolvedObject = Object.create(null); 15 | for (const [i, key] of Object.keys(object).entries()) { 16 | resolvedObject[key] = resolvedValues[i]; 17 | } 18 | return resolvedObject; 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /benchmark/list-sync-benchmark.js: -------------------------------------------------------------------------------- 1 | import { execute } from 'graphql/execution/execute.js'; 2 | import { parse } from 'graphql/language/parser.js'; 3 | import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; 4 | 5 | const schema = buildSchema('type Query { listField: [String] }'); 6 | const document = parse('{ listField }'); 7 | 8 | function listField() { 9 | const results = []; 10 | for (let index = 0; index < 1000; index++) { 11 | results.push(index); 12 | } 13 | return results; 14 | } 15 | 16 | export const benchmark = { 17 | name: 'Execute Synchronous List Field', 18 | count: 10, 19 | async measure() { 20 | await execute({ 21 | schema, 22 | document, 23 | rootValue: { listField }, 24 | }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /benchmark/list-async-benchmark.js: -------------------------------------------------------------------------------- 1 | import { execute } from 'graphql/execution/execute.js'; 2 | import { parse } from 'graphql/language/parser.js'; 3 | import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; 4 | 5 | const schema = buildSchema('type Query { listField: [String] }'); 6 | const document = parse('{ listField }'); 7 | 8 | function listField() { 9 | const results = []; 10 | for (let index = 0; index < 1000; index++) { 11 | results.push(Promise.resolve(index)); 12 | } 13 | return results; 14 | } 15 | 16 | export const benchmark = { 17 | name: 'Execute Asynchronous List Field', 18 | count: 10, 19 | async measure() { 20 | await execute({ 21 | schema, 22 | document, 23 | rootValue: { listField }, 24 | }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /benchmark/validateInvalidGQL-benchmark.js: -------------------------------------------------------------------------------- 1 | import { parse } from 'graphql/language/parser.js'; 2 | import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; 3 | import { validate } from 'graphql/validation/validate.js'; 4 | 5 | import { bigSchemaSDL } from './fixtures.js'; 6 | 7 | const schema = buildSchema(bigSchemaSDL, { assumeValid: true }); 8 | const queryAST = parse(` 9 | { 10 | unknownField 11 | ... on unknownType { 12 | anotherUnknownField 13 | ...unknownFragment 14 | } 15 | } 16 | 17 | fragment TestFragment on anotherUnknownType { 18 | yetAnotherUnknownField 19 | } 20 | `); 21 | 22 | export const benchmark = { 23 | name: 'Validate Invalid Query', 24 | count: 50, 25 | measure() { 26 | validate(schema, queryAST); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/capitalize-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { capitalize } from '../capitalize'; 5 | 6 | describe('capitalize', () => { 7 | it('Converts the first character of string to upper case and the remaining to lower case', () => { 8 | expect(capitalize('')).to.equal(''); 9 | 10 | expect(capitalize('a')).to.equal('A'); 11 | expect(capitalize('A')).to.equal('A'); 12 | 13 | expect(capitalize('ab')).to.equal('Ab'); 14 | expect(capitalize('aB')).to.equal('Ab'); 15 | expect(capitalize('Ab')).to.equal('Ab'); 16 | expect(capitalize('AB')).to.equal('Ab'); 17 | 18 | expect(capitalize('platypus')).to.equal('Platypus'); 19 | expect(capitalize('PLATYPUS')).to.equal('Platypus'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/jsutils/Path.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from './Maybe'; 2 | 3 | export interface Path { 4 | readonly prev: Path | undefined; 5 | readonly key: string | number; 6 | readonly typename: string | undefined; 7 | } 8 | 9 | /** 10 | * Given a Path and a key, return a new Path containing the new key. 11 | */ 12 | export function addPath( 13 | prev: Readonly | undefined, 14 | key: string | number, 15 | typename: string | undefined, 16 | ): Path { 17 | return { prev, key, typename }; 18 | } 19 | 20 | /** 21 | * Given a Path, return an Array of the path keys. 22 | */ 23 | export function pathToArray( 24 | path: Maybe>, 25 | ): Array { 26 | const flattened = []; 27 | let curr = path; 28 | while (curr) { 29 | flattened.push(curr.key); 30 | curr = curr.prev; 31 | } 32 | return flattened.reverse(); 33 | } 34 | -------------------------------------------------------------------------------- /src/jsutils/keyValMap.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap } from './ObjMap'; 2 | 3 | /** 4 | * Creates a keyed JS object from an array, given a function to produce the keys 5 | * and a function to produce the values from each item in the array. 6 | * ```ts 7 | * const phoneBook = [ 8 | * { name: 'Jon', num: '555-1234' }, 9 | * { name: 'Jenny', num: '867-5309' } 10 | * ] 11 | * 12 | * // { Jon: '555-1234', Jenny: '867-5309' } 13 | * const phonesByName = keyValMap( 14 | * phoneBook, 15 | * entry => entry.name, 16 | * entry => entry.num 17 | * ) 18 | * ``` 19 | */ 20 | export function keyValMap( 21 | list: ReadonlyArray, 22 | keyFn: (item: T) => string, 23 | valFn: (item: T) => V, 24 | ): ObjMap { 25 | const result = Object.create(null); 26 | for (const item of list) { 27 | result[keyFn(item)] = valFn(item); 28 | } 29 | return result; 30 | } 31 | -------------------------------------------------------------------------------- /src/language/directiveLocation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The set of allowed directive location values. 3 | */ 4 | export enum DirectiveLocation { 5 | /** Request Definitions */ 6 | QUERY = 'QUERY', 7 | MUTATION = 'MUTATION', 8 | SUBSCRIPTION = 'SUBSCRIPTION', 9 | FIELD = 'FIELD', 10 | FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION', 11 | FRAGMENT_SPREAD = 'FRAGMENT_SPREAD', 12 | INLINE_FRAGMENT = 'INLINE_FRAGMENT', 13 | VARIABLE_DEFINITION = 'VARIABLE_DEFINITION', 14 | /** Type System Definitions */ 15 | SCHEMA = 'SCHEMA', 16 | SCALAR = 'SCALAR', 17 | OBJECT = 'OBJECT', 18 | FIELD_DEFINITION = 'FIELD_DEFINITION', 19 | ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION', 20 | INTERFACE = 'INTERFACE', 21 | UNION = 'UNION', 22 | ENUM = 'ENUM', 23 | ENUM_VALUE = 'ENUM_VALUE', 24 | INPUT_OBJECT = 'INPUT_OBJECT', 25 | INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION', 26 | } 27 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/isObjectLike-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc'; 5 | import { isObjectLike } from '../isObjectLike'; 6 | 7 | describe('isObjectLike', () => { 8 | it('should return `true` for objects', () => { 9 | expect(isObjectLike({})).to.equal(true); 10 | expect(isObjectLike(Object.create(null))).to.equal(true); 11 | expect(isObjectLike(/a/)).to.equal(true); 12 | expect(isObjectLike([])).to.equal(true); 13 | }); 14 | 15 | it('should return `false` for non-objects', () => { 16 | expect(isObjectLike(undefined)).to.equal(false); 17 | expect(isObjectLike(null)).to.equal(false); 18 | expect(isObjectLike(true)).to.equal(false); 19 | expect(isObjectLike('')).to.equal(false); 20 | expect(isObjectLike(identityFunc)).to.equal(false); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /src/jsutils/formatList.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from './invariant'; 2 | 3 | /** 4 | * Given [ A, B, C ] return 'A, B, or C'. 5 | */ 6 | export function orList(items: ReadonlyArray): string { 7 | return formatList('or', items); 8 | } 9 | 10 | /** 11 | * Given [ A, B, C ] return 'A, B, and C'. 12 | */ 13 | export function andList(items: ReadonlyArray): string { 14 | return formatList('and', items); 15 | } 16 | 17 | function formatList(conjunction: string, items: ReadonlyArray): string { 18 | invariant(items.length !== 0); 19 | 20 | switch (items.length) { 21 | case 1: 22 | return items[0]; 23 | case 2: 24 | return items[0] + ' ' + conjunction + ' ' + items[1]; 25 | } 26 | 27 | const allButLast = items.slice(0, -1); 28 | const lastItem = items[items.length - 1]; 29 | return allButLast.join(', ') + ', ' + conjunction + ' ' + lastItem; 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | on: push 3 | jobs: 4 | ci: 5 | uses: ./.github/workflows/ci.yml 6 | deploy-to-npm-branch: 7 | name: Deploy to `npm` branch 8 | needs: ci 9 | if: github.ref == 'refs/heads/main' 10 | uses: ./.github/workflows/deploy-artifact-as-branch.yml 11 | with: 12 | environment: npm-branch 13 | artifact_name: npmDist 14 | target_branch: npm 15 | commit_message: "Deploy ${{github.event.workflow_run.head_sha}} to 'npm' branch" 16 | 17 | deploy-to-deno-branch: 18 | name: Deploy to `deno` branch 19 | needs: ci 20 | if: github.ref == 'refs/heads/main' 21 | uses: ./.github/workflows/deploy-artifact-as-branch.yml 22 | with: 23 | environment: deno-branch 24 | artifact_name: denoDist 25 | target_branch: deno 26 | commit_message: "Deploy ${{github.event.workflow_run.head_sha}} to 'deno' branch" 27 | -------------------------------------------------------------------------------- /src/jsutils/isIterableObject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the provided object is an Object (i.e. not a string literal) 3 | * and implements the Iterator protocol. 4 | * 5 | * This may be used in place of [Array.isArray()][isArray] to determine if 6 | * an object should be iterated-over e.g. Array, Map, Set, Int8Array, 7 | * TypedArray, etc. but excludes string literals. 8 | * 9 | * @example 10 | * ```ts 11 | * isIterableObject([ 1, 2, 3 ]) // true 12 | * isIterableObject(new Map()) // true 13 | * isIterableObject('ABC') // false 14 | * isIterableObject({ key: 'value' }) // false 15 | * isIterableObject({ length: 1, 0: 'Alpha' }) // false 16 | * ``` 17 | */ 18 | export function isIterableObject( 19 | maybeIterable: any, 20 | ): maybeIterable is Iterable { 21 | return ( 22 | typeof maybeIterable === 'object' && 23 | typeof maybeIterable?.[Symbol.iterator] === 'function' 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/jsutils/promiseReduce.ts: -------------------------------------------------------------------------------- 1 | import { isPromise } from './isPromise'; 2 | import type { PromiseOrValue } from './PromiseOrValue'; 3 | 4 | /** 5 | * Similar to Array.prototype.reduce(), however the reducing callback may return 6 | * a Promise, in which case reduction will continue after each promise resolves. 7 | * 8 | * If the callback does not return a Promise, then this function will also not 9 | * return a Promise. 10 | */ 11 | export function promiseReduce( 12 | values: Iterable, 13 | callbackFn: (accumulator: U, currentValue: T) => PromiseOrValue, 14 | initialValue: PromiseOrValue, 15 | ): PromiseOrValue { 16 | let accumulator = initialValue; 17 | for (const value of values) { 18 | accumulator = isPromise(accumulator) 19 | ? accumulator.then((resolved) => callbackFn(resolved, value)) 20 | : callbackFn(accumulator, value); 21 | } 22 | return accumulator; 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/mutation-testing.yml: -------------------------------------------------------------------------------- 1 | name: Mutation Testing 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 0 * * *' # run once every day at 00:00 UTC 6 | jobs: 7 | lint: 8 | name: Run mutation testing 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@v3 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | cache: npm 20 | node-version-file: '.node-version' 21 | 22 | - name: Install Dependencies 23 | run: npm ci --ignore-scripts 24 | 25 | - name: Run mutation testing 26 | run: npm run testonly:mutate 27 | 28 | - name: Upload mutation testing report 29 | uses: actions/upload-artifact@v3 30 | with: 31 | name: mutationTestingReport 32 | path: ./reports/mutation/mutation.html 33 | -------------------------------------------------------------------------------- /integrationTests/ts/basic-test.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionResult } from 'graphql/execution'; 2 | 3 | import { graphqlSync } from 'graphql'; 4 | import { GraphQLString, GraphQLSchema, GraphQLObjectType } from 'graphql/type'; 5 | 6 | const queryType: GraphQLObjectType = new GraphQLObjectType({ 7 | name: 'Query', 8 | fields: () => ({ 9 | sayHi: { 10 | type: GraphQLString, 11 | args: { 12 | who: { 13 | type: GraphQLString, 14 | defaultValue: 'World', 15 | }, 16 | }, 17 | resolve(_root, args: { who: string }) { 18 | return 'Hello ' + args.who; 19 | }, 20 | }, 21 | }), 22 | }); 23 | 24 | const schema: GraphQLSchema = new GraphQLSchema({ query: queryType }); 25 | 26 | const result: ExecutionResult = graphqlSync({ 27 | schema, 28 | source: ` 29 | query helloWho($who: String){ 30 | test(who: $who) 31 | } 32 | `, 33 | variableValues: { who: 'Dolly' }, 34 | }); 35 | -------------------------------------------------------------------------------- /src/language/location.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../jsutils/invariant'; 2 | 3 | import type { Source } from './source'; 4 | 5 | const LineRegExp = /\r\n|[\n\r]/g; 6 | 7 | /** 8 | * Represents a location in a Source. 9 | */ 10 | export interface SourceLocation { 11 | readonly line: number; 12 | readonly column: number; 13 | } 14 | 15 | /** 16 | * Takes a Source and a UTF-8 character offset, and returns the corresponding 17 | * line and column as a SourceLocation. 18 | */ 19 | export function getLocation(source: Source, position: number): SourceLocation { 20 | let lastLineStart = 0; 21 | let line = 1; 22 | 23 | for (const match of source.body.matchAll(LineRegExp)) { 24 | invariant(typeof match.index === 'number'); 25 | if (match.index >= position) { 26 | break; 27 | } 28 | lastLineStart = match.index + match[0].length; 29 | line += 1; 30 | } 31 | 32 | return { line, column: position + 1 - lastLineStart }; 33 | } 34 | -------------------------------------------------------------------------------- /src/utilities/typedQueryDocumentNode.ts: -------------------------------------------------------------------------------- 1 | import type { DocumentNode, ExecutableDefinitionNode } from '../language/ast'; 2 | /** 3 | * Wrapper type that contains DocumentNode and types that can be deduced from it. 4 | */ 5 | export interface TypedQueryDocumentNode< 6 | TResponseData = { [key: string]: any }, 7 | TRequestVariables = { [key: string]: any }, 8 | > extends DocumentNode { 9 | readonly definitions: ReadonlyArray; 10 | // FIXME: remove once TS implements proper way to enforce nominal typing 11 | /** 12 | * This type is used to ensure that the variables you pass in to the query are assignable to Variables 13 | * and that the Result is assignable to whatever you pass your result to. The method is never actually 14 | * implemented, but the type is valid because we list it as optional 15 | */ 16 | __ensureTypesOfVariablesAndResultMatching?: ( 17 | variables: TRequestVariables, 18 | ) => TResponseData; 19 | } 20 | -------------------------------------------------------------------------------- /website/docs/tutorials/express-graphql.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: express-graphql 3 | --- 4 | 5 | The `express-graphql` module provides a simple way to create an [Express](https://expressjs.com/) server that runs a GraphQL API. 6 | 7 | ```js 8 | import { graphqlHTTP } from 'express-graphql'; // ES6 9 | var { graphqlHTTP } = require('express-graphql'); // CommonJS 10 | ``` 11 | 12 | ### graphqlHTTP 13 | 14 | ```js 15 | graphqlHTTP({ 16 | schema: GraphQLSchema, 17 | graphiql?: ?boolean, 18 | rootValue?: ?any, 19 | context?: ?any, 20 | pretty?: ?boolean, 21 | formatError?: ?Function, 22 | validationRules?: ?Array, 23 | }): Middleware 24 | ``` 25 | 26 | Constructs an Express application based on a GraphQL schema. 27 | 28 | See the [express-graphql tutorial](./running-an-express-graphql-server.md) for sample usage. 29 | 30 | See the [GitHub README](https://github.com/graphql/express-graphql) for more extensive documentation of the details of this method. 31 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/only-ascii.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | meta: { 5 | schema: [ 6 | { 7 | type: 'object', 8 | properties: { 9 | allowEmoji: { 10 | type: 'boolean', 11 | }, 12 | }, 13 | additionalProperties: false, 14 | }, 15 | ], 16 | }, 17 | create: onlyASCII, 18 | }; 19 | 20 | function onlyASCII(context) { 21 | const regExp = 22 | context.options[0]?.allowEmoji === true 23 | ? /[^\p{ASCII}\p{Emoji}]+/gu 24 | : /\P{ASCII}+/gu; 25 | 26 | return { 27 | Program() { 28 | const sourceCode = context.getSourceCode(); 29 | const text = sourceCode.getText(); 30 | 31 | for (const match of text.matchAll(regExp)) { 32 | context.report({ 33 | loc: sourceCode.getLocFromIndex(match.index), 34 | message: `Non-ASCII character "${match[0]}" found.`, 35 | }); 36 | } 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/jsutils/memoize3.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Memoizes the provided three-argument function. 3 | */ 4 | export function memoize3< 5 | A1 extends object, 6 | A2 extends object, 7 | A3 extends object, 8 | R, 9 | >(fn: (a1: A1, a2: A2, a3: A3) => R): (a1: A1, a2: A2, a3: A3) => R { 10 | let cache0: WeakMap>>; 11 | 12 | return function memoized(a1, a2, a3) { 13 | if (cache0 === undefined) { 14 | cache0 = new WeakMap(); 15 | } 16 | 17 | let cache1 = cache0.get(a1); 18 | if (cache1 === undefined) { 19 | cache1 = new WeakMap(); 20 | cache0.set(a1, cache1); 21 | } 22 | 23 | let cache2 = cache1.get(a2); 24 | if (cache2 === undefined) { 25 | cache2 = new WeakMap(); 26 | cache1.set(a2, cache2); 27 | } 28 | 29 | let fnResult = cache2.get(a3); 30 | if (fnResult === undefined) { 31 | fnResult = fn(a1, a2, a3); 32 | cache2.set(a3, fnResult); 33 | } 34 | 35 | return fnResult; 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/validation/rules/KnownFragmentNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor'; 4 | 5 | import type { ValidationContext } from '../ValidationContext'; 6 | 7 | /** 8 | * Known fragment names 9 | * 10 | * A GraphQL document is only valid if all `...Fragment` fragment spreads refer 11 | * to fragments defined in the same document. 12 | * 13 | * See https://spec.graphql.org/draft/#sec-Fragment-spread-target-defined 14 | */ 15 | export function KnownFragmentNamesRule(context: ValidationContext): ASTVisitor { 16 | return { 17 | FragmentSpread(node) { 18 | const fragmentName = node.name.value; 19 | const fragment = context.getFragment(fragmentName); 20 | if (!fragment) { 21 | context.reportError( 22 | new GraphQLError(`Unknown fragment "${fragmentName}".`, { 23 | nodes: node.name, 24 | }), 25 | ); 26 | } 27 | }, 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/jsutils/didYouMean.ts: -------------------------------------------------------------------------------- 1 | import { orList } from './formatList'; 2 | 3 | const MAX_SUGGESTIONS = 5; 4 | 5 | /** 6 | * Given [ A, B, C ] return ' Did you mean A, B, or C?'. 7 | */ 8 | export function didYouMean(suggestions: ReadonlyArray): string; 9 | export function didYouMean( 10 | subMessage: string, 11 | suggestions: ReadonlyArray, 12 | ): string; 13 | export function didYouMean( 14 | firstArg: string | ReadonlyArray, 15 | secondArg?: ReadonlyArray, 16 | ) { 17 | const [subMessage, suggestions] = secondArg 18 | ? [firstArg as string, secondArg] 19 | : [undefined, firstArg as ReadonlyArray]; 20 | 21 | if (suggestions.length === 0) { 22 | return ''; 23 | } 24 | 25 | let message = ' Did you mean '; 26 | if (subMessage) { 27 | message += subMessage + ' '; 28 | } 29 | 30 | const suggestionList = orList( 31 | suggestions.slice(0, MAX_SUGGESTIONS).map((x) => `"${x}"`), 32 | ); 33 | return message + suggestionList + '?'; 34 | } 35 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | ## GraphQL JS 2 | 3 | The primary `graphql` module includes everything you need to define a GraphQL 4 | schema and fulfill GraphQL requests. 5 | 6 | ```js 7 | import { ... } from 'graphql'; // ES6 8 | var GraphQL = require('graphql'); // CommonJS 9 | ``` 10 | 11 | Each sub directory within is a sub-module of graphql-js: 12 | 13 | - [`graphql/language`](language/README.md): Parse and operate on the GraphQL 14 | language. 15 | - [`graphql/type`](type/README.md): Define GraphQL types and schema. 16 | - [`graphql/validation`](validation/README.md): The Validation phase of 17 | fulfilling a GraphQL result. 18 | - [`graphql/execution`](execution/README.md): The Execution phase of fulfilling 19 | a GraphQL request. 20 | - [`graphql/error`](error/README.md): Creating and formatting GraphQL errors. 21 | - [`graphql/utilities`](utilities/README.md): Common useful computations upon 22 | the GraphQL language and type objects. 23 | - [`graphql/subscription`](subscription/README.md): Subscribe to data updates. 24 | -------------------------------------------------------------------------------- /src/__testUtils__/genFuzzStrings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generator that produces all possible combinations of allowed characters. 3 | */ 4 | export function* genFuzzStrings(options: { 5 | allowedChars: ReadonlyArray; 6 | maxLength: number; 7 | }): Generator { 8 | const { allowedChars, maxLength } = options; 9 | const numAllowedChars = allowedChars.length; 10 | 11 | let numCombinations = 0; 12 | for (let length = 1; length <= maxLength; ++length) { 13 | numCombinations += numAllowedChars ** length; 14 | } 15 | 16 | yield ''; // special case for empty string 17 | for (let combination = 0; combination < numCombinations; ++combination) { 18 | let permutation = ''; 19 | 20 | let leftOver = combination; 21 | while (leftOver >= 0) { 22 | const reminder = leftOver % numAllowedChars; 23 | permutation = allowedChars[reminder] + permutation; 24 | leftOver = (leftOver - reminder) / numAllowedChars - 1; 25 | } 26 | 27 | yield permutation; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/utilities/__tests__/concatAST-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { dedent } from '../../__testUtils__/dedent'; 5 | 6 | import { parse } from '../../language/parser'; 7 | import { print } from '../../language/printer'; 8 | import { Source } from '../../language/source'; 9 | 10 | import { concatAST } from '../concatAST'; 11 | 12 | describe('concatAST', () => { 13 | it('concatenates two ASTs together', () => { 14 | const sourceA = new Source(` 15 | { a, b, ...Frag } 16 | `); 17 | 18 | const sourceB = new Source(` 19 | fragment Frag on T { 20 | c 21 | } 22 | `); 23 | 24 | const astA = parse(sourceA); 25 | const astB = parse(sourceB); 26 | const astC = concatAST([astA, astB]); 27 | 28 | expect(print(astC)).to.equal(dedent` 29 | { 30 | a 31 | b 32 | ...Frag 33 | } 34 | 35 | fragment Frag on T { 36 | c 37 | } 38 | `); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /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 noDirImportRule(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 | -------------------------------------------------------------------------------- /src/jsutils/keyMap.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap } from './ObjMap'; 2 | 3 | /** 4 | * Creates a keyed JS object from an array, given a function to produce the keys 5 | * for each value in the array. 6 | * 7 | * This provides a convenient lookup for the array items if the key function 8 | * produces unique results. 9 | * ```ts 10 | * const phoneBook = [ 11 | * { name: 'Jon', num: '555-1234' }, 12 | * { name: 'Jenny', num: '867-5309' } 13 | * ] 14 | * 15 | * const entriesByName = keyMap( 16 | * phoneBook, 17 | * entry => entry.name 18 | * ) 19 | * 20 | * // { 21 | * // Jon: { name: 'Jon', num: '555-1234' }, 22 | * // Jenny: { name: 'Jenny', num: '867-5309' } 23 | * // } 24 | * 25 | * const jennyEntry = entriesByName['Jenny'] 26 | * 27 | * // { name: 'Jenny', num: '857-6309' } 28 | * ``` 29 | */ 30 | export function keyMap( 31 | list: ReadonlyArray, 32 | keyFn: (item: T) => string, 33 | ): ObjMap { 34 | const result = Object.create(null); 35 | for (const item of list) { 36 | result[keyFn(item)] = item; 37 | } 38 | return result; 39 | } 40 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/Path-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { addPath, pathToArray } from '../Path'; 5 | 6 | describe('Path', () => { 7 | it('can create a Path', () => { 8 | const first = addPath(undefined, 1, 'First'); 9 | 10 | expect(first).to.deep.equal({ 11 | prev: undefined, 12 | key: 1, 13 | typename: 'First', 14 | }); 15 | }); 16 | 17 | it('can add a new key to an existing Path', () => { 18 | const first = addPath(undefined, 1, 'First'); 19 | const second = addPath(first, 'two', 'Second'); 20 | 21 | expect(second).to.deep.equal({ 22 | prev: first, 23 | key: 'two', 24 | typename: 'Second', 25 | }); 26 | }); 27 | 28 | it('can convert a Path to an array of its keys', () => { 29 | const root = addPath(undefined, 0, 'Root'); 30 | const first = addPath(root, 'one', 'First'); 31 | const second = addPath(first, 2, 'Second'); 32 | 33 | const path = pathToArray(second); 34 | expect(path).to.deep.equal([0, 'one', 2]); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /resources/gen-version.ts: -------------------------------------------------------------------------------- 1 | import { readPackageJSON, writeGeneratedFile } from './utils'; 2 | 3 | const { version } = readPackageJSON(); 4 | const versionMatch = /^(\d+)\.(\d+)\.(\d+)-?(.*)?$/.exec(version); 5 | if (!versionMatch) { 6 | throw new Error('Version does not match semver spec: ' + version); 7 | } 8 | 9 | const [, major, minor, patch, preReleaseTag] = versionMatch; 10 | 11 | const body = ` 12 | // Note: This file is autogenerated using "resources/gen-version.js" script and 13 | // automatically updated by "npm version" command. 14 | 15 | /** 16 | * A string containing the version of the GraphQL.js library 17 | */ 18 | export const version = '${version}' as string; 19 | 20 | /** 21 | * An object containing the components of the GraphQL.js version string 22 | */ 23 | export const versionInfo = Object.freeze({ 24 | major: ${major} as number, 25 | minor: ${minor} as number, 26 | patch: ${patch} as number, 27 | preReleaseTag: ${ 28 | preReleaseTag ? `'${preReleaseTag}'` : 'null' 29 | } as string | null, 30 | }); 31 | `; 32 | 33 | writeGeneratedFile('./src/version.ts', body); 34 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/AccumulatorMap-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { AccumulatorMap } from '../AccumulatorMap'; 5 | 6 | function expectMap(map: Map) { 7 | return expect(Object.fromEntries(map.entries())); 8 | } 9 | 10 | describe('AccumulatorMap', () => { 11 | it('can be Object.toStringified', () => { 12 | const accumulatorMap = new AccumulatorMap(); 13 | 14 | expect(Object.prototype.toString.call(accumulatorMap)).to.equal( 15 | '[object AccumulatorMap]', 16 | ); 17 | }); 18 | 19 | it('accumulate items', () => { 20 | const accumulatorMap = new AccumulatorMap(); 21 | 22 | expectMap(accumulatorMap).to.deep.equal({}); 23 | 24 | accumulatorMap.add('a', 1); 25 | accumulatorMap.add('b', 2); 26 | accumulatorMap.add('c', 3); 27 | accumulatorMap.add('b', 4); 28 | accumulatorMap.add('c', 5); 29 | accumulatorMap.add('c', 6); 30 | expectMap(accumulatorMap).to.deep.equal({ 31 | a: [1], 32 | b: [2, 4], 33 | c: [3, 5, 6], 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/didYouMean-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { didYouMean } from '../didYouMean'; 5 | 6 | describe('didYouMean', () => { 7 | it('Does accept an empty list', () => { 8 | expect(didYouMean([])).to.equal(''); 9 | }); 10 | 11 | it('Handles single suggestion', () => { 12 | expect(didYouMean(['A'])).to.equal(' Did you mean "A"?'); 13 | }); 14 | 15 | it('Handles two suggestions', () => { 16 | expect(didYouMean(['A', 'B'])).to.equal(' Did you mean "A" or "B"?'); 17 | }); 18 | 19 | it('Handles multiple suggestions', () => { 20 | expect(didYouMean(['A', 'B', 'C'])).to.equal( 21 | ' Did you mean "A", "B", or "C"?', 22 | ); 23 | }); 24 | 25 | it('Limits to five suggestions', () => { 26 | expect(didYouMean(['A', 'B', 'C', 'D', 'E', 'F'])).to.equal( 27 | ' Did you mean "A", "B", "C", "D", or "E"?', 28 | ); 29 | }); 30 | 31 | it('Adds sub-message', () => { 32 | expect(didYouMean('the letter', ['A'])).to.equal( 33 | ' Did you mean the letter "A"?', 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/__testUtils__/dedent.ts: -------------------------------------------------------------------------------- 1 | export function dedentString(string: string): string { 2 | const trimmedStr = string 3 | .replace(/^\n*/m, '') // remove leading newline 4 | .replace(/[ \t\n]*$/, ''); // remove trailing spaces and tabs 5 | 6 | // fixes indentation by removing leading spaces and tabs from each line 7 | let indent = ''; 8 | for (const char of trimmedStr) { 9 | if (char !== ' ' && char !== '\t') { 10 | break; 11 | } 12 | indent += char; 13 | } 14 | 15 | return trimmedStr.replace(RegExp('^' + indent, 'mg'), ''); // remove indent 16 | } 17 | 18 | /** 19 | * An ES6 string tag that fixes indentation and also trims string. 20 | * 21 | * Example usage: 22 | * ```ts 23 | * const str = dedent` 24 | * { 25 | * test 26 | * } 27 | * `; 28 | * str === "{\n test\n}"; 29 | * ``` 30 | */ 31 | export function dedent( 32 | strings: ReadonlyArray, 33 | ...values: ReadonlyArray 34 | ): string { 35 | let str = strings[0]; 36 | 37 | for (let i = 1; i < strings.length; ++i) { 38 | str += values[i - 1] + strings[i]; // interpolation 39 | } 40 | return dedentString(str); 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PullRequest 2 | on: pull_request 3 | jobs: 4 | ci: 5 | uses: ./.github/workflows/ci.yml 6 | 7 | diff-npm-package: 8 | name: Diff content of NPM package 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repo 12 | uses: actions/checkout@v3 13 | with: 14 | persist-credentials: false 15 | 16 | - name: Deepen cloned repo 17 | env: 18 | BASE_SHA: ${{ github.event.pull_request.base.sha }} 19 | run: 'git fetch --depth=1 origin "$BASE_SHA:refs/tags/BASE"' 20 | 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | cache: npm 25 | node-version-file: '.node-version' 26 | 27 | - name: Install Dependencies 28 | run: npm ci --ignore-scripts 29 | 30 | - name: Generate report 31 | run: 'npm run diff:npm BASE HEAD' 32 | 33 | - name: Upload generated report 34 | uses: actions/upload-artifact@v3 35 | with: 36 | name: npm-dist-diff.html 37 | path: ./reports/npm-dist-diff.html 38 | if-no-files-found: ignore 39 | -------------------------------------------------------------------------------- /website/src/pages/index.jsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import Layout from '@theme/Layout'; 4 | import clsx from 'clsx'; 5 | import React from 'react'; 6 | 7 | import styles from './index.module.css'; 8 | 9 | function HomepageHeader() { 10 | const { siteConfig } = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 |

{siteConfig.title}

15 |

{siteConfig.tagline}

16 |
17 | 21 | Get Started 22 | 23 |
24 |
25 |
26 | ); 27 | } 28 | 29 | const Home = () => ( 30 | 34 | 35 | 36 | ); 37 | 38 | export default Home; 39 | -------------------------------------------------------------------------------- /cspell.yml: -------------------------------------------------------------------------------- 1 | language: en 2 | useGitignore: true 3 | # TODO enableGlobDot: true 4 | ignorePaths: 5 | # Excluded from spelling check 6 | - cspell.yml 7 | - package.json 8 | - package-lock.json 9 | - tsconfig.json 10 | - benchmark/github-schema.graphql 11 | - benchmark/github-schema.json 12 | overrides: 13 | - filename: '**/docs-old/APIReference-*.md' 14 | ignoreRegExpList: ['/href="[^"]*"/'] 15 | - filename: 'website/**' 16 | dictionaries: 17 | - fullstack 18 | words: 19 | - clsx 20 | - infima 21 | - noopener 22 | - noreferrer 23 | - xlink 24 | 25 | ignoreRegExpList: 26 | - u\{[0-9a-f]{1,8}\} 27 | 28 | words: 29 | - graphiql 30 | - sublinks 31 | - instanceof 32 | 33 | # Different names used inside tests 34 | - Skywalker 35 | - Leia 36 | - Wilhuff 37 | - Tarkin 38 | - Artoo 39 | - Threepio 40 | - Odie 41 | - Odie's 42 | - Damerau 43 | - Alderaan 44 | - Tatooine 45 | - astromech 46 | 47 | # TODO: contribute upstream 48 | - deno 49 | 50 | # TODO: remove bellow words 51 | - QLID # GraphQLID 52 | - QLJS # GraphQLJS 53 | - iface 54 | - Reqs 55 | - FXXX 56 | - XXXF 57 | - bfnrt 58 | - wrds 59 | -------------------------------------------------------------------------------- /src/type/assertName.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../error/GraphQLError'; 2 | 3 | import { isNameContinue, isNameStart } from '../language/characterClasses'; 4 | 5 | /** 6 | * Upholds the spec rules about naming. 7 | */ 8 | export function assertName(name: string): string { 9 | if (name.length === 0) { 10 | throw new GraphQLError('Expected name to be a non-empty string.'); 11 | } 12 | 13 | for (let i = 1; i < name.length; ++i) { 14 | if (!isNameContinue(name.charCodeAt(i))) { 15 | throw new GraphQLError( 16 | `Names must only contain [_a-zA-Z0-9] but "${name}" does not.`, 17 | ); 18 | } 19 | } 20 | 21 | if (!isNameStart(name.charCodeAt(0))) { 22 | throw new GraphQLError( 23 | `Names must start with [_a-zA-Z] but "${name}" does not.`, 24 | ); 25 | } 26 | 27 | return name; 28 | } 29 | 30 | /** 31 | * Upholds the spec rules about naming enum values. 32 | * 33 | * @internal 34 | */ 35 | export function assertEnumValueName(name: string): string { 36 | if (name === 'true' || name === 'false' || name === 'null') { 37 | throw new GraphQLError(`Enum values cannot be named: ${name}`); 38 | } 39 | return assertName(name); 40 | } 41 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueFragmentNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor'; 4 | 5 | import type { ASTValidationContext } from '../ValidationContext'; 6 | 7 | /** 8 | * Unique fragment names 9 | * 10 | * A GraphQL document is only valid if all defined fragments have unique names. 11 | * 12 | * See https://spec.graphql.org/draft/#sec-Fragment-Name-Uniqueness 13 | */ 14 | export function UniqueFragmentNamesRule( 15 | context: ASTValidationContext, 16 | ): ASTVisitor { 17 | const knownFragmentNames = Object.create(null); 18 | return { 19 | OperationDefinition: () => false, 20 | FragmentDefinition(node) { 21 | const fragmentName = node.name.value; 22 | if (knownFragmentNames[fragmentName]) { 23 | context.reportError( 24 | new GraphQLError( 25 | `There can be only one fragment named "${fragmentName}".`, 26 | { nodes: [knownFragmentNames[fragmentName], node.name] }, 27 | ), 28 | ); 29 | } else { 30 | knownFragmentNames[fragmentName] = node.name; 31 | } 32 | return false; 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/require-to-string-tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function requireToStringTag(context) { 4 | const sourceCode = context.getSourceCode(); 5 | 6 | return { 7 | 'ExportNamedDeclaration > ClassDeclaration': (classNode) => { 8 | const properties = classNode.body.body; 9 | if (properties.some(isToStringTagProperty)) { 10 | return; 11 | } 12 | 13 | const jsDoc = context.getJSDocComment(classNode)?.value; 14 | // FIXME: use proper TSDoc parser instead of includes once we fix TSDoc comments 15 | if (jsDoc?.includes('@internal') === true) { 16 | return; 17 | } 18 | 19 | context.report({ 20 | node: classNode, 21 | message: 22 | 'All classes in public API required to have [Symbol.toStringTag] method', 23 | }); 24 | }, 25 | }; 26 | 27 | function isToStringTagProperty(propertyNode) { 28 | if ( 29 | propertyNode.type !== 'MethodDefinition' || 30 | propertyNode.kind !== 'get' 31 | ) { 32 | return false; 33 | } 34 | const keyText = sourceCode.getText(propertyNode.key); 35 | return keyText === 'Symbol.toStringTag'; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/language/__tests__/source-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { Source } from '../source'; 5 | 6 | describe('Source', () => { 7 | it('can be Object.toStringified', () => { 8 | const source = new Source(''); 9 | 10 | expect(Object.prototype.toString.call(source)).to.equal('[object Source]'); 11 | }); 12 | 13 | it('rejects invalid locationOffset', () => { 14 | function createSource(locationOffset: { line: number; column: number }) { 15 | return new Source('', '', locationOffset); 16 | } 17 | 18 | expect(() => createSource({ line: 0, column: 1 })).to.throw( 19 | 'line in locationOffset is 1-indexed and must be positive.', 20 | ); 21 | expect(() => createSource({ line: -1, column: 1 })).to.throw( 22 | 'line in locationOffset is 1-indexed and must be positive.', 23 | ); 24 | 25 | expect(() => createSource({ line: 1, column: 0 })).to.throw( 26 | 'column in locationOffset is 1-indexed and must be positive.', 27 | ); 28 | expect(() => createSource({ line: 1, column: -1 })).to.throw( 29 | 'column in locationOffset is 1-indexed and must be positive.', 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/utilities/getOperationAST.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../jsutils/Maybe'; 2 | 3 | import type { DocumentNode, OperationDefinitionNode } from '../language/ast'; 4 | import { Kind } from '../language/kinds'; 5 | 6 | /** 7 | * Returns an operation AST given a document AST and optionally an operation 8 | * name. If a name is not provided, an operation is only returned if only one is 9 | * provided in the document. 10 | */ 11 | export function getOperationAST( 12 | documentAST: DocumentNode, 13 | operationName?: Maybe, 14 | ): Maybe { 15 | let operation = null; 16 | for (const definition of documentAST.definitions) { 17 | if (definition.kind === Kind.OPERATION_DEFINITION) { 18 | if (operationName == null) { 19 | // If no operation name was provided, only return an Operation if there 20 | // is one defined in the document. Upon encountering the second, return 21 | // null. 22 | if (operation) { 23 | return null; 24 | } 25 | operation = definition; 26 | } else if (definition.name?.value === operationName) { 27 | return definition; 28 | } 29 | } 30 | } 31 | return operation; 32 | } 33 | -------------------------------------------------------------------------------- /website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Rubik:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap'); 2 | /* stylelint-disable docusaurus/copyright-header */ 3 | /** 4 | * Any CSS included here will be global. The classic template 5 | * bundles Infima by default. Infima is a CSS framework designed to 6 | * work well for content-centric websites. 7 | */ 8 | 9 | /* You can override the default Infima variables here. */ 10 | :root { 11 | --ifm-color-primary: #e434aa; 12 | --ifm-color-primary-dark: #de1e9f; 13 | --ifm-color-primary-darker: #d21c96; 14 | --ifm-color-primary-darkest: #ad177c; 15 | --ifm-color-primary-light: #e74db4; 16 | --ifm-color-primary-lighter: #e959ba; 17 | --ifm-color-primary-lightest: #ee7ec9; 18 | --ifm-code-font-size: 95%; 19 | --ifm-font-family-base: 'Rubik', sans-serif; 20 | } 21 | 22 | .docusaurus-highlight-code-line { 23 | background-color: rgba(0, 0, 0, 0.1); 24 | display: block; 25 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 26 | padding: 0 var(--ifm-pre-padding); 27 | } 28 | 29 | html[data-theme='dark'] .docusaurus-highlight-code-line { 30 | background-color: rgba(0, 0, 0, 0.3); 31 | } 32 | -------------------------------------------------------------------------------- /src/validation/rules/LoneAnonymousOperationRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import { Kind } from '../../language/kinds'; 4 | import type { ASTVisitor } from '../../language/visitor'; 5 | 6 | import type { ASTValidationContext } from '../ValidationContext'; 7 | 8 | /** 9 | * Lone anonymous operation 10 | * 11 | * A GraphQL document is only valid if when it contains an anonymous operation 12 | * (the query short-hand) that it contains only that one operation definition. 13 | * 14 | * See https://spec.graphql.org/draft/#sec-Lone-Anonymous-Operation 15 | */ 16 | export function LoneAnonymousOperationRule( 17 | context: ASTValidationContext, 18 | ): ASTVisitor { 19 | let operationCount = 0; 20 | return { 21 | Document(node) { 22 | operationCount = node.definitions.filter( 23 | (definition) => definition.kind === Kind.OPERATION_DEFINITION, 24 | ).length; 25 | }, 26 | OperationDefinition(node) { 27 | if (!node.name && operationCount > 1) { 28 | context.reportError( 29 | new GraphQLError( 30 | 'This anonymous operation must be the only defined operation.', 31 | { nodes: node }, 32 | ), 33 | ); 34 | } 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/utilities/sortValueNode.ts: -------------------------------------------------------------------------------- 1 | import { naturalCompare } from '../jsutils/naturalCompare'; 2 | 3 | import type { ObjectFieldNode, ValueNode } from '../language/ast'; 4 | import { Kind } from '../language/kinds'; 5 | 6 | /** 7 | * Sort ValueNode. 8 | * 9 | * This function returns a sorted copy of the given ValueNode. 10 | * 11 | * @internal 12 | */ 13 | export function sortValueNode(valueNode: ValueNode): ValueNode { 14 | switch (valueNode.kind) { 15 | case Kind.OBJECT: 16 | return { 17 | ...valueNode, 18 | fields: sortFields(valueNode.fields), 19 | }; 20 | case Kind.LIST: 21 | return { 22 | ...valueNode, 23 | values: valueNode.values.map(sortValueNode), 24 | }; 25 | case Kind.INT: 26 | case Kind.FLOAT: 27 | case Kind.STRING: 28 | case Kind.BOOLEAN: 29 | case Kind.NULL: 30 | case Kind.ENUM: 31 | case Kind.VARIABLE: 32 | return valueNode; 33 | } 34 | } 35 | 36 | function sortFields( 37 | fields: ReadonlyArray, 38 | ): Array { 39 | return fields 40 | .map((fieldNode) => ({ 41 | ...fieldNode, 42 | value: sortValueNode(fieldNode.value), 43 | })) 44 | .sort((fieldA, fieldB) => 45 | naturalCompare(fieldA.name.value, fieldB.name.value), 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/error/locatedError.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../jsutils/Maybe'; 2 | import { toError } from '../jsutils/toError'; 3 | 4 | import type { ASTNode } from '../language/ast'; 5 | 6 | import { GraphQLError } from './GraphQLError'; 7 | 8 | /** 9 | * Given an arbitrary value, presumably thrown while attempting to execute a 10 | * GraphQL operation, produce a new GraphQLError aware of the location in the 11 | * document responsible for the original Error. 12 | */ 13 | export function locatedError( 14 | rawOriginalError: unknown, 15 | nodes: ASTNode | ReadonlyArray | undefined | null, 16 | path?: Maybe>, 17 | ): GraphQLError { 18 | const originalError = toError(rawOriginalError); 19 | 20 | // Note: this uses a brand-check to support GraphQL errors originating from other contexts. 21 | if (isLocatedGraphQLError(originalError)) { 22 | return originalError; 23 | } 24 | 25 | return new GraphQLError(originalError.message, { 26 | nodes: (originalError as GraphQLError).nodes ?? nodes, 27 | source: (originalError as GraphQLError).source, 28 | positions: (originalError as GraphQLError).positions, 29 | path, 30 | originalError, 31 | }); 32 | } 33 | 34 | function isLocatedGraphQLError(error: any): error is GraphQLError { 35 | return Array.isArray(error.path); 36 | } 37 | -------------------------------------------------------------------------------- /src/validation/rules/LoneSchemaDefinitionRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor'; 4 | 5 | import type { SDLValidationContext } from '../ValidationContext'; 6 | 7 | /** 8 | * Lone Schema definition 9 | * 10 | * A GraphQL document is only valid if it contains only one schema definition. 11 | */ 12 | export function LoneSchemaDefinitionRule( 13 | context: SDLValidationContext, 14 | ): ASTVisitor { 15 | const oldSchema = context.getSchema(); 16 | const alreadyDefined = 17 | oldSchema?.astNode ?? 18 | oldSchema?.getQueryType() ?? 19 | oldSchema?.getMutationType() ?? 20 | oldSchema?.getSubscriptionType(); 21 | 22 | let schemaDefinitionsCount = 0; 23 | return { 24 | SchemaDefinition(node) { 25 | if (alreadyDefined) { 26 | context.reportError( 27 | new GraphQLError( 28 | 'Cannot define a new schema within a schema extension.', 29 | { nodes: node }, 30 | ), 31 | ); 32 | return; 33 | } 34 | 35 | if (schemaDefinitionsCount > 0) { 36 | context.reportError( 37 | new GraphQLError('Must provide only one schema definition.', { 38 | nodes: node, 39 | }), 40 | ); 41 | } 42 | ++schemaDefinitionsCount; 43 | }, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueVariableNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../../jsutils/groupBy'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError'; 4 | 5 | import type { ASTVisitor } from '../../language/visitor'; 6 | 7 | import type { ASTValidationContext } from '../ValidationContext'; 8 | 9 | /** 10 | * Unique variable names 11 | * 12 | * A GraphQL operation is only valid if all its variables are uniquely named. 13 | */ 14 | export function UniqueVariableNamesRule( 15 | context: ASTValidationContext, 16 | ): ASTVisitor { 17 | return { 18 | OperationDefinition(operationNode) { 19 | // See: https://github.com/graphql/graphql-js/issues/2203 20 | /* c8 ignore next */ 21 | const variableDefinitions = operationNode.variableDefinitions ?? []; 22 | 23 | const seenVariableDefinitions = groupBy( 24 | variableDefinitions, 25 | (node) => node.variable.name.value, 26 | ); 27 | 28 | for (const [variableName, variableNodes] of seenVariableDefinitions) { 29 | if (variableNodes.length > 1) { 30 | context.reportError( 31 | new GraphQLError( 32 | `There can be only one variable named "$${variableName}".`, 33 | { nodes: variableNodes.map((node) => node.variable.name) }, 34 | ), 35 | ); 36 | } 37 | } 38 | }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/validation/__tests__/ValidationContext-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../../jsutils/identityFunc'; 5 | 6 | import { parse } from '../../language/parser'; 7 | 8 | import { GraphQLSchema } from '../../type/schema'; 9 | 10 | import { TypeInfo } from '../../utilities/TypeInfo'; 11 | 12 | import { 13 | ASTValidationContext, 14 | SDLValidationContext, 15 | ValidationContext, 16 | } from '../ValidationContext'; 17 | 18 | describe('ValidationContext', () => { 19 | it('can be Object.toStringified', () => { 20 | const schema = new GraphQLSchema({}); 21 | const typeInfo = new TypeInfo(schema); 22 | const ast = parse('{ foo }'); 23 | const onError = identityFunc; 24 | 25 | const astContext = new ASTValidationContext(ast, onError); 26 | expect(Object.prototype.toString.call(astContext)).to.equal( 27 | '[object ASTValidationContext]', 28 | ); 29 | 30 | const sdlContext = new SDLValidationContext(ast, schema, onError); 31 | expect(Object.prototype.toString.call(sdlContext)).to.equal( 32 | '[object SDLValidationContext]', 33 | ); 34 | 35 | const context = new ValidationContext(schema, ast, typeInfo, onError); 36 | expect(Object.prototype.toString.call(context)).to.equal( 37 | '[object ValidationContext]', 38 | ); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /src/validation/rules/ExecutableDefinitionsRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import { Kind } from '../../language/kinds'; 4 | import { isExecutableDefinitionNode } from '../../language/predicates'; 5 | import type { ASTVisitor } from '../../language/visitor'; 6 | 7 | import type { ASTValidationContext } from '../ValidationContext'; 8 | 9 | /** 10 | * Executable definitions 11 | * 12 | * A GraphQL document is only valid for execution if all definitions are either 13 | * operation or fragment definitions. 14 | * 15 | * See https://spec.graphql.org/draft/#sec-Executable-Definitions 16 | */ 17 | export function ExecutableDefinitionsRule( 18 | context: ASTValidationContext, 19 | ): ASTVisitor { 20 | return { 21 | Document(node) { 22 | for (const definition of node.definitions) { 23 | if (!isExecutableDefinitionNode(definition)) { 24 | const defName = 25 | definition.kind === Kind.SCHEMA_DEFINITION || 26 | definition.kind === Kind.SCHEMA_EXTENSION 27 | ? 'schema' 28 | : '"' + definition.name.value + '"'; 29 | context.reportError( 30 | new GraphQLError(`The ${defName} definition is not executable.`, { 31 | nodes: definition, 32 | }), 33 | ); 34 | } 35 | } 36 | return false; 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Questions regarding how to use GraphQL 2 | 3 | We want to keep signal strong in the GitHub issue tracker – to make sure that it remains the best place to track bugs and features that affect development. 4 | 5 | If you have a question on how to use GraphQL, please [post it to Stack Overflow](https://stackoverflow.com/questions/ask?tags=graphql) with the tag [#graphql](https://stackoverflow.com/questions/tagged/graphql). 6 | 7 | Please do not post general questions directly as GitHub issues. They may sit for weeks unanswered, or may be spontaneously closed without answer. 8 | 9 | # Reporting issues with GraphQL.js 10 | 11 | Before filing a new issue, make sure an issue for your problem doesn't already exist. 12 | 13 | The best way to get a bug fixed is to provide a _pull request_ with a simplified failing test case (or better yet, include a fix). 14 | 15 | # Feature requests 16 | 17 | GraphQL.js is a reference implementation of the [GraphQL specification](https://github.com/graphql/graphql-spec). To discuss new features which are not GraphQL.js specific and fundamentally change the way GraphQL works, open an issue against the specification. 18 | 19 | # Security bugs 20 | 21 | Facebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. With that in mind, please do not file public issues; go through the process outlined on that page. 22 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueOperationNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor'; 4 | 5 | import type { ASTValidationContext } from '../ValidationContext'; 6 | 7 | /** 8 | * Unique operation names 9 | * 10 | * A GraphQL document is only valid if all defined operations have unique names. 11 | * 12 | * See https://spec.graphql.org/draft/#sec-Operation-Name-Uniqueness 13 | */ 14 | export function UniqueOperationNamesRule( 15 | context: ASTValidationContext, 16 | ): ASTVisitor { 17 | const knownOperationNames = Object.create(null); 18 | return { 19 | OperationDefinition(node) { 20 | const operationName = node.name; 21 | if (operationName) { 22 | if (knownOperationNames[operationName.value]) { 23 | context.reportError( 24 | new GraphQLError( 25 | `There can be only one operation named "${operationName.value}".`, 26 | { 27 | nodes: [ 28 | knownOperationNames[operationName.value], 29 | operationName, 30 | ], 31 | }, 32 | ), 33 | ); 34 | } else { 35 | knownOperationNames[operationName.value] = operationName; 36 | } 37 | } 38 | return false; 39 | }, 40 | FragmentDefinition: () => false, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/__testUtils__/kitchenSinkQuery.ts: -------------------------------------------------------------------------------- 1 | export const kitchenSinkQuery: string = String.raw` 2 | query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery { 3 | whoever123is: node(id: [123, 456]) { 4 | id 5 | ... on User @onInlineFragment { 6 | field2 { 7 | id 8 | alias: field1(first: 10, after: $foo) @include(if: $foo) { 9 | id 10 | ...frag @onFragmentSpread 11 | } 12 | } 13 | } 14 | ... @skip(unless: $foo) { 15 | id 16 | } 17 | ... { 18 | id 19 | } 20 | } 21 | } 22 | 23 | mutation likeStory @onMutation { 24 | like(story: 123) @onField { 25 | story { 26 | id @onField 27 | } 28 | } 29 | } 30 | 31 | subscription StoryLikeSubscription( 32 | $input: StoryLikeSubscribeInput @onVariableDefinition 33 | ) 34 | @onSubscription { 35 | storyLikeSubscribe(input: $input) { 36 | story { 37 | likers { 38 | count 39 | } 40 | likeSentence { 41 | text 42 | } 43 | } 44 | } 45 | } 46 | 47 | fragment frag on Friend @onFragmentDefinition { 48 | foo( 49 | size: $size 50 | bar: $b 51 | obj: { 52 | key: "value" 53 | block: """ 54 | block string uses \""" 55 | """ 56 | } 57 | ) 58 | } 59 | 60 | { 61 | unnamed(truthy: true, falsy: false, nullish: null) 62 | query 63 | } 64 | 65 | query { 66 | __typename 67 | } 68 | `; 69 | -------------------------------------------------------------------------------- /src/utilities/introspectionFromSchema.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../jsutils/invariant'; 2 | 3 | import { parse } from '../language/parser'; 4 | 5 | import type { GraphQLSchema } from '../type/schema'; 6 | 7 | import { executeSync } from '../execution/execute'; 8 | 9 | import type { 10 | IntrospectionOptions, 11 | IntrospectionQuery, 12 | } from './getIntrospectionQuery'; 13 | import { getIntrospectionQuery } from './getIntrospectionQuery'; 14 | 15 | /** 16 | * Build an IntrospectionQuery from a GraphQLSchema 17 | * 18 | * IntrospectionQuery is useful for utilities that care about type and field 19 | * relationships, but do not need to traverse through those relationships. 20 | * 21 | * This is the inverse of buildClientSchema. The primary use case is outside 22 | * of the server context, for instance when doing schema comparisons. 23 | */ 24 | export function introspectionFromSchema( 25 | schema: GraphQLSchema, 26 | options?: IntrospectionOptions, 27 | ): IntrospectionQuery { 28 | const optionsWithDefaults = { 29 | specifiedByUrl: true, 30 | directiveIsRepeatable: true, 31 | schemaDescription: true, 32 | inputValueDeprecation: true, 33 | ...options, 34 | }; 35 | 36 | const document = parse(getIntrospectionQuery(optionsWithDefaults)); 37 | const result = executeSync({ schema, document }); 38 | invariant(result.errors == null && result.data != null); 39 | return result.data as any; 40 | } 41 | -------------------------------------------------------------------------------- /src/validation/rules/VariablesAreInputTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { VariableDefinitionNode } from '../../language/ast'; 4 | import { print } from '../../language/printer'; 5 | import type { ASTVisitor } from '../../language/visitor'; 6 | 7 | import { isInputType } from '../../type/definition'; 8 | 9 | import { typeFromAST } from '../../utilities/typeFromAST'; 10 | 11 | import type { ValidationContext } from '../ValidationContext'; 12 | 13 | /** 14 | * Variables are input types 15 | * 16 | * A GraphQL operation is only valid if all the variables it defines are of 17 | * input types (scalar, enum, or input object). 18 | * 19 | * See https://spec.graphql.org/draft/#sec-Variables-Are-Input-Types 20 | */ 21 | export function VariablesAreInputTypesRule( 22 | context: ValidationContext, 23 | ): ASTVisitor { 24 | return { 25 | VariableDefinition(node: VariableDefinitionNode) { 26 | const type = typeFromAST(context.getSchema(), node.type); 27 | 28 | if (type !== undefined && !isInputType(type)) { 29 | const variableName = node.variable.name.value; 30 | const typeName = print(node.type); 31 | 32 | context.reportError( 33 | new GraphQLError( 34 | `Variable "$${variableName}" cannot be non-input type "${typeName}".`, 35 | { nodes: node.type }, 36 | ), 37 | ); 38 | } 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /resources/inline-invariant.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** 4 | * Eliminates function call to `invariant` if the condition is met. 5 | * 6 | * Transforms: 7 | * 8 | * invariant(, ...) 9 | * 10 | * to: 11 | * 12 | * () || invariant(false ...) 13 | */ 14 | export function inlineInvariant(context: ts.TransformationContext) { 15 | const { factory } = context; 16 | 17 | return visitSourceFile; 18 | 19 | function visitSourceFile(sourceFile: ts.SourceFile) { 20 | return ts.visitNode(sourceFile, visitNode); 21 | } 22 | 23 | function visitNode(node: ts.Node): ts.Node { 24 | if (ts.isCallExpression(node)) { 25 | const { expression, arguments: args } = node; 26 | 27 | if (ts.isIdentifier(expression) && args.length > 0) { 28 | const funcName = expression.escapedText; 29 | if (funcName === 'invariant' || funcName === 'devAssert') { 30 | const [condition, ...otherArgs] = args; 31 | 32 | return factory.createBinaryExpression( 33 | factory.createParenthesizedExpression(condition), 34 | ts.SyntaxKind.BarBarToken, 35 | factory.createCallExpression( 36 | factory.createIdentifier(funcName), 37 | undefined, 38 | [factory.createFalse(), ...otherArgs], 39 | ), 40 | ); 41 | } 42 | } 43 | } 44 | return ts.visitEachChild(node, visitNode, context); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/__testUtils__/expectJSON.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { isObjectLike } from '../jsutils/isObjectLike'; 4 | import { mapValue } from '../jsutils/mapValue'; 5 | 6 | /** 7 | * Deeply transforms an arbitrary value to a JSON-safe value by calling toJSON 8 | * on any nested value which defines it. 9 | */ 10 | function toJSONDeep(value: unknown): unknown { 11 | if (!isObjectLike(value)) { 12 | return value; 13 | } 14 | 15 | if (typeof value.toJSON === 'function') { 16 | return value.toJSON(); 17 | } 18 | 19 | if (Array.isArray(value)) { 20 | return value.map(toJSONDeep); 21 | } 22 | 23 | return mapValue(value, toJSONDeep); 24 | } 25 | 26 | export function expectJSON(actual: unknown) { 27 | const actualJSON = toJSONDeep(actual); 28 | 29 | return { 30 | toDeepEqual(expected: unknown) { 31 | const expectedJSON = toJSONDeep(expected); 32 | expect(actualJSON).to.deep.equal(expectedJSON); 33 | }, 34 | toDeepNestedProperty(path: string, expected: unknown) { 35 | const expectedJSON = toJSONDeep(expected); 36 | expect(actualJSON).to.deep.nested.property(path, expectedJSON); 37 | }, 38 | }; 39 | } 40 | 41 | export function expectToThrowJSON(fn: () => unknown) { 42 | function mapException(): unknown { 43 | try { 44 | return fn(); 45 | } catch (error) { 46 | throw toJSONDeep(error); 47 | } 48 | } 49 | 50 | return expect(mapException).to.throw(); 51 | } 52 | -------------------------------------------------------------------------------- /src/validation/rules/custom/NoSchemaIntrospectionCustomRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../../error/GraphQLError'; 2 | 3 | import type { FieldNode } from '../../../language/ast'; 4 | import type { ASTVisitor } from '../../../language/visitor'; 5 | 6 | import { getNamedType } from '../../../type/definition'; 7 | import { isIntrospectionType } from '../../../type/introspection'; 8 | 9 | import type { ValidationContext } from '../../ValidationContext'; 10 | 11 | /** 12 | * Prohibit introspection queries 13 | * 14 | * A GraphQL document is only valid if all fields selected are not fields that 15 | * return an introspection type. 16 | * 17 | * Note: This rule is optional and is not part of the Validation section of the 18 | * GraphQL Specification. This rule effectively disables introspection, which 19 | * does not reflect best practices and should only be done if absolutely necessary. 20 | */ 21 | export function NoSchemaIntrospectionCustomRule( 22 | context: ValidationContext, 23 | ): ASTVisitor { 24 | return { 25 | Field(node: FieldNode) { 26 | const type = getNamedType(context.getType()); 27 | if (type && isIntrospectionType(type)) { 28 | context.reportError( 29 | new GraphQLError( 30 | `GraphQL introspection has been disabled, but the requested query contained the field "${node.name.value}".`, 31 | { nodes: node }, 32 | ), 33 | ); 34 | } 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueDirectiveNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor'; 4 | 5 | import type { SDLValidationContext } from '../ValidationContext'; 6 | 7 | /** 8 | * Unique directive names 9 | * 10 | * A GraphQL document is only valid if all defined directives have unique names. 11 | */ 12 | export function UniqueDirectiveNamesRule( 13 | context: SDLValidationContext, 14 | ): ASTVisitor { 15 | const knownDirectiveNames = Object.create(null); 16 | const schema = context.getSchema(); 17 | 18 | return { 19 | DirectiveDefinition(node) { 20 | const directiveName = node.name.value; 21 | 22 | if (schema?.getDirective(directiveName)) { 23 | context.reportError( 24 | new GraphQLError( 25 | `Directive "@${directiveName}" already exists in the schema. It cannot be redefined.`, 26 | { nodes: node.name }, 27 | ), 28 | ); 29 | return; 30 | } 31 | 32 | if (knownDirectiveNames[directiveName]) { 33 | context.reportError( 34 | new GraphQLError( 35 | `There can be only one directive named "@${directiveName}".`, 36 | { nodes: [knownDirectiveNames[directiveName], node.name] }, 37 | ), 38 | ); 39 | } else { 40 | knownDirectiveNames[directiveName] = node.name; 41 | } 42 | 43 | return false; 44 | }, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueArgumentNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../../jsutils/groupBy'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError'; 4 | 5 | import type { ArgumentNode } from '../../language/ast'; 6 | import type { ASTVisitor } from '../../language/visitor'; 7 | 8 | import type { ASTValidationContext } from '../ValidationContext'; 9 | 10 | /** 11 | * Unique argument names 12 | * 13 | * A GraphQL field or directive is only valid if all supplied arguments are 14 | * uniquely named. 15 | * 16 | * See https://spec.graphql.org/draft/#sec-Argument-Names 17 | */ 18 | export function UniqueArgumentNamesRule( 19 | context: ASTValidationContext, 20 | ): ASTVisitor { 21 | return { 22 | Field: checkArgUniqueness, 23 | Directive: checkArgUniqueness, 24 | }; 25 | 26 | function checkArgUniqueness(parentNode: { 27 | arguments?: ReadonlyArray; 28 | }) { 29 | // FIXME: https://github.com/graphql/graphql-js/issues/2203 30 | /* c8 ignore next */ 31 | const argumentNodes = parentNode.arguments ?? []; 32 | 33 | const seenArgs = groupBy(argumentNodes, (arg) => arg.name.value); 34 | 35 | for (const [argName, argNodes] of seenArgs) { 36 | if (argNodes.length > 1) { 37 | context.reportError( 38 | new GraphQLError( 39 | `There can be only one argument named "${argName}".`, 40 | { nodes: argNodes.map((node) => node.name) }, 41 | ), 42 | ); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/error/__tests__/locatedError-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { GraphQLError } from '../GraphQLError'; 5 | import { locatedError } from '../locatedError'; 6 | 7 | describe('locatedError', () => { 8 | it('passes GraphQLError through', () => { 9 | const e = new GraphQLError('msg', { path: ['path', 3, 'to', 'field'] }); 10 | 11 | expect(locatedError(e, [], [])).to.deep.equal(e); 12 | }); 13 | 14 | it('wraps non-errors', () => { 15 | const testObject = Object.freeze({}); 16 | const error = locatedError(testObject, [], []); 17 | 18 | expect(error).to.be.instanceOf(GraphQLError); 19 | expect(error.originalError).to.include({ 20 | name: 'NonErrorThrown', 21 | thrownValue: testObject, 22 | }); 23 | }); 24 | 25 | it('passes GraphQLError-ish through', () => { 26 | const e = new Error(); 27 | // @ts-expect-error 28 | e.locations = []; 29 | // @ts-expect-error 30 | e.path = []; 31 | // @ts-expect-error 32 | e.nodes = []; 33 | // @ts-expect-error 34 | e.source = null; 35 | // @ts-expect-error 36 | e.positions = []; 37 | e.name = 'GraphQLError'; 38 | 39 | expect(locatedError(e, [], [])).to.deep.equal(e); 40 | }); 41 | 42 | it('does not pass through elasticsearch-like errors', () => { 43 | const e = new Error('I am from elasticsearch'); 44 | // @ts-expect-error 45 | e.path = '/something/feed/_search'; 46 | 47 | expect(locatedError(e, [], [])).to.not.deep.equal(e); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/language/characterClasses.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ``` 3 | * WhiteSpace :: 4 | * - "Horizontal Tab (U+0009)" 5 | * - "Space (U+0020)" 6 | * ``` 7 | * @internal 8 | */ 9 | export function isWhiteSpace(code: number): boolean { 10 | return code === 0x0009 || code === 0x0020; 11 | } 12 | 13 | /** 14 | * ``` 15 | * Digit :: one of 16 | * - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` 17 | * ``` 18 | * @internal 19 | */ 20 | export function isDigit(code: number): boolean { 21 | return code >= 0x0030 && code <= 0x0039; 22 | } 23 | 24 | /** 25 | * ``` 26 | * Letter :: one of 27 | * - `A` `B` `C` `D` `E` `F` `G` `H` `I` `J` `K` `L` `M` 28 | * - `N` `O` `P` `Q` `R` `S` `T` `U` `V` `W` `X` `Y` `Z` 29 | * - `a` `b` `c` `d` `e` `f` `g` `h` `i` `j` `k` `l` `m` 30 | * - `n` `o` `p` `q` `r` `s` `t` `u` `v` `w` `x` `y` `z` 31 | * ``` 32 | * @internal 33 | */ 34 | export function isLetter(code: number): boolean { 35 | return ( 36 | (code >= 0x0061 && code <= 0x007a) || // A-Z 37 | (code >= 0x0041 && code <= 0x005a) // a-z 38 | ); 39 | } 40 | 41 | /** 42 | * ``` 43 | * NameStart :: 44 | * - Letter 45 | * - `_` 46 | * ``` 47 | * @internal 48 | */ 49 | export function isNameStart(code: number): boolean { 50 | return isLetter(code) || code === 0x005f; 51 | } 52 | 53 | /** 54 | * ``` 55 | * NameContinue :: 56 | * - Letter 57 | * - Digit 58 | * - `_` 59 | * ``` 60 | * @internal 61 | */ 62 | export function isNameContinue(code: number): boolean { 63 | return isLetter(code) || isDigit(code) || code === 0x005f; 64 | } 65 | -------------------------------------------------------------------------------- /resources/build-deno.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | 4 | import * as ts from 'typescript'; 5 | 6 | import { addExtensionToImportPaths } from './add-extension-to-import-paths'; 7 | import { inlineInvariant } from './inline-invariant'; 8 | import { readdirRecursive, showDirStats, writeGeneratedFile } from './utils'; 9 | 10 | fs.rmSync('./denoDist', { recursive: true, force: true }); 11 | fs.mkdirSync('./denoDist'); 12 | 13 | const srcFiles = readdirRecursive('./src', { ignoreDir: /^__.*__$/ }); 14 | for (const filepath of srcFiles) { 15 | if (filepath.endsWith('.ts')) { 16 | const srcPath = path.join('./src', filepath); 17 | 18 | const sourceFile = ts.createSourceFile( 19 | srcPath, 20 | fs.readFileSync(srcPath, 'utf-8'), 21 | ts.ScriptTarget.Latest, 22 | ); 23 | 24 | const transformed = ts.transform(sourceFile, [ 25 | addExtensionToImportPaths({ extension: '.ts' }), 26 | inlineInvariant, 27 | ]); 28 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 29 | const newContent = printer.printBundle( 30 | ts.createBundle(transformed.transformed), 31 | ); 32 | 33 | transformed.dispose(); 34 | 35 | const destPath = path.join('./denoDist', filepath); 36 | fs.mkdirSync(path.dirname(destPath), { recursive: true }); 37 | writeGeneratedFile(destPath, newContent); 38 | } 39 | } 40 | 41 | fs.copyFileSync('./LICENSE', './denoDist/LICENSE'); 42 | fs.copyFileSync('./README.md', './denoDist/README.md'); 43 | 44 | showDirStats('./denoDist'); 45 | -------------------------------------------------------------------------------- /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 main. 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 main. 9 | # 10 | 11 | # Check that local copy has no modifications 12 | GIT_MODIFIED_FILES=$(git ls-files -dm 2> /dev/null); 13 | GIT_STAGED_FILES=$(git diff --cached --name-only 2> /dev/null); 14 | if [ "$GIT_MODIFIED_FILES" != "" -o "$GIT_STAGED_FILES" != "" ]; then 15 | read -p "Git has local modifications. Continue? (y|N) " yn; 16 | if [ "$yn" != "y" ]; then exit 1; fi; 17 | fi; 18 | 19 | # First fetch to ensure git is up to date. Fail-fast if this fails. 20 | git fetch; 21 | if [[ $? -ne 0 ]]; then exit 1; fi; 22 | 23 | # Extract useful information. 24 | GIT_BRANCH=$(git branch -v 2> /dev/null | sed '/^[^*]/d'); 25 | GIT_BRANCH_NAME=$(echo "$GIT_BRANCH" | sed 's/* \([A-Za-z0-9_\-]*\).*/\1/'); 26 | GIT_BRANCH_SYNC=$(echo "$GIT_BRANCH" | sed 's/* [^[]*.\([^]]*\).*/\1/'); 27 | 28 | # Check if main is checked out 29 | if [ "$GIT_BRANCH_NAME" != "main" ]; then 30 | read -p "Git not on main but $GIT_BRANCH_NAME. Continue? (y|N) " yn; 31 | if [ "$yn" != "y" ]; then exit 1; fi; 32 | fi; 33 | 34 | # Check if branch is synced with remote 35 | if [ "$GIT_BRANCH_SYNC" != "" ]; then 36 | read -p "Git not up to date but $GIT_BRANCH_SYNC. Continue? (y|N) " yn; 37 | if [ "$yn" != "y" ]; then exit 1; fi; 38 | fi; 39 | -------------------------------------------------------------------------------- /src/jsutils/naturalCompare.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns a number indicating whether a reference string comes before, or after, 3 | * or is the same as the given string in natural sort order. 4 | * 5 | * See: https://en.wikipedia.org/wiki/Natural_sort_order 6 | * 7 | */ 8 | export function naturalCompare(aStr: string, bStr: string): number { 9 | let aIndex = 0; 10 | let bIndex = 0; 11 | 12 | while (aIndex < aStr.length && bIndex < bStr.length) { 13 | let aChar = aStr.charCodeAt(aIndex); 14 | let bChar = bStr.charCodeAt(bIndex); 15 | 16 | if (isDigit(aChar) && isDigit(bChar)) { 17 | let aNum = 0; 18 | do { 19 | ++aIndex; 20 | aNum = aNum * 10 + aChar - DIGIT_0; 21 | aChar = aStr.charCodeAt(aIndex); 22 | } while (isDigit(aChar) && aNum > 0); 23 | 24 | let bNum = 0; 25 | do { 26 | ++bIndex; 27 | bNum = bNum * 10 + bChar - DIGIT_0; 28 | bChar = bStr.charCodeAt(bIndex); 29 | } while (isDigit(bChar) && bNum > 0); 30 | 31 | if (aNum < bNum) { 32 | return -1; 33 | } 34 | 35 | if (aNum > bNum) { 36 | return 1; 37 | } 38 | } else { 39 | if (aChar < bChar) { 40 | return -1; 41 | } 42 | if (aChar > bChar) { 43 | return 1; 44 | } 45 | ++aIndex; 46 | ++bIndex; 47 | } 48 | } 49 | 50 | return aStr.length - bStr.length; 51 | } 52 | 53 | const DIGIT_0 = 48; 54 | const DIGIT_9 = 57; 55 | 56 | function isDigit(code: number): boolean { 57 | return !isNaN(code) && DIGIT_0 <= code && code <= DIGIT_9; 58 | } 59 | -------------------------------------------------------------------------------- /src/language/source.ts: -------------------------------------------------------------------------------- 1 | import { devAssert } from '../jsutils/devAssert'; 2 | import { instanceOf } from '../jsutils/instanceOf'; 3 | 4 | interface Location { 5 | line: number; 6 | column: number; 7 | } 8 | 9 | /** 10 | * A representation of source input to GraphQL. The `name` and `locationOffset` parameters are 11 | * optional, but they are useful for clients who store GraphQL documents in source files. 12 | * For example, if the GraphQL input starts at line 40 in a file named `Foo.graphql`, it might 13 | * be useful for `name` to be `"Foo.graphql"` and location to be `{ line: 40, column: 1 }`. 14 | * The `line` and `column` properties in `locationOffset` are 1-indexed. 15 | */ 16 | export class Source { 17 | body: string; 18 | name: string; 19 | locationOffset: Location; 20 | 21 | constructor( 22 | body: string, 23 | name: string = 'GraphQL request', 24 | locationOffset: Location = { line: 1, column: 1 }, 25 | ) { 26 | this.body = body; 27 | this.name = name; 28 | this.locationOffset = locationOffset; 29 | devAssert( 30 | this.locationOffset.line > 0, 31 | 'line in locationOffset is 1-indexed and must be positive.', 32 | ); 33 | devAssert( 34 | this.locationOffset.column > 0, 35 | 'column in locationOffset is 1-indexed and must be positive.', 36 | ); 37 | } 38 | 39 | get [Symbol.toStringTag]() { 40 | return 'Source'; 41 | } 42 | } 43 | 44 | /** 45 | * Test if the given value is a Source object. 46 | * 47 | * @internal 48 | */ 49 | export function isSource(source: unknown): source is Source { 50 | return instanceOf(source, Source); 51 | } 52 | -------------------------------------------------------------------------------- /src/validation/rules/NoUndefinedVariablesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor'; 4 | 5 | import type { ValidationContext } from '../ValidationContext'; 6 | 7 | /** 8 | * No undefined variables 9 | * 10 | * A GraphQL operation is only valid if all variables encountered, both directly 11 | * and via fragment spreads, are defined by that operation. 12 | * 13 | * See https://spec.graphql.org/draft/#sec-All-Variable-Uses-Defined 14 | */ 15 | export function NoUndefinedVariablesRule( 16 | context: ValidationContext, 17 | ): ASTVisitor { 18 | let variableNameDefined = Object.create(null); 19 | 20 | return { 21 | OperationDefinition: { 22 | enter() { 23 | variableNameDefined = Object.create(null); 24 | }, 25 | leave(operation) { 26 | const usages = context.getRecursiveVariableUsages(operation); 27 | 28 | for (const { node } of usages) { 29 | const varName = node.name.value; 30 | if (variableNameDefined[varName] !== true) { 31 | context.reportError( 32 | new GraphQLError( 33 | operation.name 34 | ? `Variable "$${varName}" is not defined by operation "${operation.name.value}".` 35 | : `Variable "$${varName}" is not defined.`, 36 | { nodes: [node, operation] }, 37 | ), 38 | ); 39 | } 40 | } 41 | }, 42 | }, 43 | VariableDefinition(node) { 44 | variableNameDefined[node.variable.name.value] = true; 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /src/validation/__tests__/VariablesAreInputTypesRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { VariablesAreInputTypesRule } from '../rules/VariablesAreInputTypesRule'; 4 | 5 | import { expectValidationErrors } from './harness'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(VariablesAreInputTypesRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Variables are input types', () => { 16 | it('unknown types are ignored', () => { 17 | expectValid(` 18 | query Foo($a: Unknown, $b: [[Unknown!]]!) { 19 | field(a: $a, b: $b) 20 | } 21 | `); 22 | }); 23 | 24 | it('input types are valid', () => { 25 | expectValid(` 26 | query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) { 27 | field(a: $a, b: $b, c: $c) 28 | } 29 | `); 30 | }); 31 | 32 | it('output types are invalid', () => { 33 | expectErrors(` 34 | query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) { 35 | field(a: $a, b: $b, c: $c) 36 | } 37 | `).toDeepEqual([ 38 | { 39 | locations: [{ line: 2, column: 21 }], 40 | message: 'Variable "$a" cannot be non-input type "Dog".', 41 | }, 42 | { 43 | locations: [{ line: 2, column: 30 }], 44 | message: 'Variable "$b" cannot be non-input type "[[CatOrDog!]]!".', 45 | }, 46 | { 47 | locations: [{ line: 2, column: 50 }], 48 | message: 'Variable "$c" cannot be non-input type "Pet".', 49 | }, 50 | ]); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/utilities/__tests__/sortValueNode-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { parseValue } from '../../language/parser'; 5 | import { print } from '../../language/printer'; 6 | 7 | import { sortValueNode } from '../sortValueNode'; 8 | 9 | describe('sortValueNode', () => { 10 | function expectSortedValue(source: string) { 11 | return expect(print(sortValueNode(parseValue(source)))); 12 | } 13 | 14 | it('do not change non-object values', () => { 15 | expectSortedValue('1').to.equal('1'); 16 | expectSortedValue('3.14').to.equal('3.14'); 17 | expectSortedValue('null').to.equal('null'); 18 | expectSortedValue('true').to.equal('true'); 19 | expectSortedValue('false').to.equal('false'); 20 | expectSortedValue('"cba"').to.equal('"cba"'); 21 | expectSortedValue('"""cba"""').to.equal('"""cba"""'); 22 | expectSortedValue('[1, 3.14, null, false, "cba"]').to.equal( 23 | '[1, 3.14, null, false, "cba"]', 24 | ); 25 | expectSortedValue('[[1, 3.14, null, false, "cba"]]').to.equal( 26 | '[[1, 3.14, null, false, "cba"]]', 27 | ); 28 | }); 29 | 30 | it('sort input object fields', () => { 31 | expectSortedValue('{ b: 2, a: 1 }').to.equal('{ a: 1, b: 2 }'); 32 | expectSortedValue('{ a: { c: 3, b: 2 } }').to.equal( 33 | '{ a: { b: 2, c: 3 } }', 34 | ); 35 | expectSortedValue('[{ b: 2, a: 1 }, { d: 4, c: 3 }]').to.equal( 36 | '[{ a: 1, b: 2 }, { c: 3, d: 4 }]', 37 | ); 38 | expectSortedValue( 39 | '{ b: { g: 7, f: 6 }, c: 3 , a: { d: 4, e: 5 } }', 40 | ).to.equal('{ a: { d: 4, e: 5 }, b: { f: 6, g: 7 }, c: 3 }'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/validation/__tests__/UniqueVariableNamesRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { UniqueVariableNamesRule } from '../rules/UniqueVariableNamesRule'; 4 | 5 | import { expectValidationErrors } from './harness'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(UniqueVariableNamesRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Unique variable names', () => { 16 | it('unique variable names', () => { 17 | expectValid(` 18 | query A($x: Int, $y: String) { __typename } 19 | query B($x: String, $y: Int) { __typename } 20 | `); 21 | }); 22 | 23 | it('duplicate variable names', () => { 24 | expectErrors(` 25 | query A($x: Int, $x: Int, $x: String) { __typename } 26 | query B($x: String, $x: Int) { __typename } 27 | query C($x: Int, $x: Int) { __typename } 28 | `).toDeepEqual([ 29 | { 30 | message: 'There can be only one variable named "$x".', 31 | locations: [ 32 | { line: 2, column: 16 }, 33 | { line: 2, column: 25 }, 34 | { line: 2, column: 34 }, 35 | ], 36 | }, 37 | { 38 | message: 'There can be only one variable named "$x".', 39 | locations: [ 40 | { line: 3, column: 16 }, 41 | { line: 3, column: 28 }, 42 | ], 43 | }, 44 | { 45 | message: 'There can be only one variable named "$x".', 46 | locations: [ 47 | { line: 4, column: 16 }, 48 | { line: 4, column: 25 }, 49 | ], 50 | }, 51 | ]); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueInputFieldNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../../jsutils/invariant'; 2 | import type { ObjMap } from '../../jsutils/ObjMap'; 3 | 4 | import { GraphQLError } from '../../error/GraphQLError'; 5 | 6 | import type { NameNode } from '../../language/ast'; 7 | import type { ASTVisitor } from '../../language/visitor'; 8 | 9 | import type { ASTValidationContext } from '../ValidationContext'; 10 | 11 | /** 12 | * Unique input field names 13 | * 14 | * A GraphQL input object value is only valid if all supplied fields are 15 | * uniquely named. 16 | * 17 | * See https://spec.graphql.org/draft/#sec-Input-Object-Field-Uniqueness 18 | */ 19 | export function UniqueInputFieldNamesRule( 20 | context: ASTValidationContext, 21 | ): ASTVisitor { 22 | const knownNameStack: Array> = []; 23 | let knownNames: ObjMap = Object.create(null); 24 | 25 | return { 26 | ObjectValue: { 27 | enter() { 28 | knownNameStack.push(knownNames); 29 | knownNames = Object.create(null); 30 | }, 31 | leave() { 32 | const prevKnownNames = knownNameStack.pop(); 33 | invariant(prevKnownNames != null); 34 | knownNames = prevKnownNames; 35 | }, 36 | }, 37 | ObjectField(node) { 38 | const fieldName = node.name.value; 39 | if (knownNames[fieldName]) { 40 | context.reportError( 41 | new GraphQLError( 42 | `There can be only one input field named "${fieldName}".`, 43 | { nodes: [knownNames[fieldName], node.name] }, 44 | ), 45 | ); 46 | } else { 47 | knownNames[fieldName] = node.name; 48 | } 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueTypeNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { TypeDefinitionNode } from '../../language/ast'; 4 | import type { ASTVisitor } from '../../language/visitor'; 5 | 6 | import type { SDLValidationContext } from '../ValidationContext'; 7 | 8 | /** 9 | * Unique type names 10 | * 11 | * A GraphQL document is only valid if all defined types have unique names. 12 | */ 13 | export function UniqueTypeNamesRule(context: SDLValidationContext): ASTVisitor { 14 | const knownTypeNames = Object.create(null); 15 | const schema = context.getSchema(); 16 | 17 | return { 18 | ScalarTypeDefinition: checkTypeName, 19 | ObjectTypeDefinition: checkTypeName, 20 | InterfaceTypeDefinition: checkTypeName, 21 | UnionTypeDefinition: checkTypeName, 22 | EnumTypeDefinition: checkTypeName, 23 | InputObjectTypeDefinition: checkTypeName, 24 | }; 25 | 26 | function checkTypeName(node: TypeDefinitionNode) { 27 | const typeName = node.name.value; 28 | 29 | if (schema?.getType(typeName)) { 30 | context.reportError( 31 | new GraphQLError( 32 | `Type "${typeName}" already exists in the schema. It cannot also be defined in this type definition.`, 33 | { nodes: node.name }, 34 | ), 35 | ); 36 | return; 37 | } 38 | 39 | if (knownTypeNames[typeName]) { 40 | context.reportError( 41 | new GraphQLError(`There can be only one type named "${typeName}".`, { 42 | nodes: [knownTypeNames[typeName], node.name], 43 | }), 44 | ); 45 | } else { 46 | knownTypeNames[typeName] = node.name; 47 | } 48 | 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/__tests__/version-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { version, versionInfo } from '../version'; 5 | 6 | describe('Version', () => { 7 | it('versionInfo', () => { 8 | expect(versionInfo).to.be.an('object'); 9 | expect(versionInfo).to.have.all.keys( 10 | 'major', 11 | 'minor', 12 | 'patch', 13 | 'preReleaseTag', 14 | ); 15 | 16 | const { major, minor, patch, preReleaseTag } = versionInfo; 17 | expect(major).to.be.a('number').at.least(0); 18 | expect(minor).to.be.a('number').at.least(0); 19 | expect(patch).to.be.a('number').at.least(0); 20 | 21 | // Can't be verified on all versions 22 | /* c8 ignore start */ 23 | switch (preReleaseTag?.split('.').length) { 24 | case undefined: 25 | break; 26 | case 2: 27 | expect(preReleaseTag).to.match( 28 | /^(alpha|beta|rc|experimental-[\w-]+)\.\d+/, 29 | ); 30 | break; 31 | case 4: 32 | expect(preReleaseTag).to.match( 33 | /^(alpha|beta|rc)\.\d+.experimental-[\w-]+\.\d+/, 34 | ); 35 | break; 36 | default: 37 | expect.fail('Invalid pre-release tag: ' + preReleaseTag); 38 | } 39 | /* c8 ignore stop */ 40 | }); 41 | 42 | it('version', () => { 43 | expect(version).to.be.a('string'); 44 | 45 | const { major, minor, patch, preReleaseTag } = versionInfo; 46 | expect(version).to.equal( 47 | // Can't be verified on all versions 48 | /* c8 ignore next 3 */ 49 | preReleaseTag === null 50 | ? `${major}.${minor}.${patch}` 51 | : `${major}.${minor}.${patch}-${preReleaseTag}`, 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /integrationTests/integration-test.ts: -------------------------------------------------------------------------------- 1 | import * as childProcess from 'node:child_process'; 2 | import * as fs from 'node:fs'; 3 | import * as os from 'node:os'; 4 | import * as path from 'node:path'; 5 | 6 | import { describe, it } from 'mocha'; 7 | 8 | function exec(command: string, options = {}) { 9 | const output = childProcess.execSync(command, { 10 | encoding: 'utf-8', 11 | ...options, 12 | }); 13 | return output?.trimEnd(); 14 | } 15 | 16 | describe('Integration Tests', () => { 17 | const tmpDir = path.join(os.tmpdir(), 'graphql-js-integrationTmp'); 18 | fs.rmSync(tmpDir, { recursive: true, force: true }); 19 | fs.mkdirSync(tmpDir); 20 | 21 | const distDir = path.resolve('./npmDist'); 22 | const archiveName = exec(`npm --quiet pack ${distDir}`, { cwd: tmpDir }); 23 | fs.renameSync( 24 | path.join(tmpDir, archiveName), 25 | path.join(tmpDir, 'graphql.tgz'), 26 | ); 27 | 28 | function testOnNodeProject(projectName: string) { 29 | const projectPath = path.join(__dirname, projectName); 30 | 31 | const packageJSONPath = path.join(projectPath, 'package.json'); 32 | const packageJSON = JSON.parse(fs.readFileSync(packageJSONPath, 'utf-8')); 33 | 34 | it(packageJSON.description, () => { 35 | fs.cpSync(projectPath, path.join(tmpDir, projectName), { 36 | recursive: true, 37 | }); 38 | 39 | const cwd = path.join(tmpDir, projectName); 40 | // TODO: figure out a way to run it with --ignore-scripts 41 | exec('npm --quiet install', { cwd, stdio: 'inherit' }); 42 | exec('npm --quiet test', { cwd, stdio: 'inherit' }); 43 | }).timeout(120000); 44 | } 45 | 46 | testOnNodeProject('ts'); 47 | testOnNodeProject('node'); 48 | testOnNodeProject('webpack'); 49 | }); 50 | -------------------------------------------------------------------------------- /src/validation/rules/ScalarLeafsRule.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from '../../jsutils/inspect'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError'; 4 | 5 | import type { FieldNode } from '../../language/ast'; 6 | import type { ASTVisitor } from '../../language/visitor'; 7 | 8 | import { getNamedType, isLeafType } from '../../type/definition'; 9 | 10 | import type { ValidationContext } from '../ValidationContext'; 11 | 12 | /** 13 | * Scalar leafs 14 | * 15 | * A GraphQL document is valid only if all leaf fields (fields without 16 | * sub selections) are of scalar or enum types. 17 | */ 18 | export function ScalarLeafsRule(context: ValidationContext): ASTVisitor { 19 | return { 20 | Field(node: FieldNode) { 21 | const type = context.getType(); 22 | const selectionSet = node.selectionSet; 23 | if (type) { 24 | if (isLeafType(getNamedType(type))) { 25 | if (selectionSet) { 26 | const fieldName = node.name.value; 27 | const typeStr = inspect(type); 28 | context.reportError( 29 | new GraphQLError( 30 | `Field "${fieldName}" must not have a selection since type "${typeStr}" has no subfields.`, 31 | { nodes: selectionSet }, 32 | ), 33 | ); 34 | } 35 | } else if (!selectionSet) { 36 | const fieldName = node.name.value; 37 | const typeStr = inspect(type); 38 | context.reportError( 39 | new GraphQLError( 40 | `Field "${fieldName}" of type "${typeStr}" must have a selection of subfields. Did you mean "${fieldName} { ... }"?`, 41 | { nodes: node }, 42 | ), 43 | ); 44 | } 45 | } 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/deploy-artifact-as-branch.yml: -------------------------------------------------------------------------------- 1 | name: Deploy specified artifact as a branch 2 | on: 3 | workflow_call: 4 | inputs: 5 | environment: 6 | description: Environment to publish under 7 | required: true 8 | type: string 9 | artifact_name: 10 | description: Artifact name 11 | required: true 12 | type: string 13 | target_branch: 14 | description: Target branch 15 | required: true 16 | type: string 17 | commit_message: 18 | description: Commit message 19 | required: true 20 | type: string 21 | jobs: 22 | deploy-artifact-as-branch: 23 | environment: 24 | name: ${{ inputs.environment }} 25 | url: ${{ github.server_url }}/${{ github.repository }}/tree/${{ inputs.target_branch }} 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout `${{ inputs.target_branch }}` branch 29 | uses: actions/checkout@v3 30 | with: 31 | ref: ${{ inputs.target_branch }} 32 | 33 | - name: Remove existing files first 34 | run: git rm -r . 35 | 36 | - uses: actions/download-artifact@v3 37 | with: 38 | name: ${{ inputs.artifact_name }} 39 | 40 | - name: Publish target branch 41 | run: | 42 | git add -A 43 | if git diff --staged --quiet; then 44 | echo 'Nothing to publish' 45 | else 46 | git config user.name 'GitHub Action Script' 47 | git config user.email 'please@open.issue' 48 | 49 | git commit -a -m "$COMMIT_MESSAGE" 50 | git push 51 | echo 'Pushed' 52 | fi 53 | env: 54 | TARGET_BRANCH: ${{ inputs.target_branch }} 55 | COMMIT_MESSAGE: ${{ inputs.commit_message }} 56 | -------------------------------------------------------------------------------- /docs-old/APIReference-Execution.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: graphql/execution 3 | layout: ../_core/GraphQLJSLayout 4 | category: API Reference 5 | permalink: /graphql-js/execution/ 6 | sublinks: execute 7 | next: /graphql-js/language/ 8 | --- 9 | 10 | The `graphql/execution` module is responsible for the execution phase of 11 | fulfilling a GraphQL request. You can import either from the `graphql/execution` module, or from the root `graphql` module. For example: 12 | 13 | ```js 14 | import { execute } from 'graphql'; // ES6 15 | var { execute } = require('graphql'); // CommonJS 16 | ``` 17 | 18 | ## Overview 19 | 20 | 28 | 29 | ## Execution 30 | 31 | ### execute 32 | 33 | ```js 34 | export function execute( 35 | schema: GraphQLSchema, 36 | documentAST: Document, 37 | rootValue?: mixed, 38 | contextValue?: mixed, 39 | variableValues?: ?{[key: string]: mixed}, 40 | operationName?: ?string 41 | ): MaybePromise 42 | 43 | type MaybePromise = Promise | T; 44 | 45 | type ExecutionResult = { 46 | data: ?Object; 47 | errors?: Array; 48 | } 49 | ``` 50 | 51 | Implements the "Evaluating requests" section of the GraphQL specification. 52 | 53 | Returns a Promise that will eventually be resolved and never rejected. 54 | 55 | If the arguments to this function do not result in a legal execution context, 56 | a GraphQLError will be thrown immediately explaining the invalid input. 57 | 58 | `ExecutionResult` represents the result of execution. `data` is the result of 59 | executing the query, `errors` is null if no errors occurred, and is a 60 | non-empty array if an error occurred. 61 | -------------------------------------------------------------------------------- /src/execution/__tests__/simplePubSub-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { SimplePubSub } from './simplePubSub'; 5 | 6 | describe('SimplePubSub', () => { 7 | it('subscribe async-iterator mock', async () => { 8 | const pubsub = new SimplePubSub(); 9 | const iterator = pubsub.getSubscriber((x) => x); 10 | 11 | // Queue up publishes 12 | expect(pubsub.emit('Apple')).to.equal(true); 13 | expect(pubsub.emit('Banana')).to.equal(true); 14 | 15 | // Read payloads 16 | expect(await iterator.next()).to.deep.equal({ 17 | done: false, 18 | value: 'Apple', 19 | }); 20 | expect(await iterator.next()).to.deep.equal({ 21 | done: false, 22 | value: 'Banana', 23 | }); 24 | 25 | // Read ahead 26 | const i3 = iterator.next().then((x) => x); 27 | const i4 = iterator.next().then((x) => x); 28 | 29 | // Publish 30 | expect(pubsub.emit('Coconut')).to.equal(true); 31 | expect(pubsub.emit('Durian')).to.equal(true); 32 | 33 | // Await out of order to get correct results 34 | expect(await i4).to.deep.equal({ done: false, value: 'Durian' }); 35 | expect(await i3).to.deep.equal({ done: false, value: 'Coconut' }); 36 | 37 | // Read ahead 38 | const i5 = iterator.next().then((x) => x); 39 | 40 | // Terminate queue 41 | await iterator.return(); 42 | 43 | // Publish is not caught after terminate 44 | expect(pubsub.emit('Fig')).to.equal(false); 45 | 46 | // Find that cancelled read-ahead got a "done" result 47 | expect(await i5).to.deep.equal({ done: true, value: undefined }); 48 | 49 | // And next returns empty completion value 50 | expect(await iterator.next()).to.deep.equal({ 51 | done: true, 52 | value: undefined, 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/validation/rules/NoUnusedVariablesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { VariableDefinitionNode } from '../../language/ast'; 4 | import type { ASTVisitor } from '../../language/visitor'; 5 | 6 | import type { ValidationContext } from '../ValidationContext'; 7 | 8 | /** 9 | * No unused variables 10 | * 11 | * A GraphQL operation is only valid if all variables defined by an operation 12 | * are used, either directly or within a spread fragment. 13 | * 14 | * See https://spec.graphql.org/draft/#sec-All-Variables-Used 15 | */ 16 | export function NoUnusedVariablesRule(context: ValidationContext): ASTVisitor { 17 | let variableDefs: Array = []; 18 | 19 | return { 20 | OperationDefinition: { 21 | enter() { 22 | variableDefs = []; 23 | }, 24 | leave(operation) { 25 | const variableNameUsed = Object.create(null); 26 | const usages = context.getRecursiveVariableUsages(operation); 27 | 28 | for (const { node } of usages) { 29 | variableNameUsed[node.name.value] = true; 30 | } 31 | 32 | for (const variableDef of variableDefs) { 33 | const variableName = variableDef.variable.name.value; 34 | if (variableNameUsed[variableName] !== true) { 35 | context.reportError( 36 | new GraphQLError( 37 | operation.name 38 | ? `Variable "$${variableName}" is never used in operation "${operation.name.value}".` 39 | : `Variable "$${variableName}" is never used.`, 40 | { nodes: variableDef }, 41 | ), 42 | ); 43 | } 44 | } 45 | }, 46 | }, 47 | VariableDefinition(def) { 48 | variableDefs.push(def); 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/utilities/valueFromASTUntyped.ts: -------------------------------------------------------------------------------- 1 | import { keyValMap } from '../jsutils/keyValMap'; 2 | import type { Maybe } from '../jsutils/Maybe'; 3 | import type { ObjMap } from '../jsutils/ObjMap'; 4 | 5 | import type { ValueNode } from '../language/ast'; 6 | import { Kind } from '../language/kinds'; 7 | 8 | /** 9 | * Produces a JavaScript value given a GraphQL Value AST. 10 | * 11 | * Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value 12 | * will reflect the provided GraphQL value AST. 13 | * 14 | * | GraphQL Value | JavaScript Value | 15 | * | -------------------- | ---------------- | 16 | * | Input Object | Object | 17 | * | List | Array | 18 | * | Boolean | Boolean | 19 | * | String / Enum | String | 20 | * | Int / Float | Number | 21 | * | Null | null | 22 | * 23 | */ 24 | export function valueFromASTUntyped( 25 | valueNode: ValueNode, 26 | variables?: Maybe>, 27 | ): unknown { 28 | switch (valueNode.kind) { 29 | case Kind.NULL: 30 | return null; 31 | case Kind.INT: 32 | return parseInt(valueNode.value, 10); 33 | case Kind.FLOAT: 34 | return parseFloat(valueNode.value); 35 | case Kind.STRING: 36 | case Kind.ENUM: 37 | case Kind.BOOLEAN: 38 | return valueNode.value; 39 | case Kind.LIST: 40 | return valueNode.values.map((node) => 41 | valueFromASTUntyped(node, variables), 42 | ); 43 | case Kind.OBJECT: 44 | return keyValMap( 45 | valueNode.fields, 46 | (field) => field.name.value, 47 | (field) => valueFromASTUntyped(field.value, variables), 48 | ); 49 | case Kind.VARIABLE: 50 | return variables?.[valueNode.name.value]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/execution/mapAsyncIterator.ts: -------------------------------------------------------------------------------- 1 | import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; 2 | 3 | /** 4 | * Given an AsyncIterable and a callback function, return an AsyncIterator 5 | * which produces values mapped via calling the callback function. 6 | */ 7 | export function mapAsyncIterator( 8 | iterable: AsyncGenerator | AsyncIterable, 9 | callback: (value: T) => PromiseOrValue, 10 | ): AsyncGenerator { 11 | const iterator = iterable[Symbol.asyncIterator](); 12 | 13 | async function mapResult( 14 | result: IteratorResult, 15 | ): Promise> { 16 | if (result.done) { 17 | return result; 18 | } 19 | 20 | try { 21 | return { value: await callback(result.value), done: false }; 22 | } catch (error) { 23 | /* c8 ignore start */ 24 | // FIXME: add test case 25 | if (typeof iterator.return === 'function') { 26 | try { 27 | await iterator.return(); 28 | } catch (_e) { 29 | /* ignore error */ 30 | } 31 | } 32 | throw error; 33 | /* c8 ignore stop */ 34 | } 35 | } 36 | 37 | return { 38 | async next() { 39 | return mapResult(await iterator.next()); 40 | }, 41 | async return(): Promise> { 42 | // If iterator.return() does not exist, then type R must be undefined. 43 | return typeof iterator.return === 'function' 44 | ? mapResult(await iterator.return()) 45 | : { value: undefined as any, done: true }; 46 | }, 47 | async throw(error?: unknown) { 48 | if (typeof iterator.throw === 'function') { 49 | return mapResult(await iterator.throw(error)); 50 | } 51 | throw error; 52 | }, 53 | [Symbol.asyncIterator]() { 54 | return this; 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /integrationTests/ts/extensions-test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql/error'; 2 | import { GraphQLString, GraphQLObjectType } from 'graphql/type'; 3 | 4 | interface SomeExtension { 5 | meaningOfLife: 42; 6 | } 7 | 8 | declare module 'graphql' { 9 | interface GraphQLObjectTypeExtensions<_TSource, _TContext> { 10 | someObjectExtension?: SomeExtension; 11 | } 12 | 13 | interface GraphQLFieldExtensions<_TSource, _TContext, _TArgs> { 14 | someFieldExtension?: SomeExtension; 15 | } 16 | 17 | interface GraphQLArgumentExtensions { 18 | someArgumentExtension?: SomeExtension; 19 | } 20 | } 21 | 22 | const queryType: GraphQLObjectType = new GraphQLObjectType({ 23 | name: 'Query', 24 | fields: () => ({ 25 | sayHi: { 26 | type: GraphQLString, 27 | args: { 28 | who: { 29 | type: GraphQLString, 30 | extensions: { 31 | someArgumentExtension: { meaningOfLife: 42 }, 32 | }, 33 | }, 34 | }, 35 | resolve: (_root, args) => 'Hello ' + (args.who || 'World'), 36 | extensions: { 37 | someFieldExtension: { meaningOfLife: 42 }, 38 | }, 39 | }, 40 | }), 41 | extensions: { 42 | someObjectExtension: { meaningOfLife: 42 }, 43 | }, 44 | }); 45 | 46 | function checkExtensionTypes(_test: SomeExtension | null | undefined) {} 47 | 48 | checkExtensionTypes(queryType.extensions.someObjectExtension); 49 | 50 | const sayHiField = queryType.getFields().sayHi; 51 | checkExtensionTypes(sayHiField.extensions.someFieldExtension); 52 | 53 | checkExtensionTypes(sayHiField.args[0].extensions.someArgumentExtension); 54 | 55 | declare module 'graphql' { 56 | export interface GraphQLErrorExtensions { 57 | someErrorExtension?: SomeExtension; 58 | } 59 | } 60 | 61 | const error = new GraphQLError('foo'); 62 | checkExtensionTypes(error.extensions.someErrorExtension); 63 | -------------------------------------------------------------------------------- /src/utilities/typeFromAST.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ListTypeNode, 3 | NamedTypeNode, 4 | NonNullTypeNode, 5 | TypeNode, 6 | } from '../language/ast'; 7 | import { Kind } from '../language/kinds'; 8 | 9 | import type { GraphQLNamedType, GraphQLType } from '../type/definition'; 10 | import { GraphQLList, GraphQLNonNull } from '../type/definition'; 11 | import type { GraphQLSchema } from '../type/schema'; 12 | 13 | /** 14 | * Given a Schema and an AST node describing a type, return a GraphQLType 15 | * definition which applies to that type. For example, if provided the parsed 16 | * AST node for `[User]`, a GraphQLList instance will be returned, containing 17 | * the type called "User" found in the schema. If a type called "User" is not 18 | * found in the schema, then undefined will be returned. 19 | */ 20 | export function typeFromAST( 21 | schema: GraphQLSchema, 22 | typeNode: NamedTypeNode, 23 | ): GraphQLNamedType | undefined; 24 | export function typeFromAST( 25 | schema: GraphQLSchema, 26 | typeNode: ListTypeNode, 27 | ): GraphQLList | undefined; 28 | export function typeFromAST( 29 | schema: GraphQLSchema, 30 | typeNode: NonNullTypeNode, 31 | ): GraphQLNonNull | undefined; 32 | export function typeFromAST( 33 | schema: GraphQLSchema, 34 | typeNode: TypeNode, 35 | ): GraphQLType | undefined; 36 | export function typeFromAST( 37 | schema: GraphQLSchema, 38 | typeNode: TypeNode, 39 | ): GraphQLType | undefined { 40 | switch (typeNode.kind) { 41 | case Kind.LIST_TYPE: { 42 | const innerType = typeFromAST(schema, typeNode.type); 43 | return innerType && new GraphQLList(innerType); 44 | } 45 | case Kind.NON_NULL_TYPE: { 46 | const innerType = typeFromAST(schema, typeNode.type); 47 | return innerType && new GraphQLNonNull(innerType); 48 | } 49 | case Kind.NAMED_TYPE: 50 | return schema.getType(typeNode.name.value); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/validation/rules/NoUnusedFragmentsRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { 4 | FragmentDefinitionNode, 5 | OperationDefinitionNode, 6 | } from '../../language/ast'; 7 | import type { ASTVisitor } from '../../language/visitor'; 8 | 9 | import type { ASTValidationContext } from '../ValidationContext'; 10 | 11 | /** 12 | * No unused fragments 13 | * 14 | * A GraphQL document is only valid if all fragment definitions are spread 15 | * within operations, or spread within other fragments spread within operations. 16 | * 17 | * See https://spec.graphql.org/draft/#sec-Fragments-Must-Be-Used 18 | */ 19 | export function NoUnusedFragmentsRule( 20 | context: ASTValidationContext, 21 | ): ASTVisitor { 22 | const operationDefs: Array = []; 23 | const fragmentDefs: Array = []; 24 | 25 | return { 26 | OperationDefinition(node) { 27 | operationDefs.push(node); 28 | return false; 29 | }, 30 | FragmentDefinition(node) { 31 | fragmentDefs.push(node); 32 | return false; 33 | }, 34 | Document: { 35 | leave() { 36 | const fragmentNameUsed = Object.create(null); 37 | for (const operation of operationDefs) { 38 | for (const fragment of context.getRecursivelyReferencedFragments( 39 | operation, 40 | )) { 41 | fragmentNameUsed[fragment.name.value] = true; 42 | } 43 | } 44 | 45 | for (const fragmentDef of fragmentDefs) { 46 | const fragName = fragmentDef.name.value; 47 | if (fragmentNameUsed[fragName] !== true) { 48 | context.reportError( 49 | new GraphQLError(`Fragment "${fragName}" is never used.`, { 50 | nodes: fragmentDef, 51 | }), 52 | ); 53 | } 54 | } 55 | }, 56 | }, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/validation/__tests__/KnownFragmentNamesRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { KnownFragmentNamesRule } from '../rules/KnownFragmentNamesRule'; 4 | 5 | import { expectValidationErrors } from './harness'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(KnownFragmentNamesRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Known fragment names', () => { 16 | it('known fragment names are valid', () => { 17 | expectValid(` 18 | { 19 | human(id: 4) { 20 | ...HumanFields1 21 | ... on Human { 22 | ...HumanFields2 23 | } 24 | ... { 25 | name 26 | } 27 | } 28 | } 29 | fragment HumanFields1 on Human { 30 | name 31 | ...HumanFields3 32 | } 33 | fragment HumanFields2 on Human { 34 | name 35 | } 36 | fragment HumanFields3 on Human { 37 | name 38 | } 39 | `); 40 | }); 41 | 42 | it('unknown fragment names are invalid', () => { 43 | expectErrors(` 44 | { 45 | human(id: 4) { 46 | ...UnknownFragment1 47 | ... on Human { 48 | ...UnknownFragment2 49 | } 50 | } 51 | } 52 | fragment HumanFields on Human { 53 | name 54 | ...UnknownFragment3 55 | } 56 | `).toDeepEqual([ 57 | { 58 | message: 'Unknown fragment "UnknownFragment1".', 59 | locations: [{ line: 4, column: 14 }], 60 | }, 61 | { 62 | message: 'Unknown fragment "UnknownFragment2".', 63 | locations: [{ line: 6, column: 16 }], 64 | }, 65 | { 66 | message: 'Unknown fragment "UnknownFragment3".', 67 | locations: [{ line: 12, column: 12 }], 68 | }, 69 | ]); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/validation/rules/FragmentsOnCompositeTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import { print } from '../../language/printer'; 4 | import type { ASTVisitor } from '../../language/visitor'; 5 | 6 | import { isCompositeType } from '../../type/definition'; 7 | 8 | import { typeFromAST } from '../../utilities/typeFromAST'; 9 | 10 | import type { ValidationContext } from '../ValidationContext'; 11 | 12 | /** 13 | * Fragments on composite type 14 | * 15 | * Fragments use a type condition to determine if they apply, since fragments 16 | * can only be spread into a composite type (object, interface, or union), the 17 | * type condition must also be a composite type. 18 | * 19 | * See https://spec.graphql.org/draft/#sec-Fragments-On-Composite-Types 20 | */ 21 | export function FragmentsOnCompositeTypesRule( 22 | context: ValidationContext, 23 | ): ASTVisitor { 24 | return { 25 | InlineFragment(node) { 26 | const typeCondition = node.typeCondition; 27 | if (typeCondition) { 28 | const type = typeFromAST(context.getSchema(), typeCondition); 29 | if (type && !isCompositeType(type)) { 30 | const typeStr = print(typeCondition); 31 | context.reportError( 32 | new GraphQLError( 33 | `Fragment cannot condition on non composite type "${typeStr}".`, 34 | { nodes: typeCondition }, 35 | ), 36 | ); 37 | } 38 | } 39 | }, 40 | FragmentDefinition(node) { 41 | const type = typeFromAST(context.getSchema(), node.typeCondition); 42 | if (type && !isCompositeType(type)) { 43 | const typeStr = print(node.typeCondition); 44 | context.reportError( 45 | new GraphQLError( 46 | `Fragment "${node.name.value}" cannot condition on non composite type "${typeStr}".`, 47 | { nodes: node.typeCondition }, 48 | ), 49 | ); 50 | } 51 | }, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/isAsyncIterable-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc'; 5 | import { isAsyncIterable } from '../isAsyncIterable'; 6 | 7 | describe('isAsyncIterable', () => { 8 | it('should return `true` for AsyncIterable', () => { 9 | const asyncIterable = { [Symbol.asyncIterator]: identityFunc }; 10 | expect(isAsyncIterable(asyncIterable)).to.equal(true); 11 | 12 | async function* asyncGeneratorFunc() { 13 | /* do nothing */ 14 | } 15 | 16 | expect(isAsyncIterable(asyncGeneratorFunc())).to.equal(true); 17 | 18 | // But async generator function itself is not iterable 19 | expect(isAsyncIterable(asyncGeneratorFunc)).to.equal(false); 20 | }); 21 | 22 | it('should return `false` for all other values', () => { 23 | expect(isAsyncIterable(null)).to.equal(false); 24 | expect(isAsyncIterable(undefined)).to.equal(false); 25 | 26 | expect(isAsyncIterable('ABC')).to.equal(false); 27 | expect(isAsyncIterable('0')).to.equal(false); 28 | expect(isAsyncIterable('')).to.equal(false); 29 | 30 | expect(isAsyncIterable([])).to.equal(false); 31 | expect(isAsyncIterable(new Int8Array(1))).to.equal(false); 32 | 33 | expect(isAsyncIterable({})).to.equal(false); 34 | expect(isAsyncIterable({ iterable: true })).to.equal(false); 35 | 36 | const asyncIteratorWithoutSymbol = { next: identityFunc }; 37 | expect(isAsyncIterable(asyncIteratorWithoutSymbol)).to.equal(false); 38 | 39 | const nonAsyncIterable = { [Symbol.iterator]: identityFunc }; 40 | expect(isAsyncIterable(nonAsyncIterable)).to.equal(false); 41 | 42 | function* generatorFunc() { 43 | /* do nothing */ 44 | } 45 | expect(isAsyncIterable(generatorFunc())).to.equal(false); 46 | 47 | const invalidAsyncIterable = { 48 | [Symbol.asyncIterator]: { next: identityFunc }, 49 | }; 50 | expect(isAsyncIterable(invalidAsyncIterable)).to.equal(false); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /resources/add-extension-to-import-paths.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'node:assert'; 2 | import * as util from 'node:util'; 3 | 4 | import * as ts from 'typescript'; 5 | 6 | /** 7 | * Adds extension to all paths imported inside MJS files 8 | * 9 | * Transforms: 10 | * 11 | * ``` 12 | * import { foo } from './bar'; 13 | * export { foo } from './bar'; 14 | * ``` 15 | * 16 | * to: 17 | * 18 | * ``` 19 | * import { foo } from './bar.mjs'; 20 | * export { foo } from './bar.mjs'; 21 | * ``` 22 | * 23 | */ 24 | export function addExtensionToImportPaths(config: { extension: string }) { 25 | const { extension } = config; 26 | return (context: ts.TransformationContext) => { 27 | const { factory } = context; 28 | 29 | return visitSourceFile; 30 | 31 | function visitSourceFile(sourceFile: ts.SourceFile) { 32 | return ts.visitNode(sourceFile, visitNode); 33 | } 34 | 35 | function visitNode(node: ts.Node): ts.Node { 36 | const source: string | undefined = (node as any).moduleSpecifier?.text; 37 | if (source?.startsWith('./') || source?.startsWith('../')) { 38 | if (ts.isImportDeclaration(node)) { 39 | return factory.updateImportDeclaration( 40 | node, 41 | node.decorators, 42 | node.modifiers, 43 | node.importClause, 44 | ts.createStringLiteral(source + extension), 45 | node.assertClause, 46 | ); 47 | } 48 | if (ts.isExportDeclaration(node)) { 49 | return factory.updateExportDeclaration( 50 | node, 51 | node.decorators, 52 | node.modifiers, 53 | node.isTypeOnly, 54 | node.exportClause, 55 | ts.createStringLiteral(source + extension), 56 | node.assertClause, 57 | ); 58 | } 59 | 60 | assert( 61 | false, 62 | 'Unexpected node with moduleSpecifier: ' + util.inspect(node), 63 | ); 64 | } 65 | return ts.visitEachChild(node, visitNode, context); 66 | } 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /src/utilities/__tests__/introspectionFromSchema-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { dedent } from '../../__testUtils__/dedent'; 5 | 6 | import { GraphQLObjectType } from '../../type/definition'; 7 | import { GraphQLString } from '../../type/scalars'; 8 | import { GraphQLSchema } from '../../type/schema'; 9 | 10 | import { buildClientSchema } from '../buildClientSchema'; 11 | import type { IntrospectionQuery } from '../getIntrospectionQuery'; 12 | import { introspectionFromSchema } from '../introspectionFromSchema'; 13 | import { printSchema } from '../printSchema'; 14 | 15 | function introspectionToSDL(introspection: IntrospectionQuery): string { 16 | return printSchema(buildClientSchema(introspection)); 17 | } 18 | 19 | describe('introspectionFromSchema', () => { 20 | const schema = new GraphQLSchema({ 21 | description: 'This is a simple schema', 22 | query: new GraphQLObjectType({ 23 | name: 'Simple', 24 | description: 'This is a simple type', 25 | fields: { 26 | string: { 27 | type: GraphQLString, 28 | description: 'This is a string field', 29 | }, 30 | }, 31 | }), 32 | }); 33 | 34 | it('converts a simple schema', () => { 35 | const introspection = introspectionFromSchema(schema); 36 | 37 | expect(introspectionToSDL(introspection)).to.deep.equal(dedent` 38 | """This is a simple schema""" 39 | schema { 40 | query: Simple 41 | } 42 | 43 | """This is a simple type""" 44 | type Simple { 45 | """This is a string field""" 46 | string: String 47 | } 48 | `); 49 | }); 50 | 51 | it('converts a simple schema without descriptions', () => { 52 | const introspection = introspectionFromSchema(schema, { 53 | descriptions: false, 54 | }); 55 | 56 | expect(introspectionToSDL(introspection)).to.deep.equal(dedent` 57 | schema { 58 | query: Simple 59 | } 60 | 61 | type Simple { 62 | string: String 63 | } 64 | `); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/toObjMap-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import type { ObjMapLike } from '../ObjMap'; 5 | import { toObjMap } from '../toObjMap'; 6 | 7 | // Workaround to make both ESLint happy 8 | const __proto__ = '__proto__'; 9 | 10 | describe('toObjMap', () => { 11 | it('convert undefined to ObjMap', () => { 12 | const result = toObjMap(undefined); 13 | expect(result).to.deep.equal({}); 14 | expect(Object.getPrototypeOf(result)).to.equal(null); 15 | }); 16 | 17 | it('convert null to ObjMap', () => { 18 | const result = toObjMap(null); 19 | expect(result).to.deep.equal({}); 20 | expect(Object.getPrototypeOf(result)).to.equal(null); 21 | }); 22 | 23 | it('convert empty object to ObjMap', () => { 24 | const result = toObjMap({}); 25 | expect(result).to.deep.equal({}); 26 | expect(Object.getPrototypeOf(result)).to.equal(null); 27 | }); 28 | 29 | it('convert object with own properties to ObjMap', () => { 30 | const obj: ObjMapLike = Object.freeze({ foo: 'bar' }); 31 | 32 | const result = toObjMap(obj); 33 | expect(result).to.deep.equal(obj); 34 | expect(Object.getPrototypeOf(result)).to.equal(null); 35 | }); 36 | 37 | it('convert object with __proto__ property to ObjMap', () => { 38 | const protoObj = Object.freeze({ toString: false }); 39 | const obj = Object.create(null); 40 | obj[__proto__] = protoObj; 41 | Object.freeze(obj); 42 | 43 | const result = toObjMap(obj); 44 | expect(Object.keys(result)).to.deep.equal(['__proto__']); 45 | expect(Object.getPrototypeOf(result)).to.equal(null); 46 | expect(result[__proto__]).to.equal(protoObj); 47 | }); 48 | 49 | it('passthrough empty ObjMap', () => { 50 | const objMap = Object.create(null); 51 | expect(toObjMap(objMap)).to.deep.equal(objMap); 52 | }); 53 | 54 | it('passthrough ObjMap with properties', () => { 55 | const objMap = Object.freeze({ 56 | __proto__: null, 57 | foo: 'bar', 58 | }); 59 | expect(toObjMap(objMap)).to.deep.equal(objMap); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/genFuzzStrings-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { genFuzzStrings } from '../genFuzzStrings'; 5 | 6 | function expectFuzzStrings(options: { 7 | allowedChars: ReadonlyArray; 8 | maxLength: number; 9 | }) { 10 | return expect([...genFuzzStrings(options)]); 11 | } 12 | 13 | describe('genFuzzStrings', () => { 14 | it('always provide empty string', () => { 15 | expectFuzzStrings({ allowedChars: [], maxLength: 0 }).to.deep.equal(['']); 16 | expectFuzzStrings({ allowedChars: [], maxLength: 1 }).to.deep.equal(['']); 17 | expectFuzzStrings({ allowedChars: ['a'], maxLength: 0 }).to.deep.equal([ 18 | '', 19 | ]); 20 | }); 21 | 22 | it('generate strings with single character', () => { 23 | expectFuzzStrings({ allowedChars: ['a'], maxLength: 1 }).to.deep.equal([ 24 | '', 25 | 'a', 26 | ]); 27 | 28 | expectFuzzStrings({ 29 | allowedChars: ['a', 'b', 'c'], 30 | maxLength: 1, 31 | }).to.deep.equal(['', 'a', 'b', 'c']); 32 | }); 33 | 34 | it('generate strings with multiple character', () => { 35 | expectFuzzStrings({ allowedChars: ['a'], maxLength: 2 }).to.deep.equal([ 36 | '', 37 | 'a', 38 | 'aa', 39 | ]); 40 | 41 | expectFuzzStrings({ 42 | allowedChars: ['a', 'b', 'c'], 43 | maxLength: 2, 44 | }).to.deep.equal([ 45 | '', 46 | 'a', 47 | 'b', 48 | 'c', 49 | 'aa', 50 | 'ab', 51 | 'ac', 52 | 'ba', 53 | 'bb', 54 | 'bc', 55 | 'ca', 56 | 'cb', 57 | 'cc', 58 | ]); 59 | }); 60 | 61 | it('generate strings longer than possible number of characters', () => { 62 | expectFuzzStrings({ 63 | allowedChars: ['a', 'b'], 64 | maxLength: 3, 65 | }).to.deep.equal([ 66 | '', 67 | 'a', 68 | 'b', 69 | 'aa', 70 | 'ab', 71 | 'ba', 72 | 'bb', 73 | 'aaa', 74 | 'aab', 75 | 'aba', 76 | 'abb', 77 | 'baa', 78 | 'bab', 79 | 'bba', 80 | 'bbb', 81 | ]); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /docs-old/APIReference-Validation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: graphql/validation 3 | layout: ../_core/GraphQLJSLayout 4 | category: API Reference 5 | permalink: /graphql-js/validation/ 6 | sublinks: specifiedRules,validate 7 | --- 8 | 9 | The `graphql/validation` module fulfills the Validation phase of fulfilling a 10 | GraphQL result. You can import either from the `graphql/validation` module, or from the root `graphql` module. For example: 11 | 12 | ```js 13 | import { validate } from 'graphql/validation'; // ES6 14 | var { validate } = require('graphql/validation'); // CommonJS 15 | ``` 16 | 17 | ## Overview 18 | 19 | 33 | 34 | ## Validation 35 | 36 | ### validate 37 | 38 | ```js 39 | function validate( 40 | schema: GraphQLSchema, 41 | ast: Document, 42 | rules?: Array 43 | ): Array 44 | ``` 45 | 46 | Implements the "Validation" section of the spec. 47 | 48 | Validation runs synchronously, returning an array of encountered errors, or 49 | an empty array if no errors were encountered and the document is valid. 50 | 51 | A list of specific validation rules may be provided. If not provided, the 52 | default list of rules defined by the GraphQL specification will be used. 53 | 54 | Each validation rules is a function which returns a visitor 55 | (see the language/visitor API). Visitor methods are expected to return 56 | GraphQLErrors, or Arrays of GraphQLErrors when invalid. 57 | 58 | Visitors can also supply `visitSpreadFragments: true` which will alter the 59 | behavior of the visitor to skip over top level defined fragments, and instead 60 | visit those fragments at every point a spread is encountered. 61 | 62 | ### specifiedRules 63 | 64 | ```js 65 | var specifiedRules: Array<(context: ValidationContext): any> 66 | ``` 67 | 68 | This set includes all validation rules defined by the GraphQL spec 69 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueOperationTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { 4 | SchemaDefinitionNode, 5 | SchemaExtensionNode, 6 | } from '../../language/ast'; 7 | import type { ASTVisitor } from '../../language/visitor'; 8 | 9 | import type { SDLValidationContext } from '../ValidationContext'; 10 | 11 | /** 12 | * Unique operation types 13 | * 14 | * A GraphQL document is only valid if it has only one type per operation. 15 | */ 16 | export function UniqueOperationTypesRule( 17 | context: SDLValidationContext, 18 | ): ASTVisitor { 19 | const schema = context.getSchema(); 20 | const definedOperationTypes = Object.create(null); 21 | const existingOperationTypes = schema 22 | ? { 23 | query: schema.getQueryType(), 24 | mutation: schema.getMutationType(), 25 | subscription: schema.getSubscriptionType(), 26 | } 27 | : {}; 28 | 29 | return { 30 | SchemaDefinition: checkOperationTypes, 31 | SchemaExtension: checkOperationTypes, 32 | }; 33 | 34 | function checkOperationTypes( 35 | node: SchemaDefinitionNode | SchemaExtensionNode, 36 | ) { 37 | // See: https://github.com/graphql/graphql-js/issues/2203 38 | /* c8 ignore next */ 39 | const operationTypesNodes = node.operationTypes ?? []; 40 | 41 | for (const operationType of operationTypesNodes) { 42 | const operation = operationType.operation; 43 | const alreadyDefinedOperationType = definedOperationTypes[operation]; 44 | 45 | if (existingOperationTypes[operation]) { 46 | context.reportError( 47 | new GraphQLError( 48 | `Type for ${operation} already defined in the schema. It cannot be redefined.`, 49 | { nodes: operationType }, 50 | ), 51 | ); 52 | } else if (alreadyDefinedOperationType) { 53 | context.reportError( 54 | new GraphQLError( 55 | `There can be only one ${operation} type in schema.`, 56 | { nodes: [alreadyDefinedOperationType, operationType] }, 57 | ), 58 | ); 59 | } else { 60 | definedOperationTypes[operation] = operationType; 61 | } 62 | } 63 | 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/type/__tests__/assertName-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { assertEnumValueName, assertName } from '../assertName'; 5 | 6 | describe('assertName', () => { 7 | it('passthrough valid name', () => { 8 | expect(assertName('_ValidName123')).to.equal('_ValidName123'); 9 | }); 10 | 11 | it('throws on empty strings', () => { 12 | expect(() => assertName('')).to.throw( 13 | 'Expected name to be a non-empty string.', 14 | ); 15 | }); 16 | 17 | it('throws for names with invalid characters', () => { 18 | expect(() => assertName('>--()-->')).to.throw( 19 | 'Names must only contain [_a-zA-Z0-9] but ">--()-->" does not.', 20 | ); 21 | }); 22 | 23 | it('throws for names starting with invalid characters', () => { 24 | expect(() => assertName('42MeaningsOfLife')).to.throw( 25 | 'Names must start with [_a-zA-Z] but "42MeaningsOfLife" does not.', 26 | ); 27 | }); 28 | }); 29 | 30 | describe('assertEnumValueName', () => { 31 | it('passthrough valid name', () => { 32 | expect(assertEnumValueName('_ValidName123')).to.equal('_ValidName123'); 33 | }); 34 | 35 | it('throws on empty strings', () => { 36 | expect(() => assertEnumValueName('')).to.throw( 37 | 'Expected name to be a non-empty string.', 38 | ); 39 | }); 40 | 41 | it('throws for names with invalid characters', () => { 42 | expect(() => assertEnumValueName('>--()-->')).to.throw( 43 | 'Names must only contain [_a-zA-Z0-9] but ">--()-->" does not.', 44 | ); 45 | }); 46 | 47 | it('throws for names starting with invalid characters', () => { 48 | expect(() => assertEnumValueName('42MeaningsOfLife')).to.throw( 49 | 'Names must start with [_a-zA-Z] but "42MeaningsOfLife" does not.', 50 | ); 51 | }); 52 | 53 | it('throws for restricted names', () => { 54 | expect(() => assertEnumValueName('true')).to.throw( 55 | 'Enum values cannot be named: true', 56 | ); 57 | expect(() => assertEnumValueName('false')).to.throw( 58 | 'Enum values cannot be named: false', 59 | ); 60 | expect(() => assertEnumValueName('null')).to.throw( 61 | 'Enum values cannot be named: null', 62 | ); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /.github/workflows/cmd-run-benchmark.yml: -------------------------------------------------------------------------------- 1 | name: run-benchmark 2 | on: 3 | workflow_call: 4 | inputs: 5 | pullRequestJSON: 6 | description: String that contain JSON payload for `pull_request` event. 7 | required: true 8 | type: string 9 | jobs: 10 | benchmark: 11 | name: Run benchmark 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v3 16 | with: 17 | persist-credentials: false 18 | ref: ${{ fromJSON(inputs.pullRequestJSON).merge_commit_sha }} 19 | 20 | - name: Deepen cloned repo 21 | env: 22 | BASE_SHA: ${{ fromJSON(inputs.pullRequestJSON).base.sha }} 23 | run: 'git fetch --depth=1 origin "$BASE_SHA:refs/tags/BASE"' 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v3 27 | with: 28 | cache: npm 29 | node-version-file: '.node-version' 30 | 31 | - name: Install Dependencies 32 | run: npm ci --ignore-scripts 33 | 34 | - name: Run Benchmark 35 | run: | 36 | npm run benchmark -- --revs HEAD BASE 37 | 38 | - name: Create replyMessage 39 | uses: actions/github-script@v6 40 | with: 41 | script: | 42 | const fs = require('node:fs'); 43 | 44 | // GH doesn't expose job's id so we need to get it through API, see 45 | // https://github.community/t/job-id-is-string-in-github-job-but-integer-in-actions-api/139060 46 | const result = await github.rest.actions.listJobsForWorkflowRun({ 47 | ...context.repo, 48 | run_id: context.runId, 49 | filter: 'latest', 50 | }) 51 | 52 | const currentJob = result.data.jobs.find( 53 | (job) => job.name === 'cmd-run-benchmark / Run benchmark', 54 | ); 55 | 56 | fs.writeFileSync( 57 | './replyMessage.txt', 58 | `Please, see benchmark results here: ${currentJob.html_url}#step:6:1`, 59 | ); 60 | 61 | - name: Upload replyMessage 62 | uses: actions/upload-artifact@v3 63 | with: 64 | name: replyMessage 65 | path: ./replyMessage.txt 66 | -------------------------------------------------------------------------------- /src/language/kinds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The set of allowed kind values for AST nodes. 3 | */ 4 | export enum Kind { 5 | /** Name */ 6 | NAME = 'Name', 7 | 8 | /** Document */ 9 | DOCUMENT = 'Document', 10 | OPERATION_DEFINITION = 'OperationDefinition', 11 | VARIABLE_DEFINITION = 'VariableDefinition', 12 | SELECTION_SET = 'SelectionSet', 13 | FIELD = 'Field', 14 | ARGUMENT = 'Argument', 15 | 16 | /** Fragments */ 17 | FRAGMENT_SPREAD = 'FragmentSpread', 18 | INLINE_FRAGMENT = 'InlineFragment', 19 | FRAGMENT_DEFINITION = 'FragmentDefinition', 20 | 21 | /** Values */ 22 | VARIABLE = 'Variable', 23 | INT = 'IntValue', 24 | FLOAT = 'FloatValue', 25 | STRING = 'StringValue', 26 | BOOLEAN = 'BooleanValue', 27 | NULL = 'NullValue', 28 | ENUM = 'EnumValue', 29 | LIST = 'ListValue', 30 | OBJECT = 'ObjectValue', 31 | OBJECT_FIELD = 'ObjectField', 32 | 33 | /** Directives */ 34 | DIRECTIVE = 'Directive', 35 | 36 | /** Types */ 37 | NAMED_TYPE = 'NamedType', 38 | LIST_TYPE = 'ListType', 39 | NON_NULL_TYPE = 'NonNullType', 40 | 41 | /** Type System Definitions */ 42 | SCHEMA_DEFINITION = 'SchemaDefinition', 43 | OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition', 44 | 45 | /** Type Definitions */ 46 | SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition', 47 | OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition', 48 | FIELD_DEFINITION = 'FieldDefinition', 49 | INPUT_VALUE_DEFINITION = 'InputValueDefinition', 50 | INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition', 51 | UNION_TYPE_DEFINITION = 'UnionTypeDefinition', 52 | ENUM_TYPE_DEFINITION = 'EnumTypeDefinition', 53 | ENUM_VALUE_DEFINITION = 'EnumValueDefinition', 54 | INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition', 55 | 56 | /** Directive Definitions */ 57 | DIRECTIVE_DEFINITION = 'DirectiveDefinition', 58 | 59 | /** Type System Extensions */ 60 | SCHEMA_EXTENSION = 'SchemaExtension', 61 | 62 | /** Type Extensions */ 63 | SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension', 64 | OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension', 65 | INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension', 66 | UNION_TYPE_EXTENSION = 'UnionTypeExtension', 67 | ENUM_TYPE_EXTENSION = 'EnumTypeExtension', 68 | INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension', 69 | } 70 | -------------------------------------------------------------------------------- /website/docs/tutorials/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started With GraphQL.js 3 | --- 4 | 5 | ## Prerequisites 6 | 7 | Before getting started, you should have Node v6 installed, although the examples should mostly work in previous versions of Node as well. For this guide, we won't use any language features that require transpilation, but we will use some ES6 features like [Promises](http://www.html5rocks.com/en/tutorials/es6/promises/), [classes](http://javascriptplayground.com/blog/2014/07/introduction-to-es6-classes-tutorial/), and [fat arrow functions](https://strongloop.com/strongblog/an-introduction-to-javascript-es6-arrow-functions/), so if you aren't familiar with them you might want to read up on them first. 8 | 9 | To create a new project and install GraphQL.js in your current directory: 10 | 11 | ```bash 12 | npm init 13 | npm install graphql --save 14 | ``` 15 | 16 | ## Writing Code 17 | 18 | To handle GraphQL queries, we need a schema that defines the `Query` type, and we need an API root with a function called a “resolver” for each API endpoint. For an API that just returns “Hello world!”, we can put this code in a file named `server.js`: 19 | 20 | ```js 21 | var { graphql, buildSchema } = require('graphql'); 22 | 23 | // Construct a schema, using GraphQL schema language 24 | var schema = buildSchema(` 25 | type Query { 26 | hello: String 27 | } 28 | `); 29 | 30 | // The root provides a resolver function for each API endpoint 31 | var root = { 32 | hello: () => { 33 | return 'Hello world!'; 34 | }, 35 | }; 36 | 37 | // Run the GraphQL query '{ hello }' and print out the response 38 | graphql(schema, '{ hello }', root).then((response) => { 39 | console.log(response); 40 | }); 41 | ``` 42 | 43 | If you run this with: 44 | 45 | ```bash 46 | node server.js 47 | ``` 48 | 49 | You should see the GraphQL response printed out: 50 | 51 | ```js 52 | { 53 | data: { 54 | hello: 'Hello world!'; 55 | } 56 | } 57 | ``` 58 | 59 | Congratulations - you just executed a GraphQL query! 60 | 61 | For practical applications, you'll probably want to run GraphQL queries from an API server, rather than executing GraphQL with a command line tool. To use GraphQL for an API server over HTTP, check out [Running an Express GraphQL Server](./running-an-express-graphql-server.md). 62 | -------------------------------------------------------------------------------- /src/language/__tests__/blockString-fuzz.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { dedent } from '../../__testUtils__/dedent'; 5 | import { genFuzzStrings } from '../../__testUtils__/genFuzzStrings'; 6 | import { inspectStr } from '../../__testUtils__/inspectStr'; 7 | 8 | import { isPrintableAsBlockString, printBlockString } from '../blockString'; 9 | import { Lexer } from '../lexer'; 10 | import { Source } from '../source'; 11 | 12 | function lexValue(str: string): string { 13 | const lexer = new Lexer(new Source(str)); 14 | const value = lexer.advance().value; 15 | 16 | assert(typeof value === 'string'); 17 | assert(lexer.advance().kind === '', 'Expected EOF'); 18 | return value; 19 | } 20 | 21 | function testPrintableBlockString( 22 | testValue: string, 23 | options?: { minimize: boolean }, 24 | ): void { 25 | const blockString = printBlockString(testValue, options); 26 | const printedValue = lexValue(blockString); 27 | assert( 28 | testValue === printedValue, 29 | dedent` 30 | Expected lexValue(${inspectStr(blockString)}) 31 | to equal ${inspectStr(testValue)} 32 | but got ${inspectStr(printedValue)} 33 | `, 34 | ); 35 | } 36 | 37 | function testNonPrintableBlockString(testValue: string): void { 38 | const blockString = printBlockString(testValue); 39 | const printedValue = lexValue(blockString); 40 | assert( 41 | testValue !== printedValue, 42 | dedent` 43 | Expected lexValue(${inspectStr(blockString)}) 44 | to not equal ${inspectStr(testValue)} 45 | `, 46 | ); 47 | } 48 | 49 | describe('printBlockString', () => { 50 | it('correctly print random strings', () => { 51 | // Testing with length >7 is taking exponentially more time. However it is 52 | // highly recommended to test with increased limit if you make any change. 53 | for (const fuzzStr of genFuzzStrings({ 54 | allowedChars: ['\n', '\t', ' ', '"', 'a', '\\'], 55 | maxLength: 7, 56 | })) { 57 | if (!isPrintableAsBlockString(fuzzStr)) { 58 | testNonPrintableBlockString(fuzzStr); 59 | continue; 60 | } 61 | 62 | testPrintableBlockString(fuzzStr); 63 | testPrintableBlockString(fuzzStr, { minimize: true }); 64 | } 65 | }).timeout(20000); 66 | }); 67 | -------------------------------------------------------------------------------- /src/execution/__tests__/simplePubSub.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | /** 4 | * Create an AsyncIterator from an EventEmitter. Useful for mocking a 5 | * PubSub system for tests. 6 | */ 7 | export class SimplePubSub { 8 | private _subscribers: Set<(value: T) => void>; 9 | 10 | constructor() { 11 | this._subscribers = new Set(); 12 | } 13 | 14 | emit(event: T): boolean { 15 | for (const subscriber of this._subscribers) { 16 | subscriber(event); 17 | } 18 | return this._subscribers.size > 0; 19 | } 20 | 21 | getSubscriber(transform: (value: T) => R): AsyncGenerator { 22 | const pullQueue: Array<(result: IteratorResult) => void> = []; 23 | const pushQueue: Array = []; 24 | let listening = true; 25 | this._subscribers.add(pushValue); 26 | 27 | const emptyQueue = () => { 28 | listening = false; 29 | this._subscribers.delete(pushValue); 30 | for (const resolve of pullQueue) { 31 | resolve({ value: undefined, done: true }); 32 | } 33 | pullQueue.length = 0; 34 | pushQueue.length = 0; 35 | }; 36 | 37 | return { 38 | next(): Promise> { 39 | if (!listening) { 40 | return Promise.resolve({ value: undefined, done: true }); 41 | } 42 | 43 | if (pushQueue.length > 0) { 44 | const value = pushQueue[0]; 45 | pushQueue.shift(); 46 | return Promise.resolve({ value, done: false }); 47 | } 48 | return new Promise((resolve) => pullQueue.push(resolve)); 49 | }, 50 | return(): Promise> { 51 | emptyQueue(); 52 | return Promise.resolve({ value: undefined, done: true }); 53 | }, 54 | throw(error: unknown) { 55 | emptyQueue(); 56 | return Promise.reject(error); 57 | }, 58 | [Symbol.asyncIterator]() { 59 | return this; 60 | }, 61 | }; 62 | 63 | function pushValue(event: T): void { 64 | const value: R = transform(event); 65 | if (pullQueue.length > 0) { 66 | const receiver = pullQueue.shift(); 67 | assert(receiver != null); 68 | receiver({ value, done: false }); 69 | } else { 70 | pushQueue.push(value); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/validation/__tests__/ExecutableDefinitionsRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { ExecutableDefinitionsRule } from '../rules/ExecutableDefinitionsRule'; 4 | 5 | import { expectValidationErrors } from './harness'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(ExecutableDefinitionsRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Executable definitions', () => { 16 | it('with only operation', () => { 17 | expectValid(` 18 | query Foo { 19 | dog { 20 | name 21 | } 22 | } 23 | `); 24 | }); 25 | 26 | it('with operation and fragment', () => { 27 | expectValid(` 28 | query Foo { 29 | dog { 30 | name 31 | ...Frag 32 | } 33 | } 34 | 35 | fragment Frag on Dog { 36 | name 37 | } 38 | `); 39 | }); 40 | 41 | it('with type definition', () => { 42 | expectErrors(` 43 | query Foo { 44 | dog { 45 | name 46 | } 47 | } 48 | 49 | type Cow { 50 | name: String 51 | } 52 | 53 | extend type Dog { 54 | color: String 55 | } 56 | `).toDeepEqual([ 57 | { 58 | message: 'The "Cow" definition is not executable.', 59 | locations: [{ line: 8, column: 7 }], 60 | }, 61 | { 62 | message: 'The "Dog" definition is not executable.', 63 | locations: [{ line: 12, column: 7 }], 64 | }, 65 | ]); 66 | }); 67 | 68 | it('with schema definition', () => { 69 | expectErrors(` 70 | schema { 71 | query: Query 72 | } 73 | 74 | type Query { 75 | test: String 76 | } 77 | 78 | extend schema @directive 79 | `).toDeepEqual([ 80 | { 81 | message: 'The schema definition is not executable.', 82 | locations: [{ line: 2, column: 7 }], 83 | }, 84 | { 85 | message: 'The "Query" definition is not executable.', 86 | locations: [{ line: 6, column: 7 }], 87 | }, 88 | { 89 | message: 'The schema definition is not executable.', 90 | locations: [{ line: 10, column: 7 }], 91 | }, 92 | ]); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /website/docs/tutorials/basic-types.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Basic Types 3 | --- 4 | 5 | In most situations, all you need to do is to specify the types for your API using the GraphQL schema language, taken as an argument to the `buildSchema` function. 6 | 7 | The GraphQL schema language supports the scalar types of `String`, `Int`, `Float`, `Boolean`, and `ID`, so you can use these directly in the schema you pass to `buildSchema`. 8 | 9 | By default, every type is nullable - it's legitimate to return `null` as any of the scalar types. Use an exclamation point to indicate a type cannot be nullable, so `String!` is a non-nullable string. 10 | 11 | To use a list type, surround the type in square brackets, so `[Int]` is a list of integers. 12 | 13 | Each of these types maps straightforwardly to JavaScript, so you can just return plain old JavaScript objects in APIs that return these types. Here's an example that shows how to use some of these basic types: 14 | 15 | ```js 16 | var express = require('express'); 17 | var { graphqlHTTP } = require('express-graphql'); 18 | var { buildSchema } = require('graphql'); 19 | 20 | // Construct a schema, using GraphQL schema language 21 | var schema = buildSchema(` 22 | type Query { 23 | quoteOfTheDay: String 24 | random: Float! 25 | rollThreeDice: [Int] 26 | } 27 | `); 28 | 29 | // The root provides a resolver function for each API endpoint 30 | var root = { 31 | quoteOfTheDay: () => { 32 | return Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within'; 33 | }, 34 | random: () => { 35 | return Math.random(); 36 | }, 37 | rollThreeDice: () => { 38 | return [1, 2, 3].map((_) => 1 + Math.floor(Math.random() * 6)); 39 | }, 40 | }; 41 | 42 | var app = express(); 43 | app.use( 44 | '/graphql', 45 | graphqlHTTP({ 46 | schema: schema, 47 | rootValue: root, 48 | graphiql: true, 49 | }), 50 | ); 51 | app.listen(4000, () => { 52 | console.log('Running a GraphQL API server at localhost:4000/graphql'); 53 | }); 54 | ``` 55 | 56 | If you run this code with `node server.js` and browse to http://localhost:4000/graphql you can try out these APIs. 57 | 58 | These examples show you how to call APIs that return different types. To send different types of data into an API, you will also need to learn about [passing arguments to a GraphQL API](./passing-arguments.md). 59 | -------------------------------------------------------------------------------- /website/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/utilities/__tests__/getOperationAST-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { parse } from '../../language/parser'; 5 | 6 | import { getOperationAST } from '../getOperationAST'; 7 | 8 | describe('getOperationAST', () => { 9 | it('Gets an operation from a simple document', () => { 10 | const doc = parse('{ field }'); 11 | expect(getOperationAST(doc)).to.equal(doc.definitions[0]); 12 | }); 13 | 14 | it('Gets an operation from a document with named op (mutation)', () => { 15 | const doc = parse('mutation Test { field }'); 16 | expect(getOperationAST(doc)).to.equal(doc.definitions[0]); 17 | }); 18 | 19 | it('Gets an operation from a document with named op (subscription)', () => { 20 | const doc = parse('subscription Test { field }'); 21 | expect(getOperationAST(doc)).to.equal(doc.definitions[0]); 22 | }); 23 | 24 | it('Does not get missing operation', () => { 25 | const doc = parse('type Foo { field: String }'); 26 | expect(getOperationAST(doc)).to.equal(null); 27 | }); 28 | 29 | it('Does not get ambiguous unnamed operation', () => { 30 | const doc = parse(` 31 | { field } 32 | mutation Test { field } 33 | subscription TestSub { field } 34 | `); 35 | expect(getOperationAST(doc)).to.equal(null); 36 | }); 37 | 38 | it('Does not get ambiguous named operation', () => { 39 | const doc = parse(` 40 | query TestQ { field } 41 | mutation TestM { field } 42 | subscription TestS { field } 43 | `); 44 | expect(getOperationAST(doc)).to.equal(null); 45 | }); 46 | 47 | it('Does not get misnamed operation', () => { 48 | const doc = parse(` 49 | { field } 50 | 51 | query TestQ { field } 52 | mutation TestM { field } 53 | subscription TestS { field } 54 | `); 55 | expect(getOperationAST(doc, 'Unknown')).to.equal(null); 56 | }); 57 | 58 | it('Gets named operation', () => { 59 | const doc = parse(` 60 | query TestQ { field } 61 | mutation TestM { field } 62 | subscription TestS { field } 63 | `); 64 | expect(getOperationAST(doc, 'TestQ')).to.equal(doc.definitions[0]); 65 | expect(getOperationAST(doc, 'TestM')).to.equal(doc.definitions[1]); 66 | expect(getOperationAST(doc, 'TestS')).to.equal(doc.definitions[2]); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueEnumValueNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError'; 2 | 3 | import type { 4 | EnumTypeDefinitionNode, 5 | EnumTypeExtensionNode, 6 | } from '../../language/ast'; 7 | import type { ASTVisitor } from '../../language/visitor'; 8 | 9 | import { isEnumType } from '../../type/definition'; 10 | 11 | import type { SDLValidationContext } from '../ValidationContext'; 12 | 13 | /** 14 | * Unique enum value names 15 | * 16 | * A GraphQL enum type is only valid if all its values are uniquely named. 17 | */ 18 | export function UniqueEnumValueNamesRule( 19 | context: SDLValidationContext, 20 | ): ASTVisitor { 21 | const schema = context.getSchema(); 22 | const existingTypeMap = schema ? schema.getTypeMap() : Object.create(null); 23 | const knownValueNames = Object.create(null); 24 | 25 | return { 26 | EnumTypeDefinition: checkValueUniqueness, 27 | EnumTypeExtension: checkValueUniqueness, 28 | }; 29 | 30 | function checkValueUniqueness( 31 | node: EnumTypeDefinitionNode | EnumTypeExtensionNode, 32 | ) { 33 | const typeName = node.name.value; 34 | 35 | if (!knownValueNames[typeName]) { 36 | knownValueNames[typeName] = Object.create(null); 37 | } 38 | 39 | // FIXME: https://github.com/graphql/graphql-js/issues/2203 40 | /* c8 ignore next */ 41 | const valueNodes = node.values ?? []; 42 | const valueNames = knownValueNames[typeName]; 43 | 44 | for (const valueDef of valueNodes) { 45 | const valueName = valueDef.name.value; 46 | 47 | const existingType = existingTypeMap[typeName]; 48 | if (isEnumType(existingType) && existingType.getValue(valueName)) { 49 | context.reportError( 50 | new GraphQLError( 51 | `Enum value "${typeName}.${valueName}" already exists in the schema. It cannot also be defined in this type extension.`, 52 | { nodes: valueDef.name }, 53 | ), 54 | ); 55 | } else if (valueNames[valueName]) { 56 | context.reportError( 57 | new GraphQLError( 58 | `Enum value "${typeName}.${valueName}" can only be defined once.`, 59 | { nodes: [valueNames[valueName], valueDef.name] }, 60 | ), 61 | ); 62 | } else { 63 | valueNames[valueName] = valueDef.name; 64 | } 65 | } 66 | 67 | return false; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/language/printString.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints a string as a GraphQL StringValue literal. Replaces control characters 3 | * and excluded characters (" U+0022 and \\ U+005C) with escape sequences. 4 | */ 5 | export function printString(str: string): string { 6 | return `"${str.replace(escapedRegExp, escapedReplacer)}"`; 7 | } 8 | 9 | // eslint-disable-next-line no-control-regex 10 | const escapedRegExp = /[\x00-\x1f\x22\x5c\x7f-\x9f]/g; 11 | 12 | function escapedReplacer(str: string): string { 13 | return escapeSequences[str.charCodeAt(0)]; 14 | } 15 | 16 | // prettier-ignore 17 | const escapeSequences = [ 18 | '\\u0000', '\\u0001', '\\u0002', '\\u0003', '\\u0004', '\\u0005', '\\u0006', '\\u0007', 19 | '\\b', '\\t', '\\n', '\\u000B', '\\f', '\\r', '\\u000E', '\\u000F', 20 | '\\u0010', '\\u0011', '\\u0012', '\\u0013', '\\u0014', '\\u0015', '\\u0016', '\\u0017', 21 | '\\u0018', '\\u0019', '\\u001A', '\\u001B', '\\u001C', '\\u001D', '\\u001E', '\\u001F', 22 | '', '', '\\"', '', '', '', '', '', 23 | '', '', '', '', '', '', '', '', // 2F 24 | '', '', '', '', '', '', '', '', 25 | '', '', '', '', '', '', '', '', // 3F 26 | '', '', '', '', '', '', '', '', 27 | '', '', '', '', '', '', '', '', // 4F 28 | '', '', '', '', '', '', '', '', 29 | '', '', '', '', '\\\\', '', '', '', // 5F 30 | '', '', '', '', '', '', '', '', 31 | '', '', '', '', '', '', '', '', // 6F 32 | '', '', '', '', '', '', '', '', 33 | '', '', '', '', '', '', '', '\\u007F', 34 | '\\u0080', '\\u0081', '\\u0082', '\\u0083', '\\u0084', '\\u0085', '\\u0086', '\\u0087', 35 | '\\u0088', '\\u0089', '\\u008A', '\\u008B', '\\u008C', '\\u008D', '\\u008E', '\\u008F', 36 | '\\u0090', '\\u0091', '\\u0092', '\\u0093', '\\u0094', '\\u0095', '\\u0096', '\\u0097', 37 | '\\u0098', '\\u0099', '\\u009A', '\\u009B', '\\u009C', '\\u009D', '\\u009E', '\\u009F', 38 | ]; 39 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/suggestionList-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { suggestionList } from '../suggestionList'; 5 | 6 | function expectSuggestions(input: string, options: ReadonlyArray) { 7 | return expect(suggestionList(input, options)); 8 | } 9 | 10 | describe('suggestionList', () => { 11 | it('Returns results when input is empty', () => { 12 | expectSuggestions('', ['a']).to.deep.equal(['a']); 13 | }); 14 | 15 | it('Returns empty array when there are no options', () => { 16 | expectSuggestions('input', []).to.deep.equal([]); 17 | }); 18 | 19 | it('Returns options with small lexical distance', () => { 20 | expectSuggestions('greenish', ['green']).to.deep.equal(['green']); 21 | expectSuggestions('green', ['greenish']).to.deep.equal(['greenish']); 22 | }); 23 | 24 | it('Rejects options with distance that exceeds threshold', () => { 25 | // spell-checker:disable 26 | expectSuggestions('aaaa', ['aaab']).to.deep.equal(['aaab']); 27 | expectSuggestions('aaaa', ['aabb']).to.deep.equal(['aabb']); 28 | expectSuggestions('aaaa', ['abbb']).to.deep.equal([]); 29 | // spell-checker:enable 30 | 31 | expectSuggestions('ab', ['ca']).to.deep.equal([]); 32 | }); 33 | 34 | it('Returns options with different case', () => { 35 | // cSpell:ignore verylongstring 36 | expectSuggestions('verylongstring', ['VERYLONGSTRING']).to.deep.equal([ 37 | 'VERYLONGSTRING', 38 | ]); 39 | 40 | expectSuggestions('VERYLONGSTRING', ['verylongstring']).to.deep.equal([ 41 | 'verylongstring', 42 | ]); 43 | 44 | expectSuggestions('VERYLONGSTRING', ['VeryLongString']).to.deep.equal([ 45 | 'VeryLongString', 46 | ]); 47 | }); 48 | 49 | it('Returns options with transpositions', () => { 50 | expectSuggestions('agr', ['arg']).to.deep.equal(['arg']); 51 | expectSuggestions('214365879', ['123456789']).to.deep.equal(['123456789']); 52 | }); 53 | 54 | it('Returns options sorted based on lexical distance', () => { 55 | expectSuggestions('abc', ['a', 'ab', 'abc']).to.deep.equal([ 56 | 'abc', 57 | 'ab', 58 | 'a', 59 | ]); 60 | }); 61 | 62 | it('Returns options with the same lexical distance sorted lexicographically', () => { 63 | expectSuggestions('a', ['az', 'ax', 'ay']).to.deep.equal([ 64 | 'ax', 65 | 'ay', 66 | 'az', 67 | ]); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /website/docs/tutorials/running-an-express-graphql-server.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Running an Express GraphQL Server 3 | sidebar_label: Running Express + GraphQL 4 | --- 5 | 6 | The simplest way to run a GraphQL API server is to use [Express](https://expressjs.com), a popular web application framework for Node.js. You will need to install two additional dependencies: 7 | 8 | ```bash 9 | npm install express express-graphql graphql --save 10 | ``` 11 | 12 | Let's modify our “hello world” example so that it's an API server rather than a script that runs a single query. We can use the 'express' module to run a webserver, and instead of executing a query directly with the `graphql` function, we can use the `express-graphql` library to mount a GraphQL API server on the “/graphql” HTTP endpoint: 13 | 14 | ```js 15 | var express = require('express'); 16 | var { graphqlHTTP } = require('express-graphql'); 17 | var { buildSchema } = require('graphql'); 18 | 19 | // Construct a schema, using GraphQL schema language 20 | var schema = buildSchema(` 21 | type Query { 22 | hello: String 23 | } 24 | `); 25 | 26 | // The root provides a resolver function for each API endpoint 27 | var root = { 28 | hello: () => { 29 | return 'Hello world!'; 30 | }, 31 | }; 32 | 33 | var app = express(); 34 | app.use( 35 | '/graphql', 36 | graphqlHTTP({ 37 | schema: schema, 38 | rootValue: root, 39 | graphiql: true, 40 | }), 41 | ); 42 | app.listen(4000, () => { 43 | console.log('Running a GraphQL API server at localhost:4000/graphql'); 44 | }); 45 | ``` 46 | 47 | You can run this GraphQL server with: 48 | 49 | ```bash 50 | node server.js 51 | ``` 52 | 53 | Since we configured `graphqlHTTP` with `graphiql: true`, you can use the GraphiQL tool to manually issue GraphQL queries. If you navigate in a web browser to `http://localhost:4000/graphql`, you should see an interface that lets you enter queries. It should look like: 54 | 55 | ![hello world graphql example](./hello.png) 56 | 57 | This screen shot shows the GraphQL query `{ hello }` being issued and giving a result of `{ data: { hello: 'Hello world!' } }`. GraphiQL is a great tool for debugging and inspecting a server, so we recommend running it whenever your application is in development mode. 58 | 59 | At this point you have learned how to run a GraphQL server and how to use GraphiQL interface to issue queries. The next step is to learn how to [issue GraphQL queries from client code](./graphql-clients.md). 60 | -------------------------------------------------------------------------------- /website/docs/tutorials/authentication-and-express-middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authentication and Express Middleware 3 | sidebar_label: Authentication & Middleware 4 | --- 5 | 6 | It's simple to use any Express middleware in conjunction with `express-graphql`. In particular, this is a great pattern for handling authentication. 7 | 8 | To use middleware with a GraphQL resolver, just use the middleware like you would with a normal Express app. The `request` object is then available as the second argument in any resolver. 9 | 10 | For example, let's say we wanted our server to log the IP address of every request, and we also want to write an API that returns the IP address of the caller. We can do the former with middleware, and the latter by accessing the `request` object in a resolver. Here's server code that implements this: 11 | 12 | ```js 13 | var express = require('express'); 14 | var { graphqlHTTP } = require('express-graphql'); 15 | var { buildSchema } = require('graphql'); 16 | 17 | var schema = buildSchema(` 18 | type Query { 19 | ip: String 20 | } 21 | `); 22 | 23 | function loggingMiddleware(req, res, next) { 24 | console.log('ip:', req.ip); 25 | next(); 26 | } 27 | 28 | var root = { 29 | ip: function (args, request) { 30 | return request.ip; 31 | }, 32 | }; 33 | 34 | var app = express(); 35 | app.use(loggingMiddleware); 36 | app.use( 37 | '/graphql', 38 | graphqlHTTP({ 39 | schema: schema, 40 | rootValue: root, 41 | graphiql: true, 42 | }), 43 | ); 44 | app.listen(4000, () => { 45 | console.log('Running a GraphQL API server at localhost:4000/graphql'); 46 | }); 47 | ``` 48 | 49 | In a REST API, authentication is often handled with a header, that contains an auth token which proves what user is making this request. Express middleware processes these headers and puts authentication data on the Express `request` object. Some middleware modules that handle authentication like this are [Passport](http://passportjs.org/), [express-jwt](https://github.com/auth0/express-jwt), and [express-session](https://github.com/expressjs/session). Each of these modules works with `express-graphql`. 50 | 51 | If you aren't familiar with any of these authentication mechanisms, we recommend using `express-jwt` because it's simple without sacrificing any future flexibility. 52 | 53 | If you've read through the docs linearly to get to this point, congratulations! You now know everything you need to build a practical GraphQL API server. 54 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/instanceOf-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { instanceOf } from '../instanceOf'; 5 | 6 | describe('instanceOf', () => { 7 | it('do not throw on values without prototype', () => { 8 | class Foo { 9 | get [Symbol.toStringTag]() { 10 | return 'Foo'; 11 | } 12 | } 13 | 14 | expect(instanceOf(true, Foo)).to.equal(false); 15 | expect(instanceOf(null, Foo)).to.equal(false); 16 | expect(instanceOf(Object.create(null), Foo)).to.equal(false); 17 | }); 18 | 19 | it('detect name clashes with older versions of this lib', () => { 20 | function oldVersion() { 21 | class Foo {} 22 | return Foo; 23 | } 24 | 25 | function newVersion() { 26 | class Foo { 27 | get [Symbol.toStringTag]() { 28 | return 'Foo'; 29 | } 30 | } 31 | return Foo; 32 | } 33 | 34 | const NewClass = newVersion(); 35 | const OldClass = oldVersion(); 36 | expect(instanceOf(new NewClass(), NewClass)).to.equal(true); 37 | expect(() => instanceOf(new OldClass(), NewClass)).to.throw(); 38 | }); 39 | 40 | it('allows instances to have share the same constructor name', () => { 41 | function getMinifiedClass(tag: string) { 42 | class SomeNameAfterMinification { 43 | get [Symbol.toStringTag]() { 44 | return tag; 45 | } 46 | } 47 | return SomeNameAfterMinification; 48 | } 49 | 50 | const Foo = getMinifiedClass('Foo'); 51 | const Bar = getMinifiedClass('Bar'); 52 | expect(instanceOf(new Foo(), Bar)).to.equal(false); 53 | expect(instanceOf(new Bar(), Foo)).to.equal(false); 54 | 55 | const DuplicateOfFoo = getMinifiedClass('Foo'); 56 | expect(() => instanceOf(new DuplicateOfFoo(), Foo)).to.throw(); 57 | expect(() => instanceOf(new Foo(), DuplicateOfFoo)).to.throw(); 58 | }); 59 | 60 | it('fails with descriptive error message', () => { 61 | function getFoo() { 62 | class Foo { 63 | get [Symbol.toStringTag]() { 64 | return 'Foo'; 65 | } 66 | } 67 | return Foo; 68 | } 69 | const Foo1 = getFoo(); 70 | const Foo2 = getFoo(); 71 | 72 | expect(() => instanceOf(new Foo1(), Foo2)).to.throw( 73 | /^Cannot use Foo "{}" from another module or realm./m, 74 | ); 75 | expect(() => instanceOf(new Foo2(), Foo1)).to.throw( 76 | /^Cannot use Foo "{}" from another module or realm./m, 77 | ); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/jsutils/instanceOf.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from './inspect'; 2 | 3 | /** 4 | * A replacement for instanceof which includes an error warning when multi-realm 5 | * constructors are detected. 6 | * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production 7 | * See: https://webpack.js.org/guides/production/ 8 | */ 9 | export const instanceOf: (value: unknown, constructor: Constructor) => boolean = 10 | /* c8 ignore next 6 */ 11 | // FIXME: https://github.com/graphql/graphql-js/issues/2317 12 | globalThis.process?.env.NODE_ENV === 'production' 13 | ? function instanceOf(value: unknown, constructor: Constructor): boolean { 14 | return value instanceof constructor; 15 | } 16 | : function instanceOf(value: unknown, constructor: Constructor): boolean { 17 | if (value instanceof constructor) { 18 | return true; 19 | } 20 | if (typeof value === 'object' && value !== null) { 21 | // Prefer Symbol.toStringTag since it is immune to minification. 22 | const className = constructor.prototype[Symbol.toStringTag]; 23 | const valueClassName = 24 | // We still need to support constructor's name to detect conflicts with older versions of this library. 25 | Symbol.toStringTag in value 26 | ? // @ts-expect-error TS bug see, https://github.com/microsoft/TypeScript/issues/38009 27 | value[Symbol.toStringTag] 28 | : value.constructor?.name; 29 | if (className === valueClassName) { 30 | const stringifiedValue = inspect(value); 31 | throw new Error( 32 | `Cannot use ${className} "${stringifiedValue}" from another module or realm. 33 | 34 | Ensure that there is only one instance of "graphql" in the node_modules 35 | directory. If different versions of "graphql" are the dependencies of other 36 | relied on modules, use "resolutions" to ensure only one version is installed. 37 | 38 | https://yarnpkg.com/en/docs/selective-version-resolutions 39 | 40 | Duplicate "graphql" modules cannot be used at the same time since different 41 | versions may have different capabilities and behavior. The data from one 42 | version used in the function from another could produce confusing and 43 | spurious results.`, 44 | ); 45 | } 46 | } 47 | return false; 48 | }; 49 | 50 | interface Constructor extends Function { 51 | prototype: { 52 | [Symbol.toStringTag]: string; 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /integrationTests/ts/TypedQueryDocumentNode-test.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionResult } from 'graphql/execution'; 2 | import type { TypedQueryDocumentNode } from 'graphql/utilities'; 3 | 4 | import { parse } from 'graphql/language'; 5 | import { execute } from 'graphql/execution'; 6 | import { buildSchema } from 'graphql/utilities'; 7 | 8 | const schema = buildSchema(` 9 | type Query { 10 | test: String 11 | } 12 | `); 13 | 14 | // Tests for TS specific TypedQueryDocumentNode type 15 | const queryDocument = parse('{ test }'); 16 | 17 | type ResponseData = { test: string }; 18 | const typedQueryDocument = queryDocument as TypedQueryDocumentNode< 19 | ResponseData, 20 | {} 21 | >; 22 | 23 | // Supports conversion to DocumentNode 24 | execute({ schema, document: typedQueryDocument }); 25 | 26 | function wrappedExecute(document: TypedQueryDocumentNode) { 27 | return execute({ schema, document }) as ExecutionResult; 28 | } 29 | 30 | const response = wrappedExecute(typedQueryDocument); 31 | const responseData: ResponseData | undefined | null = response.data; 32 | 33 | declare function runQueryA( 34 | q: TypedQueryDocumentNode<{ output: string }, { input: string | null }>, 35 | ): void; 36 | 37 | // valid 38 | declare const optionalInputRequiredOutput: TypedQueryDocumentNode< 39 | { output: string }, 40 | { input: string | null } 41 | >; 42 | runQueryA(optionalInputRequiredOutput); 43 | 44 | declare function runQueryB( 45 | q: TypedQueryDocumentNode<{ output: string | null }, { input: string }>, 46 | ): void; 47 | 48 | // still valid: We still accept {output: string} as a valid result. 49 | // We're now passing in {input: string} which is still assignable to {input: string | null} 50 | runQueryB(optionalInputRequiredOutput); 51 | 52 | // valid: we now accept {output: null} as a valid Result 53 | declare const optionalInputOptionalOutput: TypedQueryDocumentNode< 54 | { output: string | null }, 55 | { input: string | null } 56 | >; 57 | runQueryB(optionalInputOptionalOutput); 58 | 59 | // valid: we now only pass {input: string} to the query 60 | declare const requiredInputRequiredOutput: TypedQueryDocumentNode< 61 | { output: string }, 62 | { input: string } 63 | >; 64 | runQueryB(requiredInputRequiredOutput); 65 | 66 | // valid: we now accept {output: null} as a valid Result AND 67 | // we now only pass {input: string} to the query 68 | declare const requiredInputOptionalOutput: TypedQueryDocumentNode< 69 | { output: null }, 70 | { input: string } 71 | >; 72 | runQueryB(requiredInputOptionalOutput); 73 | -------------------------------------------------------------------------------- /src/utilities/__tests__/valueFromASTUntyped-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import type { Maybe } from '../../jsutils/Maybe'; 5 | import type { ObjMap } from '../../jsutils/ObjMap'; 6 | 7 | import { parseValue } from '../../language/parser'; 8 | 9 | import { valueFromASTUntyped } from '../valueFromASTUntyped'; 10 | 11 | describe('valueFromASTUntyped', () => { 12 | function expectValueFrom( 13 | valueText: string, 14 | variables?: Maybe>, 15 | ) { 16 | const ast = parseValue(valueText); 17 | const value = valueFromASTUntyped(ast, variables); 18 | return expect(value); 19 | } 20 | 21 | it('parses simple values', () => { 22 | expectValueFrom('null').to.equal(null); 23 | expectValueFrom('true').to.equal(true); 24 | expectValueFrom('false').to.equal(false); 25 | expectValueFrom('123').to.equal(123); 26 | expectValueFrom('123.456').to.equal(123.456); 27 | expectValueFrom('"abc123"').to.equal('abc123'); 28 | }); 29 | 30 | it('parses lists of values', () => { 31 | expectValueFrom('[true, false]').to.deep.equal([true, false]); 32 | expectValueFrom('[true, 123.45]').to.deep.equal([true, 123.45]); 33 | expectValueFrom('[true, null]').to.deep.equal([true, null]); 34 | expectValueFrom('[true, ["foo", 1.2]]').to.deep.equal([true, ['foo', 1.2]]); 35 | }); 36 | 37 | it('parses input objects', () => { 38 | expectValueFrom('{ int: 123, bool: false }').to.deep.equal({ 39 | int: 123, 40 | bool: false, 41 | }); 42 | expectValueFrom('{ foo: [ { bar: "baz"} ] }').to.deep.equal({ 43 | foo: [{ bar: 'baz' }], 44 | }); 45 | }); 46 | 47 | it('parses enum values as plain strings', () => { 48 | expectValueFrom('TEST_ENUM_VALUE').to.equal('TEST_ENUM_VALUE'); 49 | expectValueFrom('[TEST_ENUM_VALUE]').to.deep.equal(['TEST_ENUM_VALUE']); 50 | }); 51 | 52 | it('parses variables', () => { 53 | expectValueFrom('$testVariable', { testVariable: 'foo' }).to.equal('foo'); 54 | expectValueFrom('[$testVariable]', { testVariable: 'foo' }).to.deep.equal([ 55 | 'foo', 56 | ]); 57 | expectValueFrom('{a:[$testVariable]}', { 58 | testVariable: 'foo', 59 | }).to.deep.equal({ a: ['foo'] }); 60 | expectValueFrom('$testVariable', { testVariable: null }).to.equal(null); 61 | expectValueFrom('$testVariable', { testVariable: NaN }).to.satisfy( 62 | Number.isNaN, 63 | ); 64 | expectValueFrom('$testVariable', {}).to.equal(undefined); 65 | expectValueFrom('$testVariable', null).to.equal(undefined); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/validation/__tests__/LoneAnonymousOperationRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { LoneAnonymousOperationRule } from '../rules/LoneAnonymousOperationRule'; 4 | 5 | import { expectValidationErrors } from './harness'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(LoneAnonymousOperationRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Anonymous operation must be alone', () => { 16 | it('no operations', () => { 17 | expectValid(` 18 | fragment fragA on Type { 19 | field 20 | } 21 | `); 22 | }); 23 | 24 | it('one anon operation', () => { 25 | expectValid(` 26 | { 27 | field 28 | } 29 | `); 30 | }); 31 | 32 | it('multiple named operations', () => { 33 | expectValid(` 34 | query Foo { 35 | field 36 | } 37 | 38 | query Bar { 39 | field 40 | } 41 | `); 42 | }); 43 | 44 | it('anon operation with fragment', () => { 45 | expectValid(` 46 | { 47 | ...Foo 48 | } 49 | fragment Foo on Type { 50 | field 51 | } 52 | `); 53 | }); 54 | 55 | it('multiple anon operations', () => { 56 | expectErrors(` 57 | { 58 | fieldA 59 | } 60 | { 61 | fieldB 62 | } 63 | `).toDeepEqual([ 64 | { 65 | message: 'This anonymous operation must be the only defined operation.', 66 | locations: [{ line: 2, column: 7 }], 67 | }, 68 | { 69 | message: 'This anonymous operation must be the only defined operation.', 70 | locations: [{ line: 5, column: 7 }], 71 | }, 72 | ]); 73 | }); 74 | 75 | it('anon operation with a mutation', () => { 76 | expectErrors(` 77 | { 78 | fieldA 79 | } 80 | mutation Foo { 81 | fieldB 82 | } 83 | `).toDeepEqual([ 84 | { 85 | message: 'This anonymous operation must be the only defined operation.', 86 | locations: [{ line: 2, column: 7 }], 87 | }, 88 | ]); 89 | }); 90 | 91 | it('anon operation with a subscription', () => { 92 | expectErrors(` 93 | { 94 | fieldA 95 | } 96 | subscription Foo { 97 | fieldB 98 | } 99 | `).toDeepEqual([ 100 | { 101 | message: 'This anonymous operation must be the only defined operation.', 102 | locations: [{ line: 2, column: 7 }], 103 | }, 104 | ]); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/isIterableObject-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc'; 5 | import { isIterableObject } from '../isIterableObject'; 6 | 7 | describe('isIterableObject', () => { 8 | it('should return `true` for collections', () => { 9 | expect(isIterableObject([])).to.equal(true); 10 | expect(isIterableObject(new Int8Array(1))).to.equal(true); 11 | 12 | // eslint-disable-next-line no-new-wrappers 13 | expect(isIterableObject(new String('ABC'))).to.equal(true); 14 | 15 | function getArguments() { 16 | return arguments; 17 | } 18 | expect(isIterableObject(getArguments())).to.equal(true); 19 | 20 | const iterable = { [Symbol.iterator]: identityFunc }; 21 | expect(isIterableObject(iterable)).to.equal(true); 22 | 23 | function* generatorFunc() { 24 | /* do nothing */ 25 | } 26 | expect(isIterableObject(generatorFunc())).to.equal(true); 27 | 28 | // But generator function itself is not iterable 29 | expect(isIterableObject(generatorFunc)).to.equal(false); 30 | }); 31 | 32 | it('should return `false` for non-collections', () => { 33 | expect(isIterableObject(null)).to.equal(false); 34 | expect(isIterableObject(undefined)).to.equal(false); 35 | 36 | expect(isIterableObject('ABC')).to.equal(false); 37 | expect(isIterableObject('0')).to.equal(false); 38 | expect(isIterableObject('')).to.equal(false); 39 | 40 | expect(isIterableObject(1)).to.equal(false); 41 | expect(isIterableObject(0)).to.equal(false); 42 | expect(isIterableObject(NaN)).to.equal(false); 43 | // eslint-disable-next-line no-new-wrappers 44 | expect(isIterableObject(new Number(123))).to.equal(false); 45 | 46 | expect(isIterableObject(true)).to.equal(false); 47 | expect(isIterableObject(false)).to.equal(false); 48 | // eslint-disable-next-line no-new-wrappers 49 | expect(isIterableObject(new Boolean(true))).to.equal(false); 50 | 51 | expect(isIterableObject({})).to.equal(false); 52 | expect(isIterableObject({ iterable: true })).to.equal(false); 53 | 54 | const iteratorWithoutSymbol = { next: identityFunc }; 55 | expect(isIterableObject(iteratorWithoutSymbol)).to.equal(false); 56 | 57 | const invalidIterable = { 58 | [Symbol.iterator]: { next: identityFunc }, 59 | }; 60 | expect(isIterableObject(invalidIterable)).to.equal(false); 61 | 62 | const arrayLike: { [key: string]: unknown } = {}; 63 | arrayLike[0] = 'Alpha'; 64 | arrayLike[1] = 'Bravo'; 65 | arrayLike[2] = 'Charlie'; 66 | arrayLike.length = 3; 67 | 68 | expect(isIterableObject(arrayLike)).to.equal(false); 69 | }); 70 | }); 71 | --------------------------------------------------------------------------------