├── .c8rc.json ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── workflows │ ├── ci.yml │ ├── cmd-publish-pr-on-npm.yml │ ├── cmd-run-benchmark.yml │ ├── deploy-artifact-as-branch.yml │ ├── github-actions-bot.yml │ ├── pull_request.yml │ ├── pull_request_opened.yml │ └── push.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.json ├── .mocharc.yml ├── .node-version ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── benchmark ├── GraphQLSchema-benchmark.js ├── buildASTSchema-benchmark.js ├── buildClientSchema-benchmark.js ├── fixtures.js ├── github-schema.graphql ├── github-schema.json ├── introspectionFromSchema-benchmark.js ├── list-async-benchmark.js ├── list-asyncIterable-benchmark.js ├── list-sync-benchmark.js ├── parser-benchmark.js ├── repeated-fields-benchmark.js ├── validateGQL-benchmark.js ├── validateInvalidGQL-benchmark.js ├── validateSDL-benchmark.js ├── visit-benchmark.js └── visitInParallel-benchmark.js ├── cspell.yml ├── eslint.config.mjs ├── integrationTests ├── README.md ├── node │ ├── index.cjs │ ├── index.mjs │ ├── package.json │ └── test.js ├── ts │ ├── TypedQueryDocumentNode-test.ts │ ├── basic-test.ts │ ├── esm.ts │ ├── extensions-test.ts │ ├── internalImports-test.ts │ ├── kitchenSink-test.ts │ ├── package.json │ ├── test.js │ └── tsconfig.json └── webpack │ ├── entry-esm.mjs │ ├── entry.js │ ├── package.json │ ├── test.js │ └── webpack.config.json ├── package-lock.json ├── package.json ├── resources ├── benchmark.ts ├── build-deno.ts ├── build-npm.ts ├── change-extension-in-import-paths.ts ├── checkgit.sh ├── diff-npm-package.ts ├── eslint-internal-rules │ ├── index.js │ ├── no-dir-import.js │ ├── only-ascii.js │ └── require-to-string-tag.js ├── gen-changelog.ts ├── gen-version.ts ├── inline-invariant.ts ├── integration-test.ts └── utils.ts ├── src ├── README.md ├── __testUtils__ │ ├── __tests__ │ │ ├── dedent-test.ts │ │ ├── expectEqualPromisesOrValues-test.ts │ │ ├── expectMatchingValues-test.ts │ │ ├── expectPromise-test.ts │ │ ├── genFuzzStrings-test.ts │ │ ├── inspectStr-test.ts │ │ └── resolveOnNextTick-test.ts │ ├── dedent.ts │ ├── expectEqualPromisesOrValues.ts │ ├── expectJSON.ts │ ├── expectMatchingValues.ts │ ├── expectPromise.ts │ ├── genFuzzStrings.ts │ ├── inspectStr.ts │ ├── kitchenSinkQuery.ts │ ├── kitchenSinkSDL.ts │ ├── resolveOnNextTick.ts │ ├── viralSDL.ts │ └── viralSchema.ts ├── __tests__ │ ├── starWarsData.ts │ ├── starWarsIntrospection-test.ts │ ├── starWarsQuery-test.ts │ ├── starWarsSchema.ts │ ├── starWarsValidation-test.ts │ └── version-test.ts ├── error │ ├── GraphQLError.ts │ ├── README.md │ ├── __tests__ │ │ ├── GraphQLError-test.ts │ │ └── locatedError-test.ts │ ├── index.ts │ ├── locatedError.ts │ └── syntaxError.ts ├── execution │ ├── AbortSignalListener.ts │ ├── IncrementalGraph.ts │ ├── IncrementalPublisher.ts │ ├── README.md │ ├── __tests__ │ │ ├── AbortSignalListener-test.ts │ │ ├── abstract-test.ts │ │ ├── cancellation-test.ts │ │ ├── defer-test.ts │ │ ├── directives-test.ts │ │ ├── errorPropagation-test.ts │ │ ├── executor-test.ts │ │ ├── lists-test.ts │ │ ├── mapAsyncIterable-test.ts │ │ ├── mutations-test.ts │ │ ├── nonnull-test.ts │ │ ├── oneof-test.ts │ │ ├── resolve-test.ts │ │ ├── schema-test.ts │ │ ├── simplePubSub-test.ts │ │ ├── simplePubSub.ts │ │ ├── stream-test.ts │ │ ├── subscribe-test.ts │ │ ├── sync-test.ts │ │ ├── union-interface-test.ts │ │ └── variables-test.ts │ ├── buildExecutionPlan.ts │ ├── collectFields.ts │ ├── execute.ts │ ├── getVariableSignature.ts │ ├── index.ts │ ├── mapAsyncIterable.ts │ ├── types.ts │ └── values.ts ├── graphql.ts ├── index.ts ├── jsutils │ ├── AccumulatorMap.ts │ ├── BoxedPromiseOrValue.ts │ ├── Maybe.ts │ ├── ObjMap.ts │ ├── Path.ts │ ├── PromiseOrValue.ts │ ├── README.md │ ├── __tests__ │ │ ├── AccumulatorMap-test.ts │ │ ├── BoxedPromiseOrValue-test.ts │ │ ├── Path-test.ts │ │ ├── capitalize-test.ts │ │ ├── didYouMean-test.ts │ │ ├── identityFunc-test.ts │ │ ├── inspect-test.ts │ │ ├── instanceOf-test.ts │ │ ├── invariant-test.ts │ │ ├── isAsyncIterable-test.ts │ │ ├── isIterableObject-test.ts │ │ ├── isObjectLike-test.ts │ │ ├── naturalCompare-test.ts │ │ ├── promiseWithResolvers-test.ts │ │ ├── suggestionList-test.ts │ │ └── toObjMap-test.ts │ ├── capitalize.ts │ ├── devAssert.ts │ ├── didYouMean.ts │ ├── formatList.ts │ ├── getBySet.ts │ ├── groupBy.ts │ ├── identityFunc.ts │ ├── inspect.ts │ ├── instanceOf.ts │ ├── invariant.ts │ ├── isAsyncIterable.ts │ ├── isIterableObject.ts │ ├── isObjectLike.ts │ ├── isPromise.ts │ ├── isSameSet.ts │ ├── keyMap.ts │ ├── keyValMap.ts │ ├── mapValue.ts │ ├── memoize3.ts │ ├── naturalCompare.ts │ ├── printPathArray.ts │ ├── promiseForObject.ts │ ├── promiseReduce.ts │ ├── promiseWithResolvers.ts │ ├── suggestionList.ts │ ├── toError.ts │ └── toObjMap.ts ├── language │ ├── README.md │ ├── __tests__ │ │ ├── blockString-fuzz.ts │ │ ├── blockString-test.ts │ │ ├── kind-test.ts │ │ ├── lexer-test.ts │ │ ├── parser-test.ts │ │ ├── predicates-test.ts │ │ ├── printLocation-test.ts │ │ ├── printString-test.ts │ │ ├── printer-test.ts │ │ ├── schema-parser-test.ts │ │ ├── schema-printer-test.ts │ │ ├── source-test.ts │ │ └── visitor-test.ts │ ├── ast.ts │ ├── blockString.ts │ ├── characterClasses.ts │ ├── directiveLocation.ts │ ├── index.ts │ ├── kinds.ts │ ├── kinds_.ts │ ├── lexer.ts │ ├── location.ts │ ├── parser.ts │ ├── predicates.ts │ ├── printLocation.ts │ ├── printString.ts │ ├── printer.ts │ ├── source.ts │ ├── tokenKind.ts │ └── visitor.ts ├── type │ ├── README.md │ ├── __tests__ │ │ ├── assertName-test.ts │ │ ├── definition-test.ts │ │ ├── directive-test.ts │ │ ├── enumType-test.ts │ │ ├── extensions-test.ts │ │ ├── introspection-test.ts │ │ ├── predicate-test.ts │ │ ├── scalars-test.ts │ │ ├── schema-test.ts │ │ └── validation-test.ts │ ├── assertName.ts │ ├── definition.ts │ ├── directives.ts │ ├── index.ts │ ├── introspection.ts │ ├── scalars.ts │ ├── schema.ts │ └── validate.ts ├── utilities │ ├── README.md │ ├── TypeInfo.ts │ ├── __tests__ │ │ ├── TypeInfo-test.ts │ │ ├── astFromValue-test.ts │ │ ├── buildASTSchema-test.ts │ │ ├── buildClientSchema-test.ts │ │ ├── coerceInputValue-test.ts │ │ ├── concatAST-test.ts │ │ ├── extendSchema-test.ts │ │ ├── findBreakingChanges-test.ts │ │ ├── findSchemaChanges-test.ts │ │ ├── getIntrospectionQuery-test.ts │ │ ├── getOperationAST-test.ts │ │ ├── introspectionFromSchema-test.ts │ │ ├── lexicographicSortSchema-test.ts │ │ ├── mapSchemaConfig-test.ts │ │ ├── printSchema-test.ts │ │ ├── replaceVariables-test.ts │ │ ├── separateOperations-test.ts │ │ ├── sortValueNode-test.ts │ │ ├── stripIgnoredCharacters-fuzz.ts │ │ ├── stripIgnoredCharacters-test.ts │ │ ├── typeComparators-test.ts │ │ ├── validateInputValue-test.ts │ │ ├── valueFromAST-test.ts │ │ ├── valueFromASTUntyped-test.ts │ │ └── valueToLiteral-test.ts │ ├── astFromValue.ts │ ├── buildASTSchema.ts │ ├── buildClientSchema.ts │ ├── coerceInputValue.ts │ ├── concatAST.ts │ ├── extendSchema.ts │ ├── findSchemaChanges.ts │ ├── getDefaultValueAST.ts │ ├── getIntrospectionQuery.ts │ ├── getOperationAST.ts │ ├── index.ts │ ├── introspectionFromSchema.ts │ ├── lexicographicSortSchema.ts │ ├── mapSchemaConfig.ts │ ├── printSchema.ts │ ├── replaceVariables.ts │ ├── separateOperations.ts │ ├── sortValueNode.ts │ ├── stripIgnoredCharacters.ts │ ├── typeComparators.ts │ ├── typeFromAST.ts │ ├── typedQueryDocumentNode.ts │ ├── validateInputValue.ts │ ├── valueFromAST.ts │ ├── valueFromASTUntyped.ts │ └── valueToLiteral.ts ├── validation │ ├── README.md │ ├── ValidationContext.ts │ ├── __tests__ │ │ ├── DeferStreamDirectiveLabelRule-test.ts │ │ ├── DeferStreamDirectiveOnRootFieldRule-test.ts │ │ ├── DeferStreamDirectiveOnValidOperationsRule-test.ts │ │ ├── ExecutableDefinitionsRule-test.ts │ │ ├── FieldsOnCorrectTypeRule-test.ts │ │ ├── FragmentsOnCompositeTypesRule-test.ts │ │ ├── KnownArgumentNamesRule-test.ts │ │ ├── KnownDirectivesRule-test.ts │ │ ├── KnownFragmentNamesRule-test.ts │ │ ├── KnownOperationTypesRules-test.ts │ │ ├── KnownTypeNamesRule-test.ts │ │ ├── LoneAnonymousOperationRule-test.ts │ │ ├── LoneSchemaDefinitionRule-test.ts │ │ ├── MaxIntrospectionDepthRule-test.ts │ │ ├── NoDeprecatedCustomRule-test.ts │ │ ├── NoFragmentCyclesRule-test.ts │ │ ├── NoSchemaIntrospectionCustomRule-test.ts │ │ ├── NoUndefinedVariablesRule-test.ts │ │ ├── NoUnusedFragmentsRule-test.ts │ │ ├── NoUnusedVariablesRule-test.ts │ │ ├── OverlappingFieldsCanBeMergedRule-test.ts │ │ ├── PossibleFragmentSpreadsRule-test.ts │ │ ├── PossibleTypeExtensionsRule-test.ts │ │ ├── ProvidedRequiredArgumentsRule-test.ts │ │ ├── ScalarLeafsRule-test.ts │ │ ├── SingleFieldSubscriptionsRule-test.ts │ │ ├── StreamDirectiveOnListFieldRule-test.ts │ │ ├── UniqueArgumentDefinitionNamesRule-test.ts │ │ ├── UniqueArgumentNamesRule-test.ts │ │ ├── UniqueDirectiveNamesRule-test.ts │ │ ├── UniqueDirectivesPerLocationRule-test.ts │ │ ├── UniqueEnumValueNamesRule-test.ts │ │ ├── UniqueFieldDefinitionNamesRule-test.ts │ │ ├── UniqueFragmentNamesRule-test.ts │ │ ├── UniqueInputFieldNamesRule-test.ts │ │ ├── UniqueOperationNamesRule-test.ts │ │ ├── UniqueOperationTypesRule-test.ts │ │ ├── UniqueTypeNamesRule-test.ts │ │ ├── UniqueVariableNamesRule-test.ts │ │ ├── ValidationContext-test.ts │ │ ├── ValuesOfCorrectTypeRule-test.ts │ │ ├── VariablesAreInputTypesRule-test.ts │ │ ├── VariablesInAllowedPositionRule-test.ts │ │ ├── harness.ts │ │ └── validation-test.ts │ ├── index.ts │ ├── rules │ │ ├── DeferStreamDirectiveLabelRule.ts │ │ ├── DeferStreamDirectiveOnRootFieldRule.ts │ │ ├── DeferStreamDirectiveOnValidOperationsRule.ts │ │ ├── ExecutableDefinitionsRule.ts │ │ ├── FieldsOnCorrectTypeRule.ts │ │ ├── FragmentsOnCompositeTypesRule.ts │ │ ├── KnownArgumentNamesRule.ts │ │ ├── KnownDirectivesRule.ts │ │ ├── KnownFragmentNamesRule.ts │ │ ├── KnownOperationTypesRule.ts │ │ ├── KnownTypeNamesRule.ts │ │ ├── LoneAnonymousOperationRule.ts │ │ ├── LoneSchemaDefinitionRule.ts │ │ ├── MaxIntrospectionDepthRule.ts │ │ ├── NoFragmentCyclesRule.ts │ │ ├── NoUndefinedVariablesRule.ts │ │ ├── NoUnusedFragmentsRule.ts │ │ ├── NoUnusedVariablesRule.ts │ │ ├── OverlappingFieldsCanBeMergedRule.ts │ │ ├── PossibleFragmentSpreadsRule.ts │ │ ├── PossibleTypeExtensionsRule.ts │ │ ├── ProvidedRequiredArgumentsRule.ts │ │ ├── ScalarLeafsRule.ts │ │ ├── SingleFieldSubscriptionsRule.ts │ │ ├── StreamDirectiveOnListFieldRule.ts │ │ ├── UniqueArgumentDefinitionNamesRule.ts │ │ ├── UniqueArgumentNamesRule.ts │ │ ├── UniqueDirectiveNamesRule.ts │ │ ├── UniqueDirectivesPerLocationRule.ts │ │ ├── UniqueEnumValueNamesRule.ts │ │ ├── UniqueFieldDefinitionNamesRule.ts │ │ ├── UniqueFragmentNamesRule.ts │ │ ├── UniqueInputFieldNamesRule.ts │ │ ├── UniqueOperationNamesRule.ts │ │ ├── UniqueOperationTypesRule.ts │ │ ├── UniqueTypeNamesRule.ts │ │ ├── UniqueVariableNamesRule.ts │ │ ├── ValuesOfCorrectTypeRule.ts │ │ ├── VariablesAreInputTypesRule.ts │ │ ├── VariablesInAllowedPositionRule.ts │ │ └── custom │ │ │ ├── NoDeprecatedCustomRule.ts │ │ │ └── NoSchemaIntrospectionCustomRule.ts │ ├── specifiedRules.ts │ └── validate.ts └── version.ts ├── tsconfig.json └── website ├── css └── globals.css ├── docs └── tutorials │ └── defer-stream.md ├── icons ├── discord.svg ├── github.svg ├── graphql-wordmark.svg ├── graphql.svg ├── index.ts ├── stackoverflow.svg └── twitter.svg ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── _meta.ts ├── api-v16 │ ├── _meta.ts │ ├── error.mdx │ ├── execution.mdx │ ├── graphql-http.mdx │ ├── graphql.mdx │ ├── language.mdx │ ├── type.mdx │ ├── utilities.mdx │ └── validation.mdx ├── authentication-and-express-middleware.mdx ├── basic-types.mdx ├── constructing-types.mdx ├── defer-stream.mdx ├── getting-started.mdx ├── going-to-production.mdx ├── graphql-clients.mdx ├── index.mdx ├── mutations-and-input-types.mdx ├── object-types.mdx ├── oneof-input-objects.mdx ├── passing-arguments.mdx └── running-an-express-graphql-server.mdx ├── postcss.config.js ├── tailwind.config.js ├── theme.config.tsx └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @graphql/graphql-js-reviewers 2 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | permissions: {} 22 | jobs: 23 | deploy-artifact-as-branch: 24 | environment: 25 | name: ${{ inputs.environment }} 26 | url: ${{ github.server_url }}/${{ github.repository }}/tree/${{ inputs.target_branch }} 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: write # for actions/checkout and to push branch 30 | steps: 31 | - name: Checkout `${{ inputs.target_branch }}` branch 32 | uses: actions/checkout@v4 33 | with: 34 | ref: ${{ inputs.target_branch }} 35 | 36 | - name: Remove existing files first 37 | run: git rm -r . 38 | 39 | - uses: actions/download-artifact@v4 40 | with: 41 | name: ${{ inputs.artifact_name }} 42 | 43 | - name: Publish target branch 44 | run: | 45 | git add -A 46 | if git diff --staged --quiet; then 47 | echo 'Nothing to publish' 48 | else 49 | git config user.name 'GitHub Action Script' 50 | git config user.email 'please@open.issue' 51 | 52 | git commit -a -m "$COMMIT_MESSAGE" 53 | git push 54 | echo 'Pushed' 55 | fi 56 | env: 57 | TARGET_BRANCH: ${{ inputs.target_branch }} 58 | COMMIT_MESSAGE: ${{ inputs.commit_message }} 59 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: PullRequest 2 | on: pull_request 3 | permissions: {} 4 | jobs: 5 | ci: 6 | permissions: 7 | contents: read # for actions/checkout 8 | security-events: write # for codeql-action 9 | uses: ./.github/workflows/ci.yml 10 | 11 | dependency-review: 12 | name: Security check of added dependencies 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read # for actions/checkout 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | 22 | - name: Dependency review 23 | uses: actions/dependency-review-action@v2 24 | 25 | diff-npm-package: 26 | name: Diff content of NPM package 27 | runs-on: ubuntu-latest 28 | permissions: 29 | contents: read # for actions/checkout 30 | steps: 31 | - name: Checkout repo 32 | uses: actions/checkout@v4 33 | with: 34 | persist-credentials: false 35 | 36 | - name: Deepen cloned repo 37 | env: 38 | BASE_SHA: ${{ github.event.pull_request.base.sha }} 39 | run: 'git fetch --depth=1 origin "$BASE_SHA:refs/tags/BASE"' 40 | 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v4 43 | with: 44 | cache: npm 45 | node-version-file: '.node-version' 46 | 47 | - name: Install Dependencies 48 | run: npm ci --ignore-scripts 49 | 50 | - name: Generate report 51 | run: 'npm run diff:npm BASE HEAD' 52 | 53 | - name: Upload generated report 54 | uses: actions/upload-artifact@v4 55 | with: 56 | name: npm-dist-diff.html 57 | path: ./reports/npm-dist-diff.html 58 | if-no-files-found: ignore 59 | -------------------------------------------------------------------------------- /.github/workflows/pull_request_opened.yml: -------------------------------------------------------------------------------- 1 | name: PullRequestOpened 2 | on: 3 | pull_request: 4 | types: [opened] 5 | permissions: {} 6 | jobs: 7 | save-github-event: 8 | name: "Save `github.event` as an artifact to use in subsequent 'workflow_run' actions" 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Upload event.json 12 | uses: actions/upload-artifact@v4 13 | with: 14 | name: event.json 15 | path: ${{ github.event_path }} 16 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | on: push 3 | permissions: {} 4 | jobs: 5 | ci: 6 | permissions: 7 | contents: read # for actions/checkout 8 | security-events: write 9 | uses: ./.github/workflows/ci.yml 10 | deploy-to-npm-branch: 11 | name: Deploy to `npm` branch 12 | needs: ci 13 | if: github.ref == 'refs/heads/main' 14 | permissions: 15 | contents: write # for actions/checkout and to push branch 16 | uses: ./.github/workflows/deploy-artifact-as-branch.yml 17 | with: 18 | environment: npm-branch 19 | artifact_name: npmDist 20 | target_branch: npm 21 | commit_message: "Deploy ${{github.event.workflow_run.head_sha}} to 'npm' branch" 22 | 23 | deploy-to-deno-branch: 24 | name: Deploy to `deno` branch 25 | needs: ci 26 | if: github.ref == 'refs/heads/main' 27 | permissions: 28 | contents: write # for actions/checkout and to push branch 29 | uses: ./.github/workflows/deploy-artifact-as-branch.yml 30 | with: 31 | environment: deno-branch 32 | artifact_name: denoDist 33 | target_branch: deno 34 | commit_message: "Deploy ${{github.event.workflow_run.head_sha}} to 'deno' branch" 35 | -------------------------------------------------------------------------------- /.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 | node_modules 12 | /reports 13 | /npmDist 14 | /npmEsmDist 15 | /denoDist 16 | /website/.next 17 | /website/out 18 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run precommit 2 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,ts}": ["eslint --max-warnings 0 --fix", "prettier --write"], 3 | "*.json": "prettier --write", 4 | "*.md": "prettier --write" 5 | } 6 | -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | fail-zero: true 2 | throw-deprecation: true 3 | check-leaks: true 4 | extension: 5 | - ts 6 | node-option: 7 | - 'loader=ts-node/esm/transpile-only' 8 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Copied from '.gitignore', please keep it in sync. 2 | /diff-npm-package.html 3 | /.eslintcache 4 | /node_modules 5 | /reports 6 | /npmDist 7 | /npmEsmDist 8 | /denoDist 9 | /website/.next 10 | /website/out 11 | /website/**/*.mdx 12 | .next 13 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) GraphQL Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/fixtures.js: -------------------------------------------------------------------------------- 1 | import fs from 'node: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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/list-asyncIterable-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 | async function* listField() { 9 | for (let index = 0; index < 1000; index++) { 10 | yield index; 11 | } 12 | } 13 | 14 | export const benchmark = { 15 | name: 'Execute Async Iterable List Field', 16 | count: 10, 17 | async measure() { 18 | await execute({ 19 | schema, 20 | document, 21 | rootValue: { listField }, 22 | }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /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/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/repeated-fields-benchmark.js: -------------------------------------------------------------------------------- 1 | import { graphqlSync } from 'graphql/graphql.js'; 2 | import { buildSchema } from 'graphql/utilities/buildASTSchema.js'; 3 | 4 | const schema = buildSchema('type Query { hello: String! }'); 5 | const source = `{ ${'hello '.repeat(250)}}`; 6 | 7 | export const benchmark = { 8 | name: 'Many repeated fields', 9 | count: 5, 10 | measure() { 11 | graphqlSync({ schema, source }); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/README.md: -------------------------------------------------------------------------------- 1 | # TBD 2 | -------------------------------------------------------------------------------- /integrationTests/node/index.cjs: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('node:assert'); 4 | const { readFileSync } = require('node:fs'); 5 | 6 | const { 7 | experimentalExecuteIncrementally, 8 | graphqlSync, 9 | parse, 10 | } = require('graphql'); 11 | const { buildSchema } = require('graphql/utilities'); 12 | const { version } = require('graphql/version'); 13 | 14 | assert.deepStrictEqual( 15 | version, 16 | JSON.parse(readFileSync('./node_modules/graphql/package.json')).version, 17 | ); 18 | 19 | const schema = buildSchema('type Query { hello: String }'); 20 | 21 | let result = graphqlSync({ 22 | schema, 23 | source: '{ hello }', 24 | rootValue: { hello: 'world' }, 25 | }); 26 | 27 | assert.deepStrictEqual(result, { 28 | data: { 29 | __proto__: null, 30 | hello: 'world', 31 | }, 32 | }); 33 | 34 | /** 35 | * The below test triggers a call `invariant` method during execution (by 36 | * passing a negative number to the `initialCount` parameter on the `@stream` 37 | * directive). This ensures that the `inlineInvariant` method called by our 38 | * build script works correctly. 39 | **/ 40 | 41 | const experimentalSchema = buildSchema(` 42 | directive @stream(initialCount: Int!) on FIELD 43 | 44 | type Query { 45 | greetings: [String] 46 | } 47 | `); 48 | 49 | result = experimentalExecuteIncrementally({ 50 | schema: experimentalSchema, 51 | document: parse('{ greetings @stream(initialCount: -1) }'), 52 | rootValue: { greetings: ['hi', 'hello'] }, 53 | }); 54 | 55 | assert(result.errors?.[0] !== undefined); 56 | assert(!result.errors[0].message.includes('is not defined')); 57 | -------------------------------------------------------------------------------- /integrationTests/node/index.mjs: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import { readFileSync } from 'node:fs'; 3 | 4 | import { 5 | experimentalExecuteIncrementally, 6 | graphqlSync, 7 | parse, 8 | } from 'graphql-esm'; 9 | import { buildSchema } from 'graphql-esm/utilities'; 10 | import { version } from 'graphql-esm/version'; 11 | 12 | assert.deepStrictEqual( 13 | version + '+esm', 14 | JSON.parse(readFileSync('./node_modules/graphql-esm/package.json')).version, 15 | ); 16 | 17 | const schema = buildSchema('type Query { hello: String }'); 18 | 19 | let result = graphqlSync({ 20 | schema, 21 | source: '{ hello }', 22 | rootValue: { hello: 'world' }, 23 | }); 24 | 25 | assert.deepStrictEqual(result, { 26 | data: { 27 | __proto__: null, 28 | hello: 'world', 29 | }, 30 | }); 31 | 32 | /** 33 | * The below test triggers a call `invariant` method during execution (by 34 | * passing a negative number to the `initialCount` parameter on the `@stream` 35 | * directive). This ensures that the `inlineInvariant` method called by our 36 | * build script works correctly. 37 | **/ 38 | 39 | const experimentalSchema = buildSchema(` 40 | directive @stream(initialCount: Int!) on FIELD 41 | 42 | type Query { 43 | greetings: [String] 44 | } 45 | `); 46 | 47 | result = experimentalExecuteIncrementally({ 48 | schema: experimentalSchema, 49 | document: parse('{ greetings @stream(initialCount: -1) }'), 50 | rootValue: { greetings: ['hi', 'hello'] }, 51 | }); 52 | 53 | assert(result.errors?.[0] !== undefined); 54 | assert(!result.errors[0].message.includes('is not defined')); 55 | -------------------------------------------------------------------------------- /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 | "graphql-esm": "file:../graphql-esm.tgz" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integrationTests/node/test.js: -------------------------------------------------------------------------------- 1 | import childProcess from 'node:child_process'; 2 | import fs from 'node: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 | .replaceAll('^', '') 10 | .replaceAll('>=', '') 11 | .split(' || ') 12 | .sort((a, b) => b.localeCompare(a)); 13 | 14 | for (const version of nodeVersions) { 15 | console.log(`Testing on node@${version} ...`); 16 | 17 | childProcess.execSync( 18 | `docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app node:${version}-slim node ./index.cjs`, 19 | { stdio: 'inherit' }, 20 | ); 21 | 22 | childProcess.execSync( 23 | `docker run --rm --volume "$PWD":/usr/src/app -w /usr/src/app node:${version}-slim node ./index.mjs`, 24 | { stdio: 'inherit' }, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /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 | default: { value: '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 | -------------------------------------------------------------------------------- /integrationTests/ts/esm.ts: -------------------------------------------------------------------------------- 1 | import type { ExecutionResult } from 'graphql-esm/execution'; 2 | 3 | import { graphqlSync } from 'graphql-esm'; 4 | import { 5 | GraphQLString, 6 | GraphQLSchema, 7 | GraphQLObjectType, 8 | } from 'graphql-esm/type'; 9 | 10 | const queryType: GraphQLObjectType = new GraphQLObjectType({ 11 | name: 'Query', 12 | fields: () => ({ 13 | sayHi: { 14 | type: GraphQLString, 15 | args: { 16 | who: { 17 | type: GraphQLString, 18 | default: { value: 'World' }, 19 | }, 20 | }, 21 | resolve(_root, args: { who: string }) { 22 | return 'Hello ' + args.who; 23 | }, 24 | }, 25 | }), 26 | }); 27 | 28 | const schema: GraphQLSchema = new GraphQLSchema({ query: queryType }); 29 | 30 | const result: ExecutionResult = graphqlSync({ 31 | schema, 32 | source: ` 33 | query helloWho($who: String){ 34 | test(who: $who) 35 | } 36 | `, 37 | variableValues: { who: 'Dolly' }, 38 | }); 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /integrationTests/ts/kitchenSink-test.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLScalarType } from 'graphql/type'; 2 | import { GraphQLError } from 'graphql/error'; 3 | import type { NameNode } from 'graphql/language'; 4 | import { Kind } from 'graphql/language'; 5 | 6 | // Test subset of public APIs with "exactOptionalPropertyTypes" flag enabled 7 | new GraphQLScalarType({ 8 | name: 'SomeScalar', 9 | coerceOutputValue: undefined, 10 | coerceInputValue: undefined, 11 | coerceInputLiteral: undefined, 12 | }); 13 | 14 | new GraphQLError('test', { nodes: undefined }); 15 | 16 | const nameNode: NameNode = { kind: Kind.NAME, loc: undefined, value: 'test' }; 17 | -------------------------------------------------------------------------------- /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 | "graphql-esm": "file:../graphql-esm.tgz", 11 | "typescript-4.9": "npm:typescript@4.9.x", 12 | "typescript-5.0": "npm:typescript@5.0.x", 13 | "typescript-5.1": "npm:typescript@5.1.x", 14 | "typescript-5.2": "npm:typescript@5.2.x", 15 | "typescript-5.3": "npm:typescript@5.3.x", 16 | "typescript-5.4": "npm:typescript@5.4.x", 17 | "typescript-5.5": "npm:typescript@5.5.x" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /integrationTests/ts/test.js: -------------------------------------------------------------------------------- 1 | import childProcess from 'node:child_process'; 2 | import fs from 'node:fs'; 3 | import path from 'node: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 | -------------------------------------------------------------------------------- /integrationTests/ts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "lib": [ 5 | "es2019", 6 | "es2020.promise", 7 | "es2020.bigint", 8 | "es2020.string", 9 | "DOM" 10 | ], 11 | "noEmit": true, 12 | "types": [], 13 | "strict": true, 14 | "exactOptionalPropertyTypes": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integrationTests/webpack/entry-esm.mjs: -------------------------------------------------------------------------------- 1 | import { graphqlSync } from 'graphql-esm'; 2 | import { buildSchema } from 'graphql-esm/utilities/buildASTSchema'; 3 | 4 | const schema = buildSchema('type Query { hello: String }'); 5 | 6 | export const result = graphqlSync({ 7 | schema, 8 | source: '{ hello }', 9 | rootValue: { hello: 'world' }, 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 | -------------------------------------------------------------------------------- /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 | "graphql-esm": "file:../graphql-esm.tgz", 11 | "webpack": "5.x.x", 12 | "webpack-cli": "4.x.x" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /integrationTests/webpack/test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | 3 | /* eslint-disable n/no-missing-import */ 4 | import cjs from './dist/main-cjs.cjs'; 5 | import mjs from './dist/main-mjs.cjs'; 6 | /* eslint-enable n/no-missing-import */ 7 | 8 | assert.deepStrictEqual(cjs.result, { 9 | data: { 10 | __proto__: null, 11 | hello: 'world', 12 | }, 13 | }); 14 | 15 | assert.deepStrictEqual(mjs.result, { 16 | data: { 17 | __proto__: null, 18 | hello: 'world', 19 | }, 20 | }); 21 | 22 | console.log('Test script: Got correct result from Webpack bundle!'); 23 | -------------------------------------------------------------------------------- /integrationTests/webpack/webpack.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": "production", 3 | "entry": { 4 | "cjs": "./entry.js", 5 | "mjs": "./entry-esm.mjs" 6 | }, 7 | "output": { 8 | "filename": "main-[name].cjs", 9 | "library": { 10 | "type": "commonjs2" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /resources/build-deno.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | import ts from 'typescript'; 5 | 6 | import { changeExtensionInImportPaths } from './change-extension-in-import-paths.js'; 7 | import { inlineInvariant } from './inline-invariant.js'; 8 | import { 9 | prettify, 10 | readTSConfig, 11 | showDirStats, 12 | writeGeneratedFile, 13 | } from './utils.js'; 14 | 15 | fs.rmSync('./denoDist', { recursive: true, force: true }); 16 | fs.mkdirSync('./denoDist'); 17 | 18 | const tsProgram = ts.createProgram(['src/index.ts'], readTSConfig()); 19 | for (const sourceFile of tsProgram.getSourceFiles()) { 20 | if ( 21 | tsProgram.isSourceFileFromExternalLibrary(sourceFile) || 22 | tsProgram.isSourceFileDefaultLibrary(sourceFile) 23 | ) { 24 | continue; 25 | } 26 | 27 | const transformed = ts.transform(sourceFile, [ 28 | changeExtensionInImportPaths({ extension: '.ts' }), 29 | inlineInvariant, 30 | ]); 31 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 32 | const newContent = printer.printBundle( 33 | ts.factory.createBundle(transformed.transformed), 34 | ); 35 | 36 | transformed.dispose(); 37 | 38 | const filepath = path.relative('./src', sourceFile.fileName); 39 | const destPath = path.join('./denoDist', filepath); 40 | // eslint-disable-next-line no-await-in-loop 41 | const prettified = await prettify(destPath, newContent); 42 | writeGeneratedFile(destPath, prettified); 43 | } 44 | 45 | fs.copyFileSync('./LICENSE', './denoDist/LICENSE'); 46 | fs.copyFileSync('./README.md', './denoDist/README.md'); 47 | 48 | showDirStats('./denoDist'); 49 | -------------------------------------------------------------------------------- /resources/change-extension-in-import-paths.ts: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import util from 'node:util'; 3 | 4 | import 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.js'; 13 | * export { foo } from './bar.js'; 14 | * ``` 15 | * 16 | * to: 17 | * 18 | * ``` 19 | * import { foo } from './bar.ts'; 20 | * export { foo } from './bar.ts'; 21 | * ``` 22 | * 23 | */ 24 | export function changeExtensionInImportPaths(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, ts.isSourceFile); 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 | const newSource = source.replace(/\.js$/, extension); 39 | 40 | if (ts.isImportDeclaration(node)) { 41 | return factory.updateImportDeclaration( 42 | node, 43 | node.modifiers, 44 | node.importClause, 45 | factory.createStringLiteral(newSource), 46 | node.assertClause, 47 | ); 48 | } 49 | if (ts.isExportDeclaration(node)) { 50 | return factory.updateExportDeclaration( 51 | node, 52 | node.modifiers, 53 | node.isTypeOnly, 54 | node.exportClause, 55 | factory.createStringLiteral(newSource), 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 | -------------------------------------------------------------------------------- /resources/checkgit.sh: -------------------------------------------------------------------------------- 1 | # Exit immediately if any subcommand terminated 2 | set -e 3 | trap "exit 1" ERR 4 | 5 | # 6 | # This script determines if current git state is the up to date main. If so 7 | # it exits normally. If not it prompts for an explicit continue. This script 8 | # intends to protect from versioning for NPM without first pushing changes 9 | # and including any changes on main. 10 | # 11 | 12 | # Check that local copy has no modifications 13 | GIT_MODIFIED_FILES=$(git ls-files -dm 2> /dev/null); 14 | GIT_STAGED_FILES=$(git diff --cached --name-only 2> /dev/null); 15 | if [ "$GIT_MODIFIED_FILES" != "" -o "$GIT_STAGED_FILES" != "" ]; then 16 | read -p "Git has local modifications. Continue? (y|N) " yn; 17 | if [ "$yn" != "y" ]; then exit 1; fi; 18 | fi; 19 | 20 | # First fetch to ensure git is up to date. Fail-fast if this fails. 21 | git fetch; 22 | if [[ $? -ne 0 ]]; then exit 1; fi; 23 | 24 | # Extract useful information. 25 | GIT_BRANCH=$(git branch -v 2> /dev/null | sed '/^[^*]/d'); 26 | GIT_BRANCH_NAME=$(echo "$GIT_BRANCH" | sed 's/* \([A-Za-z0-9_\-]*\).*/\1/'); 27 | GIT_BRANCH_SYNC=$(echo "$GIT_BRANCH" | sed 's/* [^[]*.\([^]]*\).*/\1/'); 28 | 29 | # Check if main is checked out 30 | if [ "$GIT_BRANCH_NAME" != "main" ]; then 31 | read -p "Git not on main but $GIT_BRANCH_NAME. Continue? (y|N) " yn; 32 | if [ "$yn" != "y" ]; then exit 1; fi; 33 | fi; 34 | 35 | # Check if branch is synced with remote 36 | if [ "$GIT_BRANCH_SYNC" != "" ]; then 37 | read -p "Git not up to date but $GIT_BRANCH_SYNC. Continue? (y|N) " yn; 38 | if [ "$yn" != "y" ]; then exit 1; fi; 39 | fi; 40 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/index.js: -------------------------------------------------------------------------------- 1 | import { noDirImportRule } from './no-dir-import.js'; 2 | import { onlyAsciiRule } from './only-ascii.js'; 3 | import { requireToStringTagRule } from './require-to-string-tag.js'; 4 | 5 | const internalRulesPlugin = { 6 | rules: { 7 | ...onlyAsciiRule, 8 | ...noDirImportRule, 9 | ...requireToStringTagRule, 10 | }, 11 | }; 12 | 13 | export { internalRulesPlugin }; 14 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/no-dir-import.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import path from 'node:path'; 3 | 4 | const noDirImportRule = { 5 | 'no-dir-import': { 6 | create: noDirImport, 7 | }, 8 | }; 9 | 10 | export { noDirImportRule }; 11 | 12 | function noDirImport(context) { 13 | return { 14 | ImportDeclaration: checkImportPath, 15 | ExportNamedDeclaration: checkImportPath, 16 | }; 17 | 18 | function checkImportPath(node) { 19 | const { source } = node; 20 | 21 | // bail if the declaration doesn't have a source, e.g. "export { foo };" 22 | if (!source) { 23 | return; 24 | } 25 | 26 | const importPath = source.value; 27 | if (importPath.startsWith('./') || importPath.startsWith('../')) { 28 | const baseDir = path.dirname(context.getFilename()); 29 | const resolvedPath = path.resolve(baseDir, importPath); 30 | 31 | if ( 32 | fs.existsSync(resolvedPath) && 33 | fs.statSync(resolvedPath).isDirectory() 34 | ) { 35 | context.report({ 36 | node: source, 37 | message: 'It is not allowed to import from directory', 38 | }); 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/only-ascii.js: -------------------------------------------------------------------------------- 1 | const onlyAsciiRule = { 2 | 'only-ascii': { 3 | meta: { 4 | schema: [ 5 | { 6 | type: 'object', 7 | properties: { 8 | allowEmoji: { 9 | type: 'boolean', 10 | }, 11 | }, 12 | additionalProperties: false, 13 | }, 14 | ], 15 | }, 16 | create: onlyASCII, 17 | }, 18 | }; 19 | 20 | export { onlyAsciiRule }; 21 | 22 | function onlyASCII(context) { 23 | const regExp = 24 | context.options[0]?.allowEmoji === true 25 | ? /[^\p{ASCII}\p{Emoji}]+/gu 26 | : /\P{ASCII}+/gu; 27 | 28 | return { 29 | Program() { 30 | const sourceCode = context.getSourceCode(); 31 | const text = sourceCode.getText(); 32 | 33 | for (const match of text.matchAll(regExp)) { 34 | context.report({ 35 | loc: sourceCode.getLocFromIndex(match.index), 36 | message: `Non-ASCII character "${match[0]}" found.`, 37 | }); 38 | } 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /resources/eslint-internal-rules/require-to-string-tag.js: -------------------------------------------------------------------------------- 1 | const requireToStringTagRule = { 2 | 'require-to-string-tag': { 3 | create: requireToStringTag, 4 | }, 5 | }; 6 | 7 | export { requireToStringTagRule }; 8 | 9 | function requireToStringTag(context) { 10 | const sourceCode = context.getSourceCode(); 11 | 12 | return { 13 | 'ExportNamedDeclaration > ClassDeclaration': (classNode) => { 14 | const properties = classNode.body.body; 15 | if (properties.some(isToStringTagProperty)) { 16 | return; 17 | } 18 | 19 | const jsDoc = context.getSourceCode().getJSDocComment(classNode)?.value; 20 | // FIXME: use proper TSDoc parser instead of includes once we fix TSDoc comments 21 | if (jsDoc?.includes('@internal') === true) { 22 | return; 23 | } 24 | 25 | context.report({ 26 | node: classNode, 27 | message: 28 | 'All classes in public API required to have [Symbol.toStringTag] method', 29 | }); 30 | }, 31 | }; 32 | 33 | function isToStringTagProperty(propertyNode) { 34 | if ( 35 | propertyNode.type !== 'MethodDefinition' || 36 | propertyNode.kind !== 'get' 37 | ) { 38 | return false; 39 | } 40 | const keyText = sourceCode.getText(propertyNode.key); 41 | return keyText === 'Symbol.toStringTag'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /resources/gen-version.ts: -------------------------------------------------------------------------------- 1 | import { prettify, readPackageJSON, writeGeneratedFile } from './utils.js'; 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 | const prettified = await prettify('./src/version.ts', body); 34 | writeGeneratedFile('./src/version.ts', prettified); 35 | -------------------------------------------------------------------------------- /resources/inline-invariant.ts: -------------------------------------------------------------------------------- 1 | import 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, ts.isSourceFile); 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(expression, undefined, [ 36 | factory.createFalse(), 37 | ...otherArgs, 38 | ]), 39 | ); 40 | } 41 | } 42 | } 43 | return ts.visitEachChild(node, visitNode, context); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /resources/integration-test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | 3 | import { describe, it } from 'mocha'; 4 | 5 | import { localRepoPath, makeTmpDir, npm, readPackageJSON } from './utils.js'; 6 | 7 | describe('Integration Tests', () => { 8 | const { tmpDirPath } = makeTmpDir('graphql-js-integrationTmp'); 9 | fs.cpSync(localRepoPath('integrationTests'), tmpDirPath(), { 10 | recursive: true, 11 | }); 12 | 13 | npm().run('build:npm'); 14 | 15 | const distDir = localRepoPath('npmDist'); 16 | const archiveName = npm({ cwd: tmpDirPath(), quiet: true }).pack(distDir); 17 | fs.renameSync(tmpDirPath(archiveName), tmpDirPath('graphql.tgz')); 18 | 19 | const esmDistDir = localRepoPath('npmEsmDist'); 20 | const archiveEsmName = npm({ cwd: tmpDirPath(), quiet: true }).pack( 21 | esmDistDir, 22 | ); 23 | fs.renameSync(tmpDirPath(archiveEsmName), tmpDirPath('graphql-esm.tgz')); 24 | 25 | npm().run('build:deno'); 26 | 27 | function testOnNodeProject(projectName: string) { 28 | const projectPath = tmpDirPath(projectName); 29 | const packageJSON = readPackageJSON(projectPath); 30 | 31 | it(packageJSON.description, () => { 32 | // TODO: figure out a way to run it with --ignore-scripts 33 | npm({ cwd: projectPath, quiet: true }).install(); 34 | npm({ cwd: projectPath, quiet: true }).run('test'); 35 | }).timeout(120000); 36 | } 37 | 38 | testOnNodeProject('ts'); 39 | testOnNodeProject('node'); 40 | testOnNodeProject('webpack'); 41 | }); 42 | -------------------------------------------------------------------------------- /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__/__tests__/expectEqualPromisesOrValues-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { expectEqualPromisesOrValues } from '../expectEqualPromisesOrValues.js'; 5 | import { expectPromise } from '../expectPromise.js'; 6 | 7 | describe('expectEqualPromisesOrValues', () => { 8 | it('throws when given unequal values', () => { 9 | expect(() => expectEqualPromisesOrValues([{}, {}, { test: 'test' }])).throw( 10 | "expected { test: 'test' } to deeply equal {}", 11 | ); 12 | }); 13 | 14 | it('does not throw when given equal values', () => { 15 | const testValue = { test: 'test' }; 16 | expect(() => 17 | expectEqualPromisesOrValues([testValue, testValue, testValue]), 18 | ).not.to.throw(); 19 | }); 20 | 21 | it('does not throw when given equal promises', async () => { 22 | const testValue = Promise.resolve({ test: 'test' }); 23 | 24 | await expectPromise( 25 | expectEqualPromisesOrValues([testValue, testValue, testValue]), 26 | ).toResolve(); 27 | }); 28 | 29 | it('throws when given unequal promises', async () => { 30 | await expectPromise( 31 | expectEqualPromisesOrValues([ 32 | Promise.resolve({}), 33 | Promise.resolve({}), 34 | Promise.resolve({ test: 'test' }), 35 | ]), 36 | ).toRejectWith("expected { test: 'test' } to deeply equal {}"); 37 | }); 38 | 39 | it('throws when given equal values that are mixtures of values and promises', () => { 40 | const testValue = { test: 'test' }; 41 | expect(() => 42 | expectEqualPromisesOrValues([testValue, Promise.resolve(testValue)]), 43 | ).to.throw('Received an invalid mixture of promises and values.'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/expectMatchingValues-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { expectMatchingValues } from '../expectMatchingValues.js'; 5 | 6 | describe('expectMatchingValues', () => { 7 | it('throws when given unequal values', () => { 8 | expect(() => expectMatchingValues([{}, {}, { test: 'test' }])).throw( 9 | "expected { test: 'test' } to deeply equal {}", 10 | ); 11 | }); 12 | 13 | it('does not throw when given equal values', () => { 14 | const testValue = { test: 'test' }; 15 | expect(() => 16 | expectMatchingValues([testValue, testValue, testValue]), 17 | ).not.to.throw(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/expectPromise-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { expectPromise } from '../expectPromise.js'; 5 | 6 | describe('expectPromise', () => { 7 | it('throws if passed a value', () => { 8 | expect(() => expectPromise({})).to.throw( 9 | "Expected a promise, received '{}'", 10 | ); 11 | }); 12 | 13 | it('toResolve returns the resolved value', async () => { 14 | const testValue = {}; 15 | const promise = Promise.resolve(testValue); 16 | expect(await expectPromise(promise).toResolve()).to.equal(testValue); 17 | }); 18 | 19 | it('toRejectWith throws if the promise does not reject', async () => { 20 | try { 21 | await expectPromise(Promise.resolve({})).toRejectWith( 22 | 'foo', 23 | ); /* c8 ignore start */ 24 | } /* c8 ignore stop */ catch (err) { 25 | expect(err.message).to.equal( 26 | "Promise should have rejected with message 'foo', but resolved as '{}'", 27 | ); 28 | } 29 | }); 30 | 31 | it('toRejectWith throws if the promise rejects with the wrong reason', async () => { 32 | try { 33 | await expectPromise(Promise.reject(new Error('foo'))).toRejectWith( 34 | 'bar', 35 | ); /* c8 ignore start */ 36 | } /* c8 ignore stop */ catch (err) { 37 | expect(err.message).to.equal( 38 | "expected Error: foo to have property 'message' of 'bar', but got 'foo'", 39 | ); 40 | } 41 | }); 42 | 43 | it('toRejectWith does not throw if the promise rejects with the right reason', async () => { 44 | try { 45 | await expectPromise(Promise.reject(new Error('foo'))).toRejectWith( 46 | 'foo', 47 | ); /* c8 ignore start */ 48 | } catch (_err) { 49 | // Not reached. 50 | expect.fail('promise threw unexpectedly'); 51 | } /* c8 ignore stop */ 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/genFuzzStrings-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { genFuzzStrings } from '../genFuzzStrings.js'; 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 | -------------------------------------------------------------------------------- /src/__testUtils__/__tests__/inspectStr-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { inspectStr } from '../inspectStr.js'; 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.js'; 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 | -------------------------------------------------------------------------------- /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.replaceAll(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 | -------------------------------------------------------------------------------- /src/__testUtils__/expectEqualPromisesOrValues.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | 3 | import { isPromise } from '../jsutils/isPromise.js'; 4 | import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; 5 | 6 | import { expectMatchingValues } from './expectMatchingValues.js'; 7 | 8 | export function expectEqualPromisesOrValues( 9 | items: ReadonlyArray>, 10 | ): PromiseOrValue { 11 | const [firstItem, ...remainingItems] = items; 12 | if (isPromise(firstItem)) { 13 | if (remainingItems.every(isPromise)) { 14 | return Promise.all(items).then(expectMatchingValues); 15 | } 16 | } else if (remainingItems.every((item) => !isPromise(item))) { 17 | return expectMatchingValues(items); 18 | } 19 | 20 | assert(false, 'Received an invalid mixture of promises and values.'); 21 | } 22 | -------------------------------------------------------------------------------- /src/__testUtils__/expectJSON.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | 3 | import { isObjectLike } from '../jsutils/isObjectLike.js'; 4 | import { mapValue } from '../jsutils/mapValue.js'; 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/__testUtils__/expectMatchingValues.ts: -------------------------------------------------------------------------------- 1 | import { expectJSON } from './expectJSON.js'; 2 | 3 | export function expectMatchingValues(values: ReadonlyArray): T { 4 | const [firstValue, ...remainingValues] = values; 5 | for (const value of remainingValues) { 6 | expectJSON(value).toDeepEqual(firstValue); 7 | } 8 | return firstValue; 9 | } 10 | -------------------------------------------------------------------------------- /src/__testUtils__/expectPromise.ts: -------------------------------------------------------------------------------- 1 | import { assert, expect } from 'chai'; 2 | 3 | import { inspect } from '../jsutils/inspect.js'; 4 | import { isPromise } from '../jsutils/isPromise.js'; 5 | 6 | export function expectPromise(maybePromise: unknown) { 7 | assert( 8 | isPromise(maybePromise), 9 | `Expected a promise, received '${inspect(maybePromise)}'`, 10 | ); 11 | 12 | return { 13 | toResolve() { 14 | return maybePromise; 15 | }, 16 | async toRejectWith(message: string) { 17 | let caughtError: Error | undefined; 18 | let resolved; 19 | let rejected = false; 20 | try { 21 | resolved = await maybePromise; 22 | } catch (error) { 23 | rejected = true; 24 | caughtError = error; 25 | } 26 | 27 | assert( 28 | rejected, 29 | `Promise should have rejected with message '${message}', but resolved as '${inspect( 30 | resolved, 31 | )}'`, 32 | ); 33 | 34 | expect(caughtError).to.be.an.instanceOf(Error); 35 | expect(caughtError).to.have.property('message', message); 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /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/__testUtils__/inspectStr.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../jsutils/Maybe.js'; 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 | .replaceAll('\\"', '"') 13 | .replaceAll('\\\\', '\\'); 14 | } 15 | -------------------------------------------------------------------------------- /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/__testUtils__/resolveOnNextTick.ts: -------------------------------------------------------------------------------- 1 | export function resolveOnNextTick(): Promise { 2 | return Promise.resolve(undefined); 3 | } 4 | -------------------------------------------------------------------------------- /src/__testUtils__/viralSDL.ts: -------------------------------------------------------------------------------- 1 | export const viralSDL = `\ 2 | schema { 3 | query: Query 4 | } 5 | 6 | type Query { 7 | viruses: [Virus!] 8 | } 9 | 10 | type Virus { 11 | name: String! 12 | knownMutations: [Mutation!]! 13 | } 14 | 15 | type Mutation { 16 | name: String! 17 | geneSequence: String! 18 | }\ 19 | `; 20 | -------------------------------------------------------------------------------- /src/__testUtils__/viralSchema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GraphQLList, 3 | GraphQLNonNull, 4 | GraphQLObjectType, 5 | } from '../type/definition.js'; 6 | import { GraphQLString } from '../type/scalars.js'; 7 | import { GraphQLSchema } from '../type/schema.js'; 8 | 9 | const Mutation = new GraphQLObjectType({ 10 | name: 'Mutation', 11 | fields: { 12 | name: { 13 | type: new GraphQLNonNull(GraphQLString), 14 | }, 15 | geneSequence: { 16 | type: new GraphQLNonNull(GraphQLString), 17 | }, 18 | }, 19 | }); 20 | 21 | const Virus = new GraphQLObjectType({ 22 | name: 'Virus', 23 | fields: { 24 | name: { 25 | type: new GraphQLNonNull(GraphQLString), 26 | }, 27 | knownMutations: { 28 | type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Mutation))), 29 | }, 30 | }, 31 | }); 32 | 33 | const Query = new GraphQLObjectType({ 34 | name: 'Query', 35 | fields: { 36 | viruses: { 37 | type: new GraphQLList(new GraphQLNonNull(Virus)), 38 | }, 39 | }, 40 | }); 41 | 42 | export const viralSchema = new GraphQLSchema({ 43 | query: Query, 44 | }); 45 | -------------------------------------------------------------------------------- /src/__tests__/version-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { version, versionInfo } from '../version.js'; 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 | -------------------------------------------------------------------------------- /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/error/__tests__/locatedError-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { GraphQLError } from '../GraphQLError.js'; 5 | import { locatedError } from '../locatedError.js'; 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/error/index.ts: -------------------------------------------------------------------------------- 1 | export { GraphQLError } from './GraphQLError.js'; 2 | export type { 3 | GraphQLErrorOptions, 4 | GraphQLFormattedError, 5 | GraphQLErrorExtensions, 6 | GraphQLFormattedErrorExtensions, 7 | } from './GraphQLError.js'; 8 | 9 | export { syntaxError } from './syntaxError.js'; 10 | 11 | export { locatedError } from './locatedError.js'; 12 | -------------------------------------------------------------------------------- /src/error/locatedError.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../jsutils/Maybe.js'; 2 | import { toError } from '../jsutils/toError.js'; 3 | 4 | import type { ASTNode } from '../language/ast.js'; 5 | 6 | import { GraphQLError } from './GraphQLError.js'; 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/error/syntaxError.ts: -------------------------------------------------------------------------------- 1 | import type { Source } from '../language/source.js'; 2 | 3 | import { GraphQLError } from './GraphQLError.js'; 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/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/execution/__tests__/errorPropagation-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { expectJSON } from '../../__testUtils__/expectJSON.js'; 4 | 5 | import type { PromiseOrValue } from '../../jsutils/PromiseOrValue.js'; 6 | 7 | import { parse } from '../../language/parser.js'; 8 | 9 | import { buildSchema } from '../../utilities/buildASTSchema.js'; 10 | 11 | import { execute } from '../execute.js'; 12 | import type { ExecutionResult } from '../types.js'; 13 | 14 | const syncError = new Error('bar'); 15 | 16 | const throwingData = { 17 | foo() { 18 | throw syncError; 19 | }, 20 | }; 21 | 22 | const schema = buildSchema(` 23 | type Query { 24 | foo : Int! 25 | } 26 | 27 | directive @experimental_disableErrorPropagation on QUERY | MUTATION | SUBSCRIPTION 28 | `); 29 | 30 | function executeQuery( 31 | query: string, 32 | rootValue: unknown, 33 | ): PromiseOrValue { 34 | return execute({ schema, document: parse(query), rootValue }); 35 | } 36 | 37 | describe('Execute: handles errors', () => { 38 | it('with `@experimental_disableErrorPropagation returns null', async () => { 39 | const query = ` 40 | query getFoo @experimental_disableErrorPropagation { 41 | foo 42 | } 43 | `; 44 | const result = await executeQuery(query, throwingData); 45 | expectJSON(result).toDeepEqual({ 46 | data: { foo: null }, 47 | errors: [ 48 | { 49 | message: 'bar', 50 | path: ['foo'], 51 | locations: [{ line: 3, column: 9 }], 52 | }, 53 | ], 54 | }); 55 | }); 56 | it('without `experimental_disableErrorPropagation` propagates the error', async () => { 57 | const query = ` 58 | query getFoo { 59 | foo 60 | } 61 | `; 62 | const result = await executeQuery(query, throwingData); 63 | expectJSON(result).toDeepEqual({ 64 | data: null, 65 | errors: [ 66 | { 67 | message: 'bar', 68 | path: ['foo'], 69 | locations: [{ line: 3, column: 9 }], 70 | }, 71 | ], 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/execution/__tests__/simplePubSub-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { SimplePubSub } from './simplePubSub.js'; 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/execution/getVariableSignature.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../error/GraphQLError.js'; 2 | 3 | import type { 4 | ConstValueNode, 5 | VariableDefinitionNode, 6 | } from '../language/ast.js'; 7 | import { print } from '../language/printer.js'; 8 | 9 | import { isInputType } from '../type/definition.js'; 10 | import type { GraphQLInputType, GraphQLSchema } from '../type/index.js'; 11 | 12 | import { typeFromAST } from '../utilities/typeFromAST.js'; 13 | 14 | /** 15 | * A GraphQLVariableSignature is required to coerce a variable value. 16 | * 17 | * Designed to have comparable interface to GraphQLArgument so that 18 | * getArgumentValues() can be reused for fragment arguments. 19 | * */ 20 | export interface GraphQLVariableSignature { 21 | name: string; 22 | type: GraphQLInputType; 23 | defaultValue?: never; 24 | default: { literal: ConstValueNode } | undefined; 25 | } 26 | 27 | export function getVariableSignature( 28 | schema: GraphQLSchema, 29 | varDefNode: VariableDefinitionNode, 30 | ): GraphQLVariableSignature | GraphQLError { 31 | const varName = varDefNode.variable.name.value; 32 | const varType = typeFromAST(schema, varDefNode.type); 33 | 34 | if (!isInputType(varType)) { 35 | // Must use input types for variables. This should be caught during 36 | // validation, however is checked again here for safety. 37 | const varTypeStr = print(varDefNode.type); 38 | return new GraphQLError( 39 | `Variable "$${varName}" expected value of type "${varTypeStr}" which cannot be used as an input type.`, 40 | { nodes: varDefNode.type }, 41 | ); 42 | } 43 | 44 | const defaultValue = varDefNode.defaultValue; 45 | 46 | return { 47 | name: varName, 48 | type: varType, 49 | default: defaultValue && { literal: defaultValue }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/execution/index.ts: -------------------------------------------------------------------------------- 1 | export { pathToArray as responsePathAsArray } from '../jsutils/Path.js'; 2 | 3 | export { 4 | createSourceEventStream, 5 | execute, 6 | executeQueryOrMutationOrSubscriptionEvent, 7 | executeSubscriptionEvent, 8 | experimentalExecuteIncrementally, 9 | experimentalExecuteQueryOrMutationOrSubscriptionEvent, 10 | executeSync, 11 | defaultFieldResolver, 12 | defaultTypeResolver, 13 | subscribe, 14 | } from './execute.js'; 15 | 16 | export type { ExecutionArgs, ValidatedExecutionArgs } from './execute.js'; 17 | 18 | export type { 19 | ExecutionResult, 20 | ExperimentalIncrementalExecutionResults, 21 | InitialIncrementalExecutionResult, 22 | SubsequentIncrementalExecutionResult, 23 | IncrementalDeferResult, 24 | IncrementalStreamResult, 25 | IncrementalResult, 26 | FormattedExecutionResult, 27 | FormattedInitialIncrementalExecutionResult, 28 | FormattedSubsequentIncrementalExecutionResult, 29 | FormattedIncrementalDeferResult, 30 | FormattedIncrementalStreamResult, 31 | FormattedIncrementalResult, 32 | } from './types.js'; 33 | 34 | export { 35 | getArgumentValues, 36 | getVariableValues, 37 | getDirectiveValues, 38 | } from './values.js'; 39 | -------------------------------------------------------------------------------- /src/execution/mapAsyncIterable.ts: -------------------------------------------------------------------------------- 1 | import type { PromiseOrValue } from '../jsutils/PromiseOrValue.js'; 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 mapAsyncIterable( 8 | iterable: AsyncGenerator | AsyncIterable, 9 | callback: (value: T) => PromiseOrValue, 10 | onDone?: () => void, 11 | ): AsyncGenerator { 12 | const iterator = iterable[Symbol.asyncIterator](); 13 | 14 | async function mapResult( 15 | promise: Promise>, 16 | ): Promise> { 17 | let value: T; 18 | try { 19 | const result = await promise; 20 | if (result.done) { 21 | onDone?.(); 22 | return result; 23 | } 24 | value = result.value; 25 | } catch (error) { 26 | onDone?.(); 27 | throw error; 28 | } 29 | 30 | try { 31 | return { value: await callback(value), done: false }; 32 | } catch (error) { 33 | /* c8 ignore start */ 34 | // FIXME: add test case 35 | if (typeof iterator.return === 'function') { 36 | try { 37 | await iterator.return(); 38 | } catch (_e) { 39 | /* ignore error */ 40 | } 41 | } 42 | throw error; 43 | /* c8 ignore stop */ 44 | } 45 | } 46 | 47 | return { 48 | async next() { 49 | return mapResult(iterator.next()); 50 | }, 51 | async return(): Promise> { 52 | // If iterator.return() does not exist, then type R must be undefined. 53 | return typeof iterator.return === 'function' 54 | ? mapResult(iterator.return()) 55 | : { value: undefined as any, done: true }; 56 | }, 57 | async throw(error?: unknown) { 58 | if (typeof iterator.throw === 'function') { 59 | return mapResult(iterator.throw(error)); 60 | } 61 | throw error; 62 | }, 63 | [Symbol.asyncIterator]() { 64 | return this; 65 | }, 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /src/jsutils/AccumulatorMap.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ES6 Map with additional `add` method to accumulate items. 3 | */ 4 | export class AccumulatorMap extends Map> { 5 | override 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 | -------------------------------------------------------------------------------- /src/jsutils/BoxedPromiseOrValue.ts: -------------------------------------------------------------------------------- 1 | import { isPromise } from './isPromise.js'; 2 | import type { PromiseOrValue } from './PromiseOrValue.js'; 3 | 4 | /** 5 | * A BoxedPromiseOrValue is a container for a value or promise where the value 6 | * will be updated when the promise resolves. 7 | * 8 | * A BoxedPromiseOrValue may only be used with promises whose possible 9 | * rejection has already been handled, otherwise this will lead to unhandled 10 | * promise rejections. 11 | * 12 | * @internal 13 | * */ 14 | export class BoxedPromiseOrValue { 15 | value: PromiseOrValue; 16 | 17 | constructor(value: PromiseOrValue) { 18 | this.value = value; 19 | if (isPromise(value)) { 20 | // eslint-disable-next-line @typescript-eslint/no-floating-promises 21 | value.then((resolved) => { 22 | this.value = resolved; 23 | }); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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/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 interface ReadOnlyObjMapWithSymbol { 12 | readonly [key: string | symbol]: T; 13 | } 14 | 15 | export type ReadOnlyObjMapLike = 16 | | ReadOnlyObjMap 17 | | { readonly [key: string]: T }; 18 | 19 | export type ReadOnlyObjMapSymbolLike = 20 | | ReadOnlyObjMapWithSymbol 21 | | { readonly [key: string | symbol]: T }; 22 | -------------------------------------------------------------------------------- /src/jsutils/Path.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from './Maybe.js'; 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/PromiseOrValue.ts: -------------------------------------------------------------------------------- 1 | export type PromiseOrValue = Promise | T; 2 | -------------------------------------------------------------------------------- /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/__tests__/AccumulatorMap-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { AccumulatorMap } from '../AccumulatorMap.js'; 5 | 6 | function expectMap(map: Map) { 7 | return expect(Object.fromEntries(map)); 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__/BoxedPromiseOrValue-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { resolveOnNextTick } from '../../__testUtils__/resolveOnNextTick.js'; 5 | 6 | import { BoxedPromiseOrValue } from '../BoxedPromiseOrValue.js'; 7 | 8 | describe('BoxedPromiseOrValue', () => { 9 | it('can box a value', () => { 10 | const boxed = new BoxedPromiseOrValue(42); 11 | 12 | expect(boxed.value).to.equal(42); 13 | }); 14 | 15 | it('can box a promise', () => { 16 | const promise = Promise.resolve(42); 17 | const boxed = new BoxedPromiseOrValue(promise); 18 | 19 | expect(boxed.value).to.equal(promise); 20 | }); 21 | 22 | it('resets the boxed value when the passed promise resolves', async () => { 23 | const promise = Promise.resolve(42); 24 | const boxed = new BoxedPromiseOrValue(promise); 25 | 26 | await resolveOnNextTick(); 27 | 28 | expect(boxed.value).to.equal(42); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/Path-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { addPath, pathToArray } from '../Path.js'; 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 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/capitalize-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { capitalize } from '../capitalize.js'; 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/__tests__/didYouMean-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { didYouMean } from '../didYouMean.js'; 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 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/identityFunc-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc.js'; 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 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/invariant-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { invariant } from '../invariant.js'; 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 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/isAsyncIterable-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc.js'; 5 | import { isAsyncIterable } from '../isAsyncIterable.js'; 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 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/isObjectLike-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../identityFunc.js'; 5 | import { isObjectLike } from '../isObjectLike.js'; 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/__tests__/promiseWithResolvers-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { expectPromise } from '../../__testUtils__/expectPromise.js'; 5 | 6 | import { promiseWithResolvers } from '../promiseWithResolvers.js'; 7 | 8 | describe('promiseWithResolvers', () => { 9 | it('resolves values', async () => { 10 | const { promise, resolve } = promiseWithResolvers(); 11 | resolve('foo'); 12 | expect(await expectPromise(promise).toResolve()).to.equal('foo'); 13 | }); 14 | 15 | it('rejects values', async () => { 16 | const { promise, reject } = promiseWithResolvers(); 17 | const error = new Error('rejected'); 18 | reject(error); 19 | await expectPromise(promise).toRejectWith('rejected'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/jsutils/__tests__/toObjMap-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import type { ObjMapLike } from '../ObjMap.js'; 5 | import { toObjMap } from '../toObjMap.js'; 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/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/jsutils/devAssert.ts: -------------------------------------------------------------------------------- 1 | export function devAssert(condition: boolean, message: string): void { 2 | if (!condition) { 3 | throw new Error(message); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/jsutils/didYouMean.ts: -------------------------------------------------------------------------------- 1 | import { orList } from './formatList.js'; 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 != null) { 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/jsutils/formatList.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from './invariant.js'; 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.at(-1); 29 | return allButLast.join(', ') + ', ' + conjunction + ' ' + lastItem; 30 | } 31 | -------------------------------------------------------------------------------- /src/jsutils/getBySet.ts: -------------------------------------------------------------------------------- 1 | import { isSameSet } from './isSameSet.js'; 2 | 3 | export function getBySet( 4 | map: ReadonlyMap, U>, 5 | setToMatch: ReadonlySet, 6 | ): U | undefined { 7 | for (const set of map.keys()) { 8 | if (isSameSet(set, setToMatch)) { 9 | return map.get(set); 10 | } 11 | } 12 | return undefined; 13 | } 14 | -------------------------------------------------------------------------------- /src/jsutils/groupBy.ts: -------------------------------------------------------------------------------- 1 | import { AccumulatorMap } from './AccumulatorMap.js'; 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/identityFunc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the first argument it receives. 3 | */ 4 | export function identityFunc(x: T): T { 5 | return x; 6 | } 7 | -------------------------------------------------------------------------------- /src/jsutils/invariant.ts: -------------------------------------------------------------------------------- 1 | export function invariant( 2 | condition: boolean, 3 | message?: string, 4 | ): asserts condition { 5 | if (!condition) { 6 | throw new Error(message ?? 'Unexpected invariant triggered.'); 7 | } 8 | } 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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /src/jsutils/isSameSet.ts: -------------------------------------------------------------------------------- 1 | export function isSameSet( 2 | setA: ReadonlySet, 3 | setB: ReadonlySet, 4 | ): boolean { 5 | if (setA.size !== setB.size) { 6 | return false; 7 | } 8 | for (const item of setA) { 9 | if (!setB.has(item)) { 10 | return false; 11 | } 12 | } 13 | return true; 14 | } 15 | -------------------------------------------------------------------------------- /src/jsutils/keyMap.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap } from './ObjMap.js'; 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/keyValMap.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap } from './ObjMap.js'; 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/jsutils/mapValue.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap, ReadOnlyObjMap } from './ObjMap.js'; 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 | -------------------------------------------------------------------------------- /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/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/jsutils/printPathArray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a string describing the path. 3 | */ 4 | export function printPathArray(path: ReadonlyArray): string { 5 | if (path.length === 0) { 6 | return ''; 7 | } 8 | return ` at ${path 9 | .map((key) => (typeof key === 'number' ? `[${key}]` : `.${key}`)) 10 | .join('')}`; 11 | } 12 | -------------------------------------------------------------------------------- /src/jsutils/promiseForObject.ts: -------------------------------------------------------------------------------- 1 | import type { ObjMap } from './ObjMap.js'; 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 async function promiseForObject( 11 | object: ObjMap>, 12 | callback: (object: ObjMap) => U, 13 | ): Promise { 14 | const keys = Object.keys(object); 15 | const values = Object.values(object); 16 | 17 | const resolvedValues = await Promise.all(values); 18 | const resolvedObject = Object.create(null); 19 | for (let i = 0; i < keys.length; ++i) { 20 | resolvedObject[keys[i]] = resolvedValues[i]; 21 | } 22 | return callback(resolvedObject); 23 | } 24 | -------------------------------------------------------------------------------- /src/jsutils/promiseReduce.ts: -------------------------------------------------------------------------------- 1 | import { isPromise } from './isPromise.js'; 2 | import type { PromiseOrValue } from './PromiseOrValue.js'; 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 | -------------------------------------------------------------------------------- /src/jsutils/promiseWithResolvers.ts: -------------------------------------------------------------------------------- 1 | import type { PromiseOrValue } from './PromiseOrValue.js'; 2 | 3 | /** 4 | * Based on Promise.withResolvers proposal 5 | * https://github.com/tc39/proposal-promise-with-resolvers 6 | */ 7 | export function promiseWithResolvers(): { 8 | promise: Promise; 9 | resolve: (value: T | PromiseOrValue) => void; 10 | reject: (reason?: any) => void; 11 | } { 12 | // these are assigned synchronously within the Promise constructor 13 | let resolve!: (value: T | PromiseOrValue) => void; 14 | let reject!: (reason?: any) => void; 15 | const promise = new Promise((res, rej) => { 16 | resolve = res; 17 | reject = rej; 18 | }); 19 | return { promise, resolve, reject }; 20 | } 21 | -------------------------------------------------------------------------------- /src/jsutils/toError.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from './inspect.js'; 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/jsutils/toObjMap.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from './Maybe.js'; 2 | import type { 3 | ReadOnlyObjMap, 4 | ReadOnlyObjMapLike, 5 | ReadOnlyObjMapSymbolLike, 6 | ReadOnlyObjMapWithSymbol, 7 | } from './ObjMap.js'; 8 | 9 | export function toObjMap( 10 | obj: Maybe>, 11 | ): ReadOnlyObjMap { 12 | if (obj == null) { 13 | return Object.create(null); 14 | } 15 | 16 | if (Object.getPrototypeOf(obj) === null) { 17 | return obj; 18 | } 19 | 20 | const map = Object.create(null); 21 | for (const [key, value] of Object.entries(obj)) { 22 | map[key] = value; 23 | } 24 | 25 | return map; 26 | } 27 | 28 | export function toObjMapWithSymbols( 29 | obj: Maybe>, 30 | ): ReadOnlyObjMapWithSymbol { 31 | if (obj == null) { 32 | return Object.create(null); 33 | } 34 | 35 | if (Object.getPrototypeOf(obj) === null) { 36 | return obj; 37 | } 38 | 39 | const map = Object.create(null); 40 | for (const [key, value] of Object.entries(obj)) { 41 | map[key] = value; 42 | } 43 | 44 | for (const key of Object.getOwnPropertySymbols(obj)) { 45 | map[key] = obj[key]; 46 | } 47 | 48 | return map; 49 | } 50 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/language/__tests__/blockString-fuzz.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { dedent } from '../../__testUtils__/dedent.js'; 5 | import { genFuzzStrings } from '../../__testUtils__/genFuzzStrings.js'; 6 | import { inspectStr } from '../../__testUtils__/inspectStr.js'; 7 | 8 | import { isPrintableAsBlockString, printBlockString } from '../blockString.js'; 9 | import { Lexer } from '../lexer.js'; 10 | import { Source } from '../source.js'; 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/language/__tests__/kind-test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-expressions */ 2 | import { describe, it } from 'mocha'; 3 | 4 | import { Kind } from '../index.js'; 5 | 6 | describe('Kind', () => { 7 | it('is a term level namespace with term level enum members', () => { 8 | const a: Kind.NAME = Kind.NAME; 9 | a; 10 | const b: Kind = Kind.NAME; 11 | b; 12 | const c: Kind = Kind.ARGUMENT; 13 | c; 14 | }); 15 | 16 | it('is a type level namespace with type level enum members', () => { 17 | // @ts-expect-error 18 | const a: Kind.NAME = 'bad'; 19 | a; 20 | const b: Kind.NAME = 'Name'; 21 | b; 22 | // @ts-expect-error 23 | const c: Kind = 'bad'; 24 | c; 25 | const d: Kind = 'Name'; 26 | d; 27 | const e: Kind = 'Argument'; 28 | e; 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/language/__tests__/source-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { Source } from '../source.js'; 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/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 | -------------------------------------------------------------------------------- /src/language/directiveLocation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The set of allowed directive location values. 3 | */ 4 | export const DirectiveLocation = { 5 | /** Request Definitions */ 6 | QUERY: 'QUERY' as const, 7 | MUTATION: 'MUTATION' as const, 8 | SUBSCRIPTION: 'SUBSCRIPTION' as const, 9 | FIELD: 'FIELD' as const, 10 | FRAGMENT_DEFINITION: 'FRAGMENT_DEFINITION' as const, 11 | FRAGMENT_SPREAD: 'FRAGMENT_SPREAD' as const, 12 | INLINE_FRAGMENT: 'INLINE_FRAGMENT' as const, 13 | VARIABLE_DEFINITION: 'VARIABLE_DEFINITION' as const, 14 | /** Type System Definitions */ 15 | SCHEMA: 'SCHEMA' as const, 16 | SCALAR: 'SCALAR' as const, 17 | OBJECT: 'OBJECT' as const, 18 | FIELD_DEFINITION: 'FIELD_DEFINITION' as const, 19 | ARGUMENT_DEFINITION: 'ARGUMENT_DEFINITION' as const, 20 | INTERFACE: 'INTERFACE' as const, 21 | UNION: 'UNION' as const, 22 | ENUM: 'ENUM' as const, 23 | ENUM_VALUE: 'ENUM_VALUE' as const, 24 | INPUT_OBJECT: 'INPUT_OBJECT' as const, 25 | INPUT_FIELD_DEFINITION: 'INPUT_FIELD_DEFINITION' as const, 26 | FRAGMENT_VARIABLE_DEFINITION: 'FRAGMENT_VARIABLE_DEFINITION' as const, 27 | } as const; 28 | // eslint-disable-next-line @typescript-eslint/no-redeclare 29 | export type DirectiveLocation = 30 | (typeof DirectiveLocation)[keyof typeof DirectiveLocation]; 31 | -------------------------------------------------------------------------------- /src/language/kinds.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-namespace */ 2 | import type * as Kind_ from './kinds_.js'; 3 | 4 | export * as Kind from './kinds_.js'; 5 | 6 | export type Kind = (typeof Kind_)[keyof typeof Kind_]; 7 | -------------------------------------------------------------------------------- /src/language/location.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../jsutils/invariant.js'; 2 | 3 | import type { Source } from './source.js'; 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/language/source.ts: -------------------------------------------------------------------------------- 1 | import { devAssert } from '../jsutils/devAssert.js'; 2 | import { instanceOf } from '../jsutils/instanceOf.js'; 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/language/tokenKind.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An exported enum describing the different kinds of tokens that the 3 | * lexer emits. 4 | */ 5 | export const TokenKind = { 6 | SOF: '' as const, 7 | EOF: '' as const, 8 | BANG: '!' as const, 9 | DOLLAR: '$' as const, 10 | AMP: '&' as const, 11 | PAREN_L: '(' as const, 12 | PAREN_R: ')' as const, 13 | SPREAD: '...' as const, 14 | COLON: ':' as const, 15 | EQUALS: '=' as const, 16 | AT: '@' as const, 17 | BRACKET_L: '[' as const, 18 | BRACKET_R: ']' as const, 19 | BRACE_L: '{' as const, 20 | PIPE: '|' as const, 21 | BRACE_R: '}' as const, 22 | NAME: 'Name' as const, 23 | INT: 'Int' as const, 24 | FLOAT: 'Float' as const, 25 | STRING: 'String' as const, 26 | BLOCK_STRING: 'BlockString' as const, 27 | COMMENT: 'Comment' as const, 28 | } as const; 29 | // eslint-disable-next-line @typescript-eslint/no-redeclare 30 | export type TokenKind = (typeof TokenKind)[keyof typeof TokenKind]; 31 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/type/__tests__/assertName-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { assertEnumValueName, assertName } from '../assertName.js'; 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 | -------------------------------------------------------------------------------- /src/type/assertName.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../error/GraphQLError.js'; 2 | 3 | import { isNameContinue, isNameStart } from '../language/characterClasses.js'; 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/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 | -------------------------------------------------------------------------------- /src/utilities/__tests__/concatAST-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { dedent } from '../../__testUtils__/dedent.js'; 5 | 6 | import { parse } from '../../language/parser.js'; 7 | import { print } from '../../language/printer.js'; 8 | import { Source } from '../../language/source.js'; 9 | 10 | import { concatAST } from '../concatAST.js'; 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 | -------------------------------------------------------------------------------- /src/utilities/__tests__/introspectionFromSchema-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { dedent } from '../../__testUtils__/dedent.js'; 5 | 6 | import { GraphQLObjectType } from '../../type/definition.js'; 7 | import { GraphQLString } from '../../type/scalars.js'; 8 | import { GraphQLSchema } from '../../type/schema.js'; 9 | 10 | import { buildClientSchema } from '../buildClientSchema.js'; 11 | import type { IntrospectionQuery } from '../getIntrospectionQuery.js'; 12 | import { introspectionFromSchema } from '../introspectionFromSchema.js'; 13 | import { printSchema } from '../printSchema.js'; 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/utilities/__tests__/sortValueNode-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { parseValue } from '../../language/parser.js'; 5 | import { print } from '../../language/printer.js'; 6 | 7 | import { sortValueNode } from '../sortValueNode.js'; 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/utilities/concatAST.ts: -------------------------------------------------------------------------------- 1 | import type { DefinitionNode, DocumentNode } from '../language/ast.js'; 2 | import { Kind } from '../language/kinds.js'; 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/utilities/getDefaultValueAST.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../jsutils/invariant.js'; 2 | 3 | import type { ConstValueNode } from '../language/ast.js'; 4 | 5 | import type { GraphQLArgument, GraphQLInputField } from '../type/definition.js'; 6 | 7 | import { astFromValue } from './astFromValue.js'; 8 | import { valueToLiteral } from './valueToLiteral.js'; 9 | 10 | export function getDefaultValueAST( 11 | argOrInputField: GraphQLArgument | GraphQLInputField, 12 | ): ConstValueNode | undefined { 13 | const type = argOrInputField.type; 14 | const defaultInput = argOrInputField.default; 15 | if (defaultInput) { 16 | const literal = 17 | defaultInput.literal ?? valueToLiteral(defaultInput.value, type); 18 | invariant(literal != null, 'Invalid default value'); 19 | return literal; 20 | } 21 | 22 | const defaultValue = argOrInputField.defaultValue; 23 | if (defaultValue !== undefined) { 24 | const valueAST = astFromValue(defaultValue, type); 25 | invariant(valueAST != null, 'Invalid default value'); 26 | return valueAST; 27 | } 28 | return undefined; 29 | } 30 | -------------------------------------------------------------------------------- /src/utilities/getOperationAST.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../jsutils/Maybe.js'; 2 | 3 | import type { DocumentNode, OperationDefinitionNode } from '../language/ast.js'; 4 | import { Kind } from '../language/kinds.js'; 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 | -------------------------------------------------------------------------------- /src/utilities/introspectionFromSchema.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../jsutils/invariant.js'; 2 | 3 | import { parse } from '../language/parser.js'; 4 | 5 | import type { GraphQLSchema } from '../type/schema.js'; 6 | 7 | import { executeSync } from '../execution/execute.js'; 8 | 9 | import type { 10 | IntrospectionOptions, 11 | IntrospectionQuery, 12 | } from './getIntrospectionQuery.js'; 13 | import { getIntrospectionQuery } from './getIntrospectionQuery.js'; 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 | oneOf: true, 34 | ...options, 35 | }; 36 | 37 | const document = parse(getIntrospectionQuery(optionsWithDefaults)); 38 | const result = executeSync({ schema, document }); 39 | invariant(result.errors == null && result.data != null); 40 | return result.data as any; 41 | } 42 | -------------------------------------------------------------------------------- /src/utilities/sortValueNode.ts: -------------------------------------------------------------------------------- 1 | import { naturalCompare } from '../jsutils/naturalCompare.js'; 2 | 3 | import type { ObjectFieldNode, ValueNode } from '../language/ast.js'; 4 | import { Kind } from '../language/kinds.js'; 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/utilities/typeFromAST.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ListTypeNode, 3 | NamedTypeNode, 4 | NonNullTypeNode, 5 | TypeNode, 6 | } from '../language/ast.js'; 7 | import { Kind } from '../language/kinds.js'; 8 | 9 | import type { GraphQLNamedType, GraphQLType } from '../type/definition.js'; 10 | import { GraphQLList, GraphQLNonNull } from '../type/definition.js'; 11 | import type { GraphQLSchema } from '../type/schema.js'; 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/utilities/typedQueryDocumentNode.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DocumentNode, 3 | ExecutableDefinitionNode, 4 | } from '../language/ast.js'; 5 | /** 6 | * Wrapper type that contains DocumentNode and types that can be deduced from it. 7 | */ 8 | export interface TypedQueryDocumentNode< 9 | TResponseData = { [key: string]: any }, 10 | TRequestVariables = { [key: string]: any }, 11 | > extends DocumentNode { 12 | readonly definitions: ReadonlyArray; 13 | // FIXME: remove once TS implements proper way to enforce nominal typing 14 | /** 15 | * This type is used to ensure that the variables you pass in to the query are assignable to Variables 16 | * and that the Result is assignable to whatever you pass your result to. The method is never actually 17 | * implemented, but the type is valid because we list it as optional 18 | */ 19 | __ensureTypesOfVariablesAndResultMatching?: ( 20 | variables: TRequestVariables, 21 | ) => TResponseData; 22 | } 23 | -------------------------------------------------------------------------------- /src/utilities/valueFromASTUntyped.ts: -------------------------------------------------------------------------------- 1 | import { keyValMap } from '../jsutils/keyValMap.js'; 2 | import type { Maybe } from '../jsutils/Maybe.js'; 3 | import type { ObjMap } from '../jsutils/ObjMap.js'; 4 | 5 | import type { ValueNode } from '../language/ast.js'; 6 | import { Kind } from '../language/kinds.js'; 7 | 8 | /** 9 | * Produces a JavaScript value given a GraphQL Value AST. 10 | * 11 | * No type is provided. The resulting JavaScript value will reflect the 12 | * 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/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 | -------------------------------------------------------------------------------- /src/validation/__tests__/KnownFragmentNamesRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { KnownFragmentNamesRule } from '../rules/KnownFragmentNamesRule.js'; 4 | 5 | import { expectValidationErrors } from './harness.js'; 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/__tests__/KnownOperationTypesRules-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { KnownOperationTypesRule } from '../rules/KnownOperationTypesRule.js'; 4 | 5 | import { expectValidationErrors } from './harness.js'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(KnownOperationTypesRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Known operation types', () => { 16 | it('one known operation', () => { 17 | expectValid(` 18 | { field } 19 | `); 20 | }); 21 | 22 | it('unknown mutation operation', () => { 23 | expectErrors(` 24 | mutation { field } 25 | `).toDeepEqual([ 26 | { 27 | message: 'The mutation operation is not supported by the schema.', 28 | locations: [{ line: 2, column: 7 }], 29 | }, 30 | ]); 31 | }); 32 | 33 | it('unknown subscription operation', () => { 34 | expectErrors(` 35 | subscription { field } 36 | `).toDeepEqual([ 37 | { 38 | message: 'The subscription operation is not supported by the schema.', 39 | locations: [{ line: 2, column: 7 }], 40 | }, 41 | ]); 42 | }); 43 | 44 | it('mixture of known and unknown operations', () => { 45 | expectErrors(` 46 | query { field } 47 | mutation { field } 48 | subscription { field } 49 | `).toDeepEqual([ 50 | { 51 | message: 'The mutation operation is not supported by the schema.', 52 | locations: [{ line: 3, column: 7 }], 53 | }, 54 | { 55 | message: 'The subscription operation is not supported by the schema.', 56 | locations: [{ line: 4, column: 7 }], 57 | }, 58 | ]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/validation/__tests__/StreamDirectiveOnListFieldRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { StreamDirectiveOnListFieldRule } from '../rules/StreamDirectiveOnListFieldRule.js'; 4 | 5 | import { expectValidationErrors } from './harness.js'; 6 | 7 | function expectErrors(queryStr: string) { 8 | return expectValidationErrors(StreamDirectiveOnListFieldRule, queryStr); 9 | } 10 | 11 | function expectValid(queryStr: string) { 12 | expectErrors(queryStr).toDeepEqual([]); 13 | } 14 | 15 | describe('Validate: Stream directive on list field', () => { 16 | it('Stream on list field', () => { 17 | expectValid(` 18 | fragment objectFieldSelection on Human { 19 | pets @stream(initialCount: 0) { 20 | name 21 | } 22 | } 23 | `); 24 | }); 25 | 26 | it('Stream on non-null list field', () => { 27 | expectValid(` 28 | fragment objectFieldSelection on Human { 29 | relatives @stream(initialCount: 0) { 30 | name 31 | } 32 | } 33 | `); 34 | }); 35 | 36 | it("Doesn't validate other directives on list fields", () => { 37 | expectValid(` 38 | fragment objectFieldSelection on Human { 39 | pets @include(if: true) { 40 | name 41 | } 42 | } 43 | `); 44 | }); 45 | 46 | it("Doesn't validate other directives on non-list fields", () => { 47 | expectValid(` 48 | fragment objectFieldSelection on Human { 49 | pets { 50 | name @include(if: true) 51 | } 52 | } 53 | `); 54 | }); 55 | 56 | it("Doesn't validate misplaced stream directives", () => { 57 | expectValid(` 58 | fragment objectFieldSelection on Human { 59 | ... @stream(initialCount: 0) { 60 | name 61 | } 62 | } 63 | `); 64 | }); 65 | 66 | it('reports errors when stream is used on non-list field', () => { 67 | expectErrors(` 68 | fragment objectFieldSelection on Human { 69 | name @stream(initialCount: 0) 70 | } 71 | `).toDeepEqual([ 72 | { 73 | message: 74 | 'Directive "@stream" cannot be used on non-list field "Human.name".', 75 | locations: [{ line: 3, column: 14 }], 76 | }, 77 | ]); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/validation/__tests__/UniqueVariableNamesRule-test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'mocha'; 2 | 3 | import { UniqueVariableNamesRule } from '../rules/UniqueVariableNamesRule.js'; 4 | 5 | import { expectValidationErrors } from './harness.js'; 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/__tests__/ValidationContext-test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { describe, it } from 'mocha'; 3 | 4 | import { identityFunc } from '../../jsutils/identityFunc.js'; 5 | 6 | import { parse } from '../../language/parser.js'; 7 | 8 | import { GraphQLSchema } from '../../type/schema.js'; 9 | 10 | import { TypeInfo } from '../../utilities/TypeInfo.js'; 11 | 12 | import { 13 | ASTValidationContext, 14 | SDLValidationContext, 15 | ValidationContext, 16 | } from '../ValidationContext.js'; 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/DeferStreamDirectiveLabelRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { DirectiveNode } from '../../language/ast.js'; 4 | import { Kind } from '../../language/kinds.js'; 5 | import type { ASTVisitor } from '../../language/visitor.js'; 6 | 7 | import { 8 | GraphQLDeferDirective, 9 | GraphQLStreamDirective, 10 | } from '../../type/directives.js'; 11 | 12 | import type { ValidationContext } from '../ValidationContext.js'; 13 | 14 | /** 15 | * Defer and stream directive labels are unique 16 | * 17 | * A GraphQL document is only valid if defer and stream directives' label argument is static and unique. 18 | */ 19 | export function DeferStreamDirectiveLabelRule( 20 | context: ValidationContext, 21 | ): ASTVisitor { 22 | const knownLabels = new Map(); 23 | return { 24 | Directive(node) { 25 | if ( 26 | node.name.value === GraphQLDeferDirective.name || 27 | node.name.value === GraphQLStreamDirective.name 28 | ) { 29 | const labelArgument = node.arguments?.find( 30 | (arg) => arg.name.value === 'label', 31 | ); 32 | const labelValue = labelArgument?.value; 33 | if (!labelValue) { 34 | return; 35 | } 36 | if (labelValue.kind !== Kind.STRING) { 37 | context.reportError( 38 | new GraphQLError( 39 | `Argument "@${node.name.value}(label:)" must be a static string.`, 40 | { nodes: node }, 41 | ), 42 | ); 43 | return; 44 | } 45 | 46 | const knownLabel = knownLabels.get(labelValue.value); 47 | if (knownLabel != null) { 48 | context.reportError( 49 | new GraphQLError( 50 | 'Value for arguments "defer(label:)" and "stream(label:)" must be unique across all Defer/Stream directive usages.', 51 | { nodes: [knownLabel, node] }, 52 | ), 53 | ); 54 | } else { 55 | knownLabels.set(labelValue.value, node); 56 | } 57 | } 58 | }, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/validation/rules/ExecutableDefinitionsRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import { Kind } from '../../language/kinds.js'; 4 | import { isExecutableDefinitionNode } from '../../language/predicates.js'; 5 | import type { ASTVisitor } from '../../language/visitor.js'; 6 | 7 | import type { ASTValidationContext } from '../ValidationContext.js'; 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 | -------------------------------------------------------------------------------- /src/validation/rules/FragmentsOnCompositeTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import { print } from '../../language/printer.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import { isCompositeType } from '../../type/definition.js'; 7 | 8 | import { typeFromAST } from '../../utilities/typeFromAST.js'; 9 | 10 | import type { ValidationContext } from '../ValidationContext.js'; 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/validation/rules/KnownFragmentNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor.js'; 4 | 5 | import type { ValidationContext } from '../ValidationContext.js'; 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/validation/rules/KnownOperationTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor.js'; 4 | 5 | import type { ValidationContext } from '../ValidationContext.js'; 6 | 7 | /** 8 | * Known Operation Types 9 | * 10 | * A GraphQL document is only valid if when it contains an operation, 11 | * the root type for the operation exists within the schema. 12 | * 13 | * See https://spec.graphql.org/draft/#sec-Operation-Type-Existence 14 | */ 15 | export function KnownOperationTypesRule( 16 | context: ValidationContext, 17 | ): ASTVisitor { 18 | const schema = context.getSchema(); 19 | return { 20 | OperationDefinition(node) { 21 | const operation = node.operation; 22 | if (!schema.getRootType(operation)) { 23 | context.reportError( 24 | new GraphQLError( 25 | `The ${operation} operation is not supported by the schema.`, 26 | { nodes: node }, 27 | ), 28 | ); 29 | } 30 | }, 31 | }; 32 | } 33 | -------------------------------------------------------------------------------- /src/validation/rules/LoneAnonymousOperationRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import { Kind } from '../../language/kinds.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { ASTValidationContext } from '../ValidationContext.js'; 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/validation/rules/LoneSchemaDefinitionRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor.js'; 4 | 5 | import type { SDLValidationContext } from '../ValidationContext.js'; 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/NoUndefinedVariablesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { ASTVisitor } from '../../language/visitor.js'; 4 | 5 | import type { ValidationContext } from '../ValidationContext.js'; 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 | return { 19 | OperationDefinition(operation) { 20 | const variableNameDefined = new Set( 21 | operation.variableDefinitions?.map((node) => node.variable.name.value), 22 | ); 23 | 24 | const usages = context.getRecursiveVariableUsages(operation); 25 | for (const { node, fragmentVariableDefinition } of usages) { 26 | if (fragmentVariableDefinition) { 27 | continue; 28 | } 29 | const varName = node.name.value; 30 | if (!variableNameDefined.has(varName)) { 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 | } 44 | -------------------------------------------------------------------------------- /src/validation/rules/NoUnusedFragmentsRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { FragmentDefinitionNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { ASTValidationContext } from '../ValidationContext.js'; 7 | 8 | /** 9 | * No unused fragments 10 | * 11 | * A GraphQL document is only valid if all fragment definitions are spread 12 | * within operations, or spread within other fragments spread within operations. 13 | * 14 | * See https://spec.graphql.org/draft/#sec-Fragments-Must-Be-Used 15 | */ 16 | export function NoUnusedFragmentsRule( 17 | context: ASTValidationContext, 18 | ): ASTVisitor { 19 | const fragmentNameUsed = new Set(); 20 | const fragmentDefs: Array = []; 21 | 22 | return { 23 | OperationDefinition(operation) { 24 | for (const fragment of context.getRecursivelyReferencedFragments( 25 | operation, 26 | )) { 27 | fragmentNameUsed.add(fragment.name.value); 28 | } 29 | return false; 30 | }, 31 | FragmentDefinition(node) { 32 | fragmentDefs.push(node); 33 | return false; 34 | }, 35 | Document: { 36 | leave() { 37 | for (const fragmentDef of fragmentDefs) { 38 | const fragName = fragmentDef.name.value; 39 | if (!fragmentNameUsed.has(fragName)) { 40 | context.reportError( 41 | new GraphQLError(`Fragment "${fragName}" is never used.`, { 42 | nodes: fragmentDef, 43 | }), 44 | ); 45 | } 46 | } 47 | }, 48 | }, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/validation/rules/ScalarLeafsRule.ts: -------------------------------------------------------------------------------- 1 | import { inspect } from '../../jsutils/inspect.js'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError.js'; 4 | 5 | import type { FieldNode } from '../../language/ast.js'; 6 | import type { ASTVisitor } from '../../language/visitor.js'; 7 | 8 | import { getNamedType, isLeafType } from '../../type/definition.js'; 9 | 10 | import type { ValidationContext } from '../ValidationContext.js'; 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 | } else if (selectionSet.selections.length === 0) { 45 | const fieldName = node.name.value; 46 | const typeStr = inspect(type); 47 | context.reportError( 48 | new GraphQLError( 49 | `Field "${fieldName}" of type "${typeStr}" must have at least one field selected.`, 50 | { nodes: node }, 51 | ), 52 | ); 53 | } 54 | } 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/validation/rules/StreamDirectiveOnListFieldRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { DirectiveNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import { isListType, isWrappingType } from '../../type/definition.js'; 7 | import { GraphQLStreamDirective } from '../../type/directives.js'; 8 | 9 | import type { ValidationContext } from '../ValidationContext.js'; 10 | 11 | /** 12 | * Stream directives are used on list fields 13 | * 14 | * A GraphQL document is only valid if stream directives are used on list fields. 15 | */ 16 | export function StreamDirectiveOnListFieldRule( 17 | context: ValidationContext, 18 | ): ASTVisitor { 19 | return { 20 | Directive(node: DirectiveNode) { 21 | const fieldDef = context.getFieldDef(); 22 | const parentType = context.getParentType(); 23 | if ( 24 | fieldDef && 25 | parentType && 26 | node.name.value === GraphQLStreamDirective.name && 27 | !( 28 | isListType(fieldDef.type) || 29 | (isWrappingType(fieldDef.type) && isListType(fieldDef.type.ofType)) 30 | ) 31 | ) { 32 | context.reportError( 33 | new GraphQLError( 34 | `Directive "@stream" cannot be used on non-list field "${parentType}.${fieldDef.name}".`, 35 | { nodes: node }, 36 | ), 37 | ); 38 | } 39 | }, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueArgumentNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../../jsutils/groupBy.js'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError.js'; 4 | 5 | import type { ArgumentNode } from '../../language/ast.js'; 6 | import type { ASTVisitor } from '../../language/visitor.js'; 7 | 8 | import type { ASTValidationContext } from '../ValidationContext.js'; 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 | undefined; 28 | }) { 29 | const argumentNodes = parentNode.arguments ?? []; 30 | 31 | const seenArgs = groupBy(argumentNodes, (arg) => arg.name.value); 32 | 33 | for (const [argName, argNodes] of seenArgs) { 34 | if (argNodes.length > 1) { 35 | context.reportError( 36 | new GraphQLError( 37 | `There can be only one argument named "${argName}".`, 38 | { nodes: argNodes.map((node) => node.name) }, 39 | ), 40 | ); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueDirectiveNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { NameNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { SDLValidationContext } from '../ValidationContext.js'; 7 | 8 | /** 9 | * Unique directive names 10 | * 11 | * A GraphQL document is only valid if all defined directives have unique names. 12 | */ 13 | export function UniqueDirectiveNamesRule( 14 | context: SDLValidationContext, 15 | ): ASTVisitor { 16 | const knownDirectiveNames = new Map(); 17 | const schema = context.getSchema(); 18 | 19 | return { 20 | DirectiveDefinition(node) { 21 | const directiveName = node.name.value; 22 | 23 | if (schema?.getDirective(directiveName)) { 24 | context.reportError( 25 | new GraphQLError( 26 | `Directive "@${directiveName}" already exists in the schema. It cannot be redefined.`, 27 | { nodes: node.name }, 28 | ), 29 | ); 30 | return; 31 | } 32 | 33 | const knownName = knownDirectiveNames.get(directiveName); 34 | if (knownName) { 35 | context.reportError( 36 | new GraphQLError( 37 | `There can be only one directive named "@${directiveName}".`, 38 | { nodes: [knownName, node.name] }, 39 | ), 40 | ); 41 | } else { 42 | knownDirectiveNames.set(directiveName, node.name); 43 | } 44 | 45 | return false; 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueFragmentNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { NameNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { ASTValidationContext } from '../ValidationContext.js'; 7 | 8 | /** 9 | * Unique fragment names 10 | * 11 | * A GraphQL document is only valid if all defined fragments have unique names. 12 | * 13 | * See https://spec.graphql.org/draft/#sec-Fragment-Name-Uniqueness 14 | */ 15 | export function UniqueFragmentNamesRule( 16 | context: ASTValidationContext, 17 | ): ASTVisitor { 18 | const knownFragmentNames = new Map(); 19 | return { 20 | OperationDefinition: () => false, 21 | FragmentDefinition(node) { 22 | const fragmentName = node.name.value; 23 | const knownFragmentName = knownFragmentNames.get(fragmentName); 24 | if (knownFragmentName != null) { 25 | context.reportError( 26 | new GraphQLError( 27 | `There can be only one fragment named "${fragmentName}".`, 28 | { nodes: [knownFragmentName, node.name] }, 29 | ), 30 | ); 31 | } else { 32 | knownFragmentNames.set(fragmentName, node.name); 33 | } 34 | return false; 35 | }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueInputFieldNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { invariant } from '../../jsutils/invariant.js'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError.js'; 4 | 5 | import type { NameNode } from '../../language/ast.js'; 6 | import type { ASTVisitor } from '../../language/visitor.js'; 7 | 8 | import type { ASTValidationContext } from '../ValidationContext.js'; 9 | 10 | /** 11 | * Unique input field names 12 | * 13 | * A GraphQL input object value is only valid if all supplied fields are 14 | * uniquely named. 15 | * 16 | * See https://spec.graphql.org/draft/#sec-Input-Object-Field-Uniqueness 17 | */ 18 | export function UniqueInputFieldNamesRule( 19 | context: ASTValidationContext, 20 | ): ASTVisitor { 21 | const knownNameStack: Array> = []; 22 | let knownNames = new Map(); 23 | 24 | return { 25 | ObjectValue: { 26 | enter() { 27 | knownNameStack.push(knownNames); 28 | knownNames = new Map(); 29 | }, 30 | leave() { 31 | const prevKnownNames = knownNameStack.pop(); 32 | invariant(prevKnownNames != null); 33 | knownNames = prevKnownNames; 34 | }, 35 | }, 36 | ObjectField(node) { 37 | const fieldName = node.name.value; 38 | const knownName = knownNames.get(fieldName); 39 | if (knownName != null) { 40 | context.reportError( 41 | new GraphQLError( 42 | `There can be only one input field named "${fieldName}".`, 43 | { nodes: [knownName, node.name] }, 44 | ), 45 | ); 46 | } else { 47 | knownNames.set(fieldName, node.name); 48 | } 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueOperationNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { NameNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { ASTValidationContext } from '../ValidationContext.js'; 7 | 8 | /** 9 | * Unique operation names 10 | * 11 | * A GraphQL document is only valid if all defined operations have unique names. 12 | * 13 | * See https://spec.graphql.org/draft/#sec-Operation-Name-Uniqueness 14 | */ 15 | export function UniqueOperationNamesRule( 16 | context: ASTValidationContext, 17 | ): ASTVisitor { 18 | const knownOperationNames = new Map(); 19 | return { 20 | OperationDefinition(node) { 21 | const operationName = node.name; 22 | if (operationName != null) { 23 | const knownOperationName = knownOperationNames.get(operationName.value); 24 | if (knownOperationName != null) { 25 | context.reportError( 26 | new GraphQLError( 27 | `There can be only one operation named "${operationName.value}".`, 28 | { nodes: [knownOperationName, operationName] }, 29 | ), 30 | ); 31 | } else { 32 | knownOperationNames.set(operationName.value, operationName); 33 | } 34 | } 35 | return false; 36 | }, 37 | FragmentDefinition: () => false, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueOperationTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { 4 | OperationTypeDefinitionNode, 5 | SchemaDefinitionNode, 6 | SchemaExtensionNode, 7 | } from '../../language/ast.js'; 8 | import type { ASTVisitor } from '../../language/visitor.js'; 9 | 10 | import type { SDLValidationContext } from '../ValidationContext.js'; 11 | 12 | /** 13 | * Unique operation types 14 | * 15 | * A GraphQL document is only valid if it has only one type per operation. 16 | */ 17 | export function UniqueOperationTypesRule( 18 | context: SDLValidationContext, 19 | ): ASTVisitor { 20 | const schema = context.getSchema(); 21 | const definedOperationTypes = new Map(); 22 | const existingOperationTypes = schema 23 | ? { 24 | query: schema.getQueryType(), 25 | mutation: schema.getMutationType(), 26 | subscription: schema.getSubscriptionType(), 27 | } 28 | : {}; 29 | 30 | return { 31 | SchemaDefinition: checkOperationTypes, 32 | SchemaExtension: checkOperationTypes, 33 | }; 34 | 35 | function checkOperationTypes( 36 | node: SchemaDefinitionNode | SchemaExtensionNode, 37 | ) { 38 | const operationTypesNodes = node.operationTypes ?? []; 39 | 40 | for (const operationType of operationTypesNodes) { 41 | const operation = operationType.operation; 42 | const alreadyDefinedOperationType = definedOperationTypes.get(operation); 43 | 44 | if (existingOperationTypes[operation]) { 45 | context.reportError( 46 | new GraphQLError( 47 | `Type for ${operation} already defined in the schema. It cannot be redefined.`, 48 | { nodes: operationType }, 49 | ), 50 | ); 51 | } else if (alreadyDefinedOperationType) { 52 | context.reportError( 53 | new GraphQLError( 54 | `There can be only one ${operation} type in schema.`, 55 | { nodes: [alreadyDefinedOperationType, operationType] }, 56 | ), 57 | ); 58 | } else { 59 | definedOperationTypes.set(operation, operationType); 60 | } 61 | } 62 | 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueTypeNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { NameNode, TypeDefinitionNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { SDLValidationContext } from '../ValidationContext.js'; 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 = new Map(); 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 | const knownNameNode = knownTypeNames.get(typeName); 40 | if (knownNameNode != null) { 41 | context.reportError( 42 | new GraphQLError(`There can be only one type named "${typeName}".`, { 43 | nodes: [knownNameNode, node.name], 44 | }), 45 | ); 46 | } else { 47 | knownTypeNames.set(typeName, node.name); 48 | } 49 | 50 | return false; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/validation/rules/UniqueVariableNamesRule.ts: -------------------------------------------------------------------------------- 1 | import { groupBy } from '../../jsutils/groupBy.js'; 2 | 3 | import { GraphQLError } from '../../error/GraphQLError.js'; 4 | 5 | import type { ASTVisitor } from '../../language/visitor.js'; 6 | 7 | import type { ASTValidationContext } from '../ValidationContext.js'; 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 | const variableDefinitions = operationNode.variableDefinitions ?? []; 20 | 21 | const seenVariableDefinitions = groupBy( 22 | variableDefinitions, 23 | (node) => node.variable.name.value, 24 | ); 25 | 26 | for (const [variableName, variableNodes] of seenVariableDefinitions) { 27 | if (variableNodes.length > 1) { 28 | context.reportError( 29 | new GraphQLError( 30 | `There can be only one variable named "$${variableName}".`, 31 | { nodes: variableNodes.map((node) => node.variable.name) }, 32 | ), 33 | ); 34 | } 35 | } 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/validation/rules/ValuesOfCorrectTypeRule.ts: -------------------------------------------------------------------------------- 1 | import type { Maybe } from '../../jsutils/Maybe.js'; 2 | 3 | import type { ValueNode } from '../../language/ast.js'; 4 | import type { ASTVisitor } from '../../language/visitor.js'; 5 | 6 | import type { GraphQLInputType } from '../../type/index.js'; 7 | 8 | import { validateInputLiteral } from '../../utilities/validateInputValue.js'; 9 | 10 | import type { ValidationContext } from '../ValidationContext.js'; 11 | 12 | /** 13 | * Value literals of correct type 14 | * 15 | * A GraphQL document is only valid if all value literals are of the type 16 | * expected at their position. 17 | * 18 | * See https://spec.graphql.org/draft/#sec-Values-of-Correct-Type 19 | */ 20 | export function ValuesOfCorrectTypeRule( 21 | context: ValidationContext, 22 | ): ASTVisitor { 23 | return { 24 | NullValue: (node) => 25 | isValidValueNode(context, node, context.getInputType()), 26 | ListValue: (node) => 27 | // Note: TypeInfo will traverse into a list's item type, so look to the 28 | // parent input type to check if it is a list. 29 | isValidValueNode(context, node, context.getParentInputType()), 30 | ObjectValue: (node) => 31 | isValidValueNode(context, node, context.getInputType()), 32 | EnumValue: (node) => 33 | isValidValueNode(context, node, context.getInputType()), 34 | IntValue: (node) => isValidValueNode(context, node, context.getInputType()), 35 | FloatValue: (node) => 36 | isValidValueNode(context, node, context.getInputType()), 37 | StringValue: (node) => 38 | isValidValueNode(context, node, context.getInputType()), 39 | BooleanValue: (node) => 40 | isValidValueNode(context, node, context.getInputType()), 41 | }; 42 | } 43 | 44 | /** 45 | * Any value literal may be a valid representation of a Scalar, depending on 46 | * that scalar type. 47 | */ 48 | function isValidValueNode( 49 | context: ValidationContext, 50 | node: ValueNode, 51 | inputType: Maybe, 52 | ): false { 53 | if (inputType) { 54 | validateInputLiteral( 55 | node, 56 | inputType, 57 | (error) => { 58 | context.reportError(error); 59 | }, 60 | undefined, 61 | undefined, 62 | context.hideSuggestions, 63 | ); 64 | } 65 | return false; 66 | } 67 | -------------------------------------------------------------------------------- /src/validation/rules/VariablesAreInputTypesRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../error/GraphQLError.js'; 2 | 3 | import type { VariableDefinitionNode } from '../../language/ast.js'; 4 | import { print } from '../../language/printer.js'; 5 | import type { ASTVisitor } from '../../language/visitor.js'; 6 | 7 | import { isInputType } from '../../type/definition.js'; 8 | 9 | import { typeFromAST } from '../../utilities/typeFromAST.js'; 10 | 11 | import type { ValidationContext } from '../ValidationContext.js'; 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 | -------------------------------------------------------------------------------- /src/validation/rules/custom/NoSchemaIntrospectionCustomRule.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from '../../../error/GraphQLError.js'; 2 | 3 | import type { FieldNode } from '../../../language/ast.js'; 4 | import type { ASTVisitor } from '../../../language/visitor.js'; 5 | 6 | import { getNamedType } from '../../../type/definition.js'; 7 | import { isIntrospectionType } from '../../../type/introspection.js'; 8 | 9 | import type { ValidationContext } from '../../ValidationContext.js'; 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/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.8' 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.8' as string | null, 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src/**/*", 4 | "integrationTests/*", 5 | "resources/*", 6 | "benchmark/benchmark.ts" 7 | ], 8 | "compilerOptions": { 9 | "lib": [ 10 | "es2022", 11 | "dom" // Workaround for missing web-compatible globals in `@types/node` 12 | ], 13 | "target": "es2021", 14 | "sourceMap": true, 15 | "inlineSources": true, 16 | "module": "es2022", 17 | "moduleResolution": "node", 18 | "noEmit": true, 19 | "isolatedModules": true, 20 | "verbatimModuleSyntax": true, 21 | "forceConsistentCasingInFileNames": true, 22 | 23 | // Type Checking 24 | // https://www.typescriptlang.org/tsconfig#Type_Checking_6248 25 | "strict": true, 26 | "useUnknownInCatchVariables": false, // FIXME part of 'strict' but is temporary disabled 27 | // All checks that are not part of "strict" 28 | "allowUnreachableCode": false, 29 | "allowUnusedLabels": false, 30 | "exactOptionalPropertyTypes": true, 31 | "noFallthroughCasesInSwitch": false, // TODO consider 32 | "noImplicitOverride": true, 33 | "noImplicitReturns": false, // TODO consider 34 | "noPropertyAccessFromIndexSignature": false, // TODO consider 35 | "noUncheckedIndexedAccess": false, // FIXME 36 | "noUnusedLocals": true, 37 | "noUnusedParameters": true, 38 | "allowSyntheticDefaultImports": true 39 | }, 40 | "ts-node": { 41 | "esm": true 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /website/docs/tutorials/defer-stream.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enabling Defer & Stream 3 | sidebar_label: Enabling Defer & Stream 4 | --- 5 | 6 | The `@defer` and `@stream` directives are not enabled by default. In order to use these directives, you must add them to your GraphQL Schema and use the `experimentalExecuteIncrementally` function instead of `execute`. 7 | 8 | ```js 9 | import { 10 | GraphQLSchema, 11 | GraphQLDeferDirective, 12 | GraphQLStreamDirective, 13 | specifiedDirectives, 14 | } from 'graphql'; 15 | 16 | const schema = new GraphQLSchema({ 17 | query, 18 | directives: [ 19 | ...specifiedDirectives, 20 | GraphQLDeferDirective, 21 | GraphQLStreamDirective, 22 | ], 23 | }); 24 | 25 | const result = experimentalExecuteIncrementally({ 26 | schema, 27 | document, 28 | }); 29 | ``` 30 | 31 | If the `directives` option is passed to `GraphQLSchema`, the default directives will not be included. `specifiedDirectives` must be passed to ensure all standard directives are added in addition to `defer` & `stream`. 32 | -------------------------------------------------------------------------------- /website/icons/discord.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /website/icons/github.svg: -------------------------------------------------------------------------------- 1 | 7 | 12 | -------------------------------------------------------------------------------- /website/icons/graphql.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /website/icons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as DiscordIcon } from './discord.svg'; 2 | export { default as GitHubIcon } from './github.svg'; 3 | export { default as GraphQLLogo } from './graphql.svg'; 4 | export { default as GraphQLWordmarkLogo } from './graphql-wordmark.svg'; 5 | export { default as StackOverflowIcon } from './stackoverflow.svg'; 6 | export { default as TwitterIcon } from './twitter.svg'; 7 | -------------------------------------------------------------------------------- /website/icons/stackoverflow.svg: -------------------------------------------------------------------------------- 1 | 2 | 8 | 12 | -------------------------------------------------------------------------------- /website/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable camelcase */ 2 | import path from 'node:path'; 3 | 4 | import nextra from 'nextra'; 5 | 6 | const withNextra = nextra({ 7 | theme: 'nextra-theme-docs', 8 | themeConfig: './theme.config.tsx', 9 | }); 10 | 11 | const sep = path.sep === '/' ? '/' : '\\\\'; 12 | 13 | const ALLOWED_SVG_REGEX = new RegExp(`${sep}icons${sep}.+\\.svg$`); 14 | 15 | /** 16 | * @type {import('next').NextConfig} 17 | */ 18 | export default withNextra({ 19 | webpack(config) { 20 | const fileLoaderRule = config.module.rules.find((rule) => 21 | rule.test?.test?.('.svg'), 22 | ); 23 | 24 | fileLoaderRule.exclude = ALLOWED_SVG_REGEX; 25 | 26 | config.module.rules.push({ 27 | test: ALLOWED_SVG_REGEX, 28 | use: ['@svgr/webpack'], 29 | }); 30 | return config; 31 | }, 32 | output: 'export', 33 | images: { 34 | loader: 'custom', 35 | imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], 36 | deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], 37 | }, 38 | transpilePackages: ['next-image-export-optimizer'], 39 | env: { 40 | nextImageExportOptimizer_imageFolderPath: 'public/images', 41 | nextImageExportOptimizer_exportFolderPath: 'out', 42 | nextImageExportOptimizer_quality: '75', 43 | nextImageExportOptimizer_storePicturesInWEBP: 'true', 44 | nextImageExportOptimizer_exportFolderName: 'nextImageExportOptimizer', 45 | // If you do not want to use blurry placeholder images, then you can set 46 | // nextImageExportOptimizer_generateAndUseBlurImages to false and pass 47 | // `placeholder="empty"` to all components. 48 | nextImageExportOptimizer_generateAndUseBlurImages: 'true', 49 | // If you want to cache the remote images, you can set the time to live of the cache in seconds. 50 | // The default value is 0 seconds. 51 | nextImageExportOptimizer_remoteImageCacheTTL: '0', 52 | }, 53 | trailingSlash: true, 54 | }); 55 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "description": "The GraphQL.JS documentation website", 5 | "type": "module", 6 | "private": true, 7 | "directories": { 8 | "doc": "docs" 9 | }, 10 | "scripts": { 11 | "build": "next build", 12 | "dev": "next" 13 | }, 14 | "devDependencies": { 15 | "@svgr/webpack": "^8.1.0", 16 | "@tailwindcss/typography": "^0.5.10", 17 | "@types/node": "^22.7.5", 18 | "autoprefixer": "^10.4.20", 19 | "next": "^14.2.15", 20 | "nextra": "^3.0.13", 21 | "nextra-theme-docs": "^3.0.13", 22 | "postcss": "^8.4.47", 23 | "react": "^18.3.1", 24 | "react-dom": "^18.3.1", 25 | "tailwindcss": "^3.4.14", 26 | "typescript": "^5.6.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /website/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app'; 2 | import { Roboto_Flex, Roboto_Mono } from 'next/font/google'; 3 | 4 | import '../css/globals.css'; 5 | 6 | const robotoFlex = Roboto_Flex({ 7 | subsets: ['latin'], 8 | }); 9 | 10 | const robotoMono = Roboto_Mono({ 11 | subsets: ['latin'], 12 | }); 13 | 14 | // TODO: do we need google analytics? 15 | 16 | export default function App({ Component, pageProps }: AppProps) { 17 | return ( 18 | <> 19 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /website/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /website/pages/_meta.ts: -------------------------------------------------------------------------------- 1 | const meta = { 2 | index: '', 3 | '-- 1': { 4 | type: 'separator', 5 | title: 'GraphQL.JS Tutorial', 6 | }, 7 | 'getting-started': '', 8 | 'running-an-express-graphql-server': '', 9 | 'graphql-clients': '', 10 | 'basic-types': '', 11 | 'passing-arguments': '', 12 | 'object-types': '', 13 | 'mutations-and-input-types': '', 14 | 'authentication-and-express-middleware': '', 15 | '-- 2': { 16 | type: 'separator', 17 | title: 'Advanced Guides', 18 | }, 19 | 'constructing-types': '', 20 | 'oneof-input-objects': 'OneOf input objects', 21 | 'defer-stream': '', 22 | '-- 3': { 23 | type: 'separator', 24 | title: 'FAQ', 25 | }, 26 | 'going-to-production': '', 27 | 'api-v16': { 28 | type: 'menu', 29 | title: 'API', 30 | items: { 31 | 2: { 32 | title: 'V16', 33 | href: '/api-v16/graphql', 34 | }, 35 | }, 36 | }, 37 | }; 38 | 39 | export default meta; 40 | -------------------------------------------------------------------------------- /website/pages/api-v16/_meta.ts: -------------------------------------------------------------------------------- 1 | const meta = { 2 | graphql: '', 3 | error: '', 4 | execution: '', 5 | language: '', 6 | type: '', 7 | utilities: '', 8 | validation: '', 9 | 'graphql-http': '', 10 | }; 11 | 12 | export default meta; 13 | -------------------------------------------------------------------------------- /website/pages/api-v16/graphql-http.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: graphql-http 3 | --- 4 | 5 | {/* title can be removed in Nextra 4, since sidebar title will take from first h1 */} 6 | 7 | # `graphql-http` 8 | 9 | The [official `graphql-http` package](https://github.com/graphql/graphql-http) provides a simple way to create a fully compliant GraphQL server. It has a handler for Node.js native [`http`](https://nodejs.org/api/http.html), together with handlers for well-known frameworks like [Express](https://expressjs.com/), [Fastify](https://www.fastify.io/) and [Koa](https://koajs.com/); as well as handlers for different runtimes like [Deno](https://deno.land/) and [Bun](https://bun.sh/). 10 | 11 | ## Express 12 | 13 | ```js 14 | import { createHandler } from 'graphql-http/lib/use/express'; // ES6 15 | const { createHandler } = require('graphql-http/lib/use/express'); // CommonJS 16 | ``` 17 | 18 | ### createHandler 19 | 20 | ```ts 21 | function createHandler({ 22 | schema, 23 | rootValue, 24 | context, 25 | formatError, 26 | validationRules, 27 | }: { 28 | rootValue?: any; 29 | context?: any; 30 | formatError?: Function; 31 | validationRules?: any[]; 32 | }): Handler; 33 | ``` 34 | 35 | Constructs an Express handler based on a GraphQL schema. 36 | 37 | See the [tutorial](/running-an-express-graphql-server/) for sample usage. 38 | 39 | See the [GitHub README](https://github.com/graphql/graphql-http) for more extensive documentation, including how to use `graphql-http` with other server frameworks and runtimes. 40 | -------------------------------------------------------------------------------- /website/pages/api-v16/validation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: graphql/validation 3 | --- 4 | 5 | {/* title can be removed in Nextra 4, since sidebar title will take from first h1 */} 6 | 7 | # `graphql/validation` 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 | const { validate } = require('graphql/validation'); // CommonJS 15 | ``` 16 | 17 | ## Overview 18 | 19 | 32 | 33 | ## Validation 34 | 35 | ### `validate` 36 | 37 | ```ts 38 | function validate( 39 | schema: GraphQLSchema, 40 | ast: Document, 41 | rules?: any[], 42 | ): GraphQLError[]; 43 | ``` 44 | 45 | Implements the "Validation" section of the spec. 46 | 47 | Validation runs synchronously, returning an array of encountered errors, or 48 | an empty array if no errors were encountered and the document is valid. 49 | 50 | A list of specific validation rules may be provided. If not provided, the 51 | default list of rules defined by the GraphQL specification will be used. 52 | 53 | Each validation rules is a function which returns a visitor 54 | (see the language/visitor API). Visitor methods are expected to return 55 | GraphQLErrors, or Arrays of GraphQLErrors when invalid. 56 | 57 | Visitors can also supply `visitSpreadFragments: true` which will alter the 58 | behavior of the visitor to skip over top level defined fragments, and instead 59 | visit those fragments at every point a spread is encountered. 60 | 61 | ### `specifiedRules` 62 | 63 | ```ts 64 | let specifiedRules: Array<(context: ValidationContext) => any>; 65 | ``` 66 | 67 | This set includes all validation rules defined by the GraphQL spec 68 | -------------------------------------------------------------------------------- /website/pages/defer-stream.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Enabling Defer & Stream 3 | --- 4 | 5 | import { Callout } from 'nextra/components' 6 | 7 | 8 | These exports are only available in v17 and beyond. 9 | 10 | 11 | The `@defer` and `@stream` directives are not enabled by default. 12 | In order to use these directives, you must add them to your GraphQL Schema and 13 | use the `experimentalExecuteIncrementally` function instead of `execute`. 14 | 15 | ```js 16 | import { 17 | GraphQLSchema, 18 | GraphQLDeferDirective, 19 | GraphQLStreamDirective, 20 | specifiedDirectives, 21 | } from 'graphql'; 22 | 23 | const schema = new GraphQLSchema({ 24 | query, 25 | directives: [ 26 | ...specifiedDirectives, 27 | GraphQLDeferDirective, 28 | GraphQLStreamDirective, 29 | ], 30 | }); 31 | 32 | const result = experimentalExecuteIncrementally({ 33 | schema, 34 | document, 35 | }); 36 | ``` 37 | 38 | If the `directives` option is passed to `GraphQLSchema`, the default directives will not be included. `specifiedDirectives` must be passed to ensure all standard directives are added in addition to `defer` & `stream`. 39 | -------------------------------------------------------------------------------- /website/pages/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | sidebarTitle: Overview 4 | --- 5 | 6 | GraphQL.JS is the reference implementation to the [GraphQL Specification](https://spec.graphql.org/draft/), it's designed to be simple to use and easy to understand 7 | while closely following the Specification. 8 | 9 | You can build GraphQL servers, clients, and tools with this library, it's designed so you can choose which parts you use, for example, you can build your own parser 10 | and use the execution/validation from the library. There also a lot of useful utilities for schema-diffing, working with arguments and [many more](./utilities.mdx). 11 | 12 | In the following chapters you'll find out more about the three critical pieces of this library 13 | 14 | - The GraphQL language 15 | - Document validation 16 | - GraphQL Execution 17 | 18 | You can also code along on [a tutorial](./getting-started.mdx). 19 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import typography from '@tailwindcss/typography'; 2 | 3 | const config = { 4 | content: [ 5 | './pages/**/*.{ts,tsx,mdx}', 6 | './icons/**/*.{ts,tsx,mdx}', 7 | './css/**/*.css', 8 | './theme.config.tsx', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: '1rem', 14 | }, 15 | extend: { 16 | colors: { 17 | primary: '#e10098', 18 | 'conf-black': '#0e031c', 19 | black: '#1b1b1b', 20 | }, 21 | backgroundImage: { 22 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 23 | 'gradient-conic': 24 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 25 | }, 26 | animation: { 27 | scroll: 28 | 'scroll var(--animation-duration, 40s) var(--animation-direction, forwards) linear infinite', 29 | }, 30 | keyframes: { 31 | scroll: { 32 | to: { 33 | transform: 'translate(calc(-50% - .5rem))', 34 | }, 35 | }, 36 | }, 37 | }, 38 | }, 39 | plugins: [typography], 40 | darkMode: ['class', 'html[class~="dark"]'], 41 | }; 42 | 43 | export default config; 44 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": false, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "strictNullChecks": true, 21 | "module": "esnext" 22 | }, 23 | "include": ["next-env.d.mts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"], 24 | "exclude": ["node_modules"] 25 | } 26 | --------------------------------------------------------------------------------