From b286cd5e71606b6eb0a897cf071424b29297b9c8 Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 18 May 2026 17:02:21 +0300 Subject: [PATCH 1/2] docs: update banner (#4748) --- website/theme.config.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/website/theme.config.tsx b/website/theme.config.tsx index 5d8d5ff3a7..270d99a1eb 100644 --- a/website/theme.config.tsx +++ b/website/theme.config.tsx @@ -212,16 +212,23 @@ const cfg: DocsThemeConfig = { banner: { content: ( <> - 🎬 That's a Wrap for GraphQLConf 2024! • Watch the Videos •{' '} + 🎉{' '} - Check out the recorded talks and workshops + Explore GraphQLConf 2026 + {' '} + • May 19-21 • Fremont, CA •{' '} + + View the schedule ), - key: 'graphqlconf-2024', + key: 'graphqlconf-2026', }, logo: graphQLLogo, docsRepositoryBase: From 8efb9fd215322e846c298ca2bac345ac17ca961c Mon Sep 17 00:00:00 2001 From: Yaacov Rydzinski Date: Mon, 18 May 2026 17:05:12 +0300 Subject: [PATCH 2/2] docs: add v16 API docs lint coverage (#4741) Initial commit of v16 inline docs. --- .eslintrc.yml | 278 +- cspell.yml | 18 + package-lock.json | 201 ++ package.json | 1 + resources/diff-npm-package.js | 1 + resources/eslint-internal-rules/index.js | 4 + .../eslint-internal-rules/jsdoc-utils.js | 98 + .../require-graphql-public-api-docs.js | 215 ++ .../require-public-api-exports.js | 488 ++++ src/README.md | 3 +- src/error/GraphQLError.ts | 172 +- src/error/index.ts | 7 + src/error/locatedError.ts | 21 + src/error/syntaxError.ts | 16 + src/execution/collectFields.ts | 6 + src/execution/execute.ts | 253 +- src/execution/index.ts | 7 + src/execution/mapAsyncIterator.ts | 2 + src/execution/subscribe.ts | 209 +- src/execution/values.ts | 176 +- src/graphql.ts | 204 +- src/index.ts | 32 +- src/jsutils/Maybe.ts | 6 +- src/jsutils/ObjMap.ts | 4 + src/jsutils/Path.ts | 31 + src/jsutils/PromiseOrValue.ts | 1 + src/jsutils/devAssert.ts | 1 + src/jsutils/didYouMean.ts | 6 +- src/jsutils/groupBy.ts | 2 + src/jsutils/identityFunc.ts | 2 + src/jsutils/inspect.ts | 2 + src/jsutils/instanceOf.ts | 2 + src/jsutils/invariant.ts | 1 + src/jsutils/isAsyncIterable.ts | 2 + src/jsutils/isIterableObject.ts | 11 +- src/jsutils/isObjectLike.ts | 2 + src/jsutils/isPromise.ts | 2 + src/jsutils/keyMap.ts | 21 +- src/jsutils/keyValMap.ts | 15 +- src/jsutils/mapValue.ts | 2 + src/jsutils/memoize3.ts | 2 + src/jsutils/naturalCompare.ts | 2 + src/jsutils/printPathArray.ts | 2 + src/jsutils/promiseForObject.ts | 2 + src/jsutils/promiseReduce.ts | 2 + src/jsutils/suggestionList.ts | 4 + src/jsutils/toError.ts | 2 + src/jsutils/toObjMap.ts | 1 + src/language/ast.ts | 477 ++- src/language/blockString.ts | 4 +- src/language/directiveLocation.ts | 35 +- src/language/index.ts | 7 + src/language/kinds.ts | 74 +- src/language/lexer.ts | 86 +- src/language/location.ts | 20 +- src/language/parser.ts | 234 +- src/language/predicates.ts | 170 ++ src/language/printLocation.ts | 29 + src/language/printString.ts | 3 + src/language/printer.ts | 19 + src/language/schemaCoordinateLexer.ts | 16 +- src/language/source.ts | 29 + src/language/tokenKind.ts | 32 +- src/language/visitor.ts | 274 +- src/subscription/README.md | 2 +- src/subscription/index.ts | 12 +- src/type/assertName.ts | 20 +- src/type/definition.ts | 2594 ++++++++++++++++- src/type/directives.ts | 214 +- src/type/index.ts | 7 + src/type/introspection.ts | 37 + src/type/scalars.ts | 32 +- src/type/schema.ts | 495 +++- src/type/validate.ts | 32 + src/utilities/TypeInfo.ts | 444 ++- src/utilities/assertValidName.ts | 30 +- src/utilities/astFromValue.ts | 35 + src/utilities/buildASTSchema.ts | 72 +- src/utilities/buildClientSchema.ts | 16 + src/utilities/coerceInputValue.ts | 47 + src/utilities/concatAST.ts | 13 + src/utilities/extendSchema.ts | 66 +- src/utilities/findBreakingChanges.ts | 96 + src/utilities/getIntrospectionQuery.ts | 133 + src/utilities/getOperationAST.ts | 16 + src/utilities/getOperationRootType.ts | 19 +- src/utilities/index.ts | 8 + src/utilities/introspectionFromSchema.ts | 55 + src/utilities/lexicographicSortSchema.ts | 36 + src/utilities/printSchema.ts | 66 + src/utilities/resolveSchemaCoordinate.ts | 73 + src/utilities/separateOperations.ts | 31 + src/utilities/stripIgnoredCharacters.ts | 14 + src/utilities/typeComparators.ts | 85 + src/utilities/typeFromAST.ts | 88 + src/utilities/typedQueryDocumentNode.ts | 5 + src/utilities/valueFromAST.ts | 39 + src/utilities/valueFromASTUntyped.ts | 18 +- src/validation/ValidationContext.ts | 400 +++ src/validation/index.ts | 7 + .../rules/ExecutableDefinitionsRule.ts | 29 + .../rules/FieldsOnCorrectTypeRule.ts | 33 + .../rules/FragmentsOnCompositeTypesRule.ts | 29 + .../rules/KnownArgumentNamesRule.ts | 33 +- src/validation/rules/KnownDirectivesRule.ts | 29 + .../rules/KnownFragmentNamesRule.ts | 29 + src/validation/rules/KnownTypeNamesRule.ts | 29 + .../rules/LoneAnonymousOperationRule.ts | 29 + .../rules/LoneSchemaDefinitionRule.ts | 22 + .../rules/MaxIntrospectionDepthRule.ts | 34 + src/validation/rules/NoFragmentCyclesRule.ts | 29 + .../rules/NoUndefinedVariablesRule.ts | 29 + src/validation/rules/NoUnusedFragmentsRule.ts | 29 + src/validation/rules/NoUnusedVariablesRule.ts | 30 + .../rules/OverlappingFieldsCanBeMergedRule.ts | 53 +- .../rules/PossibleFragmentSpreadsRule.ts | 37 + .../rules/PossibleTypeExtensionsRule.ts | 22 + .../rules/ProvidedRequiredArgumentsRule.ts | 33 +- src/validation/rules/ScalarLeafsRule.ts | 29 + .../rules/SingleFieldSubscriptionsRule.ts | 34 + .../UniqueArgumentDefinitionNamesRule.ts | 22 + .../rules/UniqueArgumentNamesRule.ts | 29 + .../rules/UniqueDirectiveNamesRule.ts | 22 + .../rules/UniqueDirectivesPerLocationRule.ts | 29 + .../rules/UniqueEnumValueNamesRule.ts | 22 + .../rules/UniqueFieldDefinitionNamesRule.ts | 22 + .../rules/UniqueFragmentNamesRule.ts | 29 + .../rules/UniqueInputFieldNamesRule.ts | 33 + .../rules/UniqueOperationNamesRule.ts | 29 + .../rules/UniqueOperationTypesRule.ts | 22 + src/validation/rules/UniqueTypeNamesRule.ts | 22 + .../rules/UniqueVariableNamesRule.ts | 29 + .../rules/ValuesOfCorrectTypeRule.ts | 31 + .../rules/VariablesAreInputTypesRule.ts | 33 + .../rules/VariablesInAllowedPositionRule.ts | 36 +- .../rules/custom/NoDeprecatedCustomRule.ts | 42 + .../custom/NoSchemaIntrospectionCustomRule.ts | 29 + src/validation/specifiedRules.ts | 6 +- src/validation/validate.ts | 78 +- src/version.ts | 2 + tsdoc.json | 22 + 141 files changed, 10145 insertions(+), 473 deletions(-) create mode 100644 resources/eslint-internal-rules/jsdoc-utils.js create mode 100644 resources/eslint-internal-rules/require-graphql-public-api-docs.js create mode 100644 resources/eslint-internal-rules/require-public-api-exports.js create mode 100644 tsdoc.json diff --git a/.eslintrc.yml b/.eslintrc.yml index b548dcad93..5851852a3b 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -7,10 +7,37 @@ plugins: - internal-rules - node - import + - jsdoc - simple-import-sort settings: node: tryExtensions: ['.js', '.jsx', '.json', '.node', '.ts', '.d.ts'] + jsdoc: + mode: typescript + ignoreInternal: true + ignorePrivate: true + structuredTags: + category: + name: text + type: false + required: + - name + packageDocumentation: + name: false + type: false + remarks: + name: false + type: false + defaultValue: + name: text + type: false + typeParam: + name: namepath-defining + type: false + required: + - name + tagNamePreference: + template: typeParam rules: ############################################################################## @@ -655,10 +682,256 @@ overrides: '@typescript-eslint/type-annotation-spacing': off - files: 'src/**' rules: + internal-rules/require-graphql-public-api-docs: error + internal-rules/require-public-api-exports: error internal-rules/require-to-string-tag: error - - files: 'src/**/__*__/**' + + ########################################################################## + # `eslint-plugin-jsdoc` rule list based on `v38.1.x` + # https://github.com/gajus/eslint-plugin-jsdoc + ########################################################################## + + # Recommended rules + jsdoc/check-access: error + jsdoc/check-alignment: error + jsdoc/check-examples: off # Deprecated and not for ESLint >= 8 + jsdoc/check-indentation: off # Existing docs intentionally indent wrapped text + jsdoc/check-line-alignment: off # Not recommended + jsdoc/check-param-names: error + jsdoc/check-property-names: error + jsdoc/check-tag-names: + - error + - definedTags: + - category + - defaultValue + - packageDocumentation + - remarks + - typeParam + jsdoc/check-syntax: off # Not recommended + jsdoc/check-types: error + jsdoc/check-values: error + jsdoc/empty-tags: error + jsdoc/implements-on-classes: error + jsdoc/match-description: off # Not recommended + jsdoc/match-name: off # Not recommended + jsdoc/multiline-blocks: error + jsdoc/newline-after-description: [error, never] + jsdoc/no-bad-blocks: off # Not recommended + jsdoc/no-defaults: off # Not recommended + jsdoc/no-missing-syntax: off # Not recommended + jsdoc/no-multi-asterisks: error + jsdoc/no-restricted-syntax: off # Not recommended + jsdoc/no-types: off # Not recommended + jsdoc/no-undefined-types: off # TypeScript handles undefined types + jsdoc/require-asterisk-prefix: off # Not recommended + + # Requirement rules + jsdoc/require-description: + - error + - contexts: + - context: ClassDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: MethodDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: PropertyDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSEnumDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSEnumMember + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSInterfaceDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSPropertySignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSTypeAliasDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: VariableDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: ClassDeclaration + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: FunctionDeclaration + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: MethodDefinition + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: TSDeclareFunction + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: TSInterfaceDeclaration + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: TSTypeAliasDeclaration + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: VariableDeclaration + comment: 'JsdocBlock:has(JsdocTag[tag="deprecated"])' + - context: Program + comment: 'JsdocBlock:has(JsdocTag[tag="packageDocumentation"])' + jsdoc/require-description-complete-sentence: off # Not recommended + jsdoc/require-example: + - error + - checkConstructors: true + checkGetters: false + checkSetters: false + contexts: + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: 'MethodDefinition:not([kind="get"]):not([kind="set"])' + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + jsdoc/require-file-overview: off # Enabled below only for public index.ts package entrypoints + jsdoc/require-hyphen-before-param-description: off # Not recommended + jsdoc/require-jsdoc: off # Public API JSDoc presence is enforced by internal-rules/require-public-api-exports + jsdoc/require-param: + - error + - checkConstructors: true + checkGetters: false + checkSetters: true + contexts: + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: MethodDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + exemptedBy: + - internal + jsdoc/require-param-description: + - error + - contexts: + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: MethodDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + jsdoc/require-param-name: + - error + - contexts: + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: MethodDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + jsdoc/require-param-type: off # TypeScript handles parameter types + jsdoc/require-property: error + jsdoc/require-property-description: error + jsdoc/require-property-name: error + jsdoc/require-property-type: off # TypeScript handles property types + jsdoc/require-returns: + - error + - checkConstructors: false + checkGetters: true + contexts: + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: MethodDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + exemptedBy: + - internal + jsdoc/require-returns-check: off # Too noisy for declaration-style docs and parser helpers + jsdoc/require-returns-description: + - error + - contexts: + - context: FunctionDeclaration + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSDeclareFunction + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: MethodDefinition + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + - context: TSMethodSignature + comment: 'JsdocBlock:not(:has(JsdocTag[tag="internal"]))' + jsdoc/require-returns-type: off # TypeScript handles return types + jsdoc/require-throws: off # TODO consider + jsdoc/require-yields: error + jsdoc/require-yields-check: error + jsdoc/sort-tags: error + jsdoc/tag-lines: [error, never, { noEndLines: true }] + jsdoc/valid-types: error + - files: + - 'src/index.ts' + - 'src/**/index.ts' + rules: + jsdoc/require-file-overview: + - error + - tags: + packageDocumentation: + initialCommentsOnly: true + mustExist: true + preventDuplicates: true + - files: '**/__*__/**' rules: + # API documentation rules document the source API surface, not tests or + # test helpers. ESLint does not support plugin-wide rule wildcards in + # config, so plugin rules must be disabled explicitly here. + internal-rules/require-graphql-public-api-docs: off + internal-rules/require-public-api-exports: off internal-rules/require-to-string-tag: off + jsdoc/check-access: off + jsdoc/check-alignment: off + jsdoc/check-examples: off + jsdoc/check-indentation: off + jsdoc/check-line-alignment: off + jsdoc/check-param-names: off + jsdoc/check-property-names: off + jsdoc/check-syntax: off + jsdoc/check-tag-names: off + jsdoc/check-types: off + jsdoc/check-values: off + jsdoc/empty-tags: off + jsdoc/implements-on-classes: off + jsdoc/match-description: off + jsdoc/match-name: off + jsdoc/multiline-blocks: off + jsdoc/newline-after-description: off + jsdoc/no-bad-blocks: off + jsdoc/no-defaults: off + jsdoc/no-missing-syntax: off + jsdoc/no-multi-asterisks: off + jsdoc/no-restricted-syntax: off + jsdoc/no-types: off + jsdoc/no-undefined-types: off + jsdoc/require-asterisk-prefix: off + jsdoc/require-description: off + jsdoc/require-description-complete-sentence: off + jsdoc/require-example: off + jsdoc/require-file-overview: off + jsdoc/require-hyphen-before-param-description: off + jsdoc/require-jsdoc: off + jsdoc/require-param: off + jsdoc/require-param-description: off + jsdoc/require-param-name: off + jsdoc/require-param-type: off + jsdoc/require-property: off + jsdoc/require-property-description: off + jsdoc/require-property-name: off + jsdoc/require-property-type: off + jsdoc/require-returns: off + jsdoc/require-returns-check: off + jsdoc/require-returns-description: off + jsdoc/require-returns-type: off + jsdoc/require-throws: off + jsdoc/require-yields: off + jsdoc/require-yields-check: off + jsdoc/sort-tags: off + jsdoc/tag-lines: off + jsdoc/valid-types: off node/no-unpublished-import: [error, { allowModules: ['chai', 'mocha'] }] import/no-deprecated: off import/no-restricted-paths: off @@ -696,9 +969,10 @@ overrides: node: true rules: internal-rules/only-ascii: [error, { allowEmoji: true }] + import/no-extraneous-dependencies: off + node/no-extraneous-require: off node/no-unpublished-require: off node/no-sync: off - import/no-extraneous-dependencies: [error, { devDependencies: true }] import/no-nodejs-modules: off import/no-commonjs: off no-console: off diff --git a/cspell.yml b/cspell.yml index a7e255dc1e..56cade9745 100644 --- a/cspell.yml +++ b/cspell.yml @@ -11,6 +11,7 @@ ignorePaths: - benchmark/github-schema.json - website/icons - website/css + - website/pages/api-v16-new overrides: - filename: 'website/**' dictionaries: @@ -47,12 +48,18 @@ overrides: ignoreRegExpList: - u\{[0-9a-f]{1,8}\} + - href="/api-v1[67]/[^"]+" words: + - backticks - Coodinate + - entrypoints - metafield - graphiql + - Jsdocs - sublinks + - thunked + - tsdoc - instanceof # Different names used inside tests @@ -87,9 +94,15 @@ words: # used as href anchors - graphqlerror + - graphqlerroroptions - syntaxerror - formaterror + - locatederror + - executionargs + - graphqlargs + - graphqlsync - graphqlschema + - graphqlsubscription - graphqlscalartype - graphqlobjecttype - graphqlinterfacetype @@ -104,6 +117,11 @@ words: - graphqlboolean - graphqlid - getlocation + - directivelocation + - tokenkind + - getenterleaveforkind + - getroottype + - assertname - isinputtype - isoutputtype - isleaftype diff --git a/package-lock.json b/package-lock.json index 1481176b31..e93dd3511a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint": "8.13.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-internal-rules": "file:./resources/eslint-internal-rules", + "eslint-plugin-jsdoc": "38.1.6", "eslint-plugin-node": "11.1.0", "eslint-plugin-react": "7.29.4", "eslint-plugin-react-hooks": "4.4.0", @@ -1959,6 +1960,21 @@ "integrity": "sha512-/MB0RS0Gn01s4pgmjy0FvsLfr3RRMrRphEuvTRserNcM8XVtoIVAtrjig/Gg0DPwDrN8Clm0L1j7iQay6S8D0g==", "dev": true }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", + "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~2.2.5" + }, + "engines": { + "node": "^12 || ^14 || ^16 || ^17" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", @@ -2965,6 +2981,16 @@ "node": ">= 6" } }, + "node_modules/comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -3751,6 +3777,55 @@ "resolved": "resources/eslint-internal-rules", "link": true }, + "node_modules/eslint-plugin-jsdoc": { + "version": "38.1.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", + "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.22.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "regextras": "^0.8.0", + "semver": "^7.3.5", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": "^12 || ^14 || ^16 || ^17" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -5048,6 +5123,16 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", + "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6040,6 +6125,16 @@ "node": ">=4" } }, + "node_modules/regextras": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -6285,6 +6380,31 @@ "node": ">=0.10.0" } }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", @@ -8251,6 +8371,17 @@ "integrity": "sha512-/MB0RS0Gn01s4pgmjy0FvsLfr3RRMrRphEuvTRserNcM8XVtoIVAtrjig/Gg0DPwDrN8Clm0L1j7iQay6S8D0g==", "dev": true }, + "@es-joy/jsdoccomment": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", + "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "dev": true, + "requires": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~2.2.5" + } + }, "@eslint/eslintrc": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", @@ -8973,6 +9104,12 @@ "repeat-string": "^1.6.1" } }, + "comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -9674,6 +9811,36 @@ "eslint-plugin-internal-rules": { "version": "file:resources/eslint-internal-rules" }, + "eslint-plugin-jsdoc": { + "version": "38.1.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", + "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.22.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "regextras": "^0.8.0", + "semver": "^7.3.5", + "spdx-expression-parse": "^3.0.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true + } + } + }, "eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -10519,6 +10686,12 @@ "argparse": "^2.0.1" } }, + "jsdoc-type-pratt-parser": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", + "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "dev": true + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -11240,6 +11413,12 @@ "unicode-match-property-value-ecmascript": "^2.0.0" } }, + "regextras": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "dev": true + }, "regjsgen": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", @@ -11417,6 +11596,28 @@ } } }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true + }, "string.prototype.matchall": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", diff --git a/package.json b/package.json index 2591db7aa1..9195d3ac1d 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "eslint": "8.13.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-internal-rules": "file:./resources/eslint-internal-rules", + "eslint-plugin-jsdoc": "38.1.6", "eslint-plugin-node": "11.1.0", "eslint-plugin-react": "7.29.4", "eslint-plugin-react-hooks": "4.4.0", diff --git a/resources/diff-npm-package.js b/resources/diff-npm-package.js index c0d8d8c991..f0d59d3d8d 100644 --- a/resources/diff-npm-package.js +++ b/resources/diff-npm-package.js @@ -97,6 +97,7 @@ function prepareNPMPackage(revision) { function exec(command, options = {}) { const result = cp.execSync(command, { encoding: 'utf-8', + maxBuffer: 10 * 1024 * 1024, stdio: ['inherit', 'pipe', 'inherit'], ...options, }); diff --git a/resources/eslint-internal-rules/index.js b/resources/eslint-internal-rules/index.js index 4acc530f3a..4edb466cc0 100644 --- a/resources/eslint-internal-rules/index.js +++ b/resources/eslint-internal-rules/index.js @@ -2,12 +2,16 @@ const onlyASCII = require('./only-ascii.js'); const noDirImport = require('./no-dir-import.js'); +const requireGraphqlPublicApiDocs = require('./require-graphql-public-api-docs.js'); +const requirePublicApiExports = require('./require-public-api-exports.js'); const requireToStringTag = require('./require-to-string-tag.js'); module.exports = { rules: { 'only-ascii': onlyASCII, 'no-dir-import': noDirImport, + 'require-graphql-public-api-docs': requireGraphqlPublicApiDocs, + 'require-public-api-exports': requirePublicApiExports, 'require-to-string-tag': requireToStringTag, }, }; diff --git a/resources/eslint-internal-rules/jsdoc-utils.js b/resources/eslint-internal-rules/jsdoc-utils.js new file mode 100644 index 0000000000..cbdd3f9081 --- /dev/null +++ b/resources/eslint-internal-rules/jsdoc-utils.js @@ -0,0 +1,98 @@ +'use strict'; + +module.exports = { + hasTag, + isJsdoc, + parseTags, +}; + +function isJsdoc(comment) { + return comment.type === 'Block' && comment.value.startsWith('*'); +} + +function hasTag(comment, tagName) { + return tagEntries(comment).some((tag) => tag.name === tagName); +} + +function parseTags(comment) { + const tags = new Map(); + + for (const { name, text } of tagEntries(comment)) { + if (!tags.has(name)) { + tags.set(name, new Map()); + } + if (name === 'typeParam') { + const [typeName, description] = splitNameAndDescription(text); + tags.get(name).set(typeName, description); + } else { + tags.get(name).set('*', text); + } + } + + return tags; +} + +function tagEntries(comment) { + return commentLines(comment) + .map(tagEntry) + .filter((entry) => entry != null); +} + +function commentLines(comment) { + return comment.value.split('\n').map((line, index) => { + let text = index === 0 && line.startsWith('*') ? line.slice(1) : line; + text = text.trimStart(); + if (text.startsWith('*')) { + text = text.slice(1); + } + if (text.startsWith(' ')) { + text = text.slice(1); + } + return text.trim(); + }); +} + +function tagEntry(line) { + if (!line.startsWith('@')) { + return null; + } + + const [name, text] = splitFirstWord(line.slice(1)); + if (name === '') { + return null; + } + return { + name, + text, + }; +} + +function splitNameAndDescription(text) { + const [name, description] = splitFirstWord(text); + return [ + name, + description.startsWith('-') ? description.slice(1).trim() : description, + ]; +} + +function splitFirstWord(text) { + const trimmed = text.trim(); + const end = firstWhitespace(trimmed); + if (end === -1) { + return [trimmed, '']; + } + return [trimmed.slice(0, end), trimmed.slice(end).trim()]; +} + +function firstWhitespace(text) { + for (let i = 0; i < text.length; i++) { + if (isWhitespace(text[i])) { + return i; + } + } + return -1; +} + +function isWhitespace(character) { + return character === ' ' || character === '\t'; +} diff --git a/resources/eslint-internal-rules/require-graphql-public-api-docs.js b/resources/eslint-internal-rules/require-graphql-public-api-docs.js new file mode 100644 index 0000000000..555d3587a5 --- /dev/null +++ b/resources/eslint-internal-rules/require-graphql-public-api-docs.js @@ -0,0 +1,215 @@ +'use strict'; + +const { isJsdoc, parseTags } = require('./jsdoc-utils.js'); + +module.exports = { + meta: { + schema: [], + }, + create: requireGraphqlPublicApiDocs, +}; + +function requireGraphqlPublicApiDocs(context) { + const sourceCode = context.getSourceCode(); + + function report(node, message) { + context.report({ node, message }); + } + + function commentFor(node) { + const target = + node.parent?.type === 'ExportNamedDeclaration' && + node.parent.declaration === node + ? node.parent + : node; + const comments = sourceCode.getCommentsBefore(target).filter(isJsdoc); + const comment = comments[comments.length - 1]; + if (comment == null || hasCodeBetween(comment, target)) { + return null; + } + return comment; + } + + function hasCodeBetween(comment, node) { + return sourceCode + .getTokensBetween(comment, node, { includeComments: true }) + .some((token) => token.type !== 'Block' && token.type !== 'Line'); + } + + function parsedCommentFor(node) { + const comment = + node.type === 'Program' ? topComment(node) : commentFor(node); + return comment == null ? null : { tags: parseTags(comment) }; + } + + function topComment(program) { + const first = program.body[0]; + const comments = + first == null + ? sourceCode.getAllComments() + : sourceCode.getCommentsBefore(first); + return comments.find(isJsdoc) ?? null; + } + + function checkPublicDoc(node, label, fileCategory, options = {}) { + const comment = parsedCommentFor(node); + if (comment == null || comment.tags.has('internal')) { + return; + } + + // eslint-plugin-jsdoc owns generic public documentation shape checks such as + // descriptions, params, and returns. This rule only keeps the GraphQL- + // specific contract that the plugin cannot express: public docs are + // categorized either directly or by the file-level package documentation, + // and TypeScript type parameters are documented with @typeParam. + if ( + options.requireCategory !== false && + !comment.tags.has('category') && + fileCategory == null + ) { + report(node, `${label} is missing @category.`); + } + + requireNamedTags( + node, + comment, + 'typeParam', + typeParameterNames(node), + label, + ); + } + + function checkPublicMemberDocs(declaration, ownerName) { + for (const member of publicMembers(declaration)) { + checkPublicDoc(member, memberLabel(ownerName, member), null, { + requireCategory: false, + }); + } + } + + function requireNamedTags(node, comment, tag, requiredNames, label) { + const documented = comment.tags.get(tag) ?? new Map(); + for (const name of requiredNames) { + if (!documented.has(name) || documented.get(name) === '') { + report(node, `${label} is missing @${tag} ${name}.`); + } + } + } + + return { + 'Program:exit'(program) { + const moduleComment = parsedCommentFor(program); + const fileCategory = + moduleComment?.tags.get('category')?.get('*') ?? null; + + for (const statement of program.body) { + const namespaceName = namespaceExportName(statement); + if (namespaceName != null) { + checkPublicDoc(statement, namespaceName, fileCategory); + } + + const declaration = unwrapExportedDeclaration(statement); + if (!isDocumentableDeclaration(declaration)) { + continue; + } + for (const name of declarationNames(declaration)) { + checkPublicDoc(declaration, name, fileCategory); + checkPublicMemberDocs(declaration, name); + } + } + }, + }; +} + +function unwrapExportedDeclaration(statement) { + return statement.type === 'ExportNamedDeclaration' + ? statement.declaration + : statement; +} + +function namespaceExportName(statement) { + return statement.type === 'ExportAllDeclaration' + ? statement.exported?.name + : null; +} + +function typeParameterNames(node) { + const typeParameters = node.value?.typeParameters ?? node.typeParameters; + return (typeParameters?.params ?? []) + .map((param) => param.name?.name) + .filter(Boolean); +} + +function typeLiteralMembers(typeAnnotation) { + if (typeAnnotation == null) { + return []; + } + if (typeAnnotation.type === 'TSTypeLiteral') { + return typeAnnotation.members; + } + if ( + typeAnnotation.type === 'TSIntersectionType' || + typeAnnotation.type === 'TSUnionType' + ) { + return typeAnnotation.types.flatMap(typeLiteralMembers); + } + return []; +} + +function publicMembers(declaration) { + if (declaration.type === 'ClassDeclaration') { + return declaration.body.body.filter( + (member) => member.accessibility !== 'private', + ); + } + if (declaration.type === 'TSInterfaceDeclaration') { + return declaration.body.body.filter(isDocumentableTypeMember); + } + if (declaration.type === 'TSTypeAliasDeclaration') { + return typeLiteralMembers(declaration.typeAnnotation).filter( + isDocumentableTypeMember, + ); + } + return []; +} + +function isDocumentableTypeMember(member) { + return member.type !== 'TSIndexSignature'; +} + +function memberLabel(ownerName, member) { + const key = member.key ?? member.id; + if (member.kind === 'constructor') { + return `${ownerName}.constructor`; + } + if (key?.type === 'Identifier') { + return `${ownerName}.${key.name}`; + } + if (key?.type === 'Literal') { + return `${ownerName}.${String(key.value)}`; + } + return `${ownerName}.`; +} + +function isDocumentableDeclaration(node) { + return ( + node?.type === 'ClassDeclaration' || + node?.type === 'FunctionDeclaration' || + node?.type === 'TSDeclareFunction' || + node?.type === 'TSInterfaceDeclaration' || + node?.type === 'TSTypeAliasDeclaration' || + node?.type === 'TSEnumDeclaration' || + node?.type === 'VariableDeclaration' + ); +} + +function declarationNames(node) { + if (node.type === 'VariableDeclaration') { + return node.declarations + .map((declaration) => + declaration.id.type === 'Identifier' ? declaration.id.name : null, + ) + .filter(Boolean); + } + return node.id?.name == null ? [] : [node.id.name]; +} diff --git a/resources/eslint-internal-rules/require-public-api-exports.js b/resources/eslint-internal-rules/require-public-api-exports.js new file mode 100644 index 0000000000..76bc7988e8 --- /dev/null +++ b/resources/eslint-internal-rules/require-public-api-exports.js @@ -0,0 +1,488 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const ts = require('typescript'); + +const { hasTag, isJsdoc } = require('./jsdoc-utils.js'); + +const publicExportsCache = new Map(); + +module.exports = { + meta: { + schema: [ + { + type: 'object', + properties: { + publicIndexFiles: { + type: 'array', + items: { type: 'string' }, + }, + }, + additionalProperties: false, + }, + ], + }, + create: requirePublicApiExports, +}; + +function requirePublicApiExports(context) { + const sourceCode = context.getSourceCode(); + const publicApi = createPublicApi(context); + + function hasPublicDoc(node) { + const comment = ownJsdocComment(node); + return comment != null && !hasTag(comment, 'internal'); + } + + function hasInternalDoc(node) { + const comment = ownJsdocComment(node); + return comment != null && hasTag(comment, 'internal'); + } + + function ownJsdocComment(node) { + const parent = node.parent; + const commentTarget = + parent?.type === 'ExportNamedDeclaration' && parent.declaration === node + ? parent + : node; + const comments = sourceCode + .getCommentsBefore(commentTarget) + .filter(isJsdoc); + const comment = comments[comments.length - 1]; + if (comment == null || hasCodeBetween(comment, commentTarget)) { + return null; + } + return comment; + } + + function hasCodeBetween(comment, node) { + return sourceCode + .getTokensBetween(comment, node, { includeComments: true }) + .some((token) => token.type !== 'Block' && token.type !== 'Line'); + } + + function reportMissingPublicDoc(node, name) { + context.report({ + node, + message: `Public API declaration "${name}" is exported by a public index.ts file and must have public JSDoc.`, + }); + } + + function reportUnexpectedInternalDoc(node, name) { + context.report({ + node, + message: `Public API declaration "${name}" is exported by a public index.ts file and must not have @internal JSDoc.`, + }); + } + + function reportMissingInternalDoc(node, name) { + context.report({ + node, + message: `Internal declaration "${name}" is exported from src but is not exported by a public index.ts file and must have @internal JSDoc.`, + }); + } + + function requirePublicDoc(node, name) { + if (!hasPublicDoc(node)) { + reportMissingPublicDoc(node, name); + } + } + + return { + ExportNamedDeclaration(node) { + publicApi.trackLocalExports(node); + }, + 'Program:exit'(program) { + // This rule owns the repo-specific public API boundary. Exported + // declarations under src are public when they are re-exported by any + // non-test src/**/index.ts package entrypoint; otherwise they are + // internal and must be marked explicitly. + const topLevelOverloads = overloadCounts( + program.body, + (statement) => unwrapExportedDeclaration(statement)?.id?.name, + ); + + for (const statement of program.body) { + const namespaceName = namespaceExportName(statement); + if (namespaceName != null) { + if (publicApi.isPublic(namespaceName)) { + if (hasInternalDoc(statement)) { + reportUnexpectedInternalDoc(statement, namespaceName); + } + requirePublicDoc(statement, namespaceName); + } else if (!hasInternalDoc(statement)) { + reportMissingInternalDoc(statement, namespaceName); + } + } + + const declaration = publicApi.exportedDeclaration(statement); + if (isDocumentableDeclaration(declaration)) { + for (const name of declarationNames(declaration)) { + if (publicApi.isPublic(name)) { + if ( + isOverloadImplementation(declaration, topLevelOverloads, name) + ) { + if (!hasInternalDoc(declaration)) { + reportMissingInternalDoc(declaration, name); + } + continue; + } + if (hasInternalDoc(declaration)) { + reportUnexpectedInternalDoc(declaration, name); + } + requirePublicDoc(declaration, name); + for (const member of publicMembers(declaration)) { + const qualifiedMemberName = `${name}.${memberName(member)}`; + if (!hasInternalDoc(member)) { + requirePublicDoc(member, qualifiedMemberName); + } + } + } else if (!hasInternalDoc(declaration)) { + reportMissingInternalDoc(declaration, name); + } + } + } + } + }, + }; +} + +function createPublicApi(context) { + const cwd = context.getCwd(); + const filename = normalizePath(path.relative(cwd, context.getFilename())); + const publicIndexFiles = + context.options[0]?.publicIndexFiles ?? findIndexFiles(cwd); + const publicExports = collectPublicExports(cwd, publicIndexFiles); + const names = allPublicNamesForFile(publicExports, filename); + const localExports = new Set(); + + return { + isPublic: (name) => names.has(name), + trackLocalExports(node) { + if (node.source != null || node.specifiers == null) { + return; + } + for (const specifier of node.specifiers) { + if (specifier.local?.name != null) { + localExports.add(specifier.local.name); + } + } + }, + exportedDeclaration(statement) { + if (statement.type === 'ExportNamedDeclaration') { + return isDocumentableDeclaration(statement.declaration) + ? statement.declaration + : null; + } + if ( + isDocumentableDeclaration(statement) && + declarationNames(statement).some((name) => localExports.has(name)) + ) { + return statement; + } + return null; + }, + }; +} + +function unwrapExportedDeclaration(statement) { + return statement.type === 'ExportNamedDeclaration' + ? statement.declaration + : statement; +} + +function namespaceExportName(statement) { + return statement.type === 'ExportAllDeclaration' + ? statement.exported?.name + : null; +} + +function findIndexFiles(cwd, dir = 'src') { + const absoluteDir = path.join(cwd, dir); + if (!fs.existsSync(absoluteDir)) { + return []; + } + + const files = []; + for (const entry of fs.readdirSync(absoluteDir, { withFileTypes: true })) { + const file = normalizePath(path.join(dir, entry.name)); + if (entry.isDirectory()) { + if (!entry.name.startsWith('__')) { + files.push(...findIndexFiles(cwd, file)); + } + } else if (entry.isFile() && entry.name === 'index.ts') { + files.push(file); + } + } + return files.sort(); +} + +function collectPublicExports(cwd, publicIndexFiles) { + const cacheKey = `${cwd}\0${publicIndexFiles.join('\0')}`; + const cached = publicExportsCache.get(cacheKey); + if (cached != null) { + return cached; + } + + const exportTables = new Map(); + const publicExports = new Map(); + + function exportTable(indexFile) { + const cachedTable = exportTables.get(indexFile); + if (cachedTable != null) { + return cachedTable; + } + + const table = new Map(); + exportTables.set(indexFile, table); + + const ast = sourceFile(cwd, indexFile); + for (const statement of ast.statements) { + if (ts.isExportDeclaration(statement)) { + addExportDeclaration(table, statement, indexFile); + } else if (hasExportModifier(statement)) { + for (const name of tsDeclarationNames(statement)) { + addExportedOrigin(table, name, { file: indexFile, name }); + } + } + } + + return table; + } + + function addExportDeclaration(table, statement, file) { + if ( + statement.moduleSpecifier == null || + !ts.isStringLiteral(statement.moduleSpecifier) + ) { + return; + } + + const targetFile = resolveModule(cwd, file, statement.moduleSpecifier.text); + if (targetFile == null) { + return; + } + + if (statement.exportClause == null) { + return; + } + + if (ts.isNamespaceExport(statement.exportClause)) { + const namespaceName = statement.exportClause.name.text; + addExportedOrigin(table, namespaceName, { file, name: namespaceName }); + + const targetTable = exportTable(targetFile); + for (const origins of targetTable.values()) { + for (const origin of origins) { + addExportedOrigin(table, namespaceName, origin); + } + } + return; + } + + if (!ts.isNamedExports(statement.exportClause)) { + return; + } + + const targetTable = exportTable(targetFile); + + for (const element of statement.exportClause.elements) { + const importedName = (element.propertyName ?? element.name).text; + const exportedName = element.name.text; + const origins = targetTable?.get(importedName) ?? [ + { file: targetFile, name: importedName }, + ]; + for (const origin of origins) { + addExportedOrigin(table, exportedName, origin); + } + } + } + + for (const indexFile of publicIndexFiles) { + const table = exportTable(indexFile); + const exportsForIndex = new Map(); + for (const origins of table.values()) { + for (const origin of origins) { + addPublicExport(exportsForIndex, origin.file, origin.name); + } + } + publicExports.set(indexFile, exportsForIndex); + } + + publicExportsCache.set(cacheKey, publicExports); + return publicExports; +} + +function allPublicNamesForFile(publicExports, file) { + const names = new Set(); + for (const exportsForIndex of publicExports.values()) { + for (const name of exportsForIndex.get(file) ?? []) { + names.add(name); + } + } + return names; +} + +function sourceFile(cwd, file) { + return ts.createSourceFile( + file, + fs.readFileSync(path.join(cwd, file), 'utf8'), + ts.ScriptTarget.Latest, + true, + ); +} + +function resolveModule(cwd, indexFile, specifier) { + const base = normalizePath(path.join(path.dirname(indexFile), specifier)); + const candidates = specifier.endsWith('.ts') + ? [base] + : [`${base}.ts`, path.join(base, 'index.ts')]; + return candidates.find((file) => fs.existsSync(path.join(cwd, file))); +} + +function addExportedOrigin(table, exportedName, origin) { + if (!table.has(exportedName)) { + table.set(exportedName, []); + } + table.get(exportedName).push(origin); +} + +function addPublicExport(map, file, name) { + if (!map.has(file)) { + map.set(file, new Set()); + } + map.get(file).add(name); +} + +function hasExportModifier(node) { + return node.modifiers?.some( + (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword, + ); +} + +function tsDeclarationNames(node) { + if (ts.isVariableStatement(node)) { + return node.declarationList.declarations + .map((declaration) => + ts.isIdentifier(declaration.name) ? declaration.name.text : null, + ) + .filter(Boolean); + } + return node.name?.text == null ? [] : [node.name.text]; +} + +function publicMembers(declaration) { + if (declaration.type === 'ClassDeclaration') { + const overloads = overloadCounts(declaration.body.body, memberName); + return declaration.body.body.filter( + (member) => + member.accessibility !== 'private' && + !isOverloadImplementation(member, overloads, memberName(member)), + ); + } + if (declaration.type === 'TSInterfaceDeclaration') { + return declaration.body.body.filter(isDocumentableTypeMember); + } + if (declaration.type === 'TSTypeAliasDeclaration') { + return typeLiteralMembers(declaration.typeAnnotation).filter( + isDocumentableTypeMember, + ); + } + if (declaration.type === 'TSEnumDeclaration') { + return declaration.members; + } + return []; +} + +function overloadCounts(nodes, nameOfNode) { + const counts = new Map(); + for (const node of nodes) { + const name = nameOfNode(node); + if (name != null) { + counts.set(name, (counts.get(name) ?? 0) + 1); + } + } + return counts; +} + +function isOverloadImplementation(node, overloads, name) { + return ( + isCallable(node) && overloads.get(name) > 1 && callableBody(node) != null + ); +} + +function typeLiteralMembers(typeAnnotation) { + if (typeAnnotation == null) { + return []; + } + if (typeAnnotation.type === 'TSTypeLiteral') { + return typeAnnotation.members; + } + if ( + typeAnnotation.type === 'TSIntersectionType' || + typeAnnotation.type === 'TSUnionType' + ) { + return typeAnnotation.types.flatMap(typeLiteralMembers); + } + return []; +} + +function isCallable(node) { + return ( + node?.type === 'FunctionDeclaration' || + node?.type === 'MethodDefinition' || + node?.type === 'TSDeclareFunction' || + node?.type === 'TSMethodSignature' + ); +} + +function callableBody(node) { + return node.body ?? node.value?.body; +} + +function isDocumentableTypeMember(member) { + return member.type !== 'TSIndexSignature'; +} + +function isDocumentableDeclaration(node) { + return ( + node?.type === 'ClassDeclaration' || + node?.type === 'FunctionDeclaration' || + node?.type === 'TSDeclareFunction' || + node?.type === 'TSInterfaceDeclaration' || + node?.type === 'TSTypeAliasDeclaration' || + node?.type === 'TSEnumDeclaration' || + node?.type === 'VariableDeclaration' + ); +} + +function declarationNames(node) { + if (node.type === 'VariableDeclaration') { + return node.declarations + .map((declaration) => + declaration.id.type === 'Identifier' ? declaration.id.name : null, + ) + .filter(Boolean); + } + return node.id?.name == null ? [] : [node.id.name]; +} + +function memberName(member) { + const key = member.key ?? member.id; + if (member.kind === 'constructor') { + return 'constructor'; + } + if (key?.type === 'Identifier') { + return key.name; + } + if (key?.type === 'Literal') { + return String(key.value); + } + return ''; +} + +function normalizePath(file) { + return file.split(path.sep).join('/'); +} diff --git a/src/README.md b/src/README.md index 7a67bcb569..9ed6bf0052 100644 --- a/src/README.md +++ b/src/README.md @@ -20,4 +20,5 @@ Each sub directory within is a sub-module of graphql-js: - [`graphql/error`](error/README.md): Creating and formatting GraphQL errors. - [`graphql/utilities`](utilities/README.md): Common useful computations upon the GraphQL language and type objects. -- [`graphql/subscription`](subscription/README.md): Subscribe to data updates. +- [`graphql/subscription`](subscription/README.md): Deprecated compatibility + submodule for subscription helpers; use `graphql/execution` instead. diff --git a/src/error/GraphQLError.ts b/src/error/GraphQLError.ts index 77a5e78779..29420c7136 100644 --- a/src/error/GraphQLError.ts +++ b/src/error/GraphQLError.ts @@ -1,3 +1,5 @@ +/** @category Errors */ + import { isObjectLike } from '../jsutils/isObjectLike'; import type { Maybe } from '../jsutils/Maybe'; @@ -9,7 +11,6 @@ import type { Source } from '../language/source'; /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -22,7 +23,6 @@ export interface GraphQLErrorExtensions { /** * Custom formatted extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -33,12 +33,24 @@ export interface GraphQLFormattedErrorExtensions { [attributeName: string]: unknown; } +/** Options used to construct a GraphQLError. */ export interface GraphQLErrorOptions { + /** AST node or nodes associated with this error. */ nodes?: ReadonlyArray | ASTNode | null; + /** Source document used to derive error locations. */ source?: Maybe; + /** Character offsets in the source document associated with this error. */ positions?: Maybe>; + /** Response path where this error occurred during execution. */ path?: Maybe>; - originalError?: Maybe; + /** Original error that caused this GraphQLError, if one exists. */ + originalError?: Maybe< + Error & { + /** Extension fields associated with this value. */ + readonly extensions?: unknown; + } + >; + /** Extension fields to include in the formatted result. */ extensions?: Maybe; } @@ -97,9 +109,7 @@ export class GraphQLError extends Error { */ readonly path: ReadonlyArray | undefined; - /** - * An array of GraphQL AST Nodes corresponding to this error. - */ + /** An array of GraphQL AST Nodes corresponding to this error. */ readonly nodes: ReadonlyArray | undefined; /** @@ -116,18 +126,91 @@ export class GraphQLError extends Error { */ readonly positions: ReadonlyArray | undefined; - /** - * The original error thrown from a field resolver during execution. - */ + /** Original error that caused this GraphQLError, if one exists. */ readonly originalError: Error | undefined; - /** - * Extension fields to add to the formatted error. - */ + /** Extension fields to add to the formatted error. */ readonly extensions: GraphQLErrorExtensions; + /** + * Creates a GraphQLError instance. + * @param message - Human-readable error message. + * @param options - Error metadata such as source locations, response path, original error, and extensions. + * @example + * ```ts + * // Create an error from AST nodes and response metadata. + * import { parse } from 'graphql/language'; + * import { GraphQLError } from 'graphql/error'; + * + * const document = parse('{ greeting }'); + * const fieldNode = document.definitions[0].selectionSet.selections[0]; + * const error = new GraphQLError('Cannot query this field.', { + * nodes: fieldNode, + * path: ['greeting'], + * extensions: { code: 'FORBIDDEN' }, + * }); + * + * error.message; // => 'Cannot query this field.' + * error.locations; // => [{ line: 1, column: 3 }] + * error.path; // => ['greeting'] + * error.extensions; // => { code: 'FORBIDDEN' } + * ``` + * @example + * ```ts + * // This variant derives locations from source positions and preserves the original error. + * import { Source } from 'graphql/language'; + * import { GraphQLError } from 'graphql/error'; + * + * const source = new Source('{ greeting }'); + * const originalError = new Error('Database unavailable.'); + * const error = new GraphQLError('Resolver failed.', { + * source, + * positions: [2], + * path: ['greeting'], + * originalError, + * }); + * + * error.locations; // => [{ line: 1, column: 3 }] + * error.path; // => ['greeting'] + * error.originalError; // => originalError + * ``` + */ constructor(message: string, options?: GraphQLErrorOptions); /** + * Creates a GraphQLError instance using the legacy positional constructor. + * Prefer the `GraphQLErrorOptions` object overload, which keeps optional error + * metadata in a single options bag. + * @param message - Human-readable error message. + * @param nodes - AST node or nodes associated with this error. + * @param source - Source document used to derive error locations. + * @param positions - Character offsets in the source document associated with + * this error. + * @param path - Response path where this error occurred during execution. + * @param originalError - Original error that caused this GraphQLError, if one + * exists. + * @param extensions - Extension fields to include in the formatted error. + * @example + * ```ts + * import { Source } from 'graphql/language'; + * import { GraphQLError } from 'graphql/error'; + * + * const source = new Source('{ greeting }'); + * const originalError = new Error('Database unavailable.'); + * const error = new GraphQLError( + * 'Resolver failed.', + * undefined, + * source, + * [2], + * ['greeting'], + * originalError, + * { code: 'INTERNAL' }, + * ); + * + * error.locations; // => [{ line: 1, column: 3 }] + * error.path; // => ['greeting'] + * error.originalError; // => originalError + * error.extensions; // => { code: 'INTERNAL' } + * ``` * @deprecated Please use the `GraphQLErrorOptions` constructor overload instead. */ constructor( @@ -209,10 +292,30 @@ export class GraphQLError extends Error { /* c8 ignore stop */ } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag](): string { return 'GraphQLError'; } + /** + * Returns this error as a human-readable message with source locations. + * @returns The formatted error string. + * @example + * ```ts + * import { Source } from 'graphql/language'; + * import { GraphQLError } from 'graphql/error'; + * + * const error = new GraphQLError('Cannot query field "name".', { + * source: new Source('{ name }'), + * positions: [2], + * }); + * + * error.toString(); // => 'Cannot query field "name".\n\nGraphQL request:1:3\n1 | { name }\n | ^' + * ``` + */ toString(): string { let output = this.message; @@ -231,6 +334,21 @@ export class GraphQLError extends Error { return output; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLError } from 'graphql/error'; + * + * const error = new GraphQLError('Resolver failed.', { + * path: ['viewer', 'name'], + * extensions: { code: 'INTERNAL' }, + * }); + * + * error.toJSON(); // => { message: 'Resolver failed.', path: ['viewer', 'name'], extensions: { code: 'INTERNAL' } } + * ``` + */ toJSON(): GraphQLFormattedError { type WritableFormattedError = { -readonly [P in keyof GraphQLFormattedError]: GraphQLFormattedError[P]; @@ -262,9 +380,7 @@ function undefinedIfEmpty( return array === undefined || array.length === 0 ? undefined : array; } -/** - * See: https://spec.graphql.org/draft/#sec-Errors - */ +/** See: https://spec.graphql.org/draft/#sec-Errors */ export interface GraphQLFormattedError { /** * A short, human-readable summary of the problem that **SHOULD NOT** change @@ -293,8 +409,19 @@ export interface GraphQLFormattedError { /** * Prints a GraphQLError to a string, representing useful location information - * about the error's position in the source. + * about the error's position in the source. This helper is retained for + * backwards compatibility; call `error.toString()` instead because printError + * will be removed in v17. + * @param error - The error to format. + * @returns The printed string representation. + * @example + * ```ts + * import { GraphQLError, printError } from 'graphql/error'; * + * const message = printError(new GraphQLError('Example error')); + * + * message; // => 'Example error' + * ``` * @deprecated Please use `error.toString` instead. Will be removed in v17 */ export function printError(error: GraphQLError): string { @@ -303,8 +430,19 @@ export function printError(error: GraphQLError): string { /** * Given a GraphQLError, format it according to the rules described by the - * Response Format, Errors section of the GraphQL Specification. + * Response Format, Errors section of the GraphQL Specification. This helper is + * retained for backwards compatibility; call `error.toJSON()` instead because + * formatError will be removed in v17. + * @param error - The error to format. + * @returns The JSON-serializable formatted error. + * @example + * ```ts + * import { GraphQLError, formatError } from 'graphql/error'; + * + * const formatted = formatError(new GraphQLError('Example error')); * + * formatted; // => { message: 'Example error' } + * ``` * @deprecated Please use `error.toJSON` instead. Will be removed in v17 */ export function formatError(error: GraphQLError): GraphQLFormattedError { diff --git a/src/error/index.ts b/src/error/index.ts index 7e5d267f50..b501a3000a 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,3 +1,10 @@ +/** + * Create, format, and locate GraphQL errors. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + */ + export { GraphQLError, printError, formatError } from './GraphQLError'; export type { GraphQLErrorOptions, diff --git a/src/error/locatedError.ts b/src/error/locatedError.ts index bafb9da9b6..a29519ae1a 100644 --- a/src/error/locatedError.ts +++ b/src/error/locatedError.ts @@ -1,3 +1,5 @@ +/** @category Errors */ + import type { Maybe } from '../jsutils/Maybe'; import { toError } from '../jsutils/toError'; @@ -9,6 +11,25 @@ import { GraphQLError } from './GraphQLError'; * Given an arbitrary value, presumably thrown while attempting to execute a * GraphQL operation, produce a new GraphQLError aware of the location in the * document responsible for the original Error. + * @param rawOriginalError - The original error value to wrap. + * @param nodes - The AST nodes associated with the error. + * @param path - The response path associated with the error. + * @returns The GraphQL error. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { locatedError } from 'graphql/error'; + * + * const document = parse('{ viewer { name } }'); + * const fieldNode = document.definitions[0].selectionSet.selections[0]; + * const error = locatedError(new Error('Resolver failed'), fieldNode, [ + * 'viewer', + * ]); + * + * error.message; // => 'Resolver failed' + * error.locations; // => [{ line: 1, column: 3 }] + * error.path; // => ['viewer'] + * ``` */ export function locatedError( rawOriginalError: unknown, diff --git a/src/error/syntaxError.ts b/src/error/syntaxError.ts index 386ece72da..2f9e3d8ef2 100644 --- a/src/error/syntaxError.ts +++ b/src/error/syntaxError.ts @@ -1,3 +1,5 @@ +/** @category Errors */ + import type { Source } from '../language/source'; import { GraphQLError } from './GraphQLError'; @@ -5,6 +7,20 @@ import { GraphQLError } from './GraphQLError'; /** * Produces a GraphQLError representing a syntax error, containing useful * descriptive information about the syntax error's position in the source. + * @param source - The GraphQL source containing the syntax error. + * @param position - Character offset where the syntax error was encountered. + * @param description - Human-readable description of the syntax error. + * @returns A GraphQLError located at the syntax error position. + * @example + * ```ts + * import { Source } from 'graphql/language'; + * import { syntaxError } from 'graphql/error'; + * + * const error = syntaxError(new Source('query {'), 7, 'Expected Name'); + * + * error.message; // => 'Syntax Error: Expected Name' + * error.locations; // => [{ line: 1, column: 8 }] + * ``` */ export function syntaxError( source: Source, diff --git a/src/execution/collectFields.ts b/src/execution/collectFields.ts index d0961bfae8..a24ce6d956 100644 --- a/src/execution/collectFields.ts +++ b/src/execution/collectFields.ts @@ -161,6 +161,8 @@ function collectFieldsImpl( /** * Determines if a field should be included based on the `@include` and `@skip` * directives, where `@skip` has higher precedence than `@include`. + * + * @internal */ function shouldIncludeNode( variableValues: { [variable: string]: unknown }, @@ -184,6 +186,8 @@ function shouldIncludeNode( /** * Determines if a fragment is applicable to the given type. + * + * @internal */ function doesFragmentConditionMatch( schema: GraphQLSchema, @@ -206,6 +210,8 @@ function doesFragmentConditionMatch( /** * Implements the logic to compute the key of a given field's entry + * + * @internal */ function getFieldEntryKey(node: FieldNode): string { return node.alias ? node.alias.value : node.name.value; diff --git a/src/execution/execute.ts b/src/execution/execute.ts index 1e5ec12c9a..1b15c2ea9e 100644 --- a/src/execution/execute.ts +++ b/src/execution/execute.ts @@ -1,3 +1,5 @@ +/** @category Execution */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; @@ -62,6 +64,8 @@ import { getArgumentValues, getVariableValues } from './values'; * A memoized collection of relevant subfields with regard to the return * type. Memoizing ensures the subfields are not repeatedly calculated, which * saves overhead when resolving lists of values. + * + * @internal */ const collectSubfields = memoize3( ( @@ -103,6 +107,8 @@ const collectSubfields = memoize3( * * Namely, schema of the type system that is currently executing, * and the fragments defined in the query document + * + * @internal */ export interface ExecutionContext { schema: GraphQLSchema; @@ -117,9 +123,6 @@ export interface ExecutionContext { collectedErrors: CollectedErrors; } -/** - * @internal - */ class CollectedErrors { private _errorPositions: Set; private _errors: Array; @@ -157,39 +160,58 @@ class CollectedErrors { } /** - * The result of GraphQL execution. - * - * - `errors` is included when any errors occurred as a non-empty array. - * - `data` is the result of a successful execution of the query. - * - `extensions` is reserved for adding non-standard properties. + * Represents the response produced by executing a GraphQL operation. + * @typeParam TData - Shape of the execution data payload. + * @typeParam TExtensions - Shape of the extensions payload. */ export interface ExecutionResult< TData = ObjMap, TExtensions = ObjMap, > { + /** Errors raised while parsing, validating, or executing the operation. */ errors?: ReadonlyArray; + /** Data returned by execution, or null when execution could not produce data. */ data?: TData | null; + /** Extension fields to include in the formatted result. */ extensions?: TExtensions; } +/** + * A JSON-serializable GraphQL execution result. + * @typeParam TData - Shape of the formatted data payload. + * @typeParam TExtensions - Shape of the formatted extensions payload. + */ export interface FormattedExecutionResult< TData = ObjMap, TExtensions = ObjMap, > { + /** Errors raised while parsing, validating, or executing the operation. */ errors?: ReadonlyArray; + /** Data returned by execution, or null when execution could not produce data. */ data?: TData | null; + /** Extension fields to include in the formatted result. */ extensions?: TExtensions; } +/** Arguments accepted by execute and executeSync. */ export interface ExecutionArgs { + /** The schema used for validation or execution. */ schema: GraphQLSchema; + /** The parsed GraphQL document to execute. */ document: DocumentNode; + /** Initial root value passed to the operation. */ rootValue?: unknown; + /** Application context value passed to every resolver. */ contextValue?: unknown; + /** Runtime variable values keyed by variable name. */ variableValues?: Maybe<{ readonly [variable: string]: unknown }>; + /** Name of the operation to execute when the document contains multiple operations. */ operationName?: Maybe; + /** Resolver used when a field does not define its own resolver. */ fieldResolver?: Maybe>; + /** Resolver used when an abstract type does not define its own resolver. */ typeResolver?: Maybe>; + /** Resolver used for the root subscription field. */ subscribeFieldResolver?: Maybe>; /** Additional execution options. */ options?: { @@ -207,6 +229,139 @@ export interface ExecutionArgs { * * If the arguments to this function do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. + * + * Field errors are collected into the response instead of rejecting the + * returned promise. Only the field that produced the error and its descendants + * are omitted; sibling fields continue to execute. Errors from fields of + * non-null type may propagate to the nearest nullable parent, which can be the + * entire response data. + * @param args - The arguments used to perform the operation. + * @returns A completed execution result, or a promise resolving to one when execution is asynchronous. + * @example + * ```ts + * // Execute an asynchronous operation with variables. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { execute } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * greeting(name: String!): String + * } + * `); + * + * const result = await execute({ + * schema, + * document: parse('query ($name: String!) { greeting(name: $name) }'), + * rootValue: { + * greeting: ({ name }) => `Hello, ${name}!`, + * }, + * variableValues: { name: 'Ada' }, + * }); + * + * result; // => { data: { greeting: 'Hello, Ada!' } } + * ``` + * @example + * ```ts + * // This variant supplies context plus custom field and type resolvers. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { execute } from 'graphql/execution'; + * + * const schema = buildSchema(` + * interface Named { + * name: String! + * } + * + * type User implements Named { + * name: String! + * } + * + * type Query { + * viewer: Named + * } + * `); + * + * const result = await execute({ + * schema, + * document: parse('query Viewer { viewer { __typename name } }'), + * rootValue: { viewer: { kind: 'user', name: 'Ada' } }, + * contextValue: { locale: 'en' }, + * operationName: 'Viewer', + * fieldResolver: (source, _args, contextValue, info) => { + * contextValue.locale; // => 'en' + * return source[info.fieldName]; + * }, + * typeResolver: (value) => { + * return value.kind === 'user' ? 'User' : undefined; + * }, + * }); + * + * result; // => { data: { viewer: { __typename: 'User', name: 'Ada' } } } + * ``` + * @example + * ```ts + * // This variant shows how resolver errors become field errors in the result. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { execute } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * broken: String + * } + * `); + * const document = parse('{ broken }'); + * + * const result = await execute({ + * schema, + * document, + * rootValue: { + * broken: () => { + * throw new Error('Resolver failed.'); + * }, + * }, + * }); + * + * result.data.broken; // => null + * result.errors[0].message; // => 'Resolver failed.' + * ``` + * @example + * ```ts + * // This variant limits how many variable coercion errors are reported. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { execute } from 'graphql/execution'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput!): String + * } + * `); + * const document = parse(` + * query ($first: ReviewInput!, $second: ReviewInput!) { + * first: review(input: $first) + * second: review(input: $second) + * } + * `); + * + * const result = await execute({ + * schema, + * document, + * variableValues: { + * first: { stars: 'bad' }, + * second: { stars: 'also bad' }, + * }, + * options: { maxCoercionErrors: 1 }, + * }); + * + * result.errors.length; // => 2 + * result.errors[1].message; // matches /error limit reached/ + * ``` */ export function execute(args: ExecutionArgs): PromiseOrValue { // Temporary for v15 to v16 migration. Remove in v17 @@ -229,17 +384,6 @@ export function execute(args: ExecutionArgs): PromiseOrValue { return { errors: exeContext }; } - // Return a Promise that will eventually resolve to the data described by - // The "Response" section of the GraphQL specification. - // - // If errors are encountered while executing a GraphQL field, only that - // field and its descendants will be omitted, and sibling fields will still - // be executed. An execution which encounters errors will still result in a - // resolved Promise. - // - // Errors from sub-fields of a NonNull type may propagate to the top level, - // at which point we still log the error and null the parent field, which - // in this case is the entire response. try { const { operation } = exeContext; const result = executeOperation(exeContext, operation, rootValue); @@ -263,6 +407,53 @@ export function execute(args: ExecutionArgs): PromiseOrValue { * Also implements the "Executing requests" section of the GraphQL specification. * However, it guarantees to complete synchronously (or throw an error) assuming * that all field resolvers are also synchronous. + * @param args - The arguments used to perform the operation. + * @returns Completed execution output for a synchronous operation. + * @example + * ```ts + * // Execute an operation synchronously when all resolvers are synchronous. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { executeSync } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * + * const result = executeSync({ + * schema, + * document, + * rootValue: { + * greeting: 'Hello', + * }, + * }); + * + * result; // => { data: { greeting: 'Hello' } } + * ``` + * @example + * ```ts + * // This variant shows executeSync throwing when a resolver returns a promise. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { executeSync } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * executeSync({ + * schema, + * document: parse('{ greeting }'), + * rootValue: { + * greeting: async () => 'Hello', + * }, + * }); // throws an error + * ``` */ export function executeSync(args: ExecutionArgs): ExecutionResult { const result = execute(args); @@ -278,6 +469,8 @@ export function executeSync(args: ExecutionArgs): ExecutionResult { /** * Given a completed execution context and data, build the `{ errors, data }` * response defined by the "Response" section of the GraphQL specification. + * + * @internal */ function buildResponse( data: ObjMap | null, @@ -398,6 +591,8 @@ export function buildExecutionContext( /** * Implements the "Executing operations" section of the spec. + * + * @internal */ function executeOperation( exeContext: ExecutionContext, @@ -442,6 +637,8 @@ function executeOperation( /** * Implements the "Executing selection sets" section of the spec * for fields that must be executed serially. + * + * @internal */ function executeFieldsSerially( exeContext: ExecutionContext, @@ -480,6 +677,8 @@ function executeFieldsSerially( /** * Implements the "Executing selection sets" section of the spec * for fields that may be executed in parallel. + * + * @internal */ function executeFields( exeContext: ExecutionContext, @@ -535,6 +734,8 @@ function executeFields( * In particular, this function figures out the value that the field returns by * calling its resolve function, then calls completeValue to complete promises, * serialize scalars, or execute the sub-selection-set for objects. + * + * @internal */ function executeField( exeContext: ExecutionContext, @@ -608,9 +809,7 @@ function executeField( } } -/** - * @internal - */ +/** @internal */ export function buildResolveInfo( exeContext: ExecutionContext, fieldDef: GraphQLField, @@ -672,6 +871,8 @@ function handleFieldError( * * Otherwise, the field type expects a sub-selection set, and will complete the * value by executing all sub-selections. + * + * @internal */ function completeValue( exeContext: ExecutionContext, @@ -763,6 +964,8 @@ function completeValue( /** * Complete a list value by completing each item in the list with the * inner type + * + * @internal */ function completeListValue( exeContext: ExecutionContext, @@ -836,6 +1039,8 @@ function completeListValue( /** * Complete a Scalar or Enum by serializing to a valid value, returning * null if serialization is not possible. + * + * @internal */ function completeLeafValue( returnType: GraphQLLeafType, @@ -854,6 +1059,8 @@ function completeLeafValue( /** * Complete a value of an abstract type by determining the runtime object type * of that value, then complete the value for that type. + * + * @internal */ function completeAbstractValue( exeContext: ExecutionContext, @@ -961,6 +1168,8 @@ function ensureValidRuntimeType( /** * Complete an Object value by executing all sub-selections. + * + * @internal */ function completeObjectValue( exeContext: ExecutionContext, diff --git a/src/execution/index.ts b/src/execution/index.ts index 7727e6d57c..856d1f089a 100644 --- a/src/execution/index.ts +++ b/src/execution/index.ts @@ -1,3 +1,10 @@ +/** + * Execute GraphQL operations and produce GraphQL execution results. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + */ + export { pathToArray as responsePathAsArray } from '../jsutils/Path'; export { diff --git a/src/execution/mapAsyncIterator.ts b/src/execution/mapAsyncIterator.ts index 82e863c6c0..38b271febd 100644 --- a/src/execution/mapAsyncIterator.ts +++ b/src/execution/mapAsyncIterator.ts @@ -3,6 +3,8 @@ import type { PromiseOrValue } from '../jsutils/PromiseOrValue'; /** * Given an AsyncIterable and a callback function, return an AsyncIterator * which produces values mapped via calling the callback function. + * + * @internal */ export function mapAsyncIterator( iterable: AsyncGenerator | AsyncIterable, diff --git a/src/execution/subscribe.ts b/src/execution/subscribe.ts index 78dc97916e..37755ac925 100644 --- a/src/execution/subscribe.ts +++ b/src/execution/subscribe.ts @@ -1,3 +1,5 @@ +/** @category Subscriptions */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { isAsyncIterable } from '../jsutils/isAsyncIterable'; @@ -32,7 +34,7 @@ import { getArgumentValues } from './values'; /** * Implements the "Subscribe" algorithm described in the GraphQL specification. * - * Returns a Promise which resolves to either an AsyncIterator (if successful) + * Returns a Promise that resolves to either an AsyncIterator (if successful) * or an ExecutionResult (error). The promise will be rejected if the schema or * other arguments to this function are invalid, or if the resolved event stream * is not an async iterable. @@ -48,7 +50,122 @@ import { getArgumentValues } from './values'; * If the operation succeeded, the promise resolves to an AsyncIterator, which * yields a stream of ExecutionResults representing the response stream. * - * Accepts either an object with named arguments, or individual arguments. + * Each payload yielded by the source event stream is executed with the payload + * as the root value. This maps the subscription source stream into the response + * stream described by the GraphQL specification. + * + * Accepts an object with named arguments. + * @param args - The arguments used to perform the operation. + * @returns A source stream mapped to execution results, or an execution result + * containing subscription errors. + * @example + * ```ts + * // Use a same-named rootValue function to provide the source event stream. + * import assert from 'node:assert'; + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { subscribe } from 'graphql/execution'; + * + * async function* greetings() { + * yield { greeting: 'Hello' }; + * yield { greeting: 'Bonjour' }; + * } + * + * const schema = buildSchema(` + * type Query { + * noop: String + * } + * + * type Subscription { + * greeting: String + * } + * `); + * + * const result = await subscribe({ + * schema, + * document: parse('subscription { greeting }'), + * rootValue: { greeting: () => greetings() }, + * }); + * + * assert('next' in result); + * + * const firstPayload = await result.next(); + * firstPayload.value; // => { data: { greeting: 'Hello' } } + * ``` + * @example + * ```ts + * // This variant supplies events through a custom subscribeFieldResolver. + * import assert from 'node:assert'; + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { subscribe } from 'graphql/execution'; + * + * async function* defaultGreetings() { + * yield { greeting: 'Hello' }; + * } + * + * async function* frenchGreetings() { + * yield { greeting: 'Bonjour' }; + * } + * + * const schema = buildSchema(` + * type Query { + * noop: String + * } + * + * type Subscription { + * greeting(locale: String): String + * } + * `); + * + * const result = await subscribe({ + * schema, + * document: parse( + * 'subscription Greeting($locale: String) { greeting(locale: $locale) }', + * ), + * rootValue: { + * greeting: (args, contextValue) => { + * const locale = args.locale ?? contextValue.defaultLocale; + * return locale === 'fr' ? frenchGreetings() : defaultGreetings(); + * }, + * }, + * contextValue: { defaultLocale: 'fr' }, + * variableValues: { locale: 'fr' }, + * operationName: 'Greeting', + * subscribeFieldResolver: (rootValue, args, contextValue, info) => { + * args.locale; // => 'fr' + * return rootValue[info.fieldName](args, contextValue); + * }, + * }); + * + * assert('next' in result); + * + * const firstPayload = await result.next(); + * firstPayload.value; // => { data: { greeting: 'Bonjour' } } + * ``` + * @example + * ```ts + * // This variant shows the error result when the schema has no subscription root. + * import assert from 'node:assert'; + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { subscribe } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * noop: String + * } + * `); + * + * const result = await subscribe({ + * schema, + * document: parse('subscription { greeting }'), + * }); + * + * assert('errors' in result); + * + * result.errors[0].message; // => 'Schema is not configured to execute subscription operation.' + * ``` */ export async function subscribe( args: ExecutionArgs, @@ -65,19 +182,12 @@ export async function subscribe( return resultOrStream; } - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // the GraphQL specification. The `execute` function provides the - // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the - // "ExecuteQuery" algorithm, for which `execute` is also used. const mapSourceToResponse = (payload: unknown) => execute({ ...args, rootValue: payload, }); - // Map every source value to a ExecutionResult value as described above. return mapAsyncIterator(resultOrStream, mapSourceToResponse); } @@ -115,7 +225,7 @@ function toNormalizedArgs(args: BackwardsCompatibleArgs): ExecutionArgs { * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. * - * Returns a Promise which resolves to either an AsyncIterable (if successful) + * Returns a Promise that resolves to either an AsyncIterable (if successful) * or an ExecutionResult (error). The promise will be rejected if the schema or * other arguments to this function are invalid, or if the resolved event stream * is not an async iterable. @@ -124,7 +234,7 @@ function toNormalizedArgs(args: BackwardsCompatibleArgs): ExecutionArgs { * compliant subscription, a GraphQL Response (ExecutionResult) with * descriptive errors and no data will be returned. * - * If the the source stream could not be created due to faulty subscription + * If the source stream could not be created due to faulty subscription * resolver logic or underlying systems, the promise will resolve to a single * ExecutionResult containing `errors` and no `data`. * @@ -138,11 +248,85 @@ function toNormalizedArgs(args: BackwardsCompatibleArgs): ExecutionArgs { * different process or machine than the stateless GraphQL execution engine, * or otherwise separating these two steps. For more on this, see the * "Supporting Subscriptions at Scale" information in the GraphQL specification. + * @param args - The arguments used to perform the operation. + * @returns The source event stream, or an execution result containing subscription errors. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { createSourceEventStream } from 'graphql/execution'; + * + * async function* greetings() { + * yield { greeting: 'Hello' }; + * } + * + * const schema = buildSchema(` + * type Query { + * noop: String + * } + * + * type Subscription { + * greeting: String + * } + * `); + * + * const stream = await createSourceEventStream({ + * schema, + * document: parse('subscription { greeting }'), + * rootValue: { greeting: () => greetings() }, + * }); + * + * Symbol.asyncIterator in stream; // => true + * ``` */ export async function createSourceEventStream( args: ExecutionArgs, ): Promise | ExecutionResult>; -/** @deprecated will be removed in next major version in favor of named arguments */ +/** + * Creates the source event stream for a subscription operation using the legacy + * positional argument overload. Use the args object overload instead; this + * overload will be removed in the next major version. + * @param schema - GraphQL schema to use. + * @param document - The parsed GraphQL document containing the subscription + * operation. + * @param rootValue - Initial root value passed to the subscription resolver. + * @param contextValue - Application context value passed to resolvers. + * @param variableValues - Runtime variable values keyed by variable name. + * @param operationName - Name of the subscription operation to execute when + * the document contains multiple operations. + * @param subscribeFieldResolver - Resolver used for the root subscription + * field. + * @returns The source event stream, or an execution result containing + * subscription errors. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { createSourceEventStream } from 'graphql/execution'; + * + * async function* greetings() { + * yield { greeting: 'Hello' }; + * } + * + * const schema = buildSchema(` + * type Query { + * noop: String + * } + * + * type Subscription { + * greeting: String + * } + * `); + * const document = parse('subscription { greeting }'); + * + * const stream = await createSourceEventStream(schema, document, { + * greeting: () => greetings(), + * }); + * + * Symbol.asyncIterator in stream; // => true + * ``` + * @deprecated Will be removed in next major version in favor of named arguments. + */ export async function createSourceEventStream( schema: GraphQLSchema, document: DocumentNode, @@ -152,6 +336,7 @@ export async function createSourceEventStream( operationName?: Maybe, subscribeFieldResolver?: Maybe>, ): Promise | ExecutionResult>; +/** @internal */ export async function createSourceEventStream( ...rawArgs: BackwardsCompatibleArgs ) { diff --git a/src/execution/values.ts b/src/execution/values.ts index e34c32e384..ddf3e587ff 100644 --- a/src/execution/values.ts +++ b/src/execution/values.ts @@ -1,3 +1,5 @@ +/** @category Values */ + import { inspect } from '../jsutils/inspect'; import { keyMap } from '../jsutils/keyMap'; import type { Maybe } from '../jsutils/Maybe'; @@ -27,20 +29,98 @@ type CoercedVariableValues = | { errors: ReadonlyArray; coerced?: never } | { coerced: { [variable: string]: unknown }; errors?: never }; +/** + * Options used when coercing variable values before execution. + * @internal + */ +export interface GetVariableValuesOptions { + /** + * Maximum number of variable coercion errors before coercion stops. + * @internal + */ + maxErrors?: number; +} + /** * Prepares an object map of variableValues of the correct type based on the * provided variable definitions and arbitrary input. If the input cannot be - * parsed to match the variable definitions, a GraphQLError will be thrown. + * parsed to match the variable definitions, GraphQLError values are returned. * - * Note: The returned value is a plain Object with a prototype, since it is + * Note: Returned value is a plain Object with a prototype, since it is * exposed to user code. Care should be taken to not pull values from the * Object prototype. + * @param schema - GraphQL schema to use. + * @param varDefNodes - The variable definition AST nodes to coerce. + * @param inputs - The runtime variable values keyed by variable name. + * @param options - Optional variable coercion options, including error limits. + * @returns Coerced variable values, or request errors. + * @example + * ```ts + * // Coerce provided variables and apply operation defaults. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { getVariableValues } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * reviews(stars: Int!, limit: Int = 10): [String] + * } + * `); + * const document = parse(` + * query ($stars: Int!, $limit: Int = 10) { + * reviews(stars: $stars, limit: $limit) + * } + * `); + * const operation = document.definitions[0]; + * + * const result = getVariableValues( + * schema, + * operation.variableDefinitions, + * { stars: '5' }, + * ); + * + * result; // => { coerced: { stars: 5, limit: 10 } } + * ``` + * @example + * ```ts + * // This variant uses maxErrors to cap reported coercion errors. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { getVariableValues } from 'graphql/execution'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput!): String + * } + * `); + * const document = parse(` + * query ($first: ReviewInput!, $second: ReviewInput!) { + * first: review(input: $first) + * second: review(input: $second) + * } + * `); + * const operation = document.definitions[0]; + * + * const result = getVariableValues( + * schema, + * operation.variableDefinitions, + * { first: { stars: 'bad' }, second: { stars: 'also bad' } }, + * { maxErrors: 1 }, + * ); + * + * result.errors.length; // => 2 + * result.errors[1].message; // matches /error limit reached/ + * ``` */ export function getVariableValues( schema: GraphQLSchema, varDefNodes: ReadonlyArray, inputs: { readonly [variable: string]: unknown }, - options?: { maxErrors?: number }, + options?: GetVariableValuesOptions, ): CoercedVariableValues { const errors = []; const maxErrors = options?.maxErrors; @@ -145,9 +225,50 @@ function coerceVariableValues( * Prepares an object map of argument values given a list of argument * definitions and list of argument AST nodes. * - * Note: The returned value is a plain Object with a prototype, since it is + * Note: Returned value is a plain Object with a prototype, since it is * exposed to user code. Care should be taken to not pull values from the * Object prototype. + * @param def - The field or directive definition whose arguments should be coerced. + * @param node - The AST node to inspect. + * @param variableValues - The runtime variable values keyed by variable name. + * @returns Coerced argument values keyed by argument name. + * @example + * ```ts + * // Read literal argument values and defaults. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { getArgumentValues } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * reviews(stars: Int!, limit: Int = 10): [String] + * } + * `); + * const fieldDef = schema.getQueryType().getFields().reviews; + * const document = parse('{ reviews(stars: 5) }'); + * const fieldNode = document.definitions[0].selectionSet.selections[0]; + * + * getArgumentValues(fieldDef, fieldNode); // => { stars: 5, limit: 10 } + * ``` + * @example + * ```ts + * // This variant resolves argument values from operation variables. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { getArgumentValues } from 'graphql/execution'; + * + * const schema = buildSchema(` + * type Query { + * reviews(stars: Int!): [String] + * } + * `); + * const fieldDef = schema.getQueryType().getFields().reviews; + * const document = parse('query ($stars: Int!) { reviews(stars: $stars) }'); + * const fieldNode = document.definitions[0].selectionSet.selections[0]; + * + * getArgumentValues(fieldDef, fieldNode, { stars: 5 }); // => { stars: 5 } + * getArgumentValues(fieldDef, fieldNode, {}); // throws an error + * ``` */ export function getArgumentValues( def: GraphQLField | GraphQLDirective, @@ -225,6 +346,18 @@ export function getArgumentValues( return { ...coercedValues }; } +/** + * AST node shape accepted by getDirectiveValues. + * @internal + */ +export interface DirectiveValuesNode { + /** + * Directives attached to the AST node. + * @internal + */ + readonly directives?: ReadonlyArray; +} + /** * Prepares an object map of argument values given a directive definition * and a AST node which may contain directives. Optionally also accepts a map @@ -232,13 +365,44 @@ export function getArgumentValues( * * If the directive does not exist on the node, returns undefined. * - * Note: The returned value is a plain Object with a prototype, since it is + * Note: Returned value is a plain Object with a prototype, since it is * exposed to user code. Care should be taken to not pull values from the * Object prototype. + * @param directiveDef - The directive definition whose arguments should be coerced. + * @param node - The AST node to inspect. + * @param variableValues - The runtime variable values keyed by variable name. + * @returns Coerced directive argument values keyed by argument name. + * @example + * ```ts + * // Read literal directive arguments from a node. + * import { parse } from 'graphql/language'; + * import { GraphQLSkipDirective } from 'graphql/type'; + * import { getDirectiveValues } from 'graphql/execution'; + * + * const document = parse('{ name @skip(if: true) }'); + * const fieldNode = document.definitions[0].selectionSet.selections[0]; + * + * getDirectiveValues(GraphQLSkipDirective, fieldNode); // => { if: true } + * ``` + * @example + * ```ts + * // This variant resolves directive arguments from variables and handles absent directives. + * import { parse } from 'graphql/language'; + * import { GraphQLIncludeDirective } from 'graphql/type'; + * import { getDirectiveValues } from 'graphql/execution'; + * + * const document = parse('query ($includeName: Boolean!) { name @include(if: $includeName) }'); + * const fieldNode = document.definitions[0].selectionSet.selections[0]; + * + * getDirectiveValues(GraphQLIncludeDirective, fieldNode, { + * includeName: false, + * }); // => { if: false } + * getDirectiveValues(GraphQLIncludeDirective, { directives: [] }); // => undefined + * ``` */ export function getDirectiveValues( directiveDef: GraphQLDirective, - node: { readonly directives?: ReadonlyArray }, + node: DirectiveValuesNode, variableValues?: Maybe>, ): undefined | { [argument: string]: unknown } { const directiveNode = node.directives?.find( diff --git a/src/graphql.ts b/src/graphql.ts index bc6fb9bb72..ba735be074 100644 --- a/src/graphql.ts +++ b/src/graphql.ts @@ -19,65 +19,187 @@ import type { ExecutionResult } from './execution/execute'; import { execute } from './execution/execute'; /** - * This is the primary entry point function for fulfilling GraphQL operations - * by parsing, validating, and executing a GraphQL document along side a - * GraphQL schema. - * - * More sophisticated GraphQL servers, such as those which persist queries, - * may wish to separate the validation and execution phases to a static time - * tooling step, and a server runtime step. - * - * Accepts either an object with named arguments, or individual arguments: - * - * schema: - * The GraphQL type system to use when validating and executing a query. - * source: - * A GraphQL language formatted string representing the requested operation. - * rootValue: - * The value provided as the first argument to resolver functions on the top - * level type (e.g. the query object type). - * contextValue: - * The context value is provided as an argument to resolver functions after - * field arguments. It is used to pass shared information useful at any point - * during executing this query, for example the currently logged in user and - * connections to databases or other services. - * variableValues: - * A mapping of variable name to runtime value to use for all variables - * defined in the requestString. - * operationName: - * The name of the operation to use if requestString contains multiple - * possible operations. Can be omitted if requestString contains only - * one operation. - * fieldResolver: - * A resolver function to use when one is not provided by the schema. - * If not provided, the default field resolver is used (which looks for a - * value or method on the source value with the field's name). - * typeResolver: - * A type resolver function to use when none is provided by the schema. - * If not provided, the default type resolver is used (which looks for a - * `__typename` field or alternatively calls the `isTypeOf` method). + * Describes the input object accepted by `graphql` and `graphqlSync`. + * + * These arguments describe the full parse, validate, and execute lifecycle for + * a GraphQL request. + * @category Request Pipeline */ export interface GraphQLArgs { + /** The GraphQL type system to use when validating and executing a query. */ schema: GraphQLSchema; + /** + * A GraphQL language-formatted string or source object representing the + * requested operation. + */ source: string | Source; + /** + * The value provided as the first argument to resolver functions on the top + * level type, such as the query object type. + */ rootValue?: unknown; + /** + * Application context value passed to every resolver. + * + * Use this for shared request data such as the currently logged in user and + * connections to databases or other services. + */ contextValue?: unknown; + /** A mapping of variable name to runtime value for variables defined by the operation. */ variableValues?: Maybe<{ readonly [variable: string]: unknown }>; + /** + * The operation to execute when the source contains multiple possible + * operations. This can be omitted when the source contains only one operation. + */ operationName?: Maybe; + /** + * A resolver function to use when one is not provided by the schema. + * + * If not provided, the default field resolver is used, which looks for a value + * or method on the source value with the field's name. + */ fieldResolver?: Maybe>; + /** + * A type resolver function to use when none is provided by the schema. + * + * If not provided, the default type resolver is used, which looks for a + * `__typename` field or alternatively calls the `isTypeOf` method. + */ typeResolver?: Maybe>; } +/** + * Parses, validates, and executes a GraphQL document against a schema. + * + * This is the primary entry point for fulfilling GraphQL operations. Use this + * when you want a single-call request lifecycle that returns a promise in all + * cases. + * + * More sophisticated GraphQL servers, such as those which persist queries, may + * wish to separate the validation and execution phases to a static-time tooling + * step and a server runtime step. + * @param args - Request execution arguments, including schema and source. + * @returns A promise that resolves to an execution result or validation errors. + * @example + * ```ts + * // Execute a complete asynchronous request with variables. + * import { graphql, buildSchema } from 'graphql'; + * + * const schema = buildSchema(` + * type Query { + * greeting(name: String!): String + * } + * `); + * + * const result = await graphql({ + * schema, + * source: 'query SayHello($name: String!) { greeting(name: $name) }', + * rootValue: { + * greeting: ({ name }) => `Hello, ${name}!`, + * }, + * variableValues: { name: 'Ada' }, + * operationName: 'SayHello', + * }); + * + * result; // => { data: { greeting: 'Hello, Ada!' } } + * ``` + * @example + * ```ts + * // This variant supplies context plus custom field and type resolvers. + * import { graphql, buildSchema } from 'graphql'; + * + * const schema = buildSchema(` + * interface Named { + * name: String! + * } + * + * type User implements Named { + * name: String! + * } + * + * type Query { + * viewer: Named + * } + * `); + * + * const result = await graphql({ + * schema, + * source: '{ viewer { __typename name } }', + * rootValue: { viewer: { kind: 'user', name: 'Ada' } }, + * contextValue: { locale: 'en' }, + * fieldResolver: (source, _args, context, info) => { + * context.locale; // => 'en' + * return source[info.fieldName]; + * }, + * typeResolver: (value) => { + * return value.kind === 'user' ? 'User' : undefined; + * }, + * }); + * + * result; // => { data: { viewer: { __typename: 'User', name: 'Ada' } } } + * ``` + * @category Request Pipeline + */ export function graphql(args: GraphQLArgs): Promise { // Always return a Promise for a consistent API. return new Promise((resolve) => resolve(graphqlImpl(args))); } /** - * The graphqlSync function also fulfills GraphQL operations by parsing, - * validating, and executing a GraphQL document along side a GraphQL schema. - * However, it guarantees to complete synchronously (or throw an error) assuming - * that all field resolvers are also synchronous. + * Parses, validates, and executes a GraphQL document synchronously. + * + * This function guarantees that execution completes synchronously, or throws an + * error, assuming that all field resolvers are also synchronous. It throws when + * any resolver returns a promise. + * @param args - Request execution arguments, including schema and source. + * @returns Completed execution output, or request errors if parsing or + * validation fails. + * @example + * ```ts + * // Execute a complete synchronous request with variables. + * import { graphqlSync, buildSchema } from 'graphql'; + * + * const schema = buildSchema(` + * type Query { + * greeting(name: String!): String + * } + * `); + * + * const result = graphqlSync({ + * schema, + * source: 'query SayHello($name: String!) { greeting(name: $name) }', + * rootValue: { + * greeting: ({ name }) => `Hello, ${name}!`, + * }, + * variableValues: { name: 'Ada' }, + * operationName: 'SayHello', + * }); + * + * result; // => { data: { greeting: 'Hello, Ada!' } } + * ``` + * @example + * ```ts + * // This variant uses a synchronous custom field resolver and context. + * import { graphqlSync, buildSchema } from 'graphql'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * const result = graphqlSync({ + * schema, + * source: '{ greeting }', + * fieldResolver: (_source, _args, contextValue) => { + * return contextValue.defaultGreeting; + * }, + * contextValue: { defaultGreeting: 'Hello' }, + * }); + * + * result; // => { data: { greeting: 'Hello' } } + * ``` + * @category Request Pipeline */ export function graphqlSync(args: GraphQLArgs): ExecutionResult { const result = graphqlImpl(args); diff --git a/src/index.ts b/src/index.ts index 317c9f8ca9..9c4fa8cb54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,35 +1,29 @@ /** - * GraphQL.js provides a reference implementation for the GraphQL specification - * but is also a useful utility for operating on GraphQL files and building - * sophisticated tools. + * The root `graphql` package re-exports the public GraphQL.js API from its + * submodules and provides the high-level request pipeline helpers defined in + * this module. * - * This primary module exports a general purpose function for fulfilling all - * steps of the GraphQL specification in a single operation, but also includes - * utilities for every part of the GraphQL specification: - * - * - Parsing the GraphQL language. - * - Building a GraphQL type schema. - * - Validating a GraphQL request against a type schema. - * - Executing a GraphQL request against a type schema. - * - * This also includes utility functions for operating on GraphQL types and - * GraphQL documents to facilitate building tools. - * - * You may also import from each sub-directory directly. For example, the - * following two import statements are equivalent: + * You can import public exports from GraphQL.js modules through the root + * `graphql` package or through their module-specific entry point. For example, + * these two references resolve to the same `parse` function: * * ```ts * import { parse } from 'graphql'; * import { parse } from 'graphql/language'; * ``` * + * Use the root package when you want a single import surface, or use submodules + * such as `graphql/language`, `graphql/type`, `graphql/execution`, and + * `graphql/utilities` when you want module-focused imports. This module also + * defines root-only APIs, such as request pipeline helpers and version + * metadata, that do not belong to a narrower submodule. * @packageDocumentation */ -// The GraphQL.js version info. +// Version constants for the GraphQL.js package. export { version, versionInfo } from './version'; -// The primary entry point into fulfilling a GraphQL request. +// Top-level helpers for fulfilling a GraphQL request. export type { GraphQLArgs } from './graphql'; export { graphql, graphqlSync } from './graphql'; diff --git a/src/jsutils/Maybe.ts b/src/jsutils/Maybe.ts index 0ba64a4b64..db993e88ec 100644 --- a/src/jsutils/Maybe.ts +++ b/src/jsutils/Maybe.ts @@ -1,2 +1,6 @@ -/** Conveniently represents flow's "Maybe" type https://flow.org/en/docs/types/maybe/ */ +/** + * Conveniently represents flow's "Maybe" type https://flow.org/en/docs/types/maybe/ + * + * @internal + */ export type Maybe = null | undefined | T; diff --git a/src/jsutils/ObjMap.ts b/src/jsutils/ObjMap.ts index 2c20282187..4f63847e43 100644 --- a/src/jsutils/ObjMap.ts +++ b/src/jsutils/ObjMap.ts @@ -1,13 +1,17 @@ +/** @internal */ export interface ObjMap { [key: string]: T; } +/** @internal */ export type ObjMapLike = ObjMap | { [key: string]: T }; +/** @internal */ export interface ReadOnlyObjMap { readonly [key: string]: T; } +/** @internal */ export type ReadOnlyObjMapLike = | ReadOnlyObjMap | { readonly [key: string]: T }; diff --git a/src/jsutils/Path.ts b/src/jsutils/Path.ts index 64f6c78358..84a352feaf 100644 --- a/src/jsutils/Path.ts +++ b/src/jsutils/Path.ts @@ -1,13 +1,21 @@ +/** @category Paths */ + import type { Maybe } from './Maybe'; +/** Represents a linked response path from a field back to the root response. */ export interface Path { + /** The previous segment in the linked response path, or undefined at the root. */ readonly prev: Path | undefined; + /** The field name or list index for this response path segment. */ readonly key: string | number; + /** The runtime object type name associated with this path segment, if known. */ readonly typename: string | undefined; } /** * Given a Path and a key, return a new Path containing the new key. + * + * @internal */ export function addPath( prev: Readonly | undefined, @@ -19,6 +27,29 @@ export function addPath( /** * Given a Path, return an Array of the path keys. + * @param path - The linked response path to flatten. + * @returns An array of response path keys from root to leaf. + * @example + * ```ts + * import { pathToArray } from 'graphql/jsutils/Path'; + * + * const path = { + * prev: { + * prev: { + * prev: undefined, + * key: 'viewer', + * typename: 'Query', + * }, + * key: 'friends', + * typename: 'User', + * }, + * key: 0, + * typename: undefined, + * }; + * + * pathToArray(path); // => ['viewer', 'friends', 0] + * pathToArray(undefined); // => [] + * ``` */ export function pathToArray( path: Maybe>, diff --git a/src/jsutils/PromiseOrValue.ts b/src/jsutils/PromiseOrValue.ts index 6b2517ee62..25acd00689 100644 --- a/src/jsutils/PromiseOrValue.ts +++ b/src/jsutils/PromiseOrValue.ts @@ -1 +1,2 @@ +/** @internal */ export type PromiseOrValue = Promise | T; diff --git a/src/jsutils/devAssert.ts b/src/jsutils/devAssert.ts index ff97228b9f..a09162ccd1 100644 --- a/src/jsutils/devAssert.ts +++ b/src/jsutils/devAssert.ts @@ -1,3 +1,4 @@ +/** @internal */ export function devAssert(condition: unknown, message: string): void { const booleanCondition = Boolean(condition); if (!booleanCondition) { diff --git a/src/jsutils/didYouMean.ts b/src/jsutils/didYouMean.ts index 33e10a42c1..64fa6da06b 100644 --- a/src/jsutils/didYouMean.ts +++ b/src/jsutils/didYouMean.ts @@ -1,13 +1,17 @@ const MAX_SUGGESTIONS = 5; /** - * Given [ A, B, C ] return ' Did you mean A, B, or C?'. + * Given [A, B, C] return ' Did you mean A, B, or C?'. + * + * @internal */ export function didYouMean(suggestions: ReadonlyArray): string; +/** @internal */ export function didYouMean( subMessage: string, suggestions: ReadonlyArray, ): string; +/** @internal */ export function didYouMean( firstArg: string | ReadonlyArray, secondArg?: ReadonlyArray, diff --git a/src/jsutils/groupBy.ts b/src/jsutils/groupBy.ts index f3b0c076d1..e6a796bcd5 100644 --- a/src/jsutils/groupBy.ts +++ b/src/jsutils/groupBy.ts @@ -1,5 +1,7 @@ /** * Groups array items into a Map, given a function to produce grouping key. + * + * @internal */ export function groupBy( list: ReadonlyArray, diff --git a/src/jsutils/identityFunc.ts b/src/jsutils/identityFunc.ts index a249b51c34..6f55b4a8f1 100644 --- a/src/jsutils/identityFunc.ts +++ b/src/jsutils/identityFunc.ts @@ -1,5 +1,7 @@ /** * Returns the first argument it receives. + * + * @internal */ export function identityFunc(x: T): T { return x; diff --git a/src/jsutils/inspect.ts b/src/jsutils/inspect.ts index 514cbaad39..c295cd3914 100644 --- a/src/jsutils/inspect.ts +++ b/src/jsutils/inspect.ts @@ -3,6 +3,8 @@ const MAX_RECURSIVE_DEPTH = 2; /** * Used to print values in error messages. + * + * @internal */ export function inspect(value: unknown): string { return formatValue(value, []); diff --git a/src/jsutils/instanceOf.ts b/src/jsutils/instanceOf.ts index 27c4ab4d12..b0822d094f 100644 --- a/src/jsutils/instanceOf.ts +++ b/src/jsutils/instanceOf.ts @@ -11,6 +11,8 @@ const isProduction = * constructors are detected. * See: https://expressjs.com/en/advanced/best-practice-performance.html#set-node_env-to-production * See: https://webpack.js.org/guides/production/ + * + * @internal */ export const instanceOf: (value: unknown, constructor: Constructor) => boolean = /* c8 ignore next 6 */ diff --git a/src/jsutils/invariant.ts b/src/jsutils/invariant.ts index f2c5d4c625..715ca807d2 100644 --- a/src/jsutils/invariant.ts +++ b/src/jsutils/invariant.ts @@ -1,3 +1,4 @@ +/** @internal */ export function invariant( condition: unknown, message?: string, diff --git a/src/jsutils/isAsyncIterable.ts b/src/jsutils/isAsyncIterable.ts index 0eb4ab1d6e..fd0e938154 100644 --- a/src/jsutils/isAsyncIterable.ts +++ b/src/jsutils/isAsyncIterable.ts @@ -1,6 +1,8 @@ /** * Returns true if the provided object implements the AsyncIterator protocol via * implementing a `Symbol.asyncIterator` method. + * + * @internal */ export function isAsyncIterable( maybeAsyncIterable: any, diff --git a/src/jsutils/isIterableObject.ts b/src/jsutils/isIterableObject.ts index 5c9d6fb381..50dd6b7442 100644 --- a/src/jsutils/isIterableObject.ts +++ b/src/jsutils/isIterableObject.ts @@ -6,13 +6,14 @@ * an object should be iterated-over e.g. Array, Map, Set, Int8Array, * TypedArray, etc. but excludes string literals. * + * @internal * @example * ```ts - * isIterableObject([ 1, 2, 3 ]) // true - * isIterableObject(new Map()) // true - * isIterableObject('ABC') // false - * isIterableObject({ key: 'value' }) // false - * isIterableObject({ length: 1, 0: 'Alpha' }) // false + * isIterableObject([1, 2, 3]); // => true + * isIterableObject(new Map()); // => true + * isIterableObject('ABC'); // => false + * isIterableObject({ key: 'value' }); // => false + * isIterableObject({ length: 1, 0: 'Alpha' }); // => false * ``` */ export function isIterableObject( diff --git a/src/jsutils/isObjectLike.ts b/src/jsutils/isObjectLike.ts index 1d43e26718..407ad6390f 100644 --- a/src/jsutils/isObjectLike.ts +++ b/src/jsutils/isObjectLike.ts @@ -1,6 +1,8 @@ /** * Return true if `value` is object-like. A value is object-like if it's not * `null` and has a `typeof` result of "object". + * + * @internal */ export function isObjectLike( value: unknown, diff --git a/src/jsutils/isPromise.ts b/src/jsutils/isPromise.ts index 5fc3c10458..cd77215e68 100644 --- a/src/jsutils/isPromise.ts +++ b/src/jsutils/isPromise.ts @@ -1,6 +1,8 @@ /** * Returns true if the value acts like a Promise, i.e. has a "then" function, * otherwise returns false. + * + * @internal */ export function isPromise(value: any): value is Promise { return typeof value?.then === 'function'; diff --git a/src/jsutils/keyMap.ts b/src/jsutils/keyMap.ts index 592a98c83d..8ea1b3db82 100644 --- a/src/jsutils/keyMap.ts +++ b/src/jsutils/keyMap.ts @@ -6,25 +6,18 @@ import type { ObjMap } from './ObjMap'; * * This provides a convenient lookup for the array items if the key function * produces unique results. + * @internal + * @example * ```ts * const phoneBook = [ * { name: 'Jon', num: '555-1234' }, - * { name: 'Jenny', num: '867-5309' } - * ] + * { name: 'Jenny', num: '867-5309' }, + * ]; * - * const entriesByName = keyMap( - * phoneBook, - * entry => entry.name - * ) + * const entriesByName = keyMap(phoneBook, (entry) => entry.name); * - * // { - * // Jon: { name: 'Jon', num: '555-1234' }, - * // Jenny: { name: 'Jenny', num: '867-5309' } - * // } - * - * const jennyEntry = entriesByName['Jenny'] - * - * // { name: 'Jenny', num: '857-6309' } + * Object.keys(entriesByName); // => ['Jon', 'Jenny'] + * entriesByName['Jenny']; // => { name: 'Jenny', num: '867-5309' } * ``` */ export function keyMap( diff --git a/src/jsutils/keyValMap.ts b/src/jsutils/keyValMap.ts index 94d688c2c1..76e884ec97 100644 --- a/src/jsutils/keyValMap.ts +++ b/src/jsutils/keyValMap.ts @@ -3,18 +3,21 @@ import type { ObjMap } from './ObjMap'; /** * Creates a keyed JS object from an array, given a function to produce the keys * and a function to produce the values from each item in the array. + * @internal + * @example * ```ts * const phoneBook = [ * { name: 'Jon', num: '555-1234' }, - * { name: 'Jenny', num: '867-5309' } - * ] + * { name: 'Jenny', num: '867-5309' }, + * ]; * - * // { Jon: '555-1234', Jenny: '867-5309' } * const phonesByName = keyValMap( * phoneBook, - * entry => entry.name, - * entry => entry.num - * ) + * (entry) => entry.name, + * (entry) => entry.num, + * ); + * + * phonesByName; // => { Jon: '555-1234', Jenny: '867-5309' } * ``` */ export function keyValMap( diff --git a/src/jsutils/mapValue.ts b/src/jsutils/mapValue.ts index 32686a29c1..db6f3616fb 100644 --- a/src/jsutils/mapValue.ts +++ b/src/jsutils/mapValue.ts @@ -3,6 +3,8 @@ import type { ObjMap, ReadOnlyObjMap } from './ObjMap'; /** * Creates an object map with the same keys as `map` and values generated by * running each value of `map` thru `fn`. + * + * @internal */ export function mapValue( map: ReadOnlyObjMap, diff --git a/src/jsutils/memoize3.ts b/src/jsutils/memoize3.ts index 213cb95d10..d370e7be8f 100644 --- a/src/jsutils/memoize3.ts +++ b/src/jsutils/memoize3.ts @@ -1,5 +1,7 @@ /** * Memoizes the provided three-argument function. + * + * @internal */ export function memoize3< A1 extends object, diff --git a/src/jsutils/naturalCompare.ts b/src/jsutils/naturalCompare.ts index 7a56286306..8b93317bb0 100644 --- a/src/jsutils/naturalCompare.ts +++ b/src/jsutils/naturalCompare.ts @@ -4,6 +4,8 @@ * * See: https://en.wikipedia.org/wiki/Natural_sort_order * + * + * @internal */ export function naturalCompare(aStr: string, bStr: string): number { let aIndex = 0; diff --git a/src/jsutils/printPathArray.ts b/src/jsutils/printPathArray.ts index 0d9fcc2b19..c99d601011 100644 --- a/src/jsutils/printPathArray.ts +++ b/src/jsutils/printPathArray.ts @@ -1,5 +1,7 @@ /** * Build a string describing the path. + * + * @internal */ export function printPathArray(path: ReadonlyArray): string { return path diff --git a/src/jsutils/promiseForObject.ts b/src/jsutils/promiseForObject.ts index 1074676030..e9d1382bb4 100644 --- a/src/jsutils/promiseForObject.ts +++ b/src/jsutils/promiseForObject.ts @@ -6,6 +6,8 @@ import type { ObjMap } from './ObjMap'; * * This is akin to bluebird's `Promise.props`, but implemented only using * `Promise.all` so it will work with any implementation of ES6 promises. + * + * @internal */ export function promiseForObject( object: ObjMap>, diff --git a/src/jsutils/promiseReduce.ts b/src/jsutils/promiseReduce.ts index 58db2e85c8..dec1845864 100644 --- a/src/jsutils/promiseReduce.ts +++ b/src/jsutils/promiseReduce.ts @@ -7,6 +7,8 @@ import type { PromiseOrValue } from './PromiseOrValue'; * * If the callback does not return a Promise, then this function will also not * return a Promise. + * + * @internal */ export function promiseReduce( values: Iterable, diff --git a/src/jsutils/suggestionList.ts b/src/jsutils/suggestionList.ts index 53ad685c8c..3d2e9baae5 100644 --- a/src/jsutils/suggestionList.ts +++ b/src/jsutils/suggestionList.ts @@ -3,6 +3,8 @@ import { naturalCompare } from './naturalCompare'; /** * Given an invalid input string and a list of valid options, returns a filtered * list of valid options sorted based on their similarity with the input. + * + * @internal */ export function suggestionList( input: string, @@ -38,6 +40,8 @@ export function suggestionList( * of 1. * * This distance can be useful for detecting typos in input or sorting + * + * @internal */ class LexicalDistance { _input: string; diff --git a/src/jsutils/toError.ts b/src/jsutils/toError.ts index 8d562273d6..a8fde79538 100644 --- a/src/jsutils/toError.ts +++ b/src/jsutils/toError.ts @@ -2,6 +2,8 @@ import { inspect } from './inspect'; /** * Sometimes a non-error is thrown, wrap it as an Error instance to ensure a consistent Error interface. + * + * @internal */ export function toError(thrownValue: unknown): Error { return thrownValue instanceof Error diff --git a/src/jsutils/toObjMap.ts b/src/jsutils/toObjMap.ts index 6fe352db23..b0a32f29ee 100644 --- a/src/jsutils/toObjMap.ts +++ b/src/jsutils/toObjMap.ts @@ -1,6 +1,7 @@ import type { Maybe } from './Maybe'; import type { ReadOnlyObjMap, ReadOnlyObjMapLike } from './ObjMap'; +/** @internal */ export function toObjMap( obj: Maybe>, ): ReadOnlyObjMap { diff --git a/src/language/ast.ts b/src/language/ast.ts index 6810b73961..502984d54d 100644 --- a/src/language/ast.ts +++ b/src/language/ast.ts @@ -1,3 +1,5 @@ +/** @category AST */ + import type { Kind } from './kinds'; import type { Source } from './source'; import type { TokenKind } from './tokenKind'; @@ -7,31 +9,40 @@ import type { TokenKind } from './tokenKind'; * identify the region of the source from which the AST derived. */ export class Location { - /** - * The character offset at which this Node begins. - */ + /** The character offset at which this Node begins. */ readonly start: number; - /** - * The character offset at which this Node ends. - */ + /** The character offset at which this Node ends. */ readonly end: number; - /** - * The Token at which this Node begins. - */ + /** The Token at which this Node begins. */ readonly startToken: Token; - /** - * The Token at which this Node ends. - */ + /** The Token at which this Node ends. */ readonly endToken: Token; - /** - * The Source document the AST represents. - */ + /** The Source document the AST represents. */ readonly source: Source; + /** + * Creates a Location instance. + * @param startToken - The start token. + * @param endToken - The end token. + * @param source - Source document used to derive error locations. + * @example + * ```ts + * import { Location, Source, Token, TokenKind } from 'graphql/language'; + * + * const source = new Source('{ hello }'); + * const startToken = new Token(TokenKind.BRACE_L, 0, 1, 1, 1); + * const endToken = new Token(TokenKind.BRACE_R, 8, 9, 1, 9); + * const location = new Location(startToken, endToken, source); + * + * location.start; // => 0 + * location.end; // => 9 + * location.source.body; // => '{ hello }' + * ``` + */ constructor(startToken: Token, endToken: Token, source: Source) { this.start = startToken.start; this.end = endToken.end; @@ -40,10 +51,27 @@ export class Location { this.source = source; } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'Location'; } + /** + * Returns a JSON representation of this location. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * + * const document = parse('{ hello }'); + * const location = document.loc?.toJSON(); + * + * location; // => { start: 0, end: 9 } + * ``` + */ toJSON(): { start: number; end: number } { return { start: this.start, end: this.end }; } @@ -54,29 +82,19 @@ export class Location { * within a Source. */ export class Token { - /** - * The kind of Token. - */ + /** The kind of Token. */ readonly kind: TokenKind; - /** - * The character offset at which this Node begins. - */ + /** The character offset at which this Node begins. */ readonly start: number; - /** - * The character offset at which this Node ends. - */ + /** The character offset at which this Node ends. */ readonly end: number; - /** - * The 1-indexed line number on which this Token appears. - */ + /** The 1-indexed line number on which this Token appears. */ readonly line: number; - /** - * The 1-indexed column number at which this Token begins. - */ + /** The 1-indexed column number at which this Token begins. */ readonly column: number; /** @@ -93,8 +111,28 @@ export class Token { * the last. */ readonly prev: Token | null; + /** Next token in the token stream, including ignored tokens. */ readonly next: Token | null; + /** + * Creates a Token instance. + * @param kind - Token kind produced by lexical analysis. + * @param start - Character offset where this token begins. + * @param end - Character offset where this token ends. + * @param line - One-indexed line number where this token begins. + * @param column - One-indexed column number where this token begins. + * @param value - Interpreted value for non-punctuation tokens. + * @example + * ```ts + * import { Token, TokenKind } from 'graphql/language'; + * + * const token = new Token(TokenKind.NAME, 2, 7, 1, 3, 'hello'); + * + * token.kind; // => TokenKind.NAME + * token.value; // => 'hello' + * token.toJSON(); // => { kind: 'Name', value: 'hello', line: 1, column: 3 } + * ``` + */ constructor( kind: TokenKind, start: number, @@ -114,10 +152,27 @@ export class Token { this.next = null; } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'Token'; } + /** + * Returns a JSON representation of this token. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { Lexer, Source } from 'graphql/language'; + * + * const lexer = new Lexer(new Source('{ hello }')); + * const token = lexer.advance().toJSON(); + * + * token; // => { kind: '{', value: undefined, line: 1, column: 1 } + * ``` + */ toJSON(): { kind: TokenKind; value?: string; @@ -133,9 +188,7 @@ export class Token { } } -/** - * The list of all possible AST node types. - */ +/** The list of all possible AST node types. */ export type ASTNode = | NameNode | DocumentNode @@ -187,16 +240,12 @@ export type ASTNode = | DirectiveCoordinateNode | DirectiveArgumentCoordinateNode; -/** - * Utility type listing all nodes indexed by their kind. - */ +/** Utility type listing all nodes indexed by their kind. */ export type ASTKindToNode = { [NodeT in ASTNode as NodeT['kind']]: NodeT; }; -/** - * @internal - */ +/** @internal */ export const QueryDocumentKeys: { [NodeT in ASTNode as NodeT['kind']]: ReadonlyArray; } = { @@ -308,137 +357,216 @@ export const QueryDocumentKeys: { }; const kindValues = new Set(Object.keys(QueryDocumentKeys)); -/** - * @internal - */ +/** @internal */ export function isNode(maybeNode: any): maybeNode is ASTNode { const maybeKind = maybeNode?.kind; return typeof maybeKind === 'string' && kindValues.has(maybeKind); } -/** Name */ - +/** An identifier in a GraphQL document. */ export interface NameNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.NAME; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Parsed value represented by this node. */ readonly value: string; } -/** Document */ - +/** The root AST node for a parsed GraphQL document. */ export interface DocumentNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DOCUMENT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Top-level executable and type-system definitions in this document. */ readonly definitions: ReadonlyArray; + /** The number of lexical tokens parsed for this document, if token counting was enabled. */ readonly tokenCount?: number | undefined; } +/** Any top-level definition that may appear in a GraphQL document. */ export type DefinitionNode = | ExecutableDefinitionNode | TypeSystemDefinitionNode | TypeSystemExtensionNode; +/** Any executable definition that may appear in an operation document. */ export type ExecutableDefinitionNode = | OperationDefinitionNode | FragmentDefinitionNode; +/** A query, mutation, or subscription operation definition. */ export interface OperationDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OPERATION_DEFINITION; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The operation selected for execution. */ readonly operation: OperationTypeNode; + /** Name node identifying this AST node. */ readonly name?: NameNode; + /** Variable definitions declared by this operation or fragment. */ readonly variableDefinitions?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Selections made by this operation, field, or fragment. */ readonly selectionSet: SelectionSetNode; } +/** + * The operation types supported by GraphQL executable definitions. + * @category Kinds + */ enum OperationTypeNode { + /** A query operation. */ QUERY = 'query', + /** A mutation operation. */ MUTATION = 'mutation', + /** A subscription operation. */ SUBSCRIPTION = 'subscription', } export { OperationTypeNode }; +/** A variable declaration in an operation or legacy fragment definition. */ export interface VariableDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.VARIABLE_DEFINITION; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The variable being defined or referenced. */ readonly variable: VariableNode; + /** The GraphQL type reference or runtime type for this element. */ readonly type: TypeNode; + /** Default value used when no explicit value is supplied. */ readonly defaultValue?: ConstValueNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** A variable reference, such as `$id`. */ export interface VariableNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.VARIABLE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; } +/** A set of fields and fragments selected from an object, interface, or union. */ export interface SelectionSetNode { + /** The discriminator identifying the concrete AST or introspection kind. */ kind: Kind.SELECTION_SET; + /** The source location for this AST node, if location tracking was enabled. */ loc?: Location; + /** Fields and fragments contained in this selection set. */ selections: ReadonlyArray; } +/** Any selection that may appear inside a selection set. */ export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode; +/** A field selected in an executable GraphQL document. */ export interface FieldNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.FIELD; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The response-key alias for this field, if one was supplied. */ readonly alias?: NameNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Arguments supplied to this field, directive, or coordinate. */ readonly arguments?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Selections made by this operation, field, or fragment. */ readonly selectionSet?: SelectionSetNode; } +/** An argument supplied to a field or directive. */ export interface ArgumentNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ARGUMENT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Parsed value represented by this node. */ readonly value: ValueNode; } +/** An argument node whose value is guaranteed to be constant. */ export interface ConstArgumentNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ARGUMENT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Parsed value represented by this node. */ readonly value: ConstValueNode; } -/** Fragments */ +// Fragments +/** A named fragment spread, such as `...userFields`. */ export interface FragmentSpreadNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.FRAGMENT_SPREAD; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** An inline fragment spread with an optional type condition. */ export interface InlineFragmentNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INLINE_FRAGMENT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The type condition that limits where this fragment applies. */ readonly typeCondition?: NamedTypeNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Selections made by this operation, field, or fragment. */ readonly selectionSet: SelectionSetNode; } +/** A reusable fragment definition declared in an executable document. */ export interface FragmentDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.FRAGMENT_DEFINITION; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; - /** @deprecated variableDefinitions will be removed in v17.0.0 */ + /** + * Variable definitions declared by this legacy fragment definition. + * @deprecated variableDefinitions will be removed in v17.0.0 + */ readonly variableDefinitions?: ReadonlyArray; + /** The type condition that limits where this fragment applies. */ readonly typeCondition: NamedTypeNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Selections made by this operation, field, or fragment. */ readonly selectionSet: SelectionSetNode; } -/** Values */ +// Values +/** Any value literal that may appear in an executable GraphQL document. */ export type ValueNode = | VariableNode | IntValueNode @@ -450,6 +578,7 @@ export type ValueNode = | ListValueNode | ObjectValueNode; +/** Any value literal that is guaranteed not to contain a variable reference. */ export type ConstValueNode = | IntValueNode | FloatValueNode @@ -460,142 +589,228 @@ export type ConstValueNode = | ConstListValueNode | ConstObjectValueNode; +/** An integer value literal. */ export interface IntValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Parsed value represented by this node. */ readonly value: string; } +/** A floating-point value literal. */ export interface FloatValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.FLOAT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Parsed value represented by this node. */ readonly value: string; } +/** A string value literal. */ export interface StringValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.STRING; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Parsed value represented by this node. */ readonly value: string; + /** Whether this string was parsed from block string syntax. */ readonly block?: boolean; } +/** A boolean value literal. */ export interface BooleanValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.BOOLEAN; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Parsed value represented by this node. */ readonly value: boolean; } +/** A null value literal. */ export interface NullValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.NULL; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; } +/** An enum value literal. */ export interface EnumValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ENUM; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Parsed value represented by this node. */ readonly value: string; } +/** A list value literal. */ export interface ListValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.LIST; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Values contained in this enum, list, or input-object definition. */ readonly values: ReadonlyArray; } +/** A list value literal whose elements are all constant values. */ export interface ConstListValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.LIST; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Values contained in this enum, list, or input-object definition. */ readonly values: ReadonlyArray; } +/** An input object value literal. */ export interface ObjectValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OBJECT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields: ReadonlyArray; } +/** An input object value literal whose fields are all constant values. */ export interface ConstObjectValueNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OBJECT; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields: ReadonlyArray; } +/** A field inside an input object value literal. */ export interface ObjectFieldNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OBJECT_FIELD; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Parsed value represented by this node. */ readonly value: ValueNode; } +/** A field inside a constant input object value literal. */ export interface ConstObjectFieldNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OBJECT_FIELD; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Parsed value represented by this node. */ readonly value: ConstValueNode; } -/** Directives */ +// Directives +/** A directive applied to an executable or type-system location. */ export interface DirectiveNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DIRECTIVE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Arguments supplied to this field, directive, or coordinate. */ readonly arguments?: ReadonlyArray; } +/** A directive whose arguments are all constant values. */ export interface ConstDirectiveNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DIRECTIVE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Arguments supplied to this field, directive, or coordinate. */ readonly arguments?: ReadonlyArray; } -/** Type Reference */ +// Type Reference +/** Any GraphQL type reference AST node. */ export type TypeNode = NamedTypeNode | ListTypeNode | NonNullTypeNode; +/** A named type reference. */ export interface NamedTypeNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.NAMED_TYPE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; } +/** A list type reference. */ export interface ListTypeNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.LIST_TYPE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The GraphQL type reference or runtime type for this element. */ readonly type: TypeNode; } +/** A non-null type reference. */ export interface NonNullTypeNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.NON_NULL_TYPE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The GraphQL type reference or runtime type for this element. */ readonly type: NamedTypeNode | ListTypeNode; } -/** Type System Definition */ +// Type System Definition +/** Any type-system definition that may appear in a schema document. */ export type TypeSystemDefinitionNode = | SchemaDefinitionNode | TypeDefinitionNode | DirectiveDefinitionNode; +/** A schema definition in a type-system document. */ export interface SchemaDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.SCHEMA_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Root operation types declared by this schema definition or extension. */ readonly operationTypes: ReadonlyArray; } +/** A root operation type declaration inside a schema definition or extension. */ export interface OperationTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OPERATION_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The operation selected for execution. */ readonly operation: OperationTypeNode; + /** The GraphQL type reference or runtime type for this element. */ readonly type: NamedTypeNode; } -/** Type Definition */ +// Type Definition +/** Any named type definition that may appear in a schema document. */ export type TypeDefinitionNode = | ScalarTypeDefinitionNode | ObjectTypeDefinitionNode @@ -604,118 +819,199 @@ export type TypeDefinitionNode = | EnumTypeDefinitionNode | InputObjectTypeDefinitionNode; +/** A scalar type definition in a type-system document. */ export interface ScalarTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.SCALAR_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** An object type definition in a type-system document. */ export interface ObjectTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OBJECT_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Interfaces implemented by this object or interface type. */ readonly interfaces?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields?: ReadonlyArray; } +/** A field definition declared by an object or interface type. */ export interface FieldDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.FIELD_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Arguments supplied to this field, directive, or coordinate. */ readonly arguments?: ReadonlyArray; + /** The GraphQL type reference or runtime type for this element. */ readonly type: TypeNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** An argument or input-field definition. */ export interface InputValueDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INPUT_VALUE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** The GraphQL type reference or runtime type for this element. */ readonly type: TypeNode; + /** Default value used when no explicit value is supplied. */ readonly defaultValue?: ConstValueNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** An interface type definition in a type-system document. */ export interface InterfaceTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INTERFACE_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Interfaces implemented by this object or interface type. */ readonly interfaces?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields?: ReadonlyArray; } +/** A union type definition in a type-system document. */ export interface UnionTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.UNION_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Object types that belong to this union type. */ readonly types?: ReadonlyArray; } +/** An enum type definition in a type-system document. */ export interface EnumTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ENUM_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Values contained in this enum, list, or input-object definition. */ readonly values?: ReadonlyArray; } +/** An enum value definition. */ export interface EnumValueDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ENUM_VALUE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** An input object type definition in a type-system document. */ export interface InputObjectTypeDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INPUT_OBJECT_TYPE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields?: ReadonlyArray; } -/** Directive Definitions */ +// Directive Definitions +/** A directive definition in a type-system document. */ export interface DirectiveDefinitionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DIRECTIVE_DEFINITION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** The optional GraphQL description associated with this definition. */ readonly description?: StringValueNode; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Arguments supplied to this field, directive, or coordinate. */ readonly arguments?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Whether this directive may appear more than once at the same location. */ readonly repeatable: boolean; + /** Locations where this directive may be applied. */ readonly locations: ReadonlyArray; } -/** Type System Extensions */ +// Type System Extensions +/** Any type-system extension that may appear in a schema extension document. */ export type TypeSystemExtensionNode = | SchemaExtensionNode | TypeExtensionNode | DirectiveExtensionNode; +/** A schema extension in a type-system document. */ export interface SchemaExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.SCHEMA_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Root operation types declared by this schema definition or extension. */ readonly operationTypes?: ReadonlyArray; } -/** Type Extensions */ +// Type Extensions +/** Any named type extension that may appear in a schema extension document. */ export type TypeExtensionNode = | ScalarTypeExtensionNode | ObjectTypeExtensionNode @@ -724,64 +1020,107 @@ export type TypeExtensionNode = | EnumTypeExtensionNode | InputObjectTypeExtensionNode; +/** A scalar type extension. */ export interface ScalarTypeExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.SCALAR_TYPE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } +/** An object type extension. */ export interface ObjectTypeExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.OBJECT_TYPE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Interfaces implemented by this object or interface type. */ readonly interfaces?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields?: ReadonlyArray; } +/** An interface type extension. */ export interface InterfaceTypeExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INTERFACE_TYPE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Interfaces implemented by this object or interface type. */ readonly interfaces?: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields?: ReadonlyArray; } +/** A union type extension. */ export interface UnionTypeExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.UNION_TYPE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Object types that belong to this union type. */ readonly types?: ReadonlyArray; } +/** An enum type extension. */ export interface EnumTypeExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ENUM_TYPE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Values contained in this enum, list, or input-object definition. */ readonly values?: ReadonlyArray; } +/** An input object type extension. */ export interface InputObjectTypeExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.INPUT_OBJECT_TYPE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields?: ReadonlyArray; } +/** A directive extension. */ export interface DirectiveExtensionNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DIRECTIVE_EXTENSION; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** Directives available in this schema or applied to this AST node. */ readonly directives?: ReadonlyArray; } -/** Schema Coordinates */ +// Schema Coordinates +/** Any AST node representing a GraphQL schema coordinate. */ export type SchemaCoordinateNode = | TypeCoordinateNode | MemberCoordinateNode @@ -789,36 +1128,60 @@ export type SchemaCoordinateNode = | DirectiveCoordinateNode | DirectiveArgumentCoordinateNode; +/** A schema coordinate that refers to a named type. */ export interface TypeCoordinateNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.TYPE_COORDINATE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; } +/** A schema coordinate that refers to a member of a named type. */ export interface MemberCoordinateNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.MEMBER_COORDINATE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** The member name referenced by this schema coordinate. */ readonly memberName: NameNode; } +/** A schema coordinate that refers to a field or directive argument. */ export interface ArgumentCoordinateNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.ARGUMENT_COORDINATE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** The field name referenced by this schema coordinate. */ readonly fieldName: NameNode; + /** The argument name referenced by this schema coordinate. */ readonly argumentName: NameNode; } +/** A schema coordinate that refers to a directive. */ export interface DirectiveCoordinateNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DIRECTIVE_COORDINATE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; } +/** A schema coordinate that refers to a directive argument. */ export interface DirectiveArgumentCoordinateNode { + /** The discriminator identifying the concrete AST or introspection kind. */ readonly kind: Kind.DIRECTIVE_ARGUMENT_COORDINATE; + /** The source location for this AST node, if location tracking was enabled. */ readonly loc?: Location; + /** Name node identifying this AST node. */ readonly name: NameNode; + /** The argument name referenced by this schema coordinate. */ readonly argumentName: NameNode; } diff --git a/src/language/blockString.ts b/src/language/blockString.ts index 1c200c183a..62d3df24e6 100644 --- a/src/language/blockString.ts +++ b/src/language/blockString.ts @@ -48,9 +48,7 @@ function leadingWhitespace(str: string): number { return i; } -/** - * @internal - */ +/** @internal */ export function isPrintableAsBlockString(value: string): boolean { if (value === '') { return true; // empty string is printable diff --git a/src/language/directiveLocation.ts b/src/language/directiveLocation.ts index ac99f2aeea..1cfc998a8e 100644 --- a/src/language/directiveLocation.ts +++ b/src/language/directiveLocation.ts @@ -1,35 +1,54 @@ -/** - * The set of allowed directive location values. - */ +/** @category Kinds */ + +/** The set of allowed directive location values. */ enum DirectiveLocation { - /** Request Definitions */ + /** Directive location for query operations. */ QUERY = 'QUERY', + /** Directive location for mutation operations. */ MUTATION = 'MUTATION', + /** Directive location for subscription operations. */ SUBSCRIPTION = 'SUBSCRIPTION', + /** Directive location for field selections. */ FIELD = 'FIELD', + /** Directive location for fragment definitions. */ FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION', + /** Directive location for fragment spreads. */ FRAGMENT_SPREAD = 'FRAGMENT_SPREAD', + /** Directive location for inline fragments. */ INLINE_FRAGMENT = 'INLINE_FRAGMENT', + /** Directive location for variable definitions. */ VARIABLE_DEFINITION = 'VARIABLE_DEFINITION', - /** Type System Definitions */ + /** Directive location for schema definitions and extensions. */ SCHEMA = 'SCHEMA', + /** Directive location for scalar type definitions and extensions. */ SCALAR = 'SCALAR', + /** Directive location for object type definitions and extensions. */ OBJECT = 'OBJECT', + /** Directive location for field definitions. */ FIELD_DEFINITION = 'FIELD_DEFINITION', + /** Directive location for argument definitions. */ ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION', + /** Directive location for interface type definitions and extensions. */ INTERFACE = 'INTERFACE', + /** Directive location for union type definitions and extensions. */ UNION = 'UNION', + /** Directive location for enum type definitions and extensions. */ ENUM = 'ENUM', + /** Directive location for enum value definitions. */ ENUM_VALUE = 'ENUM_VALUE', + /** Directive location for input object type definitions and extensions. */ INPUT_OBJECT = 'INPUT_OBJECT', + /** Directive location for input object field definitions. */ INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION', + /** Directive location for directive definitions and extensions. */ DIRECTIVE_DEFINITION = 'DIRECTIVE_DEFINITION', } export { DirectiveLocation }; /** - * The enum type representing the directive location values. - * - * @deprecated Please use `DirectiveLocation`. Will be remove in v17. + * Legacy alias for the enum type representing directive location values. This + * is retained for backwards compatibility; use `DirectiveLocation` instead + * because DirectiveLocationEnum will be removed in v17. + * @deprecated Please use `DirectiveLocation`. Will be removed in v17. */ export type DirectiveLocationEnum = typeof DirectiveLocation; diff --git a/src/language/index.ts b/src/language/index.ts index 615ca1287a..5f2fc908b0 100644 --- a/src/language/index.ts +++ b/src/language/index.ts @@ -1,3 +1,10 @@ +/** + * Parse, print, and visit GraphQL language source files and AST nodes. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + */ + export { Source } from './source'; export { getLocation } from './location'; diff --git a/src/language/kinds.ts b/src/language/kinds.ts index fb5ecb35f2..b26df49fe5 100644 --- a/src/language/kinds.ts +++ b/src/language/kinds.ts @@ -1,85 +1,123 @@ -/** - * The set of allowed kind values for AST nodes. - */ +/** @category Kinds */ + +/** The set of allowed kind values for AST nodes. */ enum Kind { - /** Name */ + /** AST kind for name nodes. */ NAME = 'Name', - /** Document */ + /** AST kind for document nodes. */ DOCUMENT = 'Document', + /** AST kind for operation definition nodes. */ OPERATION_DEFINITION = 'OperationDefinition', + /** AST kind for variable definition nodes. */ VARIABLE_DEFINITION = 'VariableDefinition', + /** AST kind for selection set nodes. */ SELECTION_SET = 'SelectionSet', + /** AST kind for field selection nodes. */ FIELD = 'Field', + /** AST kind for argument nodes. */ ARGUMENT = 'Argument', - /** Fragments */ + /** AST kind for fragment spread nodes. */ FRAGMENT_SPREAD = 'FragmentSpread', + /** AST kind for inline fragment nodes. */ INLINE_FRAGMENT = 'InlineFragment', + /** AST kind for fragment definition nodes. */ FRAGMENT_DEFINITION = 'FragmentDefinition', - /** Values */ + /** AST kind for variable reference nodes. */ VARIABLE = 'Variable', + /** AST kind for integer value nodes. */ INT = 'IntValue', + /** AST kind for floating-point value nodes. */ FLOAT = 'FloatValue', + /** AST kind for string value nodes. */ STRING = 'StringValue', + /** AST kind for boolean value nodes. */ BOOLEAN = 'BooleanValue', + /** AST kind for null value nodes. */ NULL = 'NullValue', + /** AST kind for enum value nodes. */ ENUM = 'EnumValue', + /** AST kind for list value nodes. */ LIST = 'ListValue', + /** AST kind for object value nodes. */ OBJECT = 'ObjectValue', + /** AST kind for object field nodes. */ OBJECT_FIELD = 'ObjectField', - /** Directives */ + /** AST kind for directive nodes. */ DIRECTIVE = 'Directive', - /** Types */ + /** AST kind for named type reference nodes. */ NAMED_TYPE = 'NamedType', + /** AST kind for list type reference nodes. */ LIST_TYPE = 'ListType', + /** AST kind for non-null type reference nodes. */ NON_NULL_TYPE = 'NonNullType', - /** Type System Definitions */ + /** AST kind for schema definition nodes. */ SCHEMA_DEFINITION = 'SchemaDefinition', + /** AST kind for operation type definition nodes. */ OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition', - /** Type Definitions */ + /** AST kind for scalar type definition nodes. */ SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition', + /** AST kind for object type definition nodes. */ OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition', + /** AST kind for field definition nodes. */ FIELD_DEFINITION = 'FieldDefinition', + /** AST kind for input value definition nodes. */ INPUT_VALUE_DEFINITION = 'InputValueDefinition', + /** AST kind for interface type definition nodes. */ INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition', + /** AST kind for union type definition nodes. */ UNION_TYPE_DEFINITION = 'UnionTypeDefinition', + /** AST kind for enum type definition nodes. */ ENUM_TYPE_DEFINITION = 'EnumTypeDefinition', + /** AST kind for enum value definition nodes. */ ENUM_VALUE_DEFINITION = 'EnumValueDefinition', + /** AST kind for input object type definition nodes. */ INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition', - /** Directive Definitions */ + /** AST kind for directive definition nodes. */ DIRECTIVE_DEFINITION = 'DirectiveDefinition', - /** Type System Extensions */ + /** AST kind for schema extension nodes. */ SCHEMA_EXTENSION = 'SchemaExtension', + /** AST kind for directive extension nodes. */ DIRECTIVE_EXTENSION = 'DirectiveExtension', - /** Type Extensions */ + /** AST kind for scalar type extension nodes. */ SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension', + /** AST kind for object type extension nodes. */ OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension', + /** AST kind for interface type extension nodes. */ INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension', + /** AST kind for union type extension nodes. */ UNION_TYPE_EXTENSION = 'UnionTypeExtension', + /** AST kind for enum type extension nodes. */ ENUM_TYPE_EXTENSION = 'EnumTypeExtension', + /** AST kind for input object type extension nodes. */ INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension', - /** Schema Coordinates */ + /** AST kind for type coordinate nodes. */ TYPE_COORDINATE = 'TypeCoordinate', + /** AST kind for member coordinate nodes. */ MEMBER_COORDINATE = 'MemberCoordinate', + /** AST kind for argument coordinate nodes. */ ARGUMENT_COORDINATE = 'ArgumentCoordinate', + /** AST kind for directive coordinate nodes. */ DIRECTIVE_COORDINATE = 'DirectiveCoordinate', + /** AST kind for directive argument coordinate nodes. */ DIRECTIVE_ARGUMENT_COORDINATE = 'DirectiveArgumentCoordinate', } export { Kind }; /** - * The enum type representing the possible kind values of AST nodes. - * - * @deprecated Please use `Kind`. Will be remove in v17. + * Legacy alias for the enum type representing the possible kind values of AST + * nodes. This is retained for backwards compatibility; use `Kind` instead + * because KindEnum will be removed in v17. + * @deprecated Please use `Kind`. Will be removed in v17. */ export type KindEnum = typeof Kind; diff --git a/src/language/lexer.ts b/src/language/lexer.ts index e62ffd70d7..6a6d54fe39 100644 --- a/src/language/lexer.ts +++ b/src/language/lexer.ts @@ -1,3 +1,5 @@ +/** @category Lexing */ + import { syntaxError } from '../error/syntaxError'; import { Token } from './ast'; @@ -31,28 +33,36 @@ export interface LexerInterface { * whenever called. */ export class Lexer implements LexerInterface { + /** Source document used to derive error locations. */ source: Source; - /** - * The previously focused non-ignored token. - */ + /** Most recent non-ignored token returned by the lexer. */ lastToken: Token; - /** - * The currently focused non-ignored token. - */ + /** Current non-ignored token at the lexer cursor. */ token: Token; - /** - * The (1-indexed) line containing the current token. - */ + /** The (1-indexed) line containing the current token. */ line: number; - /** - * The character offset at which the current line begins. - */ + /** Character offset where the current line starts. */ lineStart: number; + /** + * Creates a Lexer instance. + * @param source - Source document used to derive error locations. + * @example + * ```ts + * import { Lexer, Source, TokenKind } from 'graphql/language'; + * + * const lexer = new Lexer(new Source('{ hello }')); + * + * lexer.token.kind; // => TokenKind.SOF + * lexer.advance().kind; // => TokenKind.BRACE_L + * lexer.advance().value; // => 'hello' + * lexer.advance().kind; // => TokenKind.BRACE_R + * ``` + */ constructor(source: Source) { const startOfFileToken = new Token(TokenKind.SOF, 0, 0, 0, 0); @@ -63,12 +73,27 @@ export class Lexer implements LexerInterface { this.lineStart = 0; } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'Lexer'; } /** * Advances the token stream to the next non-ignored token. + * @returns The next non-ignored token. + * @example + * ```ts + * import { Lexer, Source } from 'graphql/language'; + * + * const lexer = new Lexer(new Source('{ hello }')); + * const token = lexer.advance(); + * + * token.kind; // => '{' + * lexer.token; // => token + * ``` */ advance(): Token { this.lastToken = this.token; @@ -79,6 +104,17 @@ export class Lexer implements LexerInterface { /** * Looks ahead and returns the next non-ignored token, but does not change * the state of Lexer. + * @returns The next non-ignored token without advancing the lexer. + * @example + * ```ts + * import { Lexer, Source } from 'graphql/language'; + * + * const lexer = new Lexer(new Source('{ hello }')); + * const token = lexer.lookahead(); + * + * token.kind; // => '{' + * lexer.token.kind; // => '' + * ``` */ lookahead(): Token { let token = this.token; @@ -101,9 +137,7 @@ export class Lexer implements LexerInterface { } } -/** - * @internal - */ +/** @internal */ export function isPunctuatorTokenKind(kind: TokenKind): boolean { return ( kind === TokenKind.BANG || @@ -131,6 +165,8 @@ export function isPunctuatorTokenKind(kind: TokenKind): boolean { * * SourceCharacter :: * - "Any Unicode scalar value" + * + * @internal */ function isUnicodeScalarValue(code: number): boolean { return ( @@ -145,6 +181,8 @@ function isUnicodeScalarValue(code: number): boolean { * include surrogates. A surrogate pair is a valid source character as it * encodes a supplementary code point (above U+FFFF), but unpaired surrogate * code points are not valid source characters. + * + * @internal */ function isSupplementaryCodePoint(body: string, location: number): boolean { return ( @@ -211,6 +249,8 @@ export function createToken( * This skips over whitespace until it finds the next lexable token, then lexes * punctuators immediately or calls the appropriate helper function for more * complicated tokens. + * + * @internal */ function readNextToken(lexer: Lexer, start: number): Token { const body = lexer.source.body; @@ -348,6 +388,8 @@ function readNextToken(lexer: Lexer, start: number): Token { * * CommentChar :: SourceCharacter but not LineTerminator * ``` + * + * @internal */ function readComment(lexer: Lexer, start: number): Token { const body = lexer.source.body; @@ -409,6 +451,8 @@ function readComment(lexer: Lexer, start: number): Token { * * Sign :: one of + - * ``` + * + * @internal */ function readNumber(lexer: Lexer, start: number, firstCode: number): Token { const body = lexer.source.body; @@ -484,6 +528,8 @@ function readNumber(lexer: Lexer, start: number, firstCode: number): Token { /** * Returns the new position in the source after reading one or more digits. + * + * @internal */ function readDigits(lexer: Lexer, start: number, firstCode: number): number { if (!isDigit(firstCode)) { @@ -526,6 +572,8 @@ function readDigits(lexer: Lexer, start: number, firstCode: number): number { * * EscapedCharacter :: one of `"` `\` `/` `b` `f` `n` `r` `t` * ``` + * + * @internal */ function readString(lexer: Lexer, start: number): Token { const body = lexer.source.body; @@ -669,6 +717,8 @@ function readEscapedUnicodeFixedWidth( * will return 57005. * * Returns a negative number if any char was not a valid hexadecimal digit. + * + * @internal */ function read16BitHexCode(body: string, position: number): number { // readHexDigit() returns -1 on error. ORing a negative value with any other @@ -694,6 +744,8 @@ function read16BitHexCode(body: string, position: number): number { * - `0` `1` `2` `3` `4` `5` `6` `7` `8` `9` * - `A` `B` `C` `D` `E` `F` * - `a` `b` `c` `d` `e` `f` + * + * @internal */ function readHexDigit(code: number): number { return code >= 0x0030 && code <= 0x0039 // 0-9 @@ -716,6 +768,8 @@ function readHexDigit(code: number): number { * | `n` | U+000A | line feed (new line) | * | `r` | U+000D | carriage return | * | `t` | U+0009 | horizontal tab | + * + * @internal */ function readEscapedCharacter(lexer: Lexer, position: number): EscapeSequence { const body = lexer.source.body; @@ -759,6 +813,8 @@ function readEscapedCharacter(lexer: Lexer, position: number): EscapeSequence { * - SourceCharacter but not `"""` or `\"""` * - `\"""` * ``` + * + * @internal */ function readBlockString(lexer: Lexer, start: number): Token { const body = lexer.source.body; diff --git a/src/language/location.ts b/src/language/location.ts index 36d97f3cca..5fb7d6dfa7 100644 --- a/src/language/location.ts +++ b/src/language/location.ts @@ -1,20 +1,34 @@ +/** @category Source */ + import { invariant } from '../jsutils/invariant'; import type { Source } from './source'; const LineRegExp = /\r\n|[\n\r]/g; -/** - * Represents a location in a Source. - */ +/** Represents a location in a Source. */ export interface SourceLocation { + /** One-indexed line number in the source document. */ readonly line: number; + /** One-indexed column number in the source document. */ readonly column: number; } /** * Takes a Source and a UTF-8 character offset, and returns the corresponding * line and column as a SourceLocation. + * @param source - The source document that contains the position. + * @param position - The UTF-8 character offset in the source body. + * @returns The 1-indexed line and column for the given source position. + * @example + * ```ts + * import { Source, getLocation } from 'graphql/language'; + * + * const source = new Source('type Query { hello: String }'); + * const location = getLocation(source, 13); + * + * location; // => { line: 1, column: 14 } + * ``` */ export function getLocation(source: Source, position: number): SourceLocation { let lastLineStart = 0; diff --git a/src/language/parser.ts b/src/language/parser.ts index 96e083e1dc..fd1f455fdb 100644 --- a/src/language/parser.ts +++ b/src/language/parser.ts @@ -1,3 +1,5 @@ +/** @category Parsing */ + import type { Maybe } from '../jsutils/Maybe'; import type { GraphQLError } from '../error/GraphQLError'; @@ -76,9 +78,7 @@ import { SchemaCoordinateLexer } from './schemaCoordinateLexer'; import { isSource, Source } from './source'; import { TokenKind } from './tokenKind'; -/** - * Configuration options to control parser behavior - */ +/** Configuration options to control parser behavior */ export interface ParseOptions { /** * By default, the parser creates AST nodes that know the location @@ -97,6 +97,7 @@ export interface ParseOptions { maxTokens?: number | undefined; /** + * Allows legacy fragment variable definitions to be parsed. * @deprecated will be removed in the v17.0.0 * * If enabled, the parser will understand and parse variable definitions @@ -126,8 +127,9 @@ export interface ParseOptions { experimentalDirectivesOnDirectiveDefinitions?: boolean; /** - * You may override the Lexer class used to lex the source; this is used by - * schema coordinates to introduce a lexer with a restricted syntax. + * Internal parser hook for GraphQL.js entry points that need to parse a + * restricted grammar with an alternate lexer. + * @internal */ lexer?: LexerInterface | undefined; } @@ -135,6 +137,39 @@ export interface ParseOptions { /** * Given a GraphQL source, parses it into a Document. * Throws GraphQLError if a syntax error is encountered. + * @param source - A GraphQL source string or source object. + * @param options - Optional parser configuration. + * @returns The parsed GraphQL document AST. + * @example + * ```ts + * // Parse a GraphQL document with the default parser options. + * import { parse } from 'graphql/language'; + * + * const document = parse('{ hero { name } }'); + * + * document.kind; // => 'Document' + * ``` + * @example + * ```ts + * // This variant enables parser options and provides an explicit lexer. + * import { Lexer, Source, parse } from 'graphql/language'; + * + * const document = parse('fragment A($var: Boolean) on Query { field }', { + * allowLegacyFragmentVariables: true, + * maxTokens: 20, + * noLocation: true, + * }); + * const directiveDocument = parse('directive @foo @bar on FIELD', { + * experimentalDirectivesOnDirectiveDefinitions: true, + * }); + * const source = new Source('{ hero }'); + * const lexerDocument = parse(source, { lexer: new Lexer(source) }); + * + * document.definitions[0].kind; // => 'FragmentDefinition' + * document.loc; // => undefined + * directiveDocument.definitions[0].kind; // => 'DirectiveDefinition' + * lexerDocument.definitions[0].kind; // => 'OperationDefinition' + * ``` */ export function parse( source: string | Source, @@ -158,6 +193,17 @@ export function parse( * in isolation of complete GraphQL documents. * * Consider providing the results to the utility function: valueFromAST(). + * @param source - A GraphQL source string or source object containing a value. + * @param options - Optional parser configuration. + * @returns The parsed GraphQL value AST. + * @example + * ```ts + * import { parseValue } from 'graphql/language'; + * + * const value = parseValue('[42]'); + * + * value.kind; // => 'ListValue' + * ``` */ export function parseValue( source: string | Source, @@ -173,6 +219,18 @@ export function parseValue( /** * Similar to parseValue(), but raises a parse error if it encounters a * variable. The return type will be a constant value. + * @param source - A GraphQL source string or source object containing a constant value. + * @param options - Optional parser configuration. + * @returns The parsed GraphQL constant value AST. + * @example + * ```ts + * import { parseConstValue } from 'graphql/language'; + * + * const value = parseConstValue('{ enabled: true }'); + * + * value.kind; // => 'ObjectValue' + * parseConstValue('$variable'); // throws an error + * ``` */ export function parseConstValue( source: string | Source, @@ -194,6 +252,17 @@ export function parseConstValue( * in isolation of complete GraphQL documents. * * Consider providing the results to the utility function: typeFromAST(). + * @param source - A GraphQL source string or source object containing a type reference. + * @param options - Optional parser configuration. + * @returns The parsed GraphQL type AST. + * @example + * ```ts + * import { parseType } from 'graphql/language'; + * + * const type = parseType('[String!]'); + * + * type.kind; // => 'ListType' + * ``` */ export function parseType( source: string | Source, @@ -214,6 +283,16 @@ export function parseType( * Consider providing the results to the utility function: * resolveASTSchemaCoordinate(). Or calling resolveSchemaCoordinate() directly * with an unparsed source. + * @param source - A GraphQL source string or source object containing a schema coordinate. + * @returns The parsed GraphQL schema coordinate AST. + * @example + * ```ts + * import { parseSchemaCoordinate } from 'graphql/language'; + * + * const coordinate = parseSchemaCoordinate('Query.hero'); + * + * coordinate.kind; // => 'MemberCoordinate' + * ``` */ export function parseSchemaCoordinate( source: string | Source, @@ -263,6 +342,8 @@ export class Parser { /** * Converts a name lex token into a name parse node. + * + * @internal */ parseName(): NameNode { const token = this.expectToken(TokenKind.NAME); @@ -276,6 +357,8 @@ export class Parser { /** * Document : Definition+ + * + * @internal */ parseDocument(): DocumentNode { return this.node(this._lexer.token, { @@ -310,6 +393,8 @@ export class Parser { * - UnionTypeDefinition * - EnumTypeDefinition * - InputObjectTypeDefinition + * + * @internal */ parseDefinition(): DefinitionNode { if (this.peek(TokenKind.BRACE_L)) { @@ -381,6 +466,8 @@ export class Parser { * OperationDefinition : * - SelectionSet * - OperationType Name? VariableDefinitions? Directives? SelectionSet + * + * @internal */ parseOperationDefinition(): OperationDefinitionNode { const start = this._lexer.token; @@ -414,6 +501,8 @@ export class Parser { /** * OperationType : one of query mutation subscription + * + * @internal */ parseOperationType(): OperationTypeNode { const operationToken = this.expectToken(TokenKind.NAME); @@ -431,6 +520,8 @@ export class Parser { /** * VariableDefinitions : ( VariableDefinition+ ) + * + * @internal */ parseVariableDefinitions(): Array { return this.optionalMany( @@ -442,6 +533,8 @@ export class Parser { /** * VariableDefinition : Variable : Type DefaultValue? Directives[Const]? + * + * @internal */ parseVariableDefinition(): VariableDefinitionNode { return this.node(this._lexer.token, { @@ -458,6 +551,8 @@ export class Parser { /** * Variable : $ Name + * + * @internal */ parseVariable(): VariableNode { const start = this._lexer.token; @@ -472,6 +567,8 @@ export class Parser { * ``` * SelectionSet : { Selection+ } * ``` + * + * @internal */ parseSelectionSet(): SelectionSetNode { return this.node(this._lexer.token, { @@ -489,6 +586,8 @@ export class Parser { * - Field * - FragmentSpread * - InlineFragment + * + * @internal */ parseSelection(): SelectionNode { return this.peek(TokenKind.SPREAD) @@ -500,6 +599,8 @@ export class Parser { * Field : Alias? Name Arguments? Directives? SelectionSet? * * Alias : Name : + * + * @internal */ parseField(): FieldNode { const start = this._lexer.token; @@ -528,6 +629,8 @@ export class Parser { /** * Arguments[Const] : ( Argument[?Const]+ ) + * + * @internal */ parseArguments(isConst: true): Array; parseArguments(isConst: boolean): Array; @@ -538,6 +641,8 @@ export class Parser { /** * Argument[Const] : Name : Value[?Const] + * + * @internal */ parseArgument(isConst: true): ConstArgumentNode; parseArgument(isConst?: boolean): ArgumentNode; @@ -565,6 +670,8 @@ export class Parser { * FragmentSpread : ... FragmentName Directives? * * InlineFragment : ... TypeCondition? Directives? SelectionSet + * + * @internal */ parseFragment(): FragmentSpreadNode | InlineFragmentNode { const start = this._lexer.token; @@ -591,6 +698,8 @@ export class Parser { * - fragment FragmentName on TypeCondition Directives? SelectionSet * * TypeCondition : NamedType + * + * @internal */ parseFragmentDefinition(): FragmentDefinitionNode { const start = this._lexer.token; @@ -622,6 +731,8 @@ export class Parser { /** * FragmentName : Name but not `on` + * + * @internal */ parseFragmentName(): NameNode { if (this._lexer.token.value === 'on') { @@ -649,6 +760,8 @@ export class Parser { * NullValue : `null` * * EnumValue : Name but not `true`, `false` or `null` + * + * @internal */ parseValueLiteral(isConst: true): ConstValueNode; parseValueLiteral(isConst: boolean): ValueNode; @@ -733,6 +846,8 @@ export class Parser { * ListValue[Const] : * - [ ] * - [ Value[?Const]+ ] + * + * @internal */ parseList(isConst: true): ConstListValueNode; parseList(isConst: boolean): ListValueNode; @@ -750,6 +865,8 @@ export class Parser { * - { } * - { ObjectField[?Const]+ } * ``` + * + * @internal */ parseObject(isConst: true): ConstObjectValueNode; parseObject(isConst: boolean): ObjectValueNode; @@ -763,6 +880,8 @@ export class Parser { /** * ObjectField[Const] : Name : Value[?Const] + * + * @internal */ parseObjectField(isConst: true): ConstObjectFieldNode; parseObjectField(isConst: boolean): ObjectFieldNode; @@ -781,6 +900,8 @@ export class Parser { /** * Directives[Const] : Directive[?Const]+ + * + * @internal */ parseDirectives(isConst: true): Array; parseDirectives(isConst: boolean): Array; @@ -800,6 +921,8 @@ export class Parser { * ``` * Directive[Const] : @ Name Arguments[?Const]? * ``` + * + * @internal */ parseDirective(isConst: true): ConstDirectiveNode; parseDirective(isConst: boolean): DirectiveNode; @@ -820,6 +943,8 @@ export class Parser { * - NamedType * - ListType * - NonNullType + * + * @internal */ parseTypeReference(): TypeNode { const start = this._lexer.token; @@ -847,6 +972,8 @@ export class Parser { /** * NamedType : Name + * + * @internal */ parseNamedType(): NamedTypeNode { return this.node(this._lexer.token, { @@ -863,6 +990,8 @@ export class Parser { /** * Description : StringValue + * + * @internal */ parseDescription(): undefined | StringValueNode { if (this.peekDescription()) { @@ -874,6 +1003,8 @@ export class Parser { * ``` * SchemaDefinition : Description? schema Directives[Const]? { OperationTypeDefinition+ } * ``` + * + * @internal */ parseSchemaDefinition(): SchemaDefinitionNode { const start = this._lexer.token; @@ -895,6 +1026,8 @@ export class Parser { /** * OperationTypeDefinition : OperationType : NamedType + * + * @internal */ parseOperationTypeDefinition(): OperationTypeDefinitionNode { const start = this._lexer.token; @@ -910,6 +1043,8 @@ export class Parser { /** * ScalarTypeDefinition : Description? scalar Name Directives[Const]? + * + * @internal */ parseScalarTypeDefinition(): ScalarTypeDefinitionNode { const start = this._lexer.token; @@ -929,6 +1064,8 @@ export class Parser { * ObjectTypeDefinition : * Description? * type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition? + * + * @internal */ parseObjectTypeDefinition(): ObjectTypeDefinitionNode { const start = this._lexer.token; @@ -952,6 +1089,8 @@ export class Parser { * ImplementsInterfaces : * - implements `&`? NamedType * - ImplementsInterfaces & NamedType + * + * @internal */ parseImplementsInterfaces(): Array { return this.expectOptionalKeyword('implements') @@ -963,6 +1102,8 @@ export class Parser { * ``` * FieldsDefinition : { FieldDefinition+ } * ``` + * + * @internal */ parseFieldsDefinition(): Array { return this.optionalMany( @@ -975,6 +1116,8 @@ export class Parser { /** * FieldDefinition : * - Description? Name ArgumentsDefinition? : Type Directives[Const]? + * + * @internal */ parseFieldDefinition(): FieldDefinitionNode { const start = this._lexer.token; @@ -996,6 +1139,8 @@ export class Parser { /** * ArgumentsDefinition : ( InputValueDefinition+ ) + * + * @internal */ parseArgumentDefs(): Array { return this.optionalMany( @@ -1008,6 +1153,8 @@ export class Parser { /** * InputValueDefinition : * - Description? Name : Type DefaultValue? Directives[Const]? + * + * @internal */ parseInputValueDef(): InputValueDefinitionNode { const start = this._lexer.token; @@ -1033,6 +1180,8 @@ export class Parser { /** * InterfaceTypeDefinition : * - Description? interface Name Directives[Const]? FieldsDefinition? + * + * @internal */ parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode { const start = this._lexer.token; @@ -1055,6 +1204,8 @@ export class Parser { /** * UnionTypeDefinition : * - Description? union Name Directives[Const]? UnionMemberTypes? + * + * @internal */ parseUnionTypeDefinition(): UnionTypeDefinitionNode { const start = this._lexer.token; @@ -1076,6 +1227,8 @@ export class Parser { * UnionMemberTypes : * - = `|`? NamedType * - UnionMemberTypes | NamedType + * + * @internal */ parseUnionMemberTypes(): Array { return this.expectOptionalToken(TokenKind.EQUALS) @@ -1086,6 +1239,8 @@ export class Parser { /** * EnumTypeDefinition : * - Description? enum Name Directives[Const]? EnumValuesDefinition? + * + * @internal */ parseEnumTypeDefinition(): EnumTypeDefinitionNode { const start = this._lexer.token; @@ -1107,6 +1262,8 @@ export class Parser { * ``` * EnumValuesDefinition : { EnumValueDefinition+ } * ``` + * + * @internal */ parseEnumValuesDefinition(): Array { return this.optionalMany( @@ -1118,6 +1275,8 @@ export class Parser { /** * EnumValueDefinition : Description? EnumValue Directives[Const]? + * + * @internal */ parseEnumValueDefinition(): EnumValueDefinitionNode { const start = this._lexer.token; @@ -1134,6 +1293,8 @@ export class Parser { /** * EnumValue : Name but not `true`, `false` or `null` + * + * @internal */ parseEnumValueName(): NameNode { if ( @@ -1155,6 +1316,8 @@ export class Parser { /** * InputObjectTypeDefinition : * - Description? input Name Directives[Const]? InputFieldsDefinition? + * + * @internal */ parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode { const start = this._lexer.token; @@ -1176,6 +1339,8 @@ export class Parser { * ``` * InputFieldsDefinition : { InputValueDefinition+ } * ``` + * + * @internal */ parseInputFieldsDefinition(): Array { return this.optionalMany( @@ -1198,6 +1363,8 @@ export class Parser { * - EnumTypeExtension * - InputObjectTypeDefinition * - DirectiveDefinitionExtension + * + * @internal */ parseTypeSystemExtension(): TypeSystemExtensionNode { const keywordToken = this._lexer.lookahead(); @@ -1235,6 +1402,8 @@ export class Parser { * - extend schema Directives[Const]? { OperationTypeDefinition+ } * - extend schema Directives[Const] * ``` + * + * @internal */ parseSchemaExtension(): SchemaExtensionNode { const start = this._lexer.token; @@ -1259,6 +1428,8 @@ export class Parser { /** * ScalarTypeExtension : * - extend scalar Name Directives[Const] + * + * @internal */ parseScalarTypeExtension(): ScalarTypeExtensionNode { const start = this._lexer.token; @@ -1281,6 +1452,8 @@ export class Parser { * - extend type Name ImplementsInterfaces? Directives[Const]? FieldsDefinition * - extend type Name ImplementsInterfaces? Directives[Const] * - extend type Name ImplementsInterfaces + * + * @internal */ parseObjectTypeExtension(): ObjectTypeExtensionNode { const start = this._lexer.token; @@ -1311,6 +1484,8 @@ export class Parser { * - extend interface Name ImplementsInterfaces? Directives[Const]? FieldsDefinition * - extend interface Name ImplementsInterfaces? Directives[Const] * - extend interface Name ImplementsInterfaces + * + * @internal */ parseInterfaceTypeExtension(): InterfaceTypeExtensionNode { const start = this._lexer.token; @@ -1340,6 +1515,8 @@ export class Parser { * UnionTypeExtension : * - extend union Name Directives[Const]? UnionMemberTypes * - extend union Name Directives[Const] + * + * @internal */ parseUnionTypeExtension(): UnionTypeExtensionNode { const start = this._lexer.token; @@ -1363,6 +1540,8 @@ export class Parser { * EnumTypeExtension : * - extend enum Name Directives[Const]? EnumValuesDefinition * - extend enum Name Directives[Const] + * + * @internal */ parseEnumTypeExtension(): EnumTypeExtensionNode { const start = this._lexer.token; @@ -1386,6 +1565,8 @@ export class Parser { * InputObjectTypeExtension : * - extend input Name Directives[Const]? InputFieldsDefinition * - extend input Name Directives[Const] + * + * @internal */ parseInputObjectTypeExtension(): InputObjectTypeExtensionNode { const start = this._lexer.token; @@ -1427,6 +1608,8 @@ export class Parser { * DirectiveDefinition : * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations * ``` + * + * @internal */ parseDirectiveDefinition(): DirectiveDefinitionNode { const start = this._lexer.token; @@ -1457,6 +1640,8 @@ export class Parser { * DirectiveLocations : * - `|`? DirectiveLocation * - DirectiveLocations | DirectiveLocation + * + * @internal */ parseDirectiveLocations(): Array { return this.delimitedMany(TokenKind.PIPE, this.parseDirectiveLocation); @@ -1508,6 +1693,19 @@ export class Parser { * - Name . Name ( Name : ) * - \@ Name * - \@ Name ( Name : ) + * @returns Parsed schema coordinate AST. + * @example + * ```ts + * import { Parser, Source } from 'graphql/language'; + * + * const typeCoordinate = new Parser(new Source('User.name')).parseSchemaCoordinate(); + * const directiveCoordinate = new Parser(new Source('@include(if:)')).parseSchemaCoordinate(); + * + * typeCoordinate.name.value; // => 'User' + * typeCoordinate.memberName?.value; // => 'name' + * directiveCoordinate.name.value; // => 'deprecated' + * directiveCoordinate.argumentName?.value; // => 'reason' + * ``` */ parseSchemaCoordinate(): SchemaCoordinateNode { const start = this._lexer.token; @@ -1567,6 +1765,8 @@ export class Parser { * Returns a node that, if configured to do so, sets a "loc" field as a * location object, used to identify the place in the source that created a * given parsed object. + * + * @internal */ node(startToken: Token, node: T): T { if (this._options.noLocation !== true) { @@ -1581,6 +1781,8 @@ export class Parser { /** * Determines if the next token is of a given kind + * + * @internal */ peek(kind: TokenKind): boolean { return this._lexer.token.kind === kind; @@ -1589,6 +1791,8 @@ export class Parser { /** * If the next token is of the given kind, return that token after advancing the lexer. * Otherwise, do not change the parser state and throw an error. + * + * @internal */ expectToken(kind: TokenKind): Token { const token = this._lexer.token; @@ -1607,6 +1811,8 @@ export class Parser { /** * If the next token is of the given kind, return "true" after advancing the lexer. * Otherwise, do not change the parser state and return "false". + * + * @internal */ expectOptionalToken(kind: TokenKind): boolean { const token = this._lexer.token; @@ -1620,6 +1826,8 @@ export class Parser { /** * If the next token is a given keyword, advance the lexer. * Otherwise, do not change the parser state and throw an error. + * + * @internal */ expectKeyword(value: string): void { const token = this._lexer.token; @@ -1637,6 +1845,8 @@ export class Parser { /** * If the next token is a given keyword, return "true" after advancing the lexer. * Otherwise, do not change the parser state and return "false". + * + * @internal */ expectOptionalKeyword(value: string): boolean { const token = this._lexer.token; @@ -1649,6 +1859,8 @@ export class Parser { /** * Helper function for creating an error when an unexpected lexed token is encountered. + * + * @internal */ unexpected(atToken?: Maybe): GraphQLError { const token = atToken ?? this._lexer.token; @@ -1663,6 +1875,8 @@ export class Parser { * Returns a possibly empty list of parse nodes, determined by the parseFn. * This list begins with a lex token of openKind and ends with a lex token of closeKind. * Advances the parser to the next lex token after the closing token. + * + * @internal */ any( openKind: TokenKind, @@ -1682,6 +1896,8 @@ export class Parser { * It can be empty only if open token is missing otherwise it will always return non-empty list * that begins with a lex token of openKind and ends with a lex token of closeKind. * Advances the parser to the next lex token after the closing token. + * + * @internal */ optionalMany( openKind: TokenKind, @@ -1702,6 +1918,8 @@ export class Parser { * Returns a non-empty list of parse nodes, determined by the parseFn. * This list begins with a lex token of openKind and ends with a lex token of closeKind. * Advances the parser to the next lex token after the closing token. + * + * @internal */ many( openKind: TokenKind, @@ -1720,6 +1938,8 @@ export class Parser { * Returns a non-empty list of parse nodes, determined by the parseFn. * This list may begin with a lex token of delimiterKind followed by items separated by lex tokens of tokenKind. * Advances the parser to the next lex token after last item in the list. + * + * @internal */ delimitedMany(delimiterKind: TokenKind, parseFn: () => T): Array { this.expectOptionalToken(delimiterKind); @@ -1750,6 +1970,8 @@ export class Parser { /** * A helper function to describe a token as a string for debugging. + * + * @internal */ function getTokenDesc(token: Token): string { const value = token.value; @@ -1758,6 +1980,8 @@ function getTokenDesc(token: Token): string { /** * A helper function to describe a token kind as a string for debugging. + * + * @internal */ function getTokenKindDesc(kind: TokenKind): string { return isPunctuatorTokenKind(kind) ? `"${kind}"` : kind; diff --git a/src/language/predicates.ts b/src/language/predicates.ts index afc861c9d8..af83151502 100644 --- a/src/language/predicates.ts +++ b/src/language/predicates.ts @@ -1,3 +1,5 @@ +/** @category AST Predicates */ + import type { ASTNode, ConstValueNode, @@ -14,6 +16,20 @@ import type { } from './ast'; import { Kind } from './kinds'; +/** + * Returns true when the AST node is a definition node. + * @param node - The AST node to test. + * @returns True when the AST node is a definition node. + * @example + * ```ts + * import { parse, isDefinitionNode } from 'graphql/language'; + * + * const document = parse('{ hello }'); + * + * isDefinitionNode(document.definitions[0]); // => true + * isDefinitionNode(document); // => false + * ``` + */ export function isDefinitionNode(node: ASTNode): node is DefinitionNode { return ( isExecutableDefinitionNode(node) || @@ -22,6 +38,21 @@ export function isDefinitionNode(node: ASTNode): node is DefinitionNode { ); } +/** + * Returns true when the AST node is an executable definition node. + * @param node - The AST node to test. + * @returns True when the AST node is an executable definition node. + * @example + * ```ts + * import { parse, isExecutableDefinitionNode } from 'graphql/language'; + * + * const query = parse('{ hello }'); + * const schema = parse('type Query { hello: String }'); + * + * isExecutableDefinitionNode(query.definitions[0]); // => true + * isExecutableDefinitionNode(schema.definitions[0]); // => false + * ``` + */ export function isExecutableDefinitionNode( node: ASTNode, ): node is ExecutableDefinitionNode { @@ -31,6 +62,21 @@ export function isExecutableDefinitionNode( ); } +/** + * Returns true when the AST node is a selection node. + * @param node - The AST node to test. + * @returns True when the AST node is a selection node. + * @example + * ```ts + * import { Kind, isSelectionNode } from 'graphql/language'; + * + * const field = { kind: Kind.FIELD, name: { kind: Kind.NAME, value: 'hello' } }; + * const document = { kind: Kind.DOCUMENT, definitions: [] }; + * + * isSelectionNode(field); // => true + * isSelectionNode(document); // => false + * ``` + */ export function isSelectionNode(node: ASTNode): node is SelectionNode { return ( node.kind === Kind.FIELD || @@ -39,6 +85,21 @@ export function isSelectionNode(node: ASTNode): node is SelectionNode { ); } +/** + * Returns true when the AST node is a value node. + * @param node - The AST node to test. + * @returns True when the AST node is a value node. + * @example + * ```ts + * import { parseType, parseValue, isValueNode } from 'graphql/language'; + * + * const value = parseValue('[42]'); + * const type = parseType('[String!]'); + * + * isValueNode(value); // => true + * isValueNode(type); // => false + * ``` + */ export function isValueNode(node: ASTNode): node is ValueNode { return ( node.kind === Kind.VARIABLE || @@ -53,6 +114,21 @@ export function isValueNode(node: ASTNode): node is ValueNode { ); } +/** + * Returns true when the AST node is a constant value node. + * @param node - The AST node to test. + * @returns True when the AST node is a constant value node. + * @example + * ```ts + * import { parseConstValue, parseValue, isConstValueNode } from 'graphql/language'; + * + * const value = parseConstValue('[42]'); + * const variable = parseValue('$id'); + * + * isConstValueNode(value); // => true + * isConstValueNode(variable); // => false + * ``` + */ export function isConstValueNode(node: ASTNode): node is ConstValueNode { return ( isValueNode(node) && @@ -64,6 +140,21 @@ export function isConstValueNode(node: ASTNode): node is ConstValueNode { ); } +/** + * Returns true when the AST node is a type node. + * @param node - The AST node to test. + * @returns True when the AST node is a type node. + * @example + * ```ts + * import { parseType, parseValue, isTypeNode } from 'graphql/language'; + * + * const type = parseType('[String!]'); + * const value = parseValue('[42]'); + * + * isTypeNode(type); // => true + * isTypeNode(value); // => false + * ``` + */ export function isTypeNode(node: ASTNode): node is TypeNode { return ( node.kind === Kind.NAMED_TYPE || @@ -72,6 +163,21 @@ export function isTypeNode(node: ASTNode): node is TypeNode { ); } +/** + * Returns true when the AST node is a type system definition node. + * @param node - The AST node to test. + * @returns True when the AST node is a type system definition node. + * @example + * ```ts + * import { parse, isTypeSystemDefinitionNode } from 'graphql/language'; + * + * const schema = parse('type Query { hello: String }'); + * const query = parse('{ hello }'); + * + * isTypeSystemDefinitionNode(schema.definitions[0]); // => true + * isTypeSystemDefinitionNode(query.definitions[0]); // => false + * ``` + */ export function isTypeSystemDefinitionNode( node: ASTNode, ): node is TypeSystemDefinitionNode { @@ -82,6 +188,21 @@ export function isTypeSystemDefinitionNode( ); } +/** + * Returns true when the AST node is a type definition node. + * @param node - The AST node to test. + * @returns True when the AST node is a type definition node. + * @example + * ```ts + * import { parse, isTypeDefinitionNode } from 'graphql/language'; + * + * const typeDefinition = parse('type Query { hello: String }'); + * const directiveDefinition = parse('directive @cache on FIELD'); + * + * isTypeDefinitionNode(typeDefinition.definitions[0]); // => true + * isTypeDefinitionNode(directiveDefinition.definitions[0]); // => false + * ``` + */ export function isTypeDefinitionNode( node: ASTNode, ): node is TypeDefinitionNode { @@ -95,6 +216,21 @@ export function isTypeDefinitionNode( ); } +/** + * Returns true when the AST node is a type system extension node. + * @param node - The AST node to test. + * @returns True when the AST node is a type system extension node. + * @example + * ```ts + * import { parse, isTypeSystemExtensionNode } from 'graphql/language'; + * + * const extension = parse('extend type Query { hello: String }'); + * const definition = parse('type Query { hello: String }'); + * + * isTypeSystemExtensionNode(extension.definitions[0]); // => true + * isTypeSystemExtensionNode(definition.definitions[0]); // => false + * ``` + */ export function isTypeSystemExtensionNode( node: ASTNode, ): node is TypeSystemExtensionNode { @@ -105,6 +241,21 @@ export function isTypeSystemExtensionNode( ); } +/** + * Returns true when the AST node is a type extension node. + * @param node - The AST node to test. + * @returns True when the AST node is a type extension node. + * @example + * ```ts + * import { parse, isTypeExtensionNode } from 'graphql/language'; + * + * const extension = parse('extend type Query { hello: String }'); + * const schemaExtension = parse('extend schema { query: Query }'); + * + * isTypeExtensionNode(extension.definitions[0]); // => true + * isTypeExtensionNode(schemaExtension.definitions[0]); // => false + * ``` + */ export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode { return ( node.kind === Kind.SCALAR_TYPE_EXTENSION || @@ -116,6 +267,25 @@ export function isTypeExtensionNode(node: ASTNode): node is TypeExtensionNode { ); } +/** + * Returns true when the AST node is a schema coordinate node. + * @param node - The AST node to test. + * @returns True when the AST node is a schema coordinate node. + * @example + * ```ts + * import { + * parse, + * parseSchemaCoordinate, + * isSchemaCoordinateNode, + * } from 'graphql/language'; + * + * const coordinate = parseSchemaCoordinate('Query.hero'); + * const document = parse('{ hero }'); + * + * isSchemaCoordinateNode(coordinate); // => true + * isSchemaCoordinateNode(document); // => false + * ``` + */ export function isSchemaCoordinateNode( node: ASTNode, ): node is SchemaCoordinateNode { diff --git a/src/language/printLocation.ts b/src/language/printLocation.ts index 3d44f5cea5..9acd52b59a 100644 --- a/src/language/printLocation.ts +++ b/src/language/printLocation.ts @@ -1,3 +1,5 @@ +/** @category Source */ + import type { Location } from './ast'; import type { SourceLocation } from './location'; import { getLocation } from './location'; @@ -5,6 +7,21 @@ import type { Source } from './source'; /** * Render a helpful description of the location in the GraphQL Source document. + * @param location - The AST location to print. + * @returns A formatted source excerpt with line and column information. + * @example + * ```ts + * import { parse, printLocation } from 'graphql/language'; + * + * const document = parse('type Query { hello: String }'); + * const location = document.definitions[0].loc; + * + * if (location) { + * const printed = printLocation(location); + * + * printed; // => 'GraphQL request:1:1\n1 | type Query { hello: String }\n | ^' + * } + * ``` */ export function printLocation(location: Location): string { return printSourceLocation( @@ -15,6 +32,18 @@ export function printLocation(location: Location): string { /** * Render a helpful description of the location in the GraphQL Source document. + * @param source - The source document that contains the location. + * @param sourceLocation - The 1-indexed line and column to print. + * @returns A formatted source excerpt with line and column information. + * @example + * ```ts + * import { Source, printSourceLocation } from 'graphql/language'; + * + * const source = new Source('type Query { hello: String }'); + * const printed = printSourceLocation(source, { line: 1, column: 14 }); + * + * printed; // => 'GraphQL request:1:14\n1 | type Query { hello: String }\n | ^' + * ``` */ export function printSourceLocation( source: Source, diff --git a/src/language/printString.ts b/src/language/printString.ts index b091bcc2c1..614af7b046 100644 --- a/src/language/printString.ts +++ b/src/language/printString.ts @@ -1,11 +1,14 @@ /** * Prints a string as a GraphQL StringValue literal. Replaces control characters * and excluded characters (" U+0022 and \\ U+005C) with escape sequences. + * + * @internal */ export function printString(str: string): string { return `"${str.replace(escapedRegExp, escapedReplacer)}"`; } +/** @internal */ // eslint-disable-next-line no-control-regex const escapedRegExp = /[\x00-\x1f\x22\x5c\x7f-\x9f]/g; diff --git a/src/language/printer.ts b/src/language/printer.ts index 84746181ab..143de458ec 100644 --- a/src/language/printer.ts +++ b/src/language/printer.ts @@ -1,3 +1,5 @@ +/** @category Printing */ + import type { Maybe } from '../jsutils/Maybe'; import type { ASTNode } from './ast'; @@ -9,6 +11,17 @@ import { visit } from './visitor'; /** * Converts an AST into a string, using one set of reasonable * formatting rules. + * @param ast - The GraphQL AST node to print. + * @returns A stable string representation of the AST. + * @example + * ```ts + * import { parse, print } from 'graphql'; + * + * const ast = parse('{ hero { name } }'); + * const text = print(ast); + * + * text; // => '{\n hero {\n name\n }\n}' + * ``` */ export function print(ast: ASTNode): string { return visit(ast, printDocASTReducer); @@ -348,6 +361,8 @@ const printDocASTReducer: ASTReducer = { /** * Given maybeArray, print an empty string if it is null or empty, otherwise * print all items together separated by separator if provided + * + * @internal */ function join( maybeArray: Maybe>, @@ -358,6 +373,8 @@ function join( /** * Given array, print each item on its own line, wrapped in an indented `{ }` block. + * + * @internal */ function block(array: Maybe>): string { return wrap('{\n', indent(join(array, '\n')), '\n}'); @@ -365,6 +382,8 @@ function block(array: Maybe>): string { /** * If maybeString is not null or empty, then wrap with start and end, otherwise print an empty string. + * + * @internal */ function wrap( start: string, diff --git a/src/language/schemaCoordinateLexer.ts b/src/language/schemaCoordinateLexer.ts index 4a65f5e556..4f879ac910 100644 --- a/src/language/schemaCoordinateLexer.ts +++ b/src/language/schemaCoordinateLexer.ts @@ -14,18 +14,16 @@ import { TokenKind } from './tokenKind'; * source lexes, the final Token emitted by the lexer will be of kind * EOF, after which the lexer will repeatedly return the same EOF token * whenever called. + * + * @internal */ export class SchemaCoordinateLexer implements LexerInterface { source: Source; - /** - * The previously focused non-ignored token. - */ + /** The previously focused non-ignored token. */ lastToken: Token; - /** - * The currently focused non-ignored token. - */ + /** The currently focused non-ignored token. */ token: Token; /** @@ -54,6 +52,8 @@ export class SchemaCoordinateLexer implements LexerInterface { /** * Advances the token stream to the next non-ignored token. + * + * @internal */ advance(): Token { this.lastToken = this.token; @@ -64,6 +64,8 @@ export class SchemaCoordinateLexer implements LexerInterface { /** * Looks ahead and returns the next non-ignored token, but does not change * the current Lexer token. + * + * @internal */ lookahead(): Token { let token = this.token; @@ -82,6 +84,8 @@ export class SchemaCoordinateLexer implements LexerInterface { /** * Gets the next token from the source starting at the given position. + * + * @internal */ function readNextToken(lexer: SchemaCoordinateLexer, start: number): Token { const body = lexer.source.body; diff --git a/src/language/source.ts b/src/language/source.ts index 15f65fceee..c6daa8456b 100644 --- a/src/language/source.ts +++ b/src/language/source.ts @@ -1,3 +1,5 @@ +/** @category Source */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { instanceOf } from '../jsutils/instanceOf'; @@ -15,10 +17,33 @@ interface Location { * The `line` and `column` properties in `locationOffset` are 1-indexed. */ export class Source { + /** The GraphQL source text. */ body: string; + /** Name used in diagnostics for this source, such as a file path or request name. */ name: string; + /** One-indexed line and column where this source begins. */ locationOffset: Location; + /** + * Creates a Source instance. + * @param body - The GraphQL source text. + * @param name - Name used in diagnostics for this source. + * @param locationOffset - One-indexed line and column where this source begins. + * @example + * ```ts + * import { Source } from 'graphql/language'; + * + * const source = new Source( + * 'type Query { greeting: String }', + * 'schema.graphql', + * { line: 10, column: 1 }, + * ); + * + * source.body; // => 'type Query { greeting: String }' + * source.name; // => 'schema.graphql' + * source.locationOffset; // => { line: 10, column: 1 } + * ``` + */ constructor( body: string, name: string = 'GraphQL request', @@ -42,6 +67,10 @@ export class Source { ); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'Source'; } diff --git a/src/language/tokenKind.ts b/src/language/tokenKind.ts index 0d7c60355a..a144709c66 100644 --- a/src/language/tokenKind.ts +++ b/src/language/tokenKind.ts @@ -1,37 +1,63 @@ +/** @category Lexing */ + /** * An exported enum describing the different kinds of tokens that the * lexer emits. */ enum TokenKind { + /** Start-of-file token. */ SOF = '', + /** End-of-file token. */ EOF = '', + /** The `!` punctuation token. */ BANG = '!', + /** The `$` punctuation token. */ DOLLAR = '$', + /** The `&` punctuation token. */ AMP = '&', + /** The `(` punctuation token. */ PAREN_L = '(', + /** The `)` punctuation token. */ PAREN_R = ')', + /** The `.` punctuation token. */ DOT = '.', + /** The `...` spread punctuation token. */ SPREAD = '...', + /** The `:` punctuation token. */ COLON = ':', + /** The `=` punctuation token. */ EQUALS = '=', + /** The `@` punctuation token. */ AT = '@', + /** The `[` punctuation token. */ BRACKET_L = '[', + /** The `]` punctuation token. */ BRACKET_R = ']', + /** The `{` punctuation token. */ BRACE_L = '{', + /** The `|` punctuation token. */ PIPE = '|', + /** The `}` punctuation token. */ BRACE_R = '}', + /** A GraphQL name token or name AST node. */ NAME = 'Name', + /** An integer value token or AST node. */ INT = 'Int', + /** A floating-point value token or AST node. */ FLOAT = 'Float', + /** A string value token or AST node. */ STRING = 'String', + /** A block string value token. */ BLOCK_STRING = 'BlockString', + /** A comment token. */ COMMENT = 'Comment', } export { TokenKind }; /** - * The enum type representing the token kinds values. - * - * @deprecated Please use `TokenKind`. Will be remove in v17. + * Legacy alias for the enum type representing token kind values. This is + * retained for backwards compatibility; use `TokenKind` instead because + * TokenKindEnum will be removed in v17. + * @deprecated Please use `TokenKind`. Will be removed in v17. */ export type TokenKindEnum = typeof TokenKind; diff --git a/src/language/visitor.ts b/src/language/visitor.ts index b392feeff0..991d312895 100644 --- a/src/language/visitor.ts +++ b/src/language/visitor.ts @@ -1,3 +1,5 @@ +/** @category Visiting */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; @@ -5,10 +7,7 @@ import type { ASTNode } from './ast'; import { isNode, QueryDocumentKeys } from './ast'; import { Kind } from './kinds'; -/** - * A visitor is provided to visit, it contains the collection of - * relevant functions to be called during the visitor's traversal. - */ +/** A visitor defines the callbacks called during AST traversal. */ export type ASTVisitor = EnterLeaveVisitor | KindVisitor; type KindVisitor = { @@ -23,29 +22,30 @@ interface EnterLeaveVisitor { } /** - * A visitor is comprised of visit functions, which are called on each node - * during the visitor's traversal. + * A visitor is composed of visit functions called for each node during traversal. + * @typeParam TVisitedNode - AST node type handled by this visitor function. */ export type ASTVisitFn = ( - /** The current node being visiting. */ + /** Current node being visited. */ node: TVisitedNode, - /** The index or key to this node from the parent node or Array. */ + /** Index or key for this node within the parent node or array. */ key: string | number | undefined, - /** The parent immediately above this node, which may be an Array. */ + /** Parent immediately above this node, which may be an array. */ parent: ASTNode | ReadonlyArray | undefined, - /** The key path to get to this node from the root node. */ + /** Key path from the root node to this node. */ path: ReadonlyArray, /** - * All nodes and Arrays visited before reaching parent of this node. + * All nodes and arrays visited before reaching this node's parent. * These correspond to array indices in `path`. - * Note: ancestors includes arrays which contain the parent of visited node. + * Note: ancestors includes arrays that contain the visited node's parent. */ ancestors: ReadonlyArray>, ) => any; /** - * A reducer is comprised of reducer functions which convert AST nodes into - * another form. + * A reducer is composed of reducer functions that convert AST nodes into another form. + * + * @internal */ export type ASTReducer = { readonly [NodeT in ASTNode as NodeT['kind']]?: { @@ -55,18 +55,31 @@ export type ASTReducer = { }; type ASTReducerFn = ( - /** The current node being visiting. */ + /** + * Current node being visited. + * @internal + */ node: { [K in keyof TReducedNode]: ReducedField }, - /** The index or key to this node from the parent node or Array. */ + /** + * Index or key for this node within the parent node or array. + * @internal + */ key: string | number | undefined, - /** The parent immediately above this node, which may be an Array. */ + /** + * Parent immediately above this node, which may be an array. + * @internal + */ parent: ASTNode | ReadonlyArray | undefined, - /** The key path to get to this node from the root node. */ + /** + * Key path from the root node to this node. + * @internal + */ path: ReadonlyArray, /** - * All nodes and Arrays visited before reaching parent of this node. + * All nodes and arrays visited before reaching this node's parent. * These correspond to array indices in `path`. - * Note: ancestors includes arrays which contain the parent of visited node. + * Note: ancestors includes arrays that contain the visited node's parent. + * @internal */ ancestors: ReadonlyArray>, ) => R; @@ -78,14 +91,15 @@ type ReducedField = T extends null | undefined : R; /** - * A KeyMap describes each the traversable properties of each kind of node. - * + * Legacy visitor key map type retained for compatibility. Inline this mapped + * type at use sites; ASTVisitorKeyMap will be removed in v17. * @deprecated Please inline it. Will be removed in v17 */ export type ASTVisitorKeyMap = { [NodeT in ASTNode as NodeT['kind']]?: ReadonlyArray; }; +/** A value that can be returned from a visitor function to stop traversal. */ export const BREAK: unknown = Object.freeze({}); /** @@ -101,69 +115,90 @@ export const BREAK: unknown = Object.freeze({}); * When using visit() to edit an AST, the original AST will not be modified, and * a new version of the AST with the changes applied will be returned from the * visit function. - * + * @param root - The AST node at which to start traversal. + * @param visitor - The visitor or reducer functions to call while traversing. + * @param visitorKeys - Optional map of child keys to visit for each AST node kind. + * @returns The original AST, an edited AST, or a reduced value depending on the visitor. + * @typeParam N - The root AST node type returned when visiting without reducing. + * @example * ```ts - * const editedAST = visit(ast, { - * enter(node, key, parent, path, ancestors) { - * // @return - * // undefined: no action - * // false: skip visiting this node - * // visitor.BREAK: stop visiting altogether - * // null: delete this node - * // any value: replace this node with the returned value + * // Return values control traversal: undefined makes no change, false skips + * // a subtree, BREAK stops traversal, null removes a node, and any other + * // value replaces the current node. + * import { Kind, parse, print, visit } from 'graphql/language'; + * + * const document = parse('{ hero { name } }'); + * const editedAST = visit(document, { + * Field: (node) => { + * if (node.name.value === 'hero') { + * return { + * ...node, + * name: { kind: Kind.NAME, value: 'human' }, + * }; + * } * }, - * leave(node, key, parent, path, ancestors) { - * // @return - * // undefined: no action - * // false: no action - * // visitor.BREAK: stop visiting altogether - * // null: delete this node - * // any value: replace this node with the returned value - * } * }); + * + * print(editedAST); // => '{\n human {\n name\n }\n}' * ``` + * @example + * ```ts + * // A named visitor function runs when entering nodes of that kind. + * import { parse, visit } from 'graphql/language'; * - * Alternatively to providing enter() and leave() functions, a visitor can - * instead provide functions named the same as the kinds of AST nodes, or - * enter/leave visitors at a named key, leading to three permutations of the - * visitor API: + * const document = parse('{ hero { name } }'); + * const fieldNames = []; * - * 1) Named visitors triggered when entering a node of a specific kind. + * visit(document, { + * Field: (node) => { + * fieldNames.push(node.name.value); + * }, + * }); * - * ```ts - * visit(ast, { - * Kind(node) { - * // enter the "Kind" node - * } - * }) + * fieldNames; // => ['hero', 'name'] * ``` + * @example + * ```ts + * // A named visitor object can provide separate enter and leave handlers for + * // nodes of that kind. + * import { parse, visit } from 'graphql/language'; * - * 2) Named visitors that trigger upon entering and leaving a node of a specific kind. + * const document = parse('{ hero { name } }'); + * const events = []; * - * ```ts - * visit(ast, { - * Kind: { - * enter(node) { - * // enter the "Kind" node - * } - * leave(node) { - * // leave the "Kind" node - * } - * } - * }) + * visit(document, { + * Field: { + * enter: (node) => { + * events.push(`enter:${node.name.value}`); + * }, + * leave: (node) => { + * events.push(`leave:${node.name.value}`); + * }, + * }, + * }); + * + * events; // => ['enter:hero', 'enter:name', 'leave:name', 'leave:hero'] * ``` + * @example + * ```ts + * // Generic enter and leave handlers run for every node. + * import { parse, visit } from 'graphql/language'; * - * 3) Generic visitors that trigger upon entering and leaving any node. + * const document = parse('{ hero { name } }'); + * let enterCount = 0; + * let leaveCount = 0; * - * ```ts - * visit(ast, { - * enter(node) { - * // enter any node + * visit(document, { + * enter: (node) => { + * enterCount += 1; + * }, + * leave: (node) => { + * leaveCount += 1; * }, - * leave(node) { - * // leave any node - * } - * }) + * }); + * + * enterCount; // => leaveCount + * enterCount > 0; // => true * ``` */ export function visit( @@ -171,11 +206,59 @@ export function visit( visitor: ASTVisitor, visitorKeys?: ASTVisitorKeyMap, ): N; +/** + * Traverses an AST with reducer callbacks and returns the reduced value. + * @param root - The AST node where traversal starts. + * @param visitor - Reducer callbacks to invoke during traversal. + * @param visitorKeys - Optional mapping of child keys for each AST node kind. + * @returns The value produced by the reducer visitor. + * @typeParam R - The value produced by reducer visitor callbacks. + * @example + * ```ts + * // A reducer visitor returns values from leave handlers to build a reduced + * // result instead of returning an edited AST. + * import { parse, visit } from 'graphql/language'; + * + * const document = parse('{ hero { name } }'); + * const printed = visit(document, { + * Name: { + * leave: (node) => { + * return node.value; + * }, + * }, + * Field: { + * leave: (node) => { + * return node.selectionSet == null + * ? node.name + * : `${node.name} { ${node.selectionSet} }`; + * }, + * }, + * SelectionSet: { + * leave: (node) => { + * return node.selections.join(' '); + * }, + * }, + * OperationDefinition: { + * leave: (node) => { + * return node.selectionSet; + * }, + * }, + * Document: { + * leave: (node) => { + * return node.definitions.join('\n'); + * }, + * }, + * }); + * + * printed; // => 'hero { name }' + * ``` + */ export function visit( root: ASTNode, visitor: ASTReducer, visitorKeys?: ASTVisitorKeyMap, ): R; +/** @internal */ export function visit( root: ASTNode, visitor: ASTVisitor | ASTReducer, @@ -306,6 +389,25 @@ export function visit( * parallel. Each visitor will be visited for each node before moving on. * * If a prior visitor edits a node, no following visitors will see that node. + * @param visitors - The visitors to merge into one parallel visitor. + * @returns A visitor that delegates traversal to each provided visitor. + * @example + * ```ts + * import { parse, visit, visitInParallel } from 'graphql/language'; + * + * const document = parse('{ hero { name } }'); + * const events = []; + * + * visit( + * document, + * visitInParallel([ + * { Field: (node) => { events.push(`field:${node.name.value}`); } }, + * { Name: (node) => { events.push(`name:${node.value}`); } }, + * ]), + * ); + * + * events; // => ['field:hero', 'name:hero', 'field:name', 'name:name'] + * ``` */ export function visitInParallel( visitors: ReadonlyArray, @@ -370,6 +472,18 @@ export function visitInParallel( /** * Given a visitor instance and a node kind, return EnterLeaveVisitor for that kind. + * @param visitor - The visitor object to inspect. + * @param kind - The AST node kind to resolve handlers for. + * @returns The enter and leave handlers that apply for the given node kind. + * @example + * ```ts + * import { Kind, getEnterLeaveForKind } from 'graphql/language'; + * + * const handlers = getEnterLeaveForKind({ Field: () => {} }, Kind.FIELD); + * + * typeof handlers.enter; // => 'function' + * handlers.leave; // => undefined + * ``` */ export function getEnterLeaveForKind( visitor: ASTVisitor, @@ -394,8 +508,24 @@ export function getEnterLeaveForKind( /** * Given a visitor instance, if it is leaving or not, and a node kind, return - * the function the visitor runtime should call. + * the function the visitor runtime should call. This compatibility helper + * delegates to `getEnterLeaveForKind`; call `getEnterLeaveForKind` directly + * because getVisitFn will be removed in v17. + * @param visitor - The visitor object to inspect. + * @param kind - The AST node kind to resolve a handler for. + * @param isLeaving - Whether to resolve the leave handler instead of the enter handler. + * @returns The visit function that applies for the given node kind and traversal phase, if one exists. + * @example + * ```ts + * import { Kind, getVisitFn } from 'graphql/language'; + * + * const enter = getVisitFn({ Field: () => {} }, Kind.FIELD, false); + * const leave = getVisitFn({ Field: () => {} }, Kind.FIELD, true); * + * typeof enter; // => 'function' + * leave; // => undefined + * ``` + * @category Visiting * @deprecated Please use `getEnterLeaveForKind` instead. Will be removed in v17 */ /* c8 ignore next 8 */ diff --git a/src/subscription/README.md b/src/subscription/README.md index 7e099d2cfc..c9ed755373 100644 --- a/src/subscription/README.md +++ b/src/subscription/README.md @@ -1,6 +1,6 @@ ## GraphQL Subscription -NOTE: the `graphql/subscription` module has been deprecated with its exported functions integrated into the `graphql/execution` module, to better conform with the terminology of the GraphQL specification. For backwards compatibility, the `graphql/subscription` module currently re-exports the moved functions from the `graphql/execution` module. In the next major release, the `graphql/subscription` module will be dropped entirely. +NOTE: the `graphql/subscription` module has been deprecated with its exported functions integrated into the `graphql/execution` module, to better conform with the terminology of the GraphQL specification. For backwards compatibility, the `graphql/subscription` module currently re-exports the moved functions from the `graphql/execution` module. In v17, the `graphql/subscription` module will be dropped entirely. The `graphql/subscription` module is responsible for subscribing to updates on specific data. diff --git a/src/subscription/index.ts b/src/subscription/index.ts index 9de1b86968..3d0c673c46 100644 --- a/src/subscription/index.ts +++ b/src/subscription/index.ts @@ -5,17 +5,23 @@ * * For backwards compatibility, the `graphql/subscription` module * currently re-exports the moved functions from the `graphql/execution` - * module. In the next major release, the `graphql/subscription` module - * will be dropped entirely. + * module. In v17, the `graphql/subscription` module will be dropped entirely. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + * @category Subscriptions */ import type { ExecutionArgs } from '../execution/execute'; /** - * @deprecated use ExecutionArgs instead. Will be removed in v17 + * Legacy alias for ExecutionArgs retained by the subscription module. Use + * `ExecutionArgs` directly instead because SubscriptionArgs will be removed in + * v17. * * ExecutionArgs has been broadened to include all properties within SubscriptionArgs. * The SubscriptionArgs type is retained for backwards compatibility. + * @deprecated use ExecutionArgs instead. Will be removed in v17 */ // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface SubscriptionArgs extends ExecutionArgs {} diff --git a/src/type/assertName.ts b/src/type/assertName.ts index f4f96fda4e..276467a650 100644 --- a/src/type/assertName.ts +++ b/src/type/assertName.ts @@ -1,3 +1,5 @@ +/** @category Names */ + import { devAssert } from '../jsutils/devAssert'; import { GraphQLError } from '../error/GraphQLError'; @@ -6,6 +8,15 @@ import { isNameContinue, isNameStart } from '../language/characterClasses'; /** * Upholds the spec rules about naming. + * @param name - The GraphQL name to validate. + * @returns The validated GraphQL name. + * @example + * ```ts + * import { assertName } from 'graphql/type'; + * + * assertName('User'); // => 'User' + * assertName('123User'); // throws an error + * ``` */ export function assertName(name: string): string { devAssert(name != null, 'Must provide name.'); @@ -34,8 +45,15 @@ export function assertName(name: string): string { /** * Upholds the spec rules about naming enum values. + * @param name - The GraphQL name to validate. + * @returns The validated GraphQL name. + * @example + * ```ts + * import { assertEnumValueName } from 'graphql/type'; * - * @internal + * assertEnumValueName('ACTIVE'); // => 'ACTIVE' + * assertEnumValueName('true'); // throws an error + * ``` */ export function assertEnumValueName(name: string): string { if (name === 'true' || name === 'false' || name === 'null') { diff --git a/src/type/definition.ts b/src/type/definition.ts index 7eaac560dc..48a469ca17 100644 --- a/src/type/definition.ts +++ b/src/type/definition.ts @@ -1,3 +1,5 @@ +/** @category Definitions */ + import { devAssert } from '../jsutils/devAssert'; import { didYouMean } from '../jsutils/didYouMean'; import { identityFunc } from '../jsutils/identityFunc'; @@ -47,9 +49,7 @@ import type { GraphQLSchema } from './schema'; // Predicates & Assertions -/** - * These are all of the possible kinds of types. - */ +/** These are all of the possible kinds of types. */ export type GraphQLType = | GraphQLScalarType | GraphQLObjectType @@ -68,6 +68,27 @@ export type GraphQLType = | GraphQLList >; +/** + * Returns true when the value is any GraphQL type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is any GraphQL type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { GraphQLList, GraphQLString, isType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * isType(GraphQLString); // => true + * isType(new GraphQLList(GraphQLString)); // => true + * isType(schema.getType('Query')); // => true + * isType('String'); // => false + * ``` + */ export function isType(type: unknown): type is GraphQLType { return ( isScalarType(type) || @@ -81,6 +102,27 @@ export function isType(type: unknown): type is GraphQLType { ); } +/** + * Returns the value as a GraphQL type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const queryType = assertType(schema.getType('Query')); + * + * queryType.toString(); // => 'Query' + * assertType('Query'); // throws an error + * ``` + */ export function assertType(type: unknown): GraphQLType { if (!isType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL type.`); @@ -90,11 +132,52 @@ export function assertType(type: unknown): GraphQLType { /** * There are predicates for each kind of GraphQL type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLScalarType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isScalarType } from 'graphql/type'; + * + * const schema = buildSchema(` + * scalar DateTime + * + * type Query { + * createdAt: DateTime + * } + * `); + * + * isScalarType(schema.getType('DateTime')); // => true + * isScalarType(schema.getType('Query')); // => false + * ``` */ export function isScalarType(type: unknown): type is GraphQLScalarType { return instanceOf(type, GraphQLScalarType); } +/** + * Returns the value as a GraphQLScalarType, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLScalarType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertScalarType } from 'graphql/type'; + * + * const schema = buildSchema(` + * scalar DateTime + * + * type Query { + * createdAt: DateTime + * } + * `); + * + * const dateTimeType = assertScalarType(schema.getType('DateTime')); + * + * dateTimeType.name; // => 'DateTime' + * assertScalarType(schema.getType('Query')); // throws an error + * ``` + */ export function assertScalarType(type: unknown): GraphQLScalarType { if (!isScalarType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL Scalar type.`); @@ -102,10 +185,66 @@ export function assertScalarType(type: unknown): GraphQLScalarType { return type; } +/** + * Returns true when the value is a GraphQLObjectType. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLObjectType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type User { + * name: String + * } + * + * type Query { + * user: User + * } + * `); + * + * isObjectType(schema.getType('User')); // => true + * isObjectType(schema.getType('ReviewInput')); // => false + * ``` + */ export function isObjectType(type: unknown): type is GraphQLObjectType { return instanceOf(type, GraphQLObjectType); } +/** + * Returns the value as a GraphQLObjectType, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLObjectType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type User { + * name: String + * } + * + * type Query { + * user: User + * } + * `); + * + * const userType = assertObjectType(schema.getType('User')); + * + * Object.keys(userType.getFields()); // => ['name'] + * assertObjectType(schema.getType('ReviewInput')); // throws an error + * ``` + */ export function assertObjectType(type: unknown): GraphQLObjectType { if (!isObjectType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL Object type.`); @@ -113,10 +252,66 @@ export function assertObjectType(type: unknown): GraphQLObjectType { return type; } +/** + * Returns true when the value is a GraphQLInterfaceType. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLInterfaceType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isInterfaceType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * + * isInterfaceType(schema.getType('Node')); // => true + * isInterfaceType(schema.getType('User')); // => false + * ``` + */ export function isInterfaceType(type: unknown): type is GraphQLInterfaceType { return instanceOf(type, GraphQLInterfaceType); } +/** + * Returns the value as a GraphQLInterfaceType, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLInterfaceType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * + * const nodeType = assertInterfaceType(schema.getType('Node')); + * + * nodeType.name; // => 'Node' + * assertInterfaceType(schema.getType('User')); // throws an error + * ``` + */ export function assertInterfaceType(type: unknown): GraphQLInterfaceType { if (!isInterfaceType(type)) { throw new Error( @@ -126,10 +321,70 @@ export function assertInterfaceType(type: unknown): GraphQLInterfaceType { return type; } +/** + * Returns true when the value is a GraphQLUnionType. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLUnionType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isUnionType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Photo { + * url: String! + * } + * + * type Video { + * url: String! + * } + * + * union Media = Photo | Video + * + * type Query { + * media: [Media] + * } + * `); + * + * isUnionType(schema.getType('Media')); // => true + * isUnionType(schema.getType('Photo')); // => false + * ``` + */ export function isUnionType(type: unknown): type is GraphQLUnionType { return instanceOf(type, GraphQLUnionType); } +/** + * Returns the value as a GraphQLUnionType, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLUnionType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertUnionType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Photo { + * url: String! + * } + * + * type Video { + * url: String! + * } + * + * union Media = Photo | Video + * + * type Query { + * media: [Media] + * } + * `); + * + * const mediaType = assertUnionType(schema.getType('Media')); + * + * mediaType.getTypes().map((type) => type.name); // => ['Photo', 'Video'] + * assertUnionType(schema.getType('Photo')); // throws an error + * ``` + */ export function assertUnionType(type: unknown): GraphQLUnionType { if (!isUnionType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL Union type.`); @@ -137,10 +392,60 @@ export function assertUnionType(type: unknown): GraphQLUnionType { return type; } +/** + * Returns true when the value is a GraphQLEnumType. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLEnumType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isEnumType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * EMPIRE + * } + * + * type Query { + * favoriteEpisode: Episode + * } + * `); + * + * isEnumType(schema.getType('Episode')); // => true + * isEnumType(schema.getType('Query')); // => false + * ``` + */ export function isEnumType(type: unknown): type is GraphQLEnumType { return instanceOf(type, GraphQLEnumType); } +/** + * Returns the value as a GraphQLEnumType, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLEnumType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertEnumType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * EMPIRE + * } + * + * type Query { + * favoriteEpisode: Episode + * } + * `); + * + * const episodeType = assertEnumType(schema.getType('Episode')); + * + * episodeType.getValues().map((value) => value.name); // => ['NEW_HOPE', 'EMPIRE'] + * assertEnumType(schema.getType('Query')); // throws an error + * ``` + */ export function assertEnumType(type: unknown): GraphQLEnumType { if (!isEnumType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL Enum type.`); @@ -148,12 +453,68 @@ export function assertEnumType(type: unknown): GraphQLEnumType { return type; } +/** + * Returns true when the value is a GraphQLInputObjectType. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLInputObjectType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isInputObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput): Review + * } + * `); + * + * isInputObjectType(schema.getType('ReviewInput')); // => true + * isInputObjectType(schema.getType('Review')); // => false + * ``` + */ export function isInputObjectType( type: unknown, ): type is GraphQLInputObjectType { return instanceOf(type, GraphQLInputObjectType); } +/** + * Returns the value as a GraphQLInputObjectType, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLInputObjectType. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput): Review + * } + * `); + * + * const inputType = assertInputObjectType(schema.getType('ReviewInput')); + * + * Object.keys(inputType.getFields()); // => ['stars'] + * assertInputObjectType(schema.getType('Review')); // throws an error + * ``` + */ export function assertInputObjectType(type: unknown): GraphQLInputObjectType { if (!isInputObjectType(type)) { throw new Error( @@ -163,17 +524,87 @@ export function assertInputObjectType(type: unknown): GraphQLInputObjectType { return type; } +/** + * Returns true when the value is a GraphQLList. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLList. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { GraphQLList, GraphQLString, isListType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * tags: [String!]! + * } + * `); + * + * const tagsField = schema.getQueryType()?.getFields().tags; + * + * isListType(new GraphQLList(GraphQLString)); // => true + * isListType(GraphQLString); // => false + * isListType(tagsField?.type); // => false + * ``` + */ export function isListType( type: GraphQLInputType, ): type is GraphQLList; +/** + * Returns true when the output type is a GraphQLList. + * @param type - The GraphQL output type to inspect. + * @returns True when the output type is a list type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { getNullableType, isListType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * tags: [String!]! + * } + * `); + * + * const tagsField = schema.getQueryType()?.getFields().tags; + * const nullableTagsType = getNullableType(tagsField?.type); + * + * isListType(nullableTagsType); // => true + * ``` + */ export function isListType( type: GraphQLOutputType, ): type is GraphQLList; +/** + * Returns true when the value is a GraphQLList. + * @param type - The value to inspect. + * @returns True when the value is a list type. + * @example + * ```ts + * import { isListType } from 'graphql/type'; + * + * isListType('[String]'); // => false + * isListType(null); // => false + * ``` + */ export function isListType(type: unknown): type is GraphQLList; +/** @internal */ export function isListType(type: unknown): type is GraphQLList { return instanceOf(type, GraphQLList); } +/** + * Returns the value as a GraphQLList, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLList. + * @example + * ```ts + * import { GraphQLList, GraphQLString, assertListType } from 'graphql/type'; + * + * const listType = assertListType(new GraphQLList(GraphQLString)); + * + * listType.ofType; // => GraphQLString + * assertListType(GraphQLString); // throws an error + * ``` + */ export function assertListType(type: unknown): GraphQLList { if (!isListType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL List type.`); @@ -181,21 +612,93 @@ export function assertListType(type: unknown): GraphQLList { return type; } +/** + * Returns true when the value is a GraphQLNonNull. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQLNonNull. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { GraphQLNonNull, GraphQLString, isNonNullType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * name: String! + * nickname: String + * } + * `); + * + * const fields = schema.getQueryType()?.getFields(); + * + * isNonNullType(new GraphQLNonNull(GraphQLString)); // => true + * isNonNullType(fields?.name.type); // => true + * isNonNullType(fields?.nickname.type); // => false + * ``` + */ export function isNonNullType( type: GraphQLInputType, ): type is GraphQLNonNull; +/** + * Returns true when the output type is a GraphQLNonNull. + * @param type - The GraphQL output type to inspect. + * @returns True when the output type is a non-null type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isNonNullType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * name: String! + * nickname: String + * } + * `); + * + * const fields = schema.getQueryType()?.getFields(); + * + * isNonNullType(fields?.name.type); // => true + * isNonNullType(fields?.nickname.type); // => false + * ``` + */ export function isNonNullType( type: GraphQLOutputType, ): type is GraphQLNonNull; +/** + * Returns true when the value is a GraphQLNonNull. + * @param type - The value to inspect. + * @returns True when the value is a non-null type. + * @example + * ```ts + * import { isNonNullType } from 'graphql/type'; + * + * isNonNullType('String!'); // => false + * isNonNullType(null); // => false + * ``` + */ export function isNonNullType( type: unknown, ): type is GraphQLNonNull; +/** @internal */ export function isNonNullType( type: unknown, ): type is GraphQLNonNull { return instanceOf(type, GraphQLNonNull); } +/** + * Returns the value as a GraphQLNonNull, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQLNonNull. + * @example + * ```ts + * import { GraphQLNonNull, GraphQLString, assertNonNullType } from 'graphql/type'; + * + * const nonNullType = assertNonNullType(new GraphQLNonNull(GraphQLString)); + * + * nonNullType.ofType; // => GraphQLString + * assertNonNullType(GraphQLString); // throws an error + * ``` + */ export function assertNonNullType(type: unknown): GraphQLNonNull { if (!isNonNullType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL Non-Null type.`); @@ -203,9 +706,7 @@ export function assertNonNullType(type: unknown): GraphQLNonNull { return type; } -/** - * These types may be used as input types for arguments and directives. - */ +/** These types may be used as input types for arguments and directives. */ export type GraphQLInputType = | GraphQLScalarType | GraphQLEnumType @@ -218,6 +719,33 @@ export type GraphQLInputType = | GraphQLList >; +/** + * Returns true when the value can be used as a GraphQL input type. + * @param type - The GraphQL type to inspect. + * @returns True when the value can be used as a GraphQL input type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isInputType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput): Review + * } + * `); + * + * isInputType(schema.getType('ReviewInput')); // => true + * isInputType(schema.getType('Review')); // => false + * ``` + */ export function isInputType(type: unknown): type is GraphQLInputType { return ( isScalarType(type) || @@ -227,6 +755,35 @@ export function isInputType(type: unknown): type is GraphQLInputType { ); } +/** + * Returns the value as a GraphQL input type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL input type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInputType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput): Review + * } + * `); + * + * const inputType = assertInputType(schema.getType('ReviewInput')); + * + * inputType.toString(); // => 'ReviewInput' + * assertInputType(schema.getType('Review')); // throws an error + * ``` + */ export function assertInputType(type: unknown): GraphQLInputType { if (!isInputType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL input type.`); @@ -234,9 +791,7 @@ export function assertInputType(type: unknown): GraphQLInputType { return type; } -/** - * These types may be used as output types as the result of fields. - */ +/** These types may be used as output types as the result of fields. */ export type GraphQLOutputType = | GraphQLScalarType | GraphQLObjectType @@ -253,6 +808,33 @@ export type GraphQLOutputType = | GraphQLList >; +/** + * Returns true when the value can be used as a GraphQL output type. + * @param type - The GraphQL type to inspect. + * @returns True when the value can be used as a GraphQL output type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isOutputType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput): Review + * } + * `); + * + * isOutputType(schema.getType('Review')); // => true + * isOutputType(schema.getType('ReviewInput')); // => false + * ``` + */ export function isOutputType(type: unknown): type is GraphQLOutputType { return ( isScalarType(type) || @@ -264,6 +846,35 @@ export function isOutputType(type: unknown): type is GraphQLOutputType { ); } +/** + * Returns the value as a GraphQL output type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL output type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertOutputType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * review(input: ReviewInput): Review + * } + * `); + * + * const outputType = assertOutputType(schema.getType('Review')); + * + * outputType.toString(); // => 'Review' + * assertOutputType(schema.getType('ReviewInput')); // throws an error + * ``` + */ export function assertOutputType(type: unknown): GraphQLOutputType { if (!isOutputType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL output type.`); @@ -271,15 +882,72 @@ export function assertOutputType(type: unknown): GraphQLOutputType { return type; } -/** - * These types may describe types which may be leaf values. - */ +/** These types may describe types which may be leaf values. */ export type GraphQLLeafType = GraphQLScalarType | GraphQLEnumType; +/** + * Returns true when the value is a GraphQL scalar or enum type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQL scalar or enum type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isLeafType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * episode: Episode + * review: Review + * } + * `); + * + * isLeafType(schema.getType('Episode')); // => true + * isLeafType(schema.getType('String')); // => true + * isLeafType(schema.getType('Review')); // => false + * ``` + */ export function isLeafType(type: unknown): type is GraphQLLeafType { return isScalarType(type) || isEnumType(type); } +/** + * Returns the value as a GraphQL leaf type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL leaf type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertLeafType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * } + * + * type Review { + * stars: Int! + * } + * + * type Query { + * episode: Episode + * review: Review + * } + * `); + * + * const episodeType = assertLeafType(schema.getType('Episode')); + * + * episodeType.toString(); // => 'Episode' + * assertLeafType(schema.getType('Review')); // throws an error + * ``` + */ export function assertLeafType(type: unknown): GraphQLLeafType { if (!isLeafType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL leaf type.`); @@ -287,18 +955,77 @@ export function assertLeafType(type: unknown): GraphQLLeafType { return type; } -/** - * These types may describe the parent context of a selection set. - */ +/** These types may describe the parent context of a selection set. */ export type GraphQLCompositeType = | GraphQLObjectType | GraphQLInterfaceType | GraphQLUnionType; +/** + * Returns true when the value is a GraphQL object, interface, or union type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQL object, interface, or union type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isCompositeType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * union SearchResult = User + * + * type Query { + * node: Node + * search: [SearchResult] + * } + * `); + * + * isCompositeType(schema.getType('User')); // => true + * isCompositeType(schema.getType('Node')); // => true + * isCompositeType(schema.getType('SearchResult')); // => true + * isCompositeType(schema.getType('String')); // => false + * ``` + */ export function isCompositeType(type: unknown): type is GraphQLCompositeType { return isObjectType(type) || isInterfaceType(type) || isUnionType(type); } +/** + * Returns the value as a GraphQL composite type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL composite type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertCompositeType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * + * const userType = assertCompositeType(schema.getType('User')); + * + * userType.toString(); // => 'User' + * assertCompositeType(schema.getType('String')); // throws an error + * ``` + */ export function assertCompositeType(type: unknown): GraphQLCompositeType { if (!isCompositeType(type)) { throw new Error( @@ -308,15 +1035,73 @@ export function assertCompositeType(type: unknown): GraphQLCompositeType { return type; } -/** - * These types may describe the parent context of a selection set. - */ +/** These types may describe the parent context of a selection set. */ export type GraphQLAbstractType = GraphQLInterfaceType | GraphQLUnionType; +/** + * Returns true when the value is a GraphQL interface or union type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQL interface or union type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { isAbstractType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * union SearchResult = User + * + * type Query { + * node: Node + * search: [SearchResult] + * } + * `); + * + * isAbstractType(schema.getType('Node')); // => true + * isAbstractType(schema.getType('SearchResult')); // => true + * isAbstractType(schema.getType('User')); // => false + * ``` + */ export function isAbstractType(type: unknown): type is GraphQLAbstractType { return isInterfaceType(type) || isUnionType(type); } +/** + * Returns the value as a GraphQL abstract type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL abstract type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertAbstractType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * + * const nodeType = assertAbstractType(schema.getType('Node')); + * + * nodeType.toString(); // => 'Node' + * assertAbstractType(schema.getType('User')); // throws an error + * ``` + */ export function assertAbstractType(type: unknown): GraphQLAbstractType { if (!isAbstractType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL abstract type.`); @@ -342,10 +1127,25 @@ export function assertAbstractType(type: unknown): GraphQLAbstractType { * }) * }) * ``` + * @typeParam T - The GraphQL type wrapped by this list type. */ export class GraphQLList { + /** The type wrapped by this list or non-null type. */ readonly ofType: T; + /** + * Creates a GraphQLList instance. + * @param ofType - The type to wrap. + * @example + * ```ts + * import { GraphQLList, GraphQLString } from 'graphql/type'; + * + * const stringList = new GraphQLList(GraphQLString); + * + * stringList.ofType; // => GraphQLString + * String(stringList); // => '[String]' + * ``` + */ constructor(ofType: T) { devAssert( isType(ofType), @@ -355,14 +1155,45 @@ export class GraphQLList { this.ofType = ofType; } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLList'; } + /** + * Returns this wrapping type as a GraphQL type-reference string. + * @returns The GraphQL type-reference string. + * @example + * ```ts + * import { GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql/type'; + * + * const stringList = new GraphQLList(GraphQLString); + * const requiredStringList = new GraphQLList(new GraphQLNonNull(GraphQLString)); + * + * stringList.toString(); // => '[String]' + * requiredStringList.toString(); // => '[String!]' + * ``` + */ toString(): string { return '[' + String(this.ofType) + ']'; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLList, GraphQLString } from 'graphql/type'; + * + * const stringList = new GraphQLList(GraphQLString); + * + * stringList.toJSON(); // => '[String]' + * JSON.stringify({ type: stringList }); // => '{"type":"[String]"}' + * ``` + */ toJSON(): string { return this.toString(); } @@ -388,10 +1219,25 @@ export class GraphQLList { * }) * ``` * Note: the enforcement of non-nullability occurs within the executor. + * @typeParam T - The nullable GraphQL type wrapped by this non-null type. */ export class GraphQLNonNull { + /** The type wrapped by this list or non-null type. */ readonly ofType: T; + /** + * Creates a GraphQLNonNull instance. + * @param ofType - The type to wrap. + * @example + * ```ts + * import { GraphQLNonNull, GraphQLString } from 'graphql/type'; + * + * const requiredString = new GraphQLNonNull(GraphQLString); + * + * requiredString.ofType; // => GraphQLString + * String(requiredString); // => 'String!' + * ``` + */ constructor(ofType: T) { devAssert( isNullableType(ofType), @@ -401,31 +1247,93 @@ export class GraphQLNonNull { this.ofType = ofType; } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLNonNull'; } + /** + * Returns this wrapping type as a GraphQL type-reference string. + * @returns The GraphQL type-reference string. + * @example + * ```ts + * import { GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql/type'; + * + * const requiredString = new GraphQLNonNull(GraphQLString); + * const requiredStringList = new GraphQLNonNull( + * new GraphQLList(GraphQLString), + * ); + * + * requiredString.toString(); // => 'String!' + * requiredStringList.toString(); // => '[String]!' + * ``` + */ toString(): string { return String(this.ofType) + '!'; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLNonNull, GraphQLString } from 'graphql/type'; + * + * const requiredString = new GraphQLNonNull(GraphQLString); + * + * requiredString.toJSON(); // => 'String!' + * JSON.stringify({ type: requiredString }); // => '{"type":"String!"}' + * ``` + */ toJSON(): string { return this.toString(); } } -/** - * These types wrap and modify other types - */ - +/** These types wrap and modify other types */ export type GraphQLWrappingType = | GraphQLList | GraphQLNonNull; +/** + * Returns true when the value is a GraphQL list or non-null wrapper type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQL list or non-null wrapper type. + * @example + * ```ts + * import { + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * isWrappingType, + * } from 'graphql/type'; + * + * isWrappingType(new GraphQLList(GraphQLString)); // => true + * isWrappingType(new GraphQLNonNull(GraphQLString)); // => true + * isWrappingType(GraphQLString); // => false + * ``` + */ export function isWrappingType(type: unknown): type is GraphQLWrappingType { return isListType(type) || isNonNullType(type); } +/** + * Returns the value as a GraphQL wrapping type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL wrapping type. + * @example + * ```ts + * import { GraphQLList, GraphQLString, assertWrappingType } from 'graphql/type'; + * + * const wrappingType = assertWrappingType(new GraphQLList(GraphQLString)); + * + * wrappingType.toString(); // => '[String]' + * assertWrappingType(GraphQLString); // throws an error + * ``` + */ export function assertWrappingType(type: unknown): GraphQLWrappingType { if (!isWrappingType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL wrapping type.`); @@ -433,9 +1341,7 @@ export function assertWrappingType(type: unknown): GraphQLWrappingType { return type; } -/** - * These types can all accept null as a value. - */ +/** These types can all accept null as a value. */ export type GraphQLNullableType = | GraphQLScalarType | GraphQLObjectType @@ -445,10 +1351,41 @@ export type GraphQLNullableType = | GraphQLInputObjectType | GraphQLList; +/** + * Returns true when the value is a GraphQL type that can accept null. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQL type that can accept null. + * @example + * ```ts + * import { GraphQLNonNull, GraphQLString, isNullableType } from 'graphql/type'; + * + * isNullableType(GraphQLString); // => true + * isNullableType(new GraphQLNonNull(GraphQLString)); // => false + * isNullableType(null); // => false + * ``` + */ export function isNullableType(type: unknown): type is GraphQLNullableType { return isType(type) && !isNonNullType(type); } +/** + * Returns the value as a nullable GraphQL type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a nullable GraphQL type. + * @example + * ```ts + * import { + * GraphQLNonNull, + * GraphQLString, + * assertNullableType, + * } from 'graphql/type'; + * + * const nullableType = assertNullableType(GraphQLString); + * + * nullableType; // => GraphQLString + * assertNullableType(new GraphQLNonNull(GraphQLString)); // throws an error + * ``` + */ export function assertNullableType(type: unknown): GraphQLNullableType { if (!isNullableType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL nullable type.`); @@ -456,13 +1393,68 @@ export function assertNullableType(type: unknown): GraphQLNullableType { return type; } +/** + * Returns the nullable type. + * @param type - The GraphQL type to inspect. + * @returns The nullable type after removing one non-null wrapper, if present. + * @example + * ```ts + * import { getNullableType } from 'graphql/type'; + * + * getNullableType(null); // => undefined + * getNullableType(undefined); // => undefined + * ``` + */ export function getNullableType(type: undefined | null): void; +/** + * Returns the nullable type after removing one non-null wrapper. + * @param type - A nullable type or non-null wrapper. + * @returns The nullable type after removing one non-null wrapper, if present. + * @typeParam T - The nullable GraphQL type returned after removing one non-null wrapper. + * @example + * ```ts + * import { + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * getNullableType, + * } from 'graphql/type'; + * + * const requiredString = new GraphQLNonNull(GraphQLString); + * const stringList = new GraphQLList(GraphQLString); + * + * getNullableType(requiredString); // => GraphQLString + * getNullableType(stringList); // => stringList + * ``` + */ export function getNullableType( type: T | GraphQLNonNull, ): T; +/** + * Returns the nullable type after removing one non-null wrapper. + * @param type - The GraphQL type to inspect. + * @returns The nullable type after removing one non-null wrapper, if present. + * @example + * ```ts + * import { + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * getNullableType, + * } from 'graphql/type'; + * + * const requiredStringList = new GraphQLNonNull( + * new GraphQLList(GraphQLString), + * ); + * + * getNullableType(requiredStringList).toString(); // => '[String]' + * getNullableType(GraphQLString); // => GraphQLString + * ``` + */ export function getNullableType( type: Maybe, ): GraphQLNullableType | undefined; +/** @internal */ export function getNullableType( type: Maybe, ): GraphQLNullableType | undefined { @@ -471,16 +1463,16 @@ export function getNullableType( } } -/** - * These named types do not include modifiers like List or NonNull. - */ +/** These named types do not include modifiers like List or NonNull. */ export type GraphQLNamedType = GraphQLNamedInputType | GraphQLNamedOutputType; +/** A named GraphQL type that can be used as an input type. */ export type GraphQLNamedInputType = | GraphQLScalarType | GraphQLEnumType | GraphQLInputObjectType; +/** A named GraphQL type that can be used as an output type. */ export type GraphQLNamedOutputType = | GraphQLScalarType | GraphQLObjectType @@ -488,6 +1480,19 @@ export type GraphQLNamedOutputType = | GraphQLUnionType | GraphQLEnumType; +/** + * Returns true when the value is a GraphQL named type. + * @param type - The GraphQL type to inspect. + * @returns True when the value is a GraphQL named type. + * @example + * ```ts + * import { GraphQLList, GraphQLString, isNamedType } from 'graphql/type'; + * + * isNamedType(GraphQLString); // => true + * isNamedType(new GraphQLList(GraphQLString)); // => false + * isNamedType(null); // => false + * ``` + */ export function isNamedType(type: unknown): type is GraphQLNamedType { return ( isScalarType(type) || @@ -499,6 +1504,20 @@ export function isNamedType(type: unknown): type is GraphQLNamedType { ); } +/** + * Returns the value as a GraphQL named type, or throws if it is not one. + * @param type - The GraphQL type to inspect. + * @returns The value typed as a GraphQL named type. + * @example + * ```ts + * import { GraphQLList, GraphQLString, assertNamedType } from 'graphql/type'; + * + * const namedType = assertNamedType(GraphQLString); + * + * namedType.name; // => 'String' + * assertNamedType(new GraphQLList(GraphQLString)); // throws an error + * ``` + */ export function assertNamedType(type: unknown): GraphQLNamedType { if (!isNamedType(type)) { throw new Error(`Expected ${inspect(type)} to be a GraphQL named type.`); @@ -506,13 +1525,110 @@ export function assertNamedType(type: unknown): GraphQLNamedType { return type; } +/** + * Returns the named type. + * @param type - The GraphQL type to inspect. + * @returns The named type after unwrapping all list and non-null wrappers. + * @example + * ```ts + * import { getNamedType } from 'graphql/type'; + * + * getNamedType(null); // => undefined + * getNamedType(undefined); // => undefined + * ``` + */ export function getNamedType(type: undefined | null): void; +/** + * Returns the named input type after unwrapping all list and non-null wrappers. + * @param type - The GraphQL input type to inspect. + * @returns The named input type after unwrapping all wrappers. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { getNamedType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Query { + * review(input: [ReviewInput!]!): Boolean + * } + * `); + * + * const inputArg = schema.getQueryType()?.getFields().review.args[0]; + * + * getNamedType(inputArg?.type).toString(); // => 'ReviewInput' + * ``` + */ export function getNamedType(type: GraphQLInputType): GraphQLNamedInputType; +/** + * Returns the named output type after unwrapping all list and non-null wrappers. + * @param type - The GraphQL output type to inspect. + * @returns The named output type after unwrapping all wrappers. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { getNamedType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type User { + * name: String + * } + * + * type Query { + * users: [User!]! + * } + * `); + * + * const usersField = schema.getQueryType()?.getFields().users; + * + * getNamedType(usersField?.type).toString(); // => 'User' + * ``` + */ export function getNamedType(type: GraphQLOutputType): GraphQLNamedOutputType; +/** + * Returns the named type after unwrapping all list and non-null wrappers. + * @param type - The GraphQL type to inspect. + * @returns The named type after unwrapping all wrappers. + * @example + * ```ts + * import { + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * getNamedType, + * } from 'graphql/type'; + * + * const nestedType = new GraphQLNonNull( + * new GraphQLList(new GraphQLNonNull(GraphQLString)), + * ); + * + * getNamedType(nestedType); // => GraphQLString + * ``` + */ export function getNamedType(type: GraphQLType): GraphQLNamedType; +/** + * Returns the named type after unwrapping all list and non-null wrappers. + * @param type - The GraphQL type to inspect. + * @returns The named type after unwrapping all wrappers, or undefined for nullish input. + * @example + * ```ts + * import { + * GraphQLList, + * GraphQLString, + * getNamedType, + * } from 'graphql/type'; + * + * getNamedType(new GraphQLList(GraphQLString)); // => GraphQLString + * getNamedType(undefined); // => undefined + * ``` + */ export function getNamedType( type: Maybe, ): GraphQLNamedType | undefined; +/** @internal */ export function getNamedType( type: Maybe, ): GraphQLNamedType | undefined { @@ -528,23 +1644,59 @@ export function getNamedType( /** * Used while defining GraphQL types to allow for circular references in * otherwise immutable type definitions. + * @typeParam T - The element type returned by the thunk or array. */ export type ThunkReadonlyArray = (() => ReadonlyArray) | ReadonlyArray; +/** + * A thunk that resolves to an object map. + * @typeParam T - Value type stored in the object map. + */ export type ThunkObjMap = (() => ObjMap) | ObjMap; +/** + * Resolves a thunked readonly array. + * @param thunk - The thunk or value to resolve. + * @returns The resolved readonly array. + * @typeParam T - The element type resolved from the thunk or array. + * @example + * ```ts + * import { GraphQLString, resolveReadonlyArrayThunk } from 'graphql/type'; + * + * const lazyFields = resolveReadonlyArrayThunk(() => [GraphQLString]); + * const fields = resolveReadonlyArrayThunk([GraphQLString]); + * + * lazyFields; // => [GraphQLString] + * fields; // => [GraphQLString] + * ``` + */ export function resolveReadonlyArrayThunk( thunk: ThunkReadonlyArray, ): ReadonlyArray { return typeof thunk === 'function' ? thunk() : thunk; } +/** + * Resolves a thunked object map. + * @param thunk - The thunk or value to resolve. + * @returns The resolved object map. + * @typeParam T - The object-map value type resolved from the thunk or map. + * @example + * ```ts + * import { GraphQLString, resolveObjMapThunk } from 'graphql/type'; + * + * const lazyFields = resolveObjMapThunk(() => ({ name: GraphQLString })); + * const fields = resolveObjMapThunk({ name: GraphQLString }); + * + * lazyFields.name; // => GraphQLString + * fields.name; // => GraphQLString + * ``` + */ export function resolveObjMapThunk(thunk: ThunkObjMap): ObjMap { return typeof thunk === 'function' ? thunk() : thunk; } /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -558,20 +1710,22 @@ export interface GraphQLScalarTypeExtensions { /** * Scalar Type Definition * - * The leaf values of any request and input values to arguments are - * Scalars (or Enums) and are defined with a name and a series of functions - * used to parse input from ast or variables and to ensure validity. + * Scalar types define the leaf values of a GraphQL response and the input + * values accepted by arguments and input object fields. A scalar type has a + * name and coercion functions that validate and convert runtime values and + * GraphQL literals. * * If a type's serialize function returns `null` or does not return a value * (i.e. it returns `undefined`) then an error will be raised and a `null` - * value will be returned in the response. It is always better to validate + * value will be returned in the response. Prefer validating inputs before + * execution so clients receive input diagnostics before result coercion fails. * * Example: * * ```ts * const OddType = new GraphQLScalarType({ * name: 'Odd', - * serialize(value) { + * serialize: (value) => { * if (!Number.isFinite(value)) { * throw new Error( * `Scalar "Odd" cannot represent "${value}" since it is not a finite number.`, @@ -585,18 +1739,82 @@ export interface GraphQLScalarTypeExtensions { * } * }); * ``` + * @typeParam TInternal - The internal runtime representation accepted by this scalar. + * @typeParam TExternal - The serialized representation exposed in GraphQL results. */ export class GraphQLScalarType { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** URL identifying the behavior specified for this custom scalar. */ specifiedByURL: Maybe; + /** Function that converts internal values to externally visible scalar values. */ serialize: GraphQLScalarSerializer; + /** Function that converts variable input into this scalar's internal value. */ parseValue: GraphQLScalarValueParser; + /** Function that converts AST input literals into this scalar's internal value. */ parseLiteral: GraphQLScalarLiteralParser; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; + /** + * Creates a GraphQLScalarType instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * import { Kind, parse } from 'graphql/language'; + * import { GraphQLScalarType } from 'graphql/type'; + * + * const document = parse(` + * "Odd integer values." + * scalar Odd @specifiedBy(url: "https://example.com/odd") + * + * extend scalar Odd @specifiedBy(url: "https://example.com/odd-v2") + * `); + * + * const Odd = new GraphQLScalarType({ + * name: 'Odd', + * description: 'Odd integer values.', + * specifiedByURL: 'https://example.com/odd', + * serialize: (value) => { + * if (typeof value !== 'number' || value % 2 === 0) { + * throw new TypeError('Odd can only serialize odd numbers.'); + * } + * return value; + * }, + * parseValue: (value) => { + * if (typeof value !== 'number' || value % 2 === 0) { + * throw new TypeError('Odd can only parse odd numbers.'); + * } + * return value; + * }, + * parseLiteral: (ast) => { + * if (ast.kind !== Kind.INT) { + * throw new TypeError('Odd can only parse integer literals.'); + * } + * const value = Number(ast.value); + * if (value % 2 === 0) { + * throw new TypeError('Odd can only parse odd integer literals.'); + * } + * return value; + * }, + * extensions: { numeric: true }, + * astNode: document.definitions[0], + * extensionASTNodes: [ document.definitions[1] ], + * }); + * + * Odd.description; // => 'Odd integer values.' + * Odd.specifiedByURL; // => 'https://example.com/odd' + * Odd.serialize(3); // => 3 + * Odd.parseValue(5); // => 5 + * Odd.extensions; // => { numeric: true } + * ``` + */ constructor(config: Readonly>) { const parseValue = config.parseValue ?? @@ -636,10 +1854,35 @@ export class GraphQLScalarType { } } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLScalarType'; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { GraphQLScalarType } from 'graphql/type'; + * + * const Url = new GraphQLScalarType({ + * name: 'Url', + * description: 'An absolute URL string.', + * specifiedByURL: 'https://url.spec.whatwg.org/', + * }); + * + * const config = Url.toConfig(); + * const UrlCopy = new GraphQLScalarType(config); + * + * config.name; // => 'Url' + * config.specifiedByURL; // => 'https://url.spec.whatwg.org/' + * UrlCopy.name; // => Url.name + * ``` + */ toConfig(): GraphQLScalarTypeNormalizedConfig { return { name: this.name, @@ -654,31 +1897,77 @@ export class GraphQLScalarType { }; } + /** + * Returns the schema coordinate identifying this scalar type. + * @returns The schema coordinate for this scalar type. + * @example + * ```ts + * import { GraphQLScalarType } from 'graphql/type'; + * + * const DateTime = new GraphQLScalarType({ name: 'DateTime' }); + * + * DateTime.toString(); // => 'DateTime' + * String(DateTime); // => 'DateTime' + * ``` + */ toString(): string { return this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLScalarType } from 'graphql/type'; + * + * const DateTime = new GraphQLScalarType({ name: 'DateTime' }); + * + * DateTime.toJSON(); // => 'DateTime' + * JSON.stringify({ type: DateTime }); // => '{"type":"DateTime"}' + * ``` + */ toJSON(): string { return this.toString(); } } +/** + * Serializes a runtime value as a scalar output value. + * @typeParam TExternal - The serialized representation returned for GraphQL results. + */ export type GraphQLScalarSerializer = ( outputValue: unknown, ) => TExternal; +/** + * Parses a runtime input value as a scalar input value. + * @typeParam TInternal - The internal runtime representation produced from variable input. + */ export type GraphQLScalarValueParser = ( inputValue: unknown, ) => TInternal; +/** + * Parses a GraphQL value literal as a scalar input value. + * @typeParam TInternal - The internal runtime representation produced from literal input. + */ export type GraphQLScalarLiteralParser = ( valueNode: ValueNode, variables?: Maybe>, ) => TInternal; +/** + * Configuration used to construct a GraphQLScalarType. + * @typeParam TInternal - The internal runtime representation accepted by this scalar. + * @typeParam TExternal - The serialized representation exposed in GraphQL results. + */ export interface GraphQLScalarTypeConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** URL identifying the behavior specified for this custom scalar. */ specifiedByURL?: Maybe; /** Serializes an internal value to include in a response. */ serialize?: GraphQLScalarSerializer; @@ -686,8 +1975,11 @@ export interface GraphQLScalarTypeConfig { parseValue?: GraphQLScalarValueParser; /** Parses an externally provided literal value to use as an input. */ parseLiteral?: GraphQLScalarLiteralParser; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } @@ -702,15 +1994,15 @@ interface GraphQLScalarTypeNormalizedConfig /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases * the risk of conflicts. We recommend you add at most one extension field, * an object which can contain all the values you need. - * * We've provided these template arguments because this is an open type and * you may find them useful. + * @typeParam _TSource - Reserved source type parameter for extension typing. + * @typeParam _TContext - Reserved context type parameter for extension typing. */ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { [attributeName: string]: unknown; @@ -732,7 +2024,7 @@ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { * number: { type: GraphQLInt }, * formatted: { * type: GraphQLString, - * resolve(obj) { + * resolve: (obj) => { * return obj.number + ' ' + obj.street * } * } @@ -755,18 +2047,126 @@ export interface GraphQLObjectTypeExtensions<_TSource = any, _TContext = any> { * }) * }); * ``` + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. */ export class GraphQLObjectType { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Predicate used to determine whether a runtime value belongs to this object type. */ isTypeOf: Maybe>; + /** Extension fields to include in the formatted result. */ extensions: Readonly>; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; private _fields: ThunkObjMap>; private _interfaces: ThunkReadonlyArray; + /** + * Creates a GraphQLObjectType instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * // Configure an object type with interfaces, fields, arguments, and metadata. + * import { parse } from 'graphql/language'; + * import { + * GraphQLID, + * GraphQLInterfaceType, + * GraphQLNonNull, + * GraphQLObjectType, + * GraphQLString, + * } from 'graphql/type'; + * + * const document = parse(` + * type User implements Node { + * id: ID! + * name(format: String = "short"): String + * } + * + * extend type User { + * displayName: String + * } + * `); + * const definition = document.definitions[0]; + * const nameField = definition.fields[1]; + * const formatArg = nameField.arguments[0]; + * + * const Node = new GraphQLInterfaceType({ + * name: 'Node', + * fields: { + * id: { type: new GraphQLNonNull(GraphQLID) }, + * }, + * }); + * + * const User = new GraphQLObjectType({ + * name: 'User', + * description: 'A registered user.', + * interfaces: [Node], + * fields: { + * id: { type: new GraphQLNonNull(GraphQLID) }, + * name: { + * description: 'The formatted user name.', + * type: GraphQLString, + * args: { + * format: { + * description: 'Controls the name format.', + * type: GraphQLString, + * defaultValue: 'short', + * deprecationReason: 'Use locale instead.', + * extensions: { public: true }, + * astNode: formatArg, + * }, + * }, + * resolve: (user, { format }) => { + * return format === 'long' ? user.fullName : user.name; + * }, + * deprecationReason: 'Use displayName.', + * extensions: { cacheSeconds: 60 }, + * astNode: nameField, + * }, + * }, + * isTypeOf: (value) => { + * return typeof value === 'object' && value != null && 'id' in value; + * }, + * extensions: { entity: 'User' }, + * astNode: definition, + * extensionASTNodes: [ document.definitions[1] ], + * }); + * + * User.name; // => 'User' + * User.getInterfaces(); // => [Node] + * Object.keys(User.getFields()); // => ['id', 'name'] + * User.getFields().name.args[0].defaultValue; // => 'short' + * User.extensions; // => { entity: 'User' } + * ``` + * @example + * ```ts + * // This variant configures a subscription field with subscribe and resolve functions. + * import { GraphQLObjectType, GraphQLString } from 'graphql/type'; + * + * const Subscription = new GraphQLObjectType({ + * name: 'Subscription', + * fields: { + * greeting: { + * type: GraphQLString, + * subscribe: async function* () { + * yield { greeting: 'Hello!' }; + * }, + * resolve: (event) => { + * return event.greeting; + * }, + * }, + * }, + * }); + * + * typeof Subscription.getFields().greeting.subscribe; // => 'function' + * ``` + */ constructor(config: Readonly>) { this.name = assertName(config.name); this.description = config.description; @@ -784,10 +2184,40 @@ export class GraphQLObjectType { ); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLObjectType'; } + /** + * Returns the fields defined by this type. + * @returns The fields keyed by field name. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type User { + * id: ID! + * name: String + * } + * + * type Query { + * viewer: User + * } + * `); + * + * const User = assertObjectType(schema.getType('User')); + * const fields = User.getFields(); + * + * Object.keys(fields); // => ['id', 'name'] + * String(fields.id.type); // => 'ID!' + * ``` + */ getFields(): GraphQLFieldMap { if (typeof this._fields === 'function') { this._fields = this._fields(); @@ -795,6 +2225,33 @@ export class GraphQLObjectType { return this._fields; } + /** + * Returns the interfaces implemented by this type. + * @returns The implemented interfaces. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * viewer: User + * } + * `); + * + * const User = assertObjectType(schema.getType('User')); + * + * User.getInterfaces().map((type) => type.name); // => ['Node'] + * ``` + */ getInterfaces(): ReadonlyArray { if (typeof this._interfaces === 'function') { this._interfaces = this._interfaces(); @@ -802,6 +2259,27 @@ export class GraphQLObjectType { return this._interfaces; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { GraphQLObjectType, GraphQLString } from 'graphql/type'; + * + * const User = new GraphQLObjectType({ + * name: 'User', + * fields: { + * name: { type: GraphQLString }, + * }, + * }); + * + * const config = User.toConfig(); + * const UserCopy = new GraphQLObjectType(config); + * + * config.fields.name.type; // => GraphQLString + * UserCopy.getFields().name.type; // => GraphQLString + * ``` + */ toConfig(): GraphQLObjectTypeNormalizedConfig { return { name: this.name, @@ -815,10 +2293,49 @@ export class GraphQLObjectType { }; } + /** + * Returns the schema coordinate identifying this object type. + * @returns The schema coordinate for this object type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type User { + * name: String + * } + * + * type Query { + * viewer: User + * } + * `); + * + * const User = assertObjectType(schema.getType('User')); + * + * User.toString(); // => 'User' + * ``` + */ toString(): string { return this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLObjectType, GraphQLString } from 'graphql/type'; + * + * const User = new GraphQLObjectType({ + * name: 'User', + * fields: { name: { type: GraphQLString } }, + * }); + * + * User.toJSON(); // => 'User' + * JSON.stringify({ type: User }); // => '{"type":"User"}' + * ``` + */ toJSON(): string { return this.toString(); } @@ -880,6 +2397,7 @@ function defineFieldMap( }); } +/** @internal */ export function defineArguments( config: GraphQLFieldConfigArgumentMap, ): ReadonlyArray { @@ -913,9 +2431,7 @@ function fieldsToFieldsConfig( })); } -/** - * @internal - */ +/** @internal */ export function argsToArgsConfig( args: ReadonlyArray, ): GraphQLFieldConfigArgumentMap { @@ -933,14 +2449,27 @@ export function argsToArgsConfig( ); } +/** + * Configuration used to construct a GraphQLObjectType. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + */ export interface GraphQLObjectTypeConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Interfaces implemented by this object or interface type. */ interfaces?: ThunkReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ fields: ThunkObjMap>; + /** Predicate used to determine whether a runtime value belongs to this object type. */ isTypeOf?: Maybe>; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } @@ -952,6 +2481,11 @@ interface GraphQLObjectTypeNormalizedConfig extensionASTNodes: ReadonlyArray; } +/** + * Resolves the concrete object type for an abstract GraphQL type. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + */ export type GraphQLTypeResolver = ( value: TSource, context: TContext, @@ -959,12 +2493,24 @@ export type GraphQLTypeResolver = ( abstractType: GraphQLAbstractType, ) => PromiseOrValue; +/** + * Checks whether a runtime value belongs to a GraphQL object type. + * @typeParam TSource - Source object type tested against this object type. + * @typeParam TContext - Context object type passed to resolvers. + */ export type GraphQLIsTypeOfFn = ( source: TSource, context: TContext, info: GraphQLResolveInfo, ) => PromiseOrValue; +/** + * Resolves the runtime value for a GraphQL field. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + * @typeParam TArgs - Argument object type passed to resolvers. + * @typeParam TResult - Result value type. + */ export type GraphQLFieldResolver< TSource, TContext, @@ -977,53 +2523,79 @@ export type GraphQLFieldResolver< info: GraphQLResolveInfo, ) => TResult; +/** Information about the currently executing GraphQL field. */ export interface GraphQLResolveInfo { + /** The field name referenced by this schema coordinate. */ readonly fieldName: string; + /** AST field nodes that contributed to the current field execution. */ readonly fieldNodes: ReadonlyArray; + /** GraphQL output type declared for the current field. */ readonly returnType: GraphQLOutputType; + /** Object type that owns the current field. */ readonly parentType: GraphQLObjectType; + /** Response path where this error occurred during execution. */ readonly path: Path; + /** The schema used for validation or execution. */ readonly schema: GraphQLSchema; + /** Fragment definitions in the operation document keyed by fragment name. */ readonly fragments: ObjMap; + /** Initial root value passed to the operation. */ readonly rootValue: unknown; + /** The operation selected for execution. */ readonly operation: OperationDefinitionNode; + /** Runtime variable values keyed by variable name. */ readonly variableValues: { [variable: string]: unknown }; } /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases * the risk of conflicts. We recommend you add at most one extension field, * an object which can contain all the values you need. - * * We've provided these template arguments because this is an open type and * you may find them useful. + * @typeParam _TSource - Reserved source type parameter for extension typing. + * @typeParam _TContext - Reserved context type parameter for extension typing. + * @typeParam _TArgs - Reserved argument type parameter for extension typing. */ export interface GraphQLFieldExtensions<_TSource, _TContext, _TArgs = any> { [attributeName: string]: unknown; } +/** + * Configuration used to define a GraphQL field. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + * @typeParam TArgs - Argument object type passed to resolvers. + */ export interface GraphQLFieldConfig { + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** The GraphQL type reference or runtime type for this element. */ type: GraphQLOutputType; + /** Arguments accepted by this field or directive. */ args?: GraphQLFieldConfigArgumentMap; + /** Resolver function used to produce this field value. */ resolve?: GraphQLFieldResolver; + /** Resolver function used to create a subscription event stream for this field. */ subscribe?: GraphQLFieldResolver; + /** Reason this element is deprecated, if one was provided. */ deprecationReason?: Maybe; + /** Extension fields to include in the formatted result. */ extensions?: Maybe< Readonly> >; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; } +/** A map of argument names to argument configuration objects. */ export type GraphQLFieldConfigArgumentMap = ObjMap; /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1034,52 +2606,117 @@ export interface GraphQLArgumentExtensions { [attributeName: string]: unknown; } +/** Configuration used to define a GraphQL argument. */ export interface GraphQLArgumentConfig { + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** The GraphQL type reference or runtime type for this element. */ type: GraphQLInputType; + /** Default value used when no explicit value is supplied. */ defaultValue?: unknown; + /** Reason this element is deprecated, if one was provided. */ deprecationReason?: Maybe; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; } +/** + * A map of field names to field configuration objects. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + */ export type GraphQLFieldConfigMap = ObjMap< GraphQLFieldConfig >; +/** + * A resolved GraphQL field definition. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + * @typeParam TArgs - Argument object type passed to resolvers. + */ export interface GraphQLField { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** The GraphQL type reference or runtime type for this element. */ type: GraphQLOutputType; + /** Arguments accepted by this field or directive. */ args: ReadonlyArray; + /** Resolver function used to produce this field value. */ resolve?: GraphQLFieldResolver; + /** Resolver function used to create a subscription event stream for this field. */ subscribe?: GraphQLFieldResolver; + /** Reason this element is deprecated, if one was provided. */ deprecationReason: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly>; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; } +/** A resolved GraphQL argument definition. */ export interface GraphQLArgument { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** The GraphQL type reference or runtime type for this element. */ type: GraphQLInputType; + /** Default value used when no explicit value is supplied. */ defaultValue: unknown; + /** Reason this element is deprecated, if one was provided. */ deprecationReason: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; } +/** + * Returns true when the argument is non-null and has no default value. + * @param arg - The argument definition to inspect. + * @returns True when the argument is non-null and has no default value. + * @example + * ```ts + * import { + * GraphQLInt, + * GraphQLNonNull, + * GraphQLString, + * isRequiredArgument, + * } from 'graphql/type'; + * + * const requiredArgument = { name: 'id', type: new GraphQLNonNull(GraphQLInt) }; + * const optionalArgument = { name: 'name', type: GraphQLString }; + * const argumentWithDefault = { + * name: 'limit', + * type: new GraphQLNonNull(GraphQLInt), + * defaultValue: 10, + * }; + * + * isRequiredArgument(requiredArgument); // => true + * isRequiredArgument(optionalArgument); // => false + * isRequiredArgument(argumentWithDefault); // => false + * ``` + */ export function isRequiredArgument(arg: GraphQLArgument): boolean { return isNonNullType(arg.type) && arg.defaultValue === undefined; } +/** + * A map of field names to resolved field definitions. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + */ export type GraphQLFieldMap = ObjMap< GraphQLField >; /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1110,16 +2747,74 @@ export interface GraphQLInterfaceTypeExtensions { * ``` */ export class GraphQLInterfaceType { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Function that resolves the concrete object type for this abstract type. */ resolveType: Maybe>; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; private _fields: ThunkObjMap>; private _interfaces: ThunkReadonlyArray; + /** + * Creates a GraphQLInterfaceType instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { GraphQLID, GraphQLInterfaceType, GraphQLNonNull } from 'graphql/type'; + * + * const document = parse(` + * interface Node { + * id: ID! + * } + * + * interface Resource implements Node { + * id: ID! + * } + * + * extend interface Resource { + * url: String + * } + * `); + * + * const Node = new GraphQLInterfaceType({ + * name: 'Node', + * fields: { + * id: { type: new GraphQLNonNull(GraphQLID) }, + * }, + * }); + * + * const Resource = new GraphQLInterfaceType({ + * name: 'Resource', + * description: 'An addressable resource.', + * interfaces: [Node], + * fields: { + * id: { type: new GraphQLNonNull(GraphQLID) }, + * }, + * resolveType: (value) => { + * return typeof value === 'object' && value != null && 'url' in value + * ? 'WebPage' + * : null; + * }, + * extensions: { abstract: true }, + * astNode: document.definitions[1], + * extensionASTNodes: [ document.definitions[2] ], + * }); + * + * Resource.name; // => 'Resource' + * Resource.getInterfaces(); // => [Node] + * Object.keys(Resource.getFields()); // => ['id'] + * Resource.extensions; // => { abstract: true } + * ``` + */ constructor(config: Readonly>) { this.name = assertName(config.name); this.description = config.description; @@ -1137,10 +2832,43 @@ export class GraphQLInterfaceType { ); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLInterfaceType'; } + /** + * Returns the fields defined by this type. + * @returns The fields keyed by field name. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * + * const Node = assertInterfaceType(schema.getType('Node')); + * const fields = Node.getFields(); + * + * Object.keys(fields); // => ['id'] + * String(fields.id.type); // => 'ID!' + * ``` + */ getFields(): GraphQLFieldMap { if (typeof this._fields === 'function') { this._fields = this._fields(); @@ -1148,6 +2876,39 @@ export class GraphQLInterfaceType { return this._fields; } + /** + * Returns the interfaces implemented by this type. + * @returns The implemented interfaces. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Resource { + * url: String! + * } + * + * interface Image implements Resource { + * url: String! + * width: Int + * } + * + * type Photo implements Resource & Image { + * url: String! + * width: Int + * } + * + * type Query { + * image: Image + * } + * `); + * + * const Image = assertInterfaceType(schema.getType('Image')); + * + * Image.getInterfaces().map((type) => type.name); // => ['Resource'] + * ``` + */ getInterfaces(): ReadonlyArray { if (typeof this._interfaces === 'function') { this._interfaces = this._interfaces(); @@ -1155,6 +2916,27 @@ export class GraphQLInterfaceType { return this._interfaces; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { GraphQLID, GraphQLInterfaceType, GraphQLNonNull } from 'graphql/type'; + * + * const Node = new GraphQLInterfaceType({ + * name: 'Node', + * fields: { + * id: { type: new GraphQLNonNull(GraphQLID) }, + * }, + * }); + * + * const config = Node.toConfig(); + * const NodeCopy = new GraphQLInterfaceType(config); + * + * String(config.fields.id.type); // => 'ID!' + * String(NodeCopy.getFields().id.type); // => 'ID!' + * ``` + */ toConfig(): GraphQLInterfaceTypeNormalizedConfig { return { name: this.name, @@ -1168,19 +2950,71 @@ export class GraphQLInterfaceType { }; } + /** + * Returns the schema coordinate identifying this interface type. + * @returns The schema coordinate for this interface type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * + * const Node = assertInterfaceType(schema.getType('Node')); + * + * Node.toString(); // => 'Node' + * ``` + */ toString(): string { return this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLInterfaceType, GraphQLString } from 'graphql/type'; + * + * const Named = new GraphQLInterfaceType({ + * name: 'Named', + * fields: { name: { type: GraphQLString } }, + * }); + * + * Named.toJSON(); // => 'Named' + * JSON.stringify({ type: Named }); // => '{"type":"Named"}' + * ``` + */ toJSON(): string { return this.toString(); } } +/** + * Configuration used to construct a GraphQLInterfaceType. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + */ export interface GraphQLInterfaceTypeConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Interfaces implemented by this object or interface type. */ interfaces?: ThunkReadonlyArray; + /** Fields declared by this object, interface, input object, or literal. */ fields: ThunkObjMap>; /** * Optionally provide a custom type resolver function. If one is not provided, @@ -1188,11 +3022,15 @@ export interface GraphQLInterfaceTypeConfig { * Object type. */ resolveType?: Maybe>; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } +/** @internal */ export interface GraphQLInterfaceTypeNormalizedConfig extends GraphQLInterfaceTypeConfig { interfaces: ReadonlyArray; @@ -1203,7 +3041,6 @@ export interface GraphQLInterfaceTypeNormalizedConfig /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1226,8 +3063,8 @@ export interface GraphQLUnionTypeExtensions { * ```ts * const PetType = new GraphQLUnionType({ * name: 'Pet', - * types: [ DogType, CatType ], - * resolveType(value) { + * types: [DogType, CatType], + * resolveType: (value) => { * if (value instanceof Dog) { * return DogType; * } @@ -1239,15 +3076,63 @@ export interface GraphQLUnionTypeExtensions { * ``` */ export class GraphQLUnionType { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Function that resolves the concrete object type for this abstract type. */ resolveType: Maybe>; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; private _types: ThunkReadonlyArray; + /** + * Creates a GraphQLUnionType instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { GraphQLObjectType, GraphQLString, GraphQLUnionType } from 'graphql/type'; + * + * const document = parse(` + * union Media = Photo | Video + * + * extend union Media = Audio + * `); + * + * const Photo = new GraphQLObjectType({ + * name: 'Photo', + * fields: { url: { type: GraphQLString } }, + * }); + * const Video = new GraphQLObjectType({ + * name: 'Video', + * fields: { url: { type: GraphQLString } }, + * }); + * + * const Media = new GraphQLUnionType({ + * name: 'Media', + * description: 'Media that can appear in a search result.', + * types: [Photo, Video], + * resolveType: (value) => { + * return typeof value === 'object' && value != null && 'duration' in value + * ? 'Video' + * : 'Photo'; + * }, + * extensions: { searchable: true }, + * astNode: document.definitions[0], + * extensionASTNodes: [ document.definitions[1] ], + * }); + * + * Media.description; // => 'Media that can appear in a search result.' + * Media.getTypes().map((type) => type.name); // => ['Photo', 'Video'] + * Media.extensions; // => { searchable: true } + * ``` + */ constructor(config: Readonly>) { this.name = assertName(config.name); this.description = config.description; @@ -1264,10 +3149,43 @@ export class GraphQLUnionType { ); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLUnionType'; } + /** + * Returns the object types included in this union. + * @returns The union member object types. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertUnionType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Photo { + * url: String! + * } + * + * type Video { + * url: String! + * } + * + * union Media = Photo | Video + * + * type Query { + * media: [Media] + * } + * `); + * + * const Media = assertUnionType(schema.getType('Media')); + * + * Media.getTypes().map((type) => type.name); // => ['Photo', 'Video'] + * ``` + */ getTypes(): ReadonlyArray { if (typeof this._types === 'function') { this._types = this._types(); @@ -1275,6 +3193,32 @@ export class GraphQLUnionType { return this._types; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { GraphQLObjectType, GraphQLString, GraphQLUnionType } from 'graphql/type'; + * + * const Photo = new GraphQLObjectType({ + * name: 'Photo', + * fields: { url: { type: GraphQLString } }, + * }); + * const Video = new GraphQLObjectType({ + * name: 'Video', + * fields: { url: { type: GraphQLString } }, + * }); + * const Media = new GraphQLUnionType({ + * name: 'Media', + * types: [Photo, Video], + * }); + * + * const config = Media.toConfig(); + * const MediaCopy = new GraphQLUnionType(config); + * + * MediaCopy.getTypes().map((type) => type.name); // => ['Photo', 'Video'] + * ``` + */ toConfig(): GraphQLUnionTypeNormalizedConfig { return { name: this.name, @@ -1287,10 +3231,55 @@ export class GraphQLUnionType { }; } + /** + * Returns the schema coordinate identifying this union type. + * @returns The schema coordinate for this union type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertUnionType } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Photo { + * url: String! + * } + * + * union SearchResult = Photo + * + * type Query { + * search: [SearchResult] + * } + * `); + * + * const SearchResult = assertUnionType(schema.getType('SearchResult')); + * + * SearchResult.toString(); // => 'SearchResult' + * ``` + */ toString(): string { return this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLObjectType, GraphQLString, GraphQLUnionType } from 'graphql/type'; + * + * const Photo = new GraphQLObjectType({ + * name: 'Photo', + * fields: { url: { type: GraphQLString } }, + * }); + * const SearchResult = new GraphQLUnionType({ + * name: 'SearchResult', + * types: [Photo], + * }); + * + * SearchResult.toJSON(); // => 'SearchResult' + * JSON.stringify({ type: SearchResult }); // => '{"type":"SearchResult"}' + * ``` + */ toJSON(): string { return this.toString(); } @@ -1307,9 +3296,17 @@ function defineTypes( return types; } +/** + * Configuration used to construct a GraphQLUnionType. + * @typeParam TSource - Source object type passed to resolvers. + * @typeParam TContext - Context object type passed to resolvers. + */ export interface GraphQLUnionTypeConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Object types that belong to this union type. */ types: ThunkReadonlyArray; /** * Optionally provide a custom type resolver function. If one is not provided, @@ -1317,8 +3314,11 @@ export interface GraphQLUnionTypeConfig { * Object type. */ resolveType?: Maybe>; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } @@ -1331,7 +3331,6 @@ interface GraphQLUnionTypeNormalizedConfig /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1345,31 +3344,40 @@ export interface GraphQLEnumTypeExtensions { /** * Enum Type Definition * - * Some leaf values of requests and input values are Enums. GraphQL serializes - * Enum values as strings, however internally Enums can be represented by any - * kind of type, often integers. + * Enum types define leaf values whose serialized form is one of a fixed set + * of GraphQL enum names. Internally, enum values can map to any runtime value, + * often integers. * * Example: * * ```ts + * import { GraphQLEnumType } from 'graphql/type'; + * * const RGBType = new GraphQLEnumType({ * name: 'RGB', * values: { * RED: { value: 0 }, * GREEN: { value: 1 }, - * BLUE: { value: 2 } - * } + * BLUE: { value: 2 }, + * }, * }); + * + * RGBType.getValue('GREEN')?.value; // => 1 * ``` * * Note: If a value is not provided in a definition, the name of the enum value * will be used as its internal value. */ export class GraphQLEnumType /* */ { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; private _values: @@ -1379,6 +3387,56 @@ export class GraphQLEnumType /* */ { private _valueLookup: ReadonlyMap | null; private _nameLookup: ObjMap | null; + /** + * Creates a GraphQLEnumType instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { GraphQLEnumType } from 'graphql/type'; + * + * const document = parse(` + * enum Episode { + * NEW_HOPE + * EMPIRE + * JEDI + * } + * + * extend enum Episode { + * FORCE_AWAKENS + * } + * `); + * const definition = document.definitions[0]; + * + * const Episode = new GraphQLEnumType({ + * name: 'Episode', + * description: 'A Star Wars film episode.', + * values: { + * NEW_HOPE: { + * value: 4, + * description: 'Released in 1977.', + * extensions: { trilogy: 'original' }, + * astNode: definition.values[0], + * }, + * EMPIRE: { value: 5, astNode: definition.values[1] }, + * JEDI: { + * value: 6, + * deprecationReason: 'Use RETURN_OF_THE_JEDI.', + * astNode: definition.values[2], + * }, + * }, + * extensions: { catalog: 'films' }, + * astNode: definition, + * extensionASTNodes: [ document.definitions[1] ], + * }); + * + * Episode.description; // => 'A Star Wars film episode.' + * Episode.serialize(5); // => 'EMPIRE' + * Episode.parseValue('JEDI'); // => 6 + * Episode.getValue('JEDI').deprecationReason; // => 'Use RETURN_OF_THE_JEDI.' + * Episode.extensions; // => { catalog: 'films' } + * ``` + */ constructor(config: Readonly */>) { this.name = assertName(config.name); this.description = config.description; @@ -1394,10 +3452,39 @@ export class GraphQLEnumType /* */ { this._nameLookup = null; } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLEnumType'; } + /** + * Returns the values defined by this enum type. + * @returns Enum value definitions in schema order. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertEnumType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * EMPIRE + * JEDI + * } + * + * type Query { + * episode: Episode + * } + * `); + * + * const Episode = assertEnumType(schema.getType('Episode')); + * + * Episode.getValues().map((value) => value.name); // => ['NEW_HOPE', 'EMPIRE', 'JEDI'] + * ``` + */ getValues(): ReadonlyArray */> { if (typeof this._values === 'function') { this._values = defineEnumValues(this.name, this._values()); @@ -1405,6 +3492,32 @@ export class GraphQLEnumType /* */ { return this._values; } + /** + * Returns the enum value definition for a value name. + * @param name - The GraphQL name to look up. + * @returns The matching enum value definition, if it exists. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertEnumType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * EMPIRE + * } + * + * type Query { + * episode: Episode + * } + * `); + * + * const Episode = assertEnumType(schema.getType('Episode')); + * + * Episode.getValue('EMPIRE')?.name; // => 'EMPIRE' + * Episode.getValue('JEDI'); // => undefined + * ``` + */ getValue(name: string): Maybe { if (this._nameLookup === null) { this._nameLookup = keyMap(this.getValues(), (value) => value.name); @@ -1412,6 +3525,27 @@ export class GraphQLEnumType /* */ { return this._nameLookup[name]; } + /** + * Serializes a runtime enum value as a GraphQL enum name. + * @param outputValue - Runtime enum value to serialize. + * @returns The GraphQL enum name for the runtime value. + * @example + * ```ts + * import { GraphQLEnumType } from 'graphql/type'; + * + * const RGB = new GraphQLEnumType({ + * name: 'RGB', + * values: { + * RED: { value: 0 }, + * GREEN: { value: 1 }, + * BLUE: { value: 2 }, + * }, + * }); + * + * RGB.serialize(1); // => 'GREEN' + * RGB.serialize(3); // throws an error + * ``` + */ serialize(outputValue: unknown /* T */): Maybe { if (this._valueLookup === null) { this._valueLookup = new Map( @@ -1427,6 +3561,28 @@ export class GraphQLEnumType /* */ { return enumValue.name; } + /** + * Parses a GraphQL enum name from variable input. + * @param inputValue - Runtime input value to parse. + * @returns The internal enum value represented by the input name. + * @example + * ```ts + * import { GraphQLEnumType } from 'graphql/type'; + * + * const RGB = new GraphQLEnumType({ + * name: 'RGB', + * values: { + * RED: { value: 0 }, + * GREEN: { value: 1 }, + * BLUE: { value: 2 }, + * }, + * }); + * + * RGB.parseValue('BLUE'); // => 2 + * RGB.parseValue('PURPLE'); // throws an error + * RGB.parseValue(2); // throws an error + * ``` + */ parseValue(inputValue: unknown): Maybe /* T */ { if (typeof inputValue !== 'string') { const valueStr = inspect(inputValue); @@ -1446,6 +3602,29 @@ export class GraphQLEnumType /* */ { return enumValue.value; } + /** + * Parses a GraphQL enum name from an AST value literal. + * @param valueNode - AST value literal to parse. + * @param _variables - Runtime variable values; ignored because enum literals cannot contain variables. + * @returns The internal enum value represented by the literal. + * @example + * ```ts + * import { parseValue } from 'graphql/language'; + * import { GraphQLEnumType } from 'graphql/type'; + * + * const RGB = new GraphQLEnumType({ + * name: 'RGB', + * values: { + * RED: { value: 0 }, + * GREEN: { value: 1 }, + * BLUE: { value: 2 }, + * }, + * }); + * + * RGB.parseLiteral(parseValue('RED')); // => 0 + * RGB.parseLiteral(parseValue('"RED"')); // throws an error + * ``` + */ parseLiteral( valueNode: ValueNode, _variables: Maybe>, @@ -1472,6 +3651,29 @@ export class GraphQLEnumType /* */ { return enumValue.value; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { GraphQLEnumType } from 'graphql/type'; + * + * const RGB = new GraphQLEnumType({ + * name: 'RGB', + * values: { + * RED: { value: 0 }, + * GREEN: { value: 1 }, + * BLUE: { value: 2 }, + * }, + * }); + * + * const config = RGB.toConfig(); + * const RGBCopy = new GraphQLEnumType(config); + * + * config.values.GREEN.value; // => 1 + * RGBCopy.serialize(2); // => 'BLUE' + * ``` + */ toConfig(): GraphQLEnumTypeNormalizedConfig { const values = keyValMap( this.getValues(), @@ -1495,10 +3697,51 @@ export class GraphQLEnumType /* */ { }; } + /** + * Returns the schema coordinate identifying this enum type. + * @returns The schema coordinate for this enum type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertEnumType } from 'graphql/type'; + * + * const schema = buildSchema(` + * enum Episode { + * NEW_HOPE + * } + * + * type Query { + * episode: Episode + * } + * `); + * + * const Episode = assertEnumType(schema.getType('Episode')); + * + * Episode.toString(); // => 'Episode' + * ``` + */ toString(): string { return this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLEnumType } from 'graphql/type'; + * + * const Episode = new GraphQLEnumType({ + * name: 'Episode', + * values: { + * NEW_HOPE: {}, + * }, + * }); + * + * Episode.toJSON(); // => 'Episode' + * JSON.stringify({ type: Episode }); // => '{"type":"Episode"}' + * ``` + */ toJSON(): string { return this.toString(); } @@ -1539,12 +3782,19 @@ function defineEnumValues( }); } +/** Configuration used to construct a GraphQLEnumType. */ export interface GraphQLEnumTypeConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Values contained in this enum, list, or input-object definition. */ values: ThunkObjMap */>; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } @@ -1554,12 +3804,12 @@ interface GraphQLEnumTypeNormalizedConfig extends GraphQLEnumTypeConfig { extensionASTNodes: ReadonlyArray; } +/** A map of enum value names to enum value configuration objects. */ export type GraphQLEnumValueConfigMap /* */ = ObjMap */>; /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1570,26 +3820,38 @@ export interface GraphQLEnumValueExtensions { [attributeName: string]: unknown; } +/** Configuration used to define a GraphQL enum value. */ export interface GraphQLEnumValueConfig { + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Parsed value represented by this node. */ value?: any /* T */; + /** Reason this element is deprecated, if one was provided. */ deprecationReason?: Maybe; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; } +/** A resolved GraphQL enum value definition. */ export interface GraphQLEnumValue { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Parsed value represented by this node. */ value: any /* T */; + /** Reason this element is deprecated, if one was provided. */ deprecationReason: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; } /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1622,15 +3884,89 @@ export interface GraphQLInputObjectTypeExtensions { * ``` */ export class GraphQLInputObjectType { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; + /** Whether this input object uses the experimental OneOf input object semantics. */ isOneOf: boolean; private _fields: ThunkObjMap; + /** + * Creates a GraphQLInputObjectType instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { + * GraphQLID, + * GraphQLInputObjectType, + * GraphQLInt, + * GraphQLNonNull, + * GraphQLString, + * } from 'graphql/type'; + * + * const document = parse(` + * input ReviewInput { + * stars: Int! + * commentary: String + * } + * + * extend input ReviewInput { + * body: String + * } + * `); + * const definition = document.definitions[0]; + * + * const ReviewInput = new GraphQLInputObjectType({ + * name: 'ReviewInput', + * description: 'Input collected when reviewing a product.', + * fields: { + * stars: { + * description: 'Star rating from one to five.', + * type: new GraphQLNonNull(GraphQLInt), + * extensions: { min: 1, max: 5 }, + * astNode: definition.fields[0], + * }, + * commentary: { + * type: GraphQLString, + * defaultValue: '', + * deprecationReason: 'Use body.', + * astNode: definition.fields[1], + * }, + * }, + * extensions: { form: 'review' }, + * astNode: definition, + * extensionASTNodes: [ document.definitions[1] ], + * isOneOf: false, + * }); + * const SearchBy = new GraphQLInputObjectType({ + * name: 'SearchBy', + * fields: { + * id: { type: GraphQLID }, + * slug: { type: GraphQLString }, + * }, + * isOneOf: true, + * }); + * + * const fields = ReviewInput.getFields(); + * + * ReviewInput.description; // => 'Input collected when reviewing a product.' + * String(fields.stars.type); // => 'Int!' + * fields.stars.extensions; // => { min: 1, max: 5 } + * fields.commentary.defaultValue; // => '' + * fields.commentary.deprecationReason; // => 'Use body.' + * ReviewInput.isOneOf; // => false + * SearchBy.isOneOf; // => true + * ``` + */ constructor(config: Readonly) { this.name = assertName(config.name); this.description = config.description; @@ -1642,10 +3978,40 @@ export class GraphQLInputObjectType { this._fields = defineInputFieldMap.bind(undefined, config); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLInputObjectType'; } + /** + * Returns the fields defined by this type. + * @returns The fields keyed by field name. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * commentary: String = "" + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); + * const fields = ReviewInput.getFields(); + * + * Object.keys(fields); // => ['stars', 'commentary'] + * fields.commentary.defaultValue; // => '' + * ``` + */ getFields(): GraphQLInputFieldMap { if (typeof this._fields === 'function') { this._fields = this._fields(); @@ -1653,6 +4019,31 @@ export class GraphQLInputObjectType { return this._fields; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { + * GraphQLInputObjectType, + * GraphQLInt, + * GraphQLNonNull, + * } from 'graphql/type'; + * + * const ReviewInput = new GraphQLInputObjectType({ + * name: 'ReviewInput', + * fields: { + * stars: { type: new GraphQLNonNull(GraphQLInt) }, + * }, + * }); + * + * const config = ReviewInput.toConfig(); + * const ReviewInputCopy = new GraphQLInputObjectType(config); + * + * String(config.fields.stars.type); // => 'Int!' + * String(ReviewInputCopy.getFields().stars.type); // => 'Int!' + * ``` + */ toConfig(): GraphQLInputObjectTypeNormalizedConfig { const fields = mapValue(this.getFields(), (field) => ({ description: field.description, @@ -1674,10 +4065,51 @@ export class GraphQLInputObjectType { }; } + /** + * Returns the schema coordinate identifying this input object type. + * @returns The schema coordinate for this input object type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInputObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * input ReviewInput { + * stars: Int! + * } + * + * type Query { + * reviews(filter: ReviewInput): [String] + * } + * `); + * + * const ReviewInput = assertInputObjectType(schema.getType('ReviewInput')); + * + * ReviewInput.toString(); // => 'ReviewInput' + * ``` + */ toString(): string { return this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { GraphQLInputObjectType, GraphQLString } from 'graphql/type'; + * + * const ReviewInput = new GraphQLInputObjectType({ + * name: 'ReviewInput', + * fields: { + * commentary: { type: GraphQLString }, + * }, + * }); + * + * ReviewInput.toJSON(); // => 'ReviewInput' + * JSON.stringify({ type: ReviewInput }); // => '{"type":"ReviewInput"}' + * ``` + */ toJSON(): string { return this.toString(); } @@ -1709,13 +4141,21 @@ function defineInputFieldMap( }); } +/** Configuration used to construct a GraphQLInputObjectType. */ export interface GraphQLInputObjectTypeConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Fields declared by this object, interface, input object, or literal. */ fields: ThunkObjMap; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; + /** Whether this input object uses the experimental OneOf input object semantics. */ isOneOf?: boolean; } @@ -1728,7 +4168,6 @@ interface GraphQLInputObjectTypeNormalizedConfig /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -1739,29 +4178,72 @@ export interface GraphQLInputFieldExtensions { [attributeName: string]: unknown; } +/** Configuration used to define a GraphQL input field. */ export interface GraphQLInputFieldConfig { + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** The GraphQL type reference or runtime type for this element. */ type: GraphQLInputType; + /** Default value used when no explicit value is supplied. */ defaultValue?: unknown; + /** Reason this element is deprecated, if one was provided. */ deprecationReason?: Maybe; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; } +/** A map of input field names to input field configuration objects. */ export type GraphQLInputFieldConfigMap = ObjMap; +/** A resolved GraphQL input field definition. */ export interface GraphQLInputField { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** The GraphQL type reference or runtime type for this element. */ type: GraphQLInputType; + /** Default value used when no explicit value is supplied. */ defaultValue: unknown; + /** Reason this element is deprecated, if one was provided. */ deprecationReason: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; } +/** + * Returns true when the input field is non-null and has no default value. + * @param field - The input field definition to inspect. + * @returns True when the input field is non-null and has no default value. + * @example + * ```ts + * import { + * GraphQLInt, + * GraphQLNonNull, + * GraphQLString, + * isRequiredInputField, + * } from 'graphql/type'; + * + * const requiredField = { name: 'id', type: new GraphQLNonNull(GraphQLInt) }; + * const optionalField = { name: 'name', type: GraphQLString }; + * const fieldWithDefault = { + * name: 'limit', + * type: new GraphQLNonNull(GraphQLInt), + * defaultValue: 10, + * }; + * + * isRequiredInputField(requiredField); // => true + * isRequiredInputField(optionalField); // => false + * isRequiredInputField(fieldWithDefault); // => false + * ``` + */ export function isRequiredInputField(field: GraphQLInputField): boolean { return isNonNullType(field.type) && field.defaultValue === undefined; } +/** A map of input field names to resolved input field definitions. */ export type GraphQLInputFieldMap = ObjMap; diff --git a/src/type/directives.ts b/src/type/directives.ts index 9c293ae411..a7ca75dd23 100644 --- a/src/type/directives.ts +++ b/src/type/directives.ts @@ -1,3 +1,5 @@ +/** @category Directives */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { instanceOf } from '../jsutils/instanceOf'; @@ -25,11 +27,44 @@ import { GraphQLBoolean, GraphQLString } from './scalars'; /** * Test if the given value is a GraphQL directive. + * @param directive - Value to inspect. + * @returns True when the value is a GraphQLDirective. + * @example + * ```ts + * import { DirectiveLocation } from 'graphql/language'; + * import { GraphQLDirective, GraphQLString, isDirective } from 'graphql/type'; + * + * const upper = new GraphQLDirective({ + * name: 'upper', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * }); + * + * isDirective(upper); // => true + * isDirective(GraphQLString); // => false + * ``` */ export function isDirective(directive: unknown): directive is GraphQLDirective { return instanceOf(directive, GraphQLDirective); } +/** + * Returns the value as a GraphQLDirective, or throws if it is not a directive. + * @param directive - Value to inspect. + * @returns The value typed as a GraphQLDirective. + * @example + * ```ts + * import { DirectiveLocation } from 'graphql/language'; + * import { assertDirective, GraphQLDirective, GraphQLString } from 'graphql/type'; + * + * const upper = new GraphQLDirective({ + * name: 'upper', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * }); + * + * assertDirective(upper); // => upper + * assertDirective(GraphQLString); // throws an error + * ``` + */ export function assertDirective(directive: unknown): GraphQLDirective { if (!isDirective(directive)) { throw new Error( @@ -41,7 +76,6 @@ export function assertDirective(directive: unknown): GraphQLDirective { /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -57,16 +91,76 @@ export interface GraphQLDirectiveExtensions { * behavior. Type system creators will usually not create these directly. */ export class GraphQLDirective { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Locations where this directive may be applied. */ locations: ReadonlyArray; + /** Arguments accepted by this field or directive. */ args: ReadonlyArray; + /** Whether this directive may appear more than once at the same location. */ isRepeatable: boolean; + /** Reason this element is deprecated, if one was provided. */ deprecationReason: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; + /** + * Creates a GraphQLDirective instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * import { DirectiveLocation, parse } from 'graphql/language'; + * import { + * GraphQLBoolean, + * GraphQLDirective, + * GraphQLInt, + * GraphQLNonNull, + * } from 'graphql/type'; + * + * const document = parse(` + * directive @cacheControl(maxAge: Int) repeatable on FIELD_DEFINITION + * extend directive @cacheControl(maxAge: Int) on FIELD_DEFINITION + * `); + * const definition = document.definitions[0]; + * + * const cacheControl = new GraphQLDirective({ + * name: 'cacheControl', + * description: 'Controls HTTP cache hints for a field.', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * args: { + * inheritMaxAge: { + * description: 'Inherit the parent cache hint.', + * type: new GraphQLNonNull(GraphQLBoolean), + * defaultValue: false, + * deprecationReason: 'Use maxAge instead.', + * extensions: { scope: 'cache' }, + * }, + * maxAge: { + * type: GraphQLInt, + * astNode: definition.arguments[0], + * }, + * }, + * isRepeatable: true, + * deprecationReason: 'Use @cache instead.', + * extensions: { scope: 'cache' }, + * astNode: definition, + * extensionASTNodes: [ document.definitions[1] ], + * }); + * + * cacheControl.name; // => 'cacheControl' + * cacheControl.description; // => 'Controls HTTP cache hints for a field.' + * cacheControl.args[0].name; // => 'inheritMaxAge' + * cacheControl.args[0].defaultValue; // => false + * cacheControl.isRepeatable; // => true + * cacheControl.extensions; // => { scope: 'cache' } + * ``` + */ constructor(config: Readonly) { this.name = assertName(config.name); this.description = config.description; @@ -91,10 +185,37 @@ export class GraphQLDirective { this.args = defineArguments(args); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLDirective'; } + /** + * Returns a normalized configuration object for this object. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { DirectiveLocation } from 'graphql/language'; + * import { GraphQLDirective, GraphQLString } from 'graphql/type'; + * + * const tag = new GraphQLDirective({ + * name: 'tag', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * args: { + * name: { type: GraphQLString }, + * }, + * }); + * + * const config = tag.toConfig(); + * const tagCopy = new GraphQLDirective(config); + * + * config.args.name.type; // => GraphQLString + * tagCopy.args[0].name; // => 'name' + * ``` + */ toConfig(): GraphQLDirectiveNormalizedConfig { return { name: this.name, @@ -109,24 +230,67 @@ export class GraphQLDirective { }; } + /** + * Returns the schema coordinate identifying this directive. + * @returns The directive schema coordinate. + * @example + * ```ts + * import { DirectiveLocation } from 'graphql/language'; + * import { GraphQLDirective } from 'graphql/type'; + * + * const tag = new GraphQLDirective({ + * name: 'tag', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * }); + * + * tag.toString(); // => '@tag' + * ``` + */ toString(): string { return '@' + this.name; } + /** + * Returns the JSON representation used when this object is serialized. + * @returns The JSON-serializable representation. + * @example + * ```ts + * import { DirectiveLocation } from 'graphql/language'; + * import { GraphQLDirective } from 'graphql/type'; + * + * const tag = new GraphQLDirective({ + * name: 'tag', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * }); + * + * tag.toJSON(); // => '@tag' + * JSON.stringify({ directive: tag }); // => '{"directive":"@tag"}' + * ``` + */ toJSON(): string { return this.toString(); } } +/** Configuration used to construct a GraphQLDirective. */ export interface GraphQLDirectiveConfig { + /** The GraphQL name for this schema element. */ name: string; + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Locations where this directive may be applied. */ locations: ReadonlyArray; + /** Arguments accepted by this field or directive. */ args?: Maybe; + /** Whether this directive may appear more than once at the same location. */ isRepeatable?: Maybe; + /** Reason this element is deprecated, if one was provided. */ deprecationReason?: Maybe; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } @@ -137,9 +301,7 @@ interface GraphQLDirectiveNormalizedConfig extends GraphQLDirectiveConfig { extensionASTNodes: ReadonlyArray; } -/** - * Used to conditionally include fields or fragments. - */ +/** Used to conditionally include fields or fragments. */ export const GraphQLIncludeDirective: GraphQLDirective = new GraphQLDirective({ name: 'include', description: @@ -157,9 +319,7 @@ export const GraphQLIncludeDirective: GraphQLDirective = new GraphQLDirective({ }, }); -/** - * Used to conditionally skip (exclude) fields or fragments. - */ +/** Used to conditionally skip (exclude) fields or fragments. */ export const GraphQLSkipDirective: GraphQLDirective = new GraphQLDirective({ name: 'skip', description: @@ -177,13 +337,13 @@ export const GraphQLSkipDirective: GraphQLDirective = new GraphQLDirective({ }, }); -/** - * Constant string used for default reason for a deprecation. - */ +/** Constant string used for default reason for a deprecation. */ export const DEFAULT_DEPRECATION_REASON = 'No longer supported'; /** * Used to declare element of a GraphQL schema as deprecated. + * + * The optional `reason` argument defaults to `DEFAULT_DEPRECATION_REASON`. */ export const GraphQLDeprecatedDirective: GraphQLDirective = new GraphQLDirective({ @@ -206,9 +366,7 @@ export const GraphQLDeprecatedDirective: GraphQLDirective = }, }); -/** - * Used to provide a URL for specifying the behavior of custom scalar definitions. - */ +/** Used to provide a URL for specifying the behavior of custom scalar definitions. */ export const GraphQLSpecifiedByDirective: GraphQLDirective = new GraphQLDirective({ name: 'specifiedBy', @@ -222,9 +380,7 @@ export const GraphQLSpecifiedByDirective: GraphQLDirective = }, }); -/** - * Used to indicate an Input Object is a OneOf Input Object. - */ +/** Used to indicate an Input Object is a OneOf Input Object. */ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({ name: 'oneOf', description: @@ -233,9 +389,7 @@ export const GraphQLOneOfDirective: GraphQLDirective = new GraphQLDirective({ args: {}, }); -/** - * The full list of specified directives. - */ +/** Full list of stable directives specified by GraphQL.js. */ export const specifiedDirectives: ReadonlyArray = Object.freeze([ GraphQLIncludeDirective, @@ -245,6 +399,28 @@ export const specifiedDirectives: ReadonlyArray = GraphQLOneOfDirective, ]); +/** + * Returns true when the directive is one of the directives specified by GraphQL. + * @param directive - Directive to inspect. + * @returns True when the directive is specified by GraphQL. + * @example + * ```ts + * import { + * GraphQLDirective, + * GraphQLIncludeDirective, + * isSpecifiedDirective, + * } from 'graphql/type'; + * import { DirectiveLocation } from 'graphql/language'; + * + * const customDirective = new GraphQLDirective({ + * name: 'auth', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * }); + * + * isSpecifiedDirective(GraphQLIncludeDirective); // => true + * isSpecifiedDirective(customDirective); // => false + * ``` + */ export function isSpecifiedDirective(directive: GraphQLDirective): boolean { return specifiedDirectives.some(({ name }) => name === directive.name); } diff --git a/src/type/index.ts b/src/type/index.ts index cf276d1e02..d3cdec26e2 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -1,3 +1,10 @@ +/** + * Create and inspect GraphQL type definitions and schemas. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + */ + export type { Path as ResponsePath } from '../jsutils/Path'; export { diff --git a/src/type/introspection.ts b/src/type/introspection.ts index 86ad2218f5..0427b33d8b 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -1,3 +1,5 @@ +/** @category Introspection */ + import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; @@ -33,6 +35,7 @@ import type { GraphQLDirective } from './directives'; import { GraphQLBoolean, GraphQLString } from './scalars'; import type { GraphQLSchema } from './schema'; +/** The introspection type describing a GraphQL schema. */ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ name: '__Schema', description: @@ -88,6 +91,7 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap), }); +/** The introspection type describing a GraphQL directive. */ export const __Directive: GraphQLObjectType = new GraphQLObjectType({ name: '__Directive', description: @@ -139,6 +143,7 @@ export const __Directive: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap), }); +/** The introspection enum describing directive locations. */ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({ name: '__DirectiveLocation', description: @@ -227,6 +232,7 @@ export const __DirectiveLocation: GraphQLEnumType = new GraphQLEnumType({ }, }); +/** The introspection type describing GraphQL types. */ export const __Type: GraphQLObjectType = new GraphQLObjectType({ name: '__Type', description: @@ -357,6 +363,7 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap), }); +/** The introspection type describing object and interface fields. */ export const __Field: GraphQLObjectType = new GraphQLObjectType({ name: '__Field', description: @@ -402,6 +409,7 @@ export const __Field: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap, unknown>), }); +/** The introspection type describing arguments and input fields. */ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ name: '__InputValue', description: @@ -441,6 +449,7 @@ export const __InputValue: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap), }); +/** The introspection type describing enum values. */ export const __EnumValue: GraphQLObjectType = new GraphQLObjectType({ name: '__EnumValue', description: @@ -466,18 +475,31 @@ export const __EnumValue: GraphQLObjectType = new GraphQLObjectType({ } as GraphQLFieldConfigMap), }); +/** + * The introspection enum describing the different kinds of GraphQL types. + * @category Introspection + */ enum TypeKind { + /** A scalar type. */ SCALAR = 'SCALAR', + /** An object type. */ OBJECT = 'OBJECT', + /** An interface type. */ INTERFACE = 'INTERFACE', + /** A union type. */ UNION = 'UNION', + /** An enum type. */ ENUM = 'ENUM', + /** An input object type. */ INPUT_OBJECT = 'INPUT_OBJECT', + /** A list wrapper type. */ LIST = 'LIST', + /** A non-null wrapper type. */ NON_NULL = 'NON_NULL', } export { TypeKind }; +/** The introspection enum describing GraphQL type kinds. */ export const __TypeKind: GraphQLEnumType = new GraphQLEnumType({ name: '__TypeKind', description: 'An enum describing what kind of type a given `__Type` is.', @@ -539,6 +561,7 @@ export const SchemaMetaFieldDef: GraphQLField = { astNode: undefined, }; +/** The `__type` meta field definition used by introspection. */ export const TypeMetaFieldDef: GraphQLField = { name: '__type', type: __Type, @@ -560,6 +583,7 @@ export const TypeMetaFieldDef: GraphQLField = { astNode: undefined, }; +/** The `__typename` meta field definition used by execution and introspection. */ export const TypeNameMetaFieldDef: GraphQLField = { name: '__typename', type: new GraphQLNonNull(GraphQLString), @@ -571,6 +595,7 @@ export const TypeNameMetaFieldDef: GraphQLField = { astNode: undefined, }; +/** All introspection types defined by the GraphQL specification. */ export const introspectionTypes: ReadonlyArray = Object.freeze([ __Schema, @@ -583,6 +608,18 @@ export const introspectionTypes: ReadonlyArray = __TypeKind, ]); +/** + * Returns true when the type is one of the built-in introspection types. + * @param type - The GraphQL type to inspect. + * @returns True when the type is one of the built-in introspection types. + * @example + * ```ts + * import { GraphQLString, isIntrospectionType, __Type } from 'graphql/type'; + * + * isIntrospectionType(__Type); // => true + * isIntrospectionType(GraphQLString); // => false + * ``` + */ export function isIntrospectionType(type: GraphQLNamedType): boolean { return introspectionTypes.some(({ name }) => type.name === name); } diff --git a/src/type/scalars.ts b/src/type/scalars.ts index 4990347887..cc9623701f 100644 --- a/src/type/scalars.ts +++ b/src/type/scalars.ts @@ -1,3 +1,5 @@ +/** @category Scalars */ + import { inspect } from '../jsutils/inspect'; import { isObjectLike } from '../jsutils/isObjectLike'; @@ -12,15 +14,16 @@ import { GraphQLScalarType } from './definition'; /** * Maximum possible Int value as per GraphQL Spec (32-bit signed integer). * n.b. This differs from JavaScript's numbers that are IEEE 754 doubles safe up-to 2^53 - 1 - * */ + */ export const GRAPHQL_MAX_INT = 2147483647; /** * Minimum possible Int value as per GraphQL Spec (32-bit signed integer). * n.b. This differs from JavaScript's numbers that are IEEE 754 doubles safe starting at -(2^53 - 1) - * */ + */ export const GRAPHQL_MIN_INT = -2147483648; +/** The built-in `Int` scalar type. */ export const GraphQLInt = new GraphQLScalarType({ name: 'Int', description: @@ -84,6 +87,7 @@ export const GraphQLInt = new GraphQLScalarType({ }, }); +/** The built-in `Float` scalar type. */ export const GraphQLFloat = new GraphQLScalarType({ name: 'Float', description: @@ -129,6 +133,7 @@ export const GraphQLFloat = new GraphQLScalarType({ }, }); +/** The built-in `String` scalar type. */ export const GraphQLString = new GraphQLScalarType({ name: 'String', description: @@ -173,6 +178,7 @@ export const GraphQLString = new GraphQLScalarType({ }, }); +/** The built-in `Boolean` scalar type. */ export const GraphQLBoolean = new GraphQLScalarType({ name: 'Boolean', description: 'The `Boolean` scalar type represents `true` or `false`.', @@ -211,6 +217,7 @@ export const GraphQLBoolean = new GraphQLScalarType({ }, }); +/** The built-in `ID` scalar type. */ export const GraphQLID = new GraphQLScalarType({ name: 'ID', description: @@ -252,6 +259,7 @@ export const GraphQLID = new GraphQLScalarType({ }, }); +/** All built-in scalar types defined by the GraphQL specification. */ export const specifiedScalarTypes: ReadonlyArray = Object.freeze([ GraphQLString, @@ -261,6 +269,26 @@ export const specifiedScalarTypes: ReadonlyArray = GraphQLID, ]); +/** + * Returns true when the scalar type is one of the scalars specified by GraphQL. + * @param type - The GraphQL type to inspect. + * @returns True when the scalar type is one of the scalars specified by GraphQL. + * @example + * ```ts + * import { + * GraphQLScalarType, + * GraphQLString, + * isSpecifiedScalarType, + * } from 'graphql/type'; + * + * const DateTime = new GraphQLScalarType({ + * name: 'DateTime', + * }); + * + * isSpecifiedScalarType(GraphQLString); // => true + * isSpecifiedScalarType(DateTime); // => false + * ``` + */ export function isSpecifiedScalarType(type: GraphQLNamedType): boolean { return specifiedScalarTypes.some(({ name }) => type.name === name); } diff --git a/src/type/schema.ts b/src/type/schema.ts index 97c2782145..e26092954a 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -1,3 +1,5 @@ +/** @category Schema */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { instanceOf } from '../jsutils/instanceOf'; @@ -34,11 +36,46 @@ import { __Schema } from './introspection'; /** * Test if the given value is a GraphQL schema. + * @param schema - Value to inspect. + * @returns True when the value is a GraphQLSchema. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { GraphQLString, isSchema } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * isSchema(schema); // => true + * isSchema(GraphQLString); // => false + * ``` */ export function isSchema(schema: unknown): schema is GraphQLSchema { return instanceOf(schema, GraphQLSchema); } +/** + * Returns the value as a GraphQLSchema, or throws if it is not a schema. + * @param schema - GraphQL schema to use. + * @returns The value typed as a GraphQLSchema. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertSchema, GraphQLString } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * assertSchema(schema); // => schema + * assertSchema(GraphQLString); // throws an error + * ``` + */ export function assertSchema(schema: unknown): GraphQLSchema { if (!isSchema(schema)) { throw new Error(`Expected ${inspect(schema)} to be a GraphQL schema.`); @@ -48,7 +85,6 @@ export function assertSchema(schema: unknown): GraphQLSchema { /** * Custom extensions - * * @remarks * Use a unique identifier name for your extension, for example the name of * your library or project. Do not use a shortened identifier as this increases @@ -69,10 +105,24 @@ export interface GraphQLSchemaExtensions { * Example: * * ```ts + * const MyAppQueryRootType = new GraphQLObjectType({ + * name: 'Query', + * fields: { + * greeting: { type: GraphQLString }, + * }, + * }); + * + * const MyAppMutationRootType = new GraphQLObjectType({ + * name: 'Mutation', + * fields: { + * setGreeting: { type: GraphQLString }, + * }, + * }); + * * const MyAppSchema = new GraphQLSchema({ * query: MyAppQueryRootType, * mutation: MyAppMutationRootType, - * }) + * }); * ``` * * Note: When the schema is constructed, by default only the types that are @@ -84,34 +134,39 @@ export interface GraphQLSchemaExtensions { * ```ts * const characterInterface = new GraphQLInterfaceType({ * name: 'Character', - * ... + * fields: { + * name: { type: GraphQLString }, + * }, * }); * * const humanType = new GraphQLObjectType({ * name: 'Human', * interfaces: [characterInterface], - * ... + * fields: { + * name: { type: GraphQLString }, + * }, * }); * * const droidType = new GraphQLObjectType({ * name: 'Droid', * interfaces: [characterInterface], - * ... + * fields: { + * name: { type: GraphQLString }, + * }, * }); * * const schema = new GraphQLSchema({ * query: new GraphQLObjectType({ * name: 'Query', * fields: { - * hero: { type: characterInterface, ... }, - * } + * hero: { type: characterInterface }, + * }, * }), - * ... * // Since this schema references only the `Character` interface it's * // necessary to explicitly list the types that implement it if * // you want them to be included in the final schema. * types: [humanType, droidType], - * }) + * }); * ``` * * Note: If an array of `directives` are provided to GraphQLSchema, that will be @@ -122,18 +177,23 @@ export interface GraphQLSchemaExtensions { * * ```ts * const MyAppSchema = new GraphQLSchema({ - * ... - * directives: specifiedDirectives.concat([ myCustomDirective ]), - * }) + * query: MyAppQueryRootType, + * directives: specifiedDirectives.concat([myCustomDirective]), + * }); * ``` */ export class GraphQLSchema { + /** Human-readable description for this schema element, if provided. */ description: Maybe; + /** Extension fields to include in the formatted result. */ extensions: Readonly; + /** AST node from which this schema element was built, if available. */ astNode: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes: ReadonlyArray; // Used as a cache for validateSchema(). + /** Cached schema validation errors, if validation has already run. */ __validationErrors: Maybe>; private _queryType: Maybe; @@ -147,6 +207,100 @@ export class GraphQLSchema { interfaces: Array; }>; + /** + * Creates a GraphQLSchema instance. + * @param config - Configuration describing this object. + * @example + * ```ts + * // Create a schema with the required query root. + * import { + * GraphQLObjectType, + * GraphQLSchema, + * GraphQLString, + * } from 'graphql/type'; + * + * const Query = new GraphQLObjectType({ + * name: 'Query', + * fields: { + * greeting: { + * type: GraphQLString, + * resolve: () => 'Hello', + * }, + * }, + * }); + * + * const schema = new GraphQLSchema({ + * description: 'The application schema.', + * query: Query, + * }); + * + * schema.getQueryType(); // => Query + * schema.description; // => 'The application schema.' + * ``` + * @example + * ```ts + * // This variant configures every schema option, including directives and extensions. + * import { DirectiveLocation, parse } from 'graphql/language'; + * import { + * GraphQLBoolean, + * GraphQLDirective, + * GraphQLObjectType, + * GraphQLSchema, + * GraphQLString, + * } from 'graphql/type'; + * + * const Query = new GraphQLObjectType({ + * name: 'Query', + * fields: { greeting: { type: GraphQLString } }, + * }); + * const Mutation = new GraphQLObjectType({ + * name: 'Mutation', + * fields: { setGreeting: { type: GraphQLString } }, + * }); + * const Subscription = new GraphQLObjectType({ + * name: 'Subscription', + * fields: { greetingChanged: { type: GraphQLString } }, + * }); + * const AuditEvent = new GraphQLObjectType({ + * name: 'AuditEvent', + * fields: { message: { type: GraphQLString } }, + * }); + * const authDirective = new GraphQLDirective({ + * name: 'auth', + * locations: [DirectiveLocation.FIELD_DEFINITION], + * args: { required: { type: GraphQLBoolean } }, + * }); + * const schemaDocument = parse(` + * schema { + * query: Query + * mutation: Mutation + * subscription: Subscription + * } + * + * extend schema @auth + * `); + * + * const schema = new GraphQLSchema({ + * description: 'Operations exposed by the application.', + * query: Query, + * mutation: Mutation, + * subscription: Subscription, + * types: [AuditEvent], + * directives: [authDirective], + * extensions: { owner: 'platform' }, + * astNode: schemaDocument.definitions[0], + * extensionASTNodes: [ schemaDocument.definitions[1] ], + * assumeValid: true, + * }); + * + * schema.getMutationType(); // => Mutation + * schema.getSubscriptionType(); // => Subscription + * schema.getType('AuditEvent'); // => AuditEvent + * schema.getDirective('auth'); // => authDirective + * schema.extensions; // => { owner: 'platform' } + * schema.__validationErrors; // => [] + * ``` + */ constructor(config: Readonly) { // If this schema was built from a source known to be valid, then it may be // marked with assumeValid to avoid an additional type system validation. @@ -264,22 +418,106 @@ export class GraphQLSchema { } } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'GraphQLSchema'; } + /** + * Returns the root object type for query operations. + * @returns The query root type, if this schema defines one. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * schema.getQueryType()?.name; // => 'Query' + * ``` + */ getQueryType(): Maybe { return this._queryType; } + /** + * Returns the root object type for mutation operations. + * @returns The mutation root type, if this schema defines one. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * + * type Mutation { + * setGreeting(value: String!): String + * } + * `); + * + * schema.getMutationType()?.name; // => 'Mutation' + * ``` + */ getMutationType(): Maybe { return this._mutationType; } + /** + * Returns the root object type for subscription operations. + * @returns The subscription root type, if this schema defines one. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * + * type Subscription { + * greetings: String + * } + * `); + * + * schema.getSubscriptionType()?.name; // => 'Subscription' + * ``` + */ getSubscriptionType(): Maybe { return this._subscriptionType; } + /** + * Returns the root object type for the requested operation kind. + * @param operation - Operation kind to resolve. + * @returns The root object type for the operation kind, if this schema defines one. + * @example + * ```ts + * import { OperationTypeNode } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * + * type Mutation { + * setGreeting(value: String!): String + * } + * `); + * + * schema.getRootType(OperationTypeNode.QUERY)?.name; // => 'Query' + * schema.getRootType(OperationTypeNode.MUTATION)?.name; // => 'Mutation' + * schema.getRootType(OperationTypeNode.SUBSCRIPTION); // => undefined + * ``` + */ getRootType(operation: OperationTypeNode): Maybe { switch (operation) { case OperationTypeNode.QUERY: @@ -291,14 +529,97 @@ export class GraphQLSchema { } } + /** + * Returns all named types known to this schema. + * @returns A map of schema types keyed by type name. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type User { + * name: String + * } + * + * type Query { + * viewer: User + * } + * `); + * + * const typeMap = schema.getTypeMap(); + * + * typeMap.User.name; // => 'User' + * typeMap.Query.name; // => 'Query' + * typeMap.String.name; // => 'String' + * ``` + */ getTypeMap(): TypeMap { return this._typeMap; } + /** + * Returns the named type with the provided name. + * @param name - The GraphQL name to look up. + * @returns The named schema type, if one exists. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type User { + * name: String + * } + * + * type Query { + * viewer: User + * } + * `); + * + * schema.getType('User')?.toString(); // => 'User' + * schema.getType('Missing'); // => undefined + * ``` + */ getType(name: string): GraphQLNamedType | undefined { return this.getTypeMap()[name]; } + /** + * Returns object types that may be returned for an abstract type. + * @param abstractType - Interface or union type to inspect. + * @returns Object types that may satisfy the abstract type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType, assertUnionType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Organization implements Node { + * id: ID! + * } + * + * union SearchResult = User | Organization + * + * type Query { + * node: Node + * search: [SearchResult] + * } + * `); + * + * const Node = assertInterfaceType(schema.getType('Node')); + * const SearchResult = assertUnionType(schema.getType('SearchResult')); + * + * schema.getPossibleTypes(Node).map((type) => type.name); // => ['User', 'Organization'] + * schema.getPossibleTypes(SearchResult).map((type) => type.name); // => ['User', 'Organization'] + * ``` + */ getPossibleTypes( abstractType: GraphQLAbstractType, ): ReadonlyArray { @@ -307,6 +628,42 @@ export class GraphQLSchema { : this.getImplementations(abstractType).objects; } + /** + * Returns objects and interfaces that implement an interface type. + * @param interfaceType - Interface type to inspect. + * @returns Object and interface implementations of the interface. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Resource { + * url: String! + * } + * + * interface Image implements Resource { + * url: String! + * width: Int + * } + * + * type Photo implements Resource & Image { + * url: String! + * width: Int + * } + * + * type Query { + * resource: Resource + * } + * `); + * + * const Resource = assertInterfaceType(schema.getType('Resource')); + * const implementations = schema.getImplementations(Resource); + * + * implementations.interfaces.map((type) => type.name); // => ['Image'] + * implementations.objects.map((type) => type.name); // => ['Photo'] + * ``` + */ getImplementations(interfaceType: GraphQLInterfaceType): { objects: ReadonlyArray; interfaces: ReadonlyArray; @@ -315,6 +672,43 @@ export class GraphQLSchema { return implementations ?? { objects: [], interfaces: [] }; } + /** + * Returns whether one type is a possible runtime subtype of an abstract type. + * @param abstractType - Interface or union type to inspect. + * @param maybeSubType - Object or interface type to test as a possible subtype. + * @returns True when the subtype may satisfy the abstract type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertInterfaceType, assertObjectType } from 'graphql/type'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Review { + * body: String + * } + * + * type Query { + * node: Node + * review: Review + * } + * `); + * + * const Node = assertInterfaceType(schema.getType('Node')); + * const User = assertObjectType(schema.getType('User')); + * const Review = assertObjectType(schema.getType('Review')); + * + * schema.isSubType(Node, User); // => true + * schema.isSubType(Node, Review); // => false + * ``` + */ isSubType( abstractType: GraphQLAbstractType, maybeSubType: GraphQLObjectType | GraphQLInterfaceType, @@ -342,14 +736,76 @@ export class GraphQLSchema { return map[maybeSubType.name] !== undefined; } + /** + * Returns directives available in this schema. + * @returns Directives available in this schema. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * directive @upper on FIELD_DEFINITION + * + * type Query { + * greeting: String @upper + * } + * `); + * + * schema.getDirectives().map((directive) => directive.name); // => ['include', 'skip', 'deprecated', 'specifiedBy', 'oneOf', 'upper'] + * ``` + */ getDirectives(): ReadonlyArray { return this._directives; } + /** + * Returns the current directive definition. + * @param name - The GraphQL name to look up. + * @returns The current directive definition, if known. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * directive @upper on FIELD_DEFINITION + * + * type Query { + * greeting: String @upper + * } + * `); + * + * schema.getDirective('upper')?.name; // => 'upper' + * schema.getDirective('missing'); // => undefined + * ``` + */ getDirective(name: string): Maybe { return this.getDirectives().find((directive) => directive.name === name); } + /** + * Returns a normalized configuration object for this object. + * + * The returned config preserves the original `assumeValid` flag so the schema + * can be recreated with the same validation behavior. + * @returns A configuration object that can be used to recreate this object. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { GraphQLSchema } from 'graphql/type'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * const config = schema.toConfig(); + * const schemaCopy = new GraphQLSchema(config); + * + * config.query?.name; // => 'Query' + * schemaCopy.getQueryType()?.name; // => 'Query' + * ``` + */ toConfig(): GraphQLSchemaNormalizedConfig { return { description: this.description, @@ -368,6 +824,7 @@ export class GraphQLSchema { type TypeMap = ObjMap; +/** @internal */ export interface GraphQLSchemaValidationOptions { /** * When building a schema from a GraphQL service's introspection result, it @@ -379,21 +836,29 @@ export interface GraphQLSchemaValidationOptions { assumeValid?: boolean; } +/** Configuration used to construct a GraphQLSchema. */ export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions { + /** Human-readable description for this schema element, if provided. */ description?: Maybe; + /** Root object type for query operations. */ query?: Maybe; + /** Root object type for mutation operations. */ mutation?: Maybe; + /** Root object type for subscription operations. */ subscription?: Maybe; + /** Object types that belong to this union type. */ types?: Maybe>; + /** Directives available in this schema or applied to this AST node. */ directives?: Maybe>; + /** Extension fields to include in the formatted result. */ extensions?: Maybe>; + /** AST node from which this schema element was built, if available. */ astNode?: Maybe; + /** AST extension nodes applied to this schema element. */ extensionASTNodes?: Maybe>; } -/** - * @internal - */ +/** @internal */ export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig { description: Maybe; types: ReadonlyArray; diff --git a/src/type/validate.ts b/src/type/validate.ts index 56ad63fc64..95fa6c6601 100644 --- a/src/type/validate.ts +++ b/src/type/validate.ts @@ -1,3 +1,5 @@ +/** @category Validation */ + import { inspect } from '../jsutils/inspect'; import type { Maybe } from '../jsutils/Maybe'; @@ -50,6 +52,22 @@ import { assertSchema } from './schema'; * * Validation runs synchronously, returning an array of encountered errors, or * an empty array if no errors were encountered and the Schema is valid. + * @param schema - GraphQL schema to use. + * @returns Schema validation errors, or an empty array when the schema is valid. + * @example + * ```ts + * import { validateSchema } from 'graphql/type'; + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * const errors = validateSchema(schema); + * + * errors; // => [] + * ``` */ export function validateSchema( schema: GraphQLSchema, @@ -78,6 +96,20 @@ export function validateSchema( /** * Utility function which asserts a schema is valid by throwing an error if * it is invalid. + * @param schema - GraphQL schema to use. + * @example + * ```ts + * import { assertValidSchema } from 'graphql/type'; + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * assertValidSchema(schema); // does not throw + * ``` */ export function assertValidSchema(schema: GraphQLSchema): void { const errors = validateSchema(schema); diff --git a/src/utilities/TypeInfo.ts b/src/utilities/TypeInfo.ts index e72dfb01fb..520d56f992 100644 --- a/src/utilities/TypeInfo.ts +++ b/src/utilities/TypeInfo.ts @@ -1,3 +1,5 @@ +/** @category Type Info */ + import type { Maybe } from '../jsutils/Maybe'; import type { ASTNode, FieldNode } from '../language/ast'; @@ -55,6 +57,74 @@ export class TypeInfo { private _enumValue: Maybe; private _getFieldDef: GetFieldDefFn; + /** + * Creates a TypeInfo instance. + * @param schema - Schema used for type lookups. + * @param initialType - Optional type to use at the start of traversal. + * @param getFieldDefFn - Optional field definition lookup override. + * @example + * ```ts + * // Track field types during a visitWithTypeInfo traversal. + * import { parse, visit } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const typeInfo = new TypeInfo(schema); + * const seenTypes = []; + * + * visit( + * parse('{ greeting }'), + * visitWithTypeInfo(typeInfo, { + * Field: () => { + * seenTypes.push(String(typeInfo.getType())); + * }, + * }), + * ); + * + * seenTypes; // => ['String'] + * ``` + * @example + * ```ts + * // This variant starts from an initial type and supplies a field definition resolver. + * import { Kind } from 'graphql/language'; + * import { GraphQLString } from 'graphql/type'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const typeInfo = new TypeInfo(schema, schema.getQueryType(), () => ({ + * name: 'virtualGreeting', + * description: undefined, + * type: GraphQLString, + * args: [], + * resolve: undefined, + * subscribe: undefined, + * deprecationReason: undefined, + * extensions: Object.create(null), + * astNode: undefined, + * })); + * + * typeInfo.enter({ + * kind: Kind.SELECTION_SET, + * selections: [], + * }); + * typeInfo.enter({ + * kind: Kind.FIELD, + * name: { kind: Kind.NAME, value: 'ignored' }, + * }); + * + * typeInfo.getFieldDef()?.name; // => 'virtualGreeting' + * String(typeInfo.getType()); // => 'String' + * ``` + */ constructor( schema: GraphQLSchema, /** @@ -63,7 +133,10 @@ export class TypeInfo { */ initialType?: Maybe, - /** @deprecated will be removed in 17.0.0 */ + /** + * Legacy field definition lookup override. + * @deprecated will be removed in 17.0.0 + */ getFieldDefFn?: GetFieldDefFn, ) { this._schema = schema; @@ -89,58 +162,365 @@ export class TypeInfo { } } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'TypeInfo'; } + /** + * Returns the current output type at this point in traversal. + * @returns The current output type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * viewer: User + * } + * + * type User { + * name: String + * } + * `); + * const typeInfo = new TypeInfo(schema); + * const fieldTypes = {}; + * + * visit( + * parse('{ viewer { name } }'), + * visitWithTypeInfo(typeInfo, { + * Field: (node) => { + * fieldTypes[node.name.value] = String(typeInfo.getType()); + * }, + * }), + * ); + * + * fieldTypes; // => { viewer: 'User', name: 'String' } + * ``` + */ getType(): Maybe { if (this._typeStack.length > 0) { return this._typeStack[this._typeStack.length - 1]; } } + /** + * Returns the current parent composite type. + * @returns The current parent composite type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * viewer: User + * } + * + * type User { + * name: String + * } + * `); + * const typeInfo = new TypeInfo(schema); + * const parentTypes = {}; + * + * visit( + * parse('{ viewer { name } }'), + * visitWithTypeInfo(typeInfo, { + * Field: (node) => { + * parentTypes[node.name.value] = String(typeInfo.getParentType()); + * }, + * }), + * ); + * + * parentTypes; // => { viewer: 'Query', name: 'User' } + * ``` + */ getParentType(): Maybe { if (this._parentTypeStack.length > 0) { return this._parentTypeStack[this._parentTypeStack.length - 1]; } } + /** + * Returns the current input type at this point in traversal. + * @returns The current input type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * reviews(stars: Int!, sort: Sort = NEWEST): [String] + * } + * + * enum Sort { + * NEWEST + * OLDEST + * } + * `); + * const typeInfo = new TypeInfo(schema); + * const inputTypes = {}; + * + * visit( + * parse('{ reviews(stars: 5, sort: OLDEST) }'), + * visitWithTypeInfo(typeInfo, { + * Argument: (node) => { + * inputTypes[node.name.value] = String(typeInfo.getInputType()); + * }, + * }), + * ); + * + * inputTypes; // => { stars: 'Int!', sort: 'Sort' } + * ``` + */ getInputType(): Maybe { if (this._inputTypeStack.length > 0) { return this._inputTypeStack[this._inputTypeStack.length - 1]; } } + /** + * Returns the parent input type for the current input position. + * @returns The parent input type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * input ReviewFilter { + * stars: Int! + * } + * + * type Query { + * reviews(filter: ReviewFilter): [String] + * } + * `); + * const typeInfo = new TypeInfo(schema); + * const parentInputTypes = {}; + * + * visit( + * parse('{ reviews(filter: { stars: 5 }) }'), + * visitWithTypeInfo(typeInfo, { + * ObjectField: (node) => { + * parentInputTypes[node.name.value] = String(typeInfo.getParentInputType()); + * }, + * }), + * ); + * + * parentInputTypes; // => { stars: 'ReviewFilter' } + * ``` + */ getParentInputType(): Maybe { if (this._inputTypeStack.length > 1) { return this._inputTypeStack[this._inputTypeStack.length - 2]; } } + /** + * Returns the current field definition. + * @returns The current field definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const typeInfo = new TypeInfo(schema); + * let fieldName; + * + * visit( + * parse('{ greeting }'), + * visitWithTypeInfo(typeInfo, { + * Field: () => { + * fieldName = typeInfo.getFieldDef()?.name; + * }, + * }), + * ); + * + * fieldName; // => 'greeting' + * ``` + */ getFieldDef(): Maybe> { if (this._fieldDefStack.length > 0) { return this._fieldDefStack[this._fieldDefStack.length - 1]; } } + /** + * Returns the default value for the current input position. + * @returns The current default value, if one is available. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * reviews(limit: Int = 10): [String] + * } + * `); + * const typeInfo = new TypeInfo(schema); + * let defaultLimit; + * + * visit( + * parse('{ reviews(limit: 5) }'), + * visitWithTypeInfo(typeInfo, { + * Argument: () => { + * defaultLimit = typeInfo.getDefaultValue(); + * }, + * }), + * ); + * + * defaultLimit; // => 10 + * ``` + */ getDefaultValue(): Maybe { if (this._defaultValueStack.length > 0) { return this._defaultValueStack[this._defaultValueStack.length - 1]; } } + /** + * Returns the current directive definition. + * @returns The current directive definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const typeInfo = new TypeInfo(schema); + * let directiveName; + * + * visit( + * parse('{ greeting @include(if: true) }'), + * visitWithTypeInfo(typeInfo, { + * Directive: () => { + * directiveName = typeInfo.getDirective()?.name; + * }, + * }), + * ); + * + * directiveName; // => 'include' + * ``` + */ getDirective(): Maybe { return this._directive; } + /** + * Returns the current argument definition. + * @returns The current argument definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * reviews(limit: Int = 10): [String] + * } + * `); + * const typeInfo = new TypeInfo(schema); + * let argumentName; + * + * visit( + * parse('{ reviews(limit: 5) }'), + * visitWithTypeInfo(typeInfo, { + * Argument: () => { + * argumentName = typeInfo.getArgument()?.name; + * }, + * }), + * ); + * + * argumentName; // => 'limit' + * ``` + */ getArgument(): Maybe { return this._argument; } + /** + * Returns the current enum value definition. + * @returns The current enum value definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * enum Sort { + * NEWEST + * OLDEST + * } + * + * type Query { + * reviews(sort: Sort = NEWEST): [String] + * } + * `); + * const typeInfo = new TypeInfo(schema); + * let enumValueName; + * + * visit( + * parse('{ reviews(sort: OLDEST) }'), + * visitWithTypeInfo(typeInfo, { + * EnumValue: () => { + * enumValueName = typeInfo.getEnumValue()?.name; + * }, + * }), + * ); + * + * enumValueName; // => 'OLDEST' + * ``` + */ getEnumValue(): Maybe { return this._enumValue; } + /** + * Updates this TypeInfo instance for an entered AST node. + * @param node - AST node being entered. + * @returns Nothing. + * @example + * ```ts + * import { Kind, parse } from 'graphql/language'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * const operation = document.definitions[0]; + * const selectionSet = operation.selectionSet; + * const field = selectionSet.selections[0]; + * const typeInfo = new TypeInfo(schema); + * + * typeInfo.enter(operation); + * typeInfo.enter(selectionSet); + * typeInfo.enter(field); + * + * field.kind; // => Kind.FIELD + * typeInfo.getParentType()?.name; // => 'Query' + * String(typeInfo.getType()); // => 'String' + * ``` + */ enter(node: ASTNode) { const schema = this._schema; // Note: many of the types below are explicitly typed as "unknown" to drop @@ -252,6 +632,35 @@ export class TypeInfo { } } + /** + * Updates this TypeInfo instance for a left AST node. + * @param node - AST node being entered. + * @returns Nothing. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * const operation = document.definitions[0]; + * const selectionSet = operation.selectionSet; + * const field = selectionSet.selections[0]; + * const typeInfo = new TypeInfo(schema); + * + * typeInfo.enter(operation); + * typeInfo.enter(selectionSet); + * typeInfo.enter(field); + * String(typeInfo.getType()); // => 'String' + * + * typeInfo.leave(field); + * typeInfo.getType(); // => undefined + * ``` + */ leave(node: ASTNode) { switch (node.kind) { case Kind.SELECTION_SET: @@ -301,6 +710,8 @@ type GetFieldDefFn = ( * Not exactly the same as the executor's definition of getFieldDef, in this * statically evaluated environment we do not always have an Object type, * and need to handle Interface and Union types. + * + * @internal */ function getFieldDef( schema: GraphQLSchema, @@ -328,6 +739,37 @@ function getFieldDef( /** * Creates a new visitor instance which maintains a provided TypeInfo instance * along with visiting visitor. + * @param typeInfo - TypeInfo instance to update during traversal. + * @param visitor - Visitor callbacks to wrap with TypeInfo updates. + * @returns A visitor that keeps TypeInfo in sync while delegating callbacks. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const typeInfo = new TypeInfo(schema); + * const fields = []; + * + * visit( + * parse('{ greeting }'), + * visitWithTypeInfo(typeInfo, { + * Field: (node) => { + * fields.push({ + * name: node.name.value, + * parentType: String(typeInfo.getParentType()), + * type: String(typeInfo.getType()), + * }); + * }, + * }), + * ); + * + * fields; // => [{ name: 'greeting', parentType: 'Query', type: 'String' }] + * ``` */ export function visitWithTypeInfo( typeInfo: TypeInfo, diff --git a/src/utilities/assertValidName.ts b/src/utilities/assertValidName.ts index 3e66461ae6..3894b778b3 100644 --- a/src/utilities/assertValidName.ts +++ b/src/utilities/assertValidName.ts @@ -1,3 +1,5 @@ +/** @category Validation */ + import { devAssert } from '../jsutils/devAssert'; import { GraphQLError } from '../error/GraphQLError'; @@ -6,7 +8,18 @@ import { assertName } from '../type/assertName'; /* c8 ignore start */ /** - * Upholds the spec rules about naming. + * Upholds the spec rules about naming. This helper is retained for backwards + * compatibility; call `assertName` instead because assertValidName will be + * removed in v17. + * @param name - The GraphQL name to validate. + * @returns The validated GraphQL name. + * @example + * ```ts + * import { assertValidName } from 'graphql/utilities'; + * + * assertValidName('User'); // => 'User' + * assertValidName('__typename'); // throws an error + * ``` * @deprecated Please use `assertName` instead. Will be removed in v17 */ export function assertValidName(name: string): string { @@ -18,7 +31,20 @@ export function assertValidName(name: string): string { } /** - * Returns an Error if a name is invalid. + * Returns an Error if a name is invalid. This helper is retained for backwards + * compatibility; call `assertName` and catch the thrown GraphQLError instead + * because isValidNameError will be removed in v17. + * @param name - The GraphQL name to validate. + * @returns A GraphQLError if the name is invalid; otherwise undefined. + * @example + * ```ts + * import { isValidNameError } from 'graphql/utilities'; + * + * isValidNameError('User'); // => undefined + * + * const error = isValidNameError('__typename'); + * error.message; // => 'Name "__typename" must not begin with "__", which is reserved by GraphQL introspection.' + * ``` * @deprecated Please use `assertName` instead. Will be removed in v17 */ export function isValidNameError(name: string): GraphQLError | undefined { diff --git a/src/utilities/astFromValue.ts b/src/utilities/astFromValue.ts index 1a880449c8..53461d9d5f 100644 --- a/src/utilities/astFromValue.ts +++ b/src/utilities/astFromValue.ts @@ -1,3 +1,5 @@ +/** @category Values */ + import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { isIterableObject } from '../jsutils/isIterableObject'; @@ -36,7 +38,38 @@ import { GraphQLID } from '../type/scalars'; * | Number | Int / Float | * | Unknown | Enum Value | * | null | NullValue | + * @param value - Runtime value to convert. + * @param type - The GraphQL type to inspect. + * @returns A GraphQL value AST for the provided JavaScript value, or null when no literal can represent it. + * @example + * ```ts + * import { print } from 'graphql/language'; + * import { + * GraphQLInputObjectType, + * GraphQLInt, + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * } from 'graphql/type'; + * import { astFromValue } from 'graphql/utilities'; + * + * const ReviewInput = new GraphQLInputObjectType({ + * name: 'ReviewInput', + * fields: { + * stars: { type: new GraphQLNonNull(GraphQLInt) }, + * tags: { type: new GraphQLList(GraphQLString) }, + * }, + * }); * + * const valueNode = astFromValue( + * { stars: 5, tags: ['featured', 'verified'] }, + * ReviewInput, + * ); + * + * print(valueNode); // => '{ stars: 5, tags: ["featured", "verified"] }' + * astFromValue(undefined, GraphQLString); // => null + * astFromValue(null, new GraphQLNonNull(GraphQLString)); // => null + * ``` */ export function astFromValue( value: unknown, @@ -146,5 +179,7 @@ export function astFromValue( * IntValue: * - NegativeSign? 0 * - NegativeSign? NonZeroDigit ( Digit+ )? + * + * @internal */ const integerStringRegExp = /^-?(?:0|[1-9][0-9]*)$/; diff --git a/src/utilities/buildASTSchema.ts b/src/utilities/buildASTSchema.ts index 64494fbcd0..824db4cdea 100644 --- a/src/utilities/buildASTSchema.ts +++ b/src/utilities/buildASTSchema.ts @@ -1,3 +1,5 @@ +/** @category Schema Construction */ + import { devAssert } from '../jsutils/devAssert'; import type { DocumentNode } from '../language/ast'; @@ -14,6 +16,7 @@ import { assertValidSDL } from '../validation/validate'; import { extendSchemaImpl } from './extendSchema'; +/** Options used when building a schema from SDL or a parsed SDL document. */ export interface BuildSchemaOptions extends GraphQLSchemaValidationOptions { /** * Set to true to assume the SDL is valid. @@ -24,14 +27,41 @@ export interface BuildSchemaOptions extends GraphQLSchemaValidationOptions { } /** - * This takes the ast of a schema document produced by the parse function in - * src/language/parser.js. + * Builds a GraphQLSchema from a parsed schema definition language document. * * If no schema definition is provided, then it will look for types named Query, * Mutation and Subscription. * - * Given that AST it constructs a GraphQLSchema. The resulting schema - * has no resolve methods, so execution will use default resolvers. + * The resulting schema has no resolver functions, so execution will use the + * default field resolver. + * @param documentAST - The parsed GraphQL document AST. + * @param options - Optional configuration for this operation. + * @returns The schema built from the provided SDL document. + * @example + * ```ts + * // Build a schema from a valid parsed SDL document. + * import { parse } from 'graphql/language'; + * import { buildASTSchema } from 'graphql/utilities'; + * + * const document = parse('type Query { hello: String }'); + * const schema = buildASTSchema(document); + * + * schema.getQueryType().name; // => 'Query' + * ``` + * @example + * ```ts + * // This variant uses validation options when the SDL references unknown types. + * import { parse } from 'graphql/language'; + * import { buildASTSchema } from 'graphql/utilities'; + * + * const document = parse('type Query { broken: MissingType }'); + * + * buildASTSchema(document); // throws an error + * buildASTSchema(document, { + * assumeValid: true, + * assumeValidSDL: true, + * }); // does not throw + * ``` */ export function buildASTSchema( documentAST: DocumentNode, @@ -92,8 +122,38 @@ export function buildASTSchema( } /** - * A helper function to build a GraphQLSchema directly from a source - * document. + * Builds a GraphQLSchema directly from a schema definition language source. + * @param source - The GraphQL source text or source object. + * @param options - Optional configuration for this operation. + * @returns The schema built from the provided SDL document. + * @example + * ```ts + * // Build a schema from SDL source using the default options. + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema('type Query { hello: String }'); + * + * schema.getQueryType().name; // => 'Query' + * ``` + * @example + * ```ts + * // This variant enables parser options and omits source locations. + * import { buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema( + * 'directive @tag on FIELD_DEFINITION\n' + + * 'directive @compose @tag on FIELD_DEFINITION', + * { + * experimentalDirectivesOnDirectiveDefinitions: true, + * noLocation: true, + * }, + * ); + * + * const directive = schema.getDirective('compose'); + * + * directive.name; // => 'compose' + * directive.astNode.loc; // => undefined + * ``` */ export function buildSchema( source: string | Source, diff --git a/src/utilities/buildClientSchema.ts b/src/utilities/buildClientSchema.ts index 0c0566beda..09372fcdb0 100644 --- a/src/utilities/buildClientSchema.ts +++ b/src/utilities/buildClientSchema.ts @@ -1,3 +1,5 @@ +/** @category Introspection */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { isObjectLike } from '../jsutils/isObjectLike'; @@ -60,6 +62,20 @@ import { valueFromAST } from './valueFromAST'; * * This function expects a complete introspection result. Don't forget to check * the "errors" field of a server response before calling this function. + * @param introspection - Introspection result data to build from. + * @param options - Optional configuration for this operation. + * @returns The client schema represented by the introspection result. + * @example + * ```ts + * import { buildClientSchema, introspectionFromSchema, buildSchema } from 'graphql/utilities'; + * + * const schema = buildSchema('type Query { hello: String }'); + * const clientSchema = buildClientSchema(introspectionFromSchema(schema), { + * assumeValid: true, + * }); + * + * clientSchema.getQueryType().name; // => 'Query' + * ``` */ export function buildClientSchema( introspection: IntrospectionQuery, diff --git a/src/utilities/coerceInputValue.ts b/src/utilities/coerceInputValue.ts index 0c2020f4d2..13115cb390 100644 --- a/src/utilities/coerceInputValue.ts +++ b/src/utilities/coerceInputValue.ts @@ -1,3 +1,5 @@ +/** @category Values */ + import { didYouMean } from '../jsutils/didYouMean'; import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; @@ -26,6 +28,51 @@ type OnErrorCB = ( /** * Coerces a JavaScript value given a GraphQL Input Type. + * @param inputValue - JavaScript value to coerce. + * @param type - GraphQL input type to coerce the value against. + * @param onError - Callback invoked for each coercion error. + * @returns Coerced value, or undefined if coercion failed and errors were reported. + * @example + * ```ts + * // Coerce runtime input values and throw on invalid input by default. + * import { + * GraphQLInputObjectType, + * GraphQLInt, + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * } from 'graphql/type'; + * import { coerceInputValue } from 'graphql/utilities'; + * + * const ReviewInput = new GraphQLInputObjectType({ + * name: 'ReviewInput', + * fields: { + * stars: { type: new GraphQLNonNull(GraphQLInt) }, + * tags: { type: new GraphQLList(GraphQLString) }, + * }, + * }); + * + * coerceInputValue({ stars: '5', tags: ['featured'] }, ReviewInput); // => { stars: 5, tags: ['featured'] } + * coerceInputValue({ stars: 'bad' }, ReviewInput); // throws an error + * ``` + * @example + * ```ts + * // This variant collects coercion errors with a custom onError callback. + * import { GraphQLInt, GraphQLNonNull } from 'graphql/type'; + * import { coerceInputValue } from 'graphql/utilities'; + * + * const errors = []; + * const value = coerceInputValue( + * null, + * new GraphQLNonNull(GraphQLInt), + * (path, invalidValue, error) => { + * errors.push({ path, invalidValue, message: error.message }); + * }, + * ); + * + * value; // => undefined + * errors; // => [ { path: [], invalidValue: null, message: 'Expected non-nullable type "Int!" not to be null.' } ] + * ``` */ export function coerceInputValue( inputValue: unknown, diff --git a/src/utilities/concatAST.ts b/src/utilities/concatAST.ts index 33062f610e..4f2f9bb37e 100644 --- a/src/utilities/concatAST.ts +++ b/src/utilities/concatAST.ts @@ -1,3 +1,5 @@ +/** @category AST Utilities */ + import type { DefinitionNode, DocumentNode } from '../language/ast'; import { Kind } from '../language/kinds'; @@ -5,6 +7,17 @@ import { Kind } from '../language/kinds'; * Provided a collection of ASTs, presumably each from different files, * concatenate the ASTs together into batched AST, useful for validating many * GraphQL source files which together represent one conceptual application. + * @param documents - Document ASTs to concatenate. + * @returns A document AST containing all definitions from the provided documents. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { concatAST } from 'graphql/utilities'; + * + * const document = concatAST([parse('type Query { a: String }'), parse('type User { id: ID }')]); + * + * document.definitions.length; // => 2 + * ``` */ export function concatAST( documents: ReadonlyArray, diff --git a/src/utilities/extendSchema.ts b/src/utilities/extendSchema.ts index 9dc9fc77ee..748bef7fc7 100644 --- a/src/utilities/extendSchema.ts +++ b/src/utilities/extendSchema.ts @@ -1,3 +1,5 @@ +/** @category Schema Construction */ + import { devAssert } from '../jsutils/devAssert'; import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; @@ -104,6 +106,60 @@ interface Options extends GraphQLSchemaValidationOptions { * * This algorithm copies the provided schema, applying extensions while * producing the copy. The original schema remains unaltered. + * @param schema - GraphQL schema to use. + * @param documentAST - The parsed GraphQL document AST. + * @param options - Optional configuration for this operation. + * @returns A new schema with the extensions and definitions applied. + * @example + * ```ts + * // Extend a schema with new fields and types. + * import { parse } from 'graphql/language'; + * import { buildSchema, extendSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const extensionAST = parse(` + * extend type Query { + * farewell: String + * } + * + * type Review { + * body: String + * } + * `); + * + * const extendedSchema = extendSchema(schema, extensionAST); + * + * schema.getType('Review'); // => undefined + * extendedSchema.getType('Review')?.name; // => 'Review' + * Object.keys(extendedSchema.getQueryType().getFields()); // => ['greeting', 'farewell'] + * ``` + * @example + * ```ts + * // This variant bypasses validation for an otherwise invalid extension. + * import { parse } from 'graphql/language'; + * import { buildSchema, extendSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const invalidExtension = parse(` + * extend type Missing { + * field: String + * } + * `); + * + * extendSchema(schema, invalidExtension); // throws an error + * extendSchema(schema, invalidExtension, { + * assumeValid: true, + * assumeValidSDL: true, + * }); // does not throw + * ``` */ export function extendSchema( schema: GraphQLSchema, @@ -128,9 +184,7 @@ export function extendSchema( : new GraphQLSchema(extendedConfig); } -/** - * @internal - */ +/** @internal */ export function extendSchemaImpl( schemaConfig: GraphQLSchemaNormalizedConfig, documentAST: DocumentNode, @@ -707,6 +761,8 @@ const stdTypeMap = keyMap( /** * Given a field or enum value node, returns the string value for the * deprecation reason. + * + * @internal */ function getDeprecationReason( node: @@ -723,6 +779,8 @@ function getDeprecationReason( /** * Given a scalar node, returns the string value for the specifiedByURL. + * + * @internal */ function getSpecifiedByURL( node: ScalarTypeDefinitionNode | ScalarTypeExtensionNode, @@ -734,6 +792,8 @@ function getSpecifiedByURL( /** * Given an input object node, returns if the node should be OneOf. + * + * @internal */ function isOneOf(node: InputObjectTypeDefinitionNode): boolean { return Boolean(getDirectiveValues(GraphQLOneOfDirective, node)); diff --git a/src/utilities/findBreakingChanges.ts b/src/utilities/findBreakingChanges.ts index 2489af9d62..8efb1458e5 100644 --- a/src/utilities/findBreakingChanges.ts +++ b/src/utilities/findBreakingChanges.ts @@ -1,3 +1,5 @@ +/** @category Schema Changes */ + import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { keyMap } from '../jsutils/keyMap'; @@ -34,49 +36,111 @@ import type { GraphQLSchema } from '../type/schema'; import { astFromValue } from './astFromValue'; import { sortValueNode } from './sortValueNode'; +/** Categories of schema changes that may break existing operations. */ enum BreakingChangeType { + /** Breaking change code for type removed. */ TYPE_REMOVED = 'TYPE_REMOVED', + /** Breaking change code for type changed kind. */ TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND', + /** Breaking change code for type removed from union. */ TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION', + /** Breaking change code for value removed from enum. */ VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM', + /** Breaking change code for required input field added. */ REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED', + /** Breaking change code for implemented interface removed. */ IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED', + /** Breaking change code for field removed. */ FIELD_REMOVED = 'FIELD_REMOVED', + /** Breaking change code for field changed kind. */ FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND', + /** Breaking change code for required arg added. */ REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED', + /** Breaking change code for arg removed. */ ARG_REMOVED = 'ARG_REMOVED', + /** Breaking change code for arg changed kind. */ ARG_CHANGED_KIND = 'ARG_CHANGED_KIND', + /** Breaking change code for directive removed. */ DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED', + /** Breaking change code for directive arg removed. */ DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED', + /** Breaking change code for required directive arg added. */ REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED', + /** Breaking change code for directive repeatable removed. */ DIRECTIVE_REPEATABLE_REMOVED = 'DIRECTIVE_REPEATABLE_REMOVED', + /** Breaking change code for directive location removed. */ DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED', } export { BreakingChangeType }; +/** Categories of schema changes that may be dangerous for existing operations. */ enum DangerousChangeType { + /** Dangerous change code for value added to enum. */ VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM', + /** Dangerous change code for type added to union. */ TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION', + /** Dangerous change code for optional input field added. */ OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED', + /** Dangerous change code for optional arg added. */ OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED', + /** Dangerous change code for implemented interface added. */ IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED', + /** Dangerous change code for arg default value change. */ ARG_DEFAULT_VALUE_CHANGE = 'ARG_DEFAULT_VALUE_CHANGE', } export { DangerousChangeType }; +/** Description of a schema change that may break existing operations. */ export interface BreakingChange { + /** Specific kind of breaking schema change. */ type: BreakingChangeType; + /** Human-readable description of the breaking schema change. */ description: string; } +/** Description of a schema change that may be dangerous for existing operations. */ export interface DangerousChange { + /** Specific kind of dangerous schema change. */ type: DangerousChangeType; + /** Human-readable description of the dangerous schema change. */ description: string; } /** * Given two schemas, returns an Array containing descriptions of all the types * of breaking changes covered by the other functions down below. + * @param oldSchema - Schema before the change. + * @param newSchema - Schema after the change. + * @returns Breaking changes between the two schemas. + * @example + * ```ts + * import { buildSchema, findBreakingChanges } from 'graphql/utilities'; + * + * const oldSchema = buildSchema(` + * type User { + * id: ID! + * name: String + * } + * + * type Query { + * viewer: User + * } + * `); + * const newSchema = buildSchema(` + * type User { + * id: ID! + * } + * + * type Query { + * viewer: User + * } + * `); + * + * const changes = findBreakingChanges(oldSchema, newSchema); + * + * changes[0].type; // => 'FIELD_REMOVED' + * changes[0].description; // matches /User.name was removed/ + * ``` */ export function findBreakingChanges( oldSchema: GraphQLSchema, @@ -91,6 +155,38 @@ export function findBreakingChanges( /** * Given two schemas, returns an Array containing descriptions of all the types * of potentially dangerous changes covered by the other functions down below. + * @param oldSchema - Schema before the change. + * @param newSchema - Schema after the change. + * @returns Dangerous changes between the two schemas. + * @example + * ```ts + * import { buildSchema, findDangerousChanges } from 'graphql/utilities'; + * + * const oldSchema = buildSchema(` + * enum Episode { + * NEW_HOPE + * } + * + * type Query { + * episode: Episode + * } + * `); + * const newSchema = buildSchema(` + * enum Episode { + * NEW_HOPE + * EMPIRE + * } + * + * type Query { + * episode: Episode + * } + * `); + * + * const changes = findDangerousChanges(oldSchema, newSchema); + * + * changes[0].type; // => 'VALUE_ADDED_TO_ENUM' + * changes[0].description; // matches /EMPIRE was added/ + * ``` */ export function findDangerousChanges( oldSchema: GraphQLSchema, diff --git a/src/utilities/getIntrospectionQuery.ts b/src/utilities/getIntrospectionQuery.ts index 761b50c7ef..cec950c733 100644 --- a/src/utilities/getIntrospectionQuery.ts +++ b/src/utilities/getIntrospectionQuery.ts @@ -1,7 +1,10 @@ +/** @category Introspection */ + import type { Maybe } from '../jsutils/Maybe'; import type { DirectiveLocation } from '../language/directiveLocation'; +/** Options controlling which fields are included in the introspection query. */ export interface IntrospectionOptions { /** * Whether to include descriptions in the introspection result. @@ -59,6 +62,42 @@ export interface IntrospectionOptions { /** * Produce the GraphQL query recommended for a full schema introspection. * Accepts optional IntrospectionOptions. + * @param options - Optional configuration for this operation. + * @returns The resolved introspection query. + * @example + * ```ts + * // Generate the default introspection query. + * import { getIntrospectionQuery } from 'graphql/utilities'; + * + * const query = getIntrospectionQuery(); + * + * query; // matches /__schema/ + * query; // matches /description/ + * query; // does not match /specifiedByURL/ + * ``` + * @example + * ```ts + * // This variant customizes optional introspection fields and nesting depth. + * import { getIntrospectionQuery } from 'graphql/utilities'; + * + * const query = getIntrospectionQuery({ + * descriptions: false, + * specifiedByUrl: true, + * directiveIsRepeatable: true, + * schemaDescription: true, + * inputValueDeprecation: true, + * experimentalDirectiveDeprecation: true, + * oneOf: true, + * typeDepth: 3, + * }); + * + * query; // does not match /description/ + * query; // matches /specifiedByURL/ + * query; // matches /isRepeatable/ + * query; // matches /includeDeprecated: true/ + * query; // matches /isOneOf/ + * (query.match(/ofType/g)?.length ?? 0) > 0; // => true + * ``` */ export function getIntrospectionQuery(options?: IntrospectionOptions): string { const optionsWithDefault = { @@ -184,23 +223,33 @@ ${indent}}`; `; } +/** The result shape returned by a full introspection query. */ export interface IntrospectionQuery { + /** The schema. */ readonly __schema: IntrospectionSchema; } +/** The introspection representation of a GraphQL schema. */ export interface IntrospectionSchema { + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** The root object type used for query operations. */ readonly queryType: IntrospectionNamedTypeRef; + /** The root object type used for mutation operations, if supported. */ readonly mutationType: Maybe< IntrospectionNamedTypeRef >; + /** The root object type used for subscription operations, if supported. */ readonly subscriptionType: Maybe< IntrospectionNamedTypeRef >; + /** Object types that belong to this union type. */ readonly types: ReadonlyArray; + /** Directives available in this schema or applied to this AST node. */ readonly directives: ReadonlyArray; } +/** Any introspection representation of a GraphQL type. */ export type IntrospectionType = | IntrospectionScalarType | IntrospectionObjectType @@ -209,6 +258,7 @@ export type IntrospectionType = | IntrospectionEnumType | IntrospectionInputObjectType; +/** An introspection type that can appear in output position. */ export type IntrospectionOutputType = | IntrospectionScalarType | IntrospectionObjectType @@ -216,79 +266,127 @@ export type IntrospectionOutputType = | IntrospectionUnionType | IntrospectionEnumType; +/** An introspection type that can appear in input position. */ export type IntrospectionInputType = | IntrospectionScalarType | IntrospectionEnumType | IntrospectionInputObjectType; +/** The introspection representation of a scalar type. */ export interface IntrospectionScalarType { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'SCALAR'; + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** URL identifying the behavior specified for this custom scalar. */ readonly specifiedByURL?: Maybe; } +/** The introspection representation of an object type. */ export interface IntrospectionObjectType { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'OBJECT'; + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields: ReadonlyArray; + /** Interfaces implemented by this object or interface type. */ readonly interfaces: ReadonlyArray< IntrospectionNamedTypeRef >; } +/** The introspection representation of an interface type. */ export interface IntrospectionInterfaceType { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'INTERFACE'; + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Fields declared by this object, interface, input object, or literal. */ readonly fields: ReadonlyArray; + /** Interfaces implemented by this object or interface type. */ readonly interfaces: ReadonlyArray< IntrospectionNamedTypeRef >; + /** Object types that may be returned for this abstract type. */ readonly possibleTypes: ReadonlyArray< IntrospectionNamedTypeRef >; } +/** The introspection representation of a union type. */ export interface IntrospectionUnionType { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'UNION'; + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Object types that may be returned for this abstract type. */ readonly possibleTypes: ReadonlyArray< IntrospectionNamedTypeRef >; } +/** The introspection representation of an enum type. */ export interface IntrospectionEnumType { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'ENUM'; + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Values declared by this enum type. */ readonly enumValues: ReadonlyArray; } +/** The introspection representation of an input object type. */ export interface IntrospectionInputObjectType { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'INPUT_OBJECT'; + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Input fields declared by this input object type. */ readonly inputFields: ReadonlyArray; + /** Whether this input object uses the experimental OneOf input object semantics. */ readonly isOneOf: boolean; } +/** + * The introspection representation of a list type reference. + * @typeParam T - The introspection type reference wrapped by this list type reference. + */ export interface IntrospectionListTypeRef< T extends IntrospectionTypeRef = IntrospectionTypeRef, > { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'LIST'; + /** The type wrapped by this list or non-null type. */ readonly ofType: T; } +/** + * The introspection representation of a non-null type reference. + * @typeParam T - The introspection type reference wrapped by this non-null type reference. + */ export interface IntrospectionNonNullTypeRef< T extends IntrospectionTypeRef = IntrospectionTypeRef, > { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: 'NON_NULL'; + /** The type wrapped by this list or non-null type. */ readonly ofType: T; } +/** Any introspection representation of a type reference. */ export type IntrospectionTypeRef = | IntrospectionNamedTypeRef | IntrospectionListTypeRef @@ -296,6 +394,7 @@ export type IntrospectionTypeRef = IntrospectionNamedTypeRef | IntrospectionListTypeRef >; +/** An introspection type reference that can appear in output position. */ export type IntrospectionOutputTypeRef = | IntrospectionNamedTypeRef | IntrospectionListTypeRef @@ -304,6 +403,7 @@ export type IntrospectionOutputTypeRef = | IntrospectionListTypeRef >; +/** An introspection type reference that can appear in input position. */ export type IntrospectionInputTypeRef = | IntrospectionNamedTypeRef | IntrospectionListTypeRef @@ -312,44 +412,77 @@ export type IntrospectionInputTypeRef = | IntrospectionListTypeRef >; +/** + * The introspection representation of a named type reference. + * @typeParam T - The introspection type represented by this named type reference. + */ export interface IntrospectionNamedTypeRef< T extends IntrospectionType = IntrospectionType, > { + /** The introspection kind discriminator for this type reference or type. */ readonly kind: T['kind']; + /** The GraphQL name for this schema element. */ readonly name: string; } +/** The introspection representation of a field. */ export interface IntrospectionField { + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Arguments accepted by this field or directive. */ readonly args: ReadonlyArray; + /** The GraphQL type reference or runtime type for this element. */ readonly type: IntrospectionOutputTypeRef; + /** Whether this field, argument, enum value, or input value is deprecated. */ readonly isDeprecated: boolean; + /** Reason this element is deprecated, if one was provided. */ readonly deprecationReason: Maybe; } +/** The introspection representation of an argument or input field. */ export interface IntrospectionInputValue { + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** The GraphQL type reference or runtime type for this element. */ readonly type: IntrospectionInputTypeRef; + /** Default value used when no explicit value is supplied. */ readonly defaultValue: Maybe; + /** Whether this field, argument, enum value, or input value is deprecated. */ readonly isDeprecated?: boolean; + /** Reason this element is deprecated, if one was provided. */ readonly deprecationReason?: Maybe; } +/** The introspection representation of an enum value. */ export interface IntrospectionEnumValue { + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Whether this field, argument, enum value, or input value is deprecated. */ readonly isDeprecated: boolean; + /** Reason this element is deprecated, if one was provided. */ readonly deprecationReason: Maybe; } +/** The introspection representation of a directive. */ export interface IntrospectionDirective { + /** The GraphQL name for this schema element. */ readonly name: string; + /** Human-readable description for this schema element, if provided. */ readonly description?: Maybe; + /** Whether this directive may appear more than once at the same location. */ readonly isRepeatable?: boolean; + /** Whether this field, argument, enum value, or input value is deprecated. */ readonly isDeprecated?: boolean; + /** Reason this element is deprecated, if one was provided. */ readonly deprecationReason?: Maybe; + /** Locations where this directive may be applied. */ readonly locations: ReadonlyArray; + /** Arguments accepted by this field or directive. */ readonly args: ReadonlyArray; } diff --git a/src/utilities/getOperationAST.ts b/src/utilities/getOperationAST.ts index 8decf943f6..b495bb3a3e 100644 --- a/src/utilities/getOperationAST.ts +++ b/src/utilities/getOperationAST.ts @@ -1,3 +1,5 @@ +/** @category Operations */ + import type { Maybe } from '../jsutils/Maybe'; import type { DocumentNode, OperationDefinitionNode } from '../language/ast'; @@ -7,6 +9,20 @@ import { Kind } from '../language/kinds'; * Returns an operation AST given a document AST and optionally an operation * name. If a name is not provided, an operation is only returned if only one is * provided in the document. + * @param documentAST - The parsed GraphQL document AST. + * @param operationName - The optional operation name to select. + * @returns The resolved operation ast. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { getOperationAST } from 'graphql/utilities'; + * + * const document = parse('query GetName { name }'); + * const operation = getOperationAST(document, 'GetName'); + * + * operation.name.value; // => 'GetName' + * getOperationAST(document, 'Missing'); // => undefined + * ``` */ export function getOperationAST( documentAST: DocumentNode, diff --git a/src/utilities/getOperationRootType.ts b/src/utilities/getOperationRootType.ts index db20a793a8..e9088a65be 100644 --- a/src/utilities/getOperationRootType.ts +++ b/src/utilities/getOperationRootType.ts @@ -1,3 +1,5 @@ +/** @category Operations */ + import { GraphQLError } from '../error/GraphQLError'; import type { @@ -9,8 +11,23 @@ import type { GraphQLObjectType } from '../type/definition'; import type { GraphQLSchema } from '../type/schema'; /** - * Extracts the root type of the operation from the schema. + * Extracts the root type of the operation from the schema. This helper is + * retained for backwards compatibility; call `GraphQLSchema.getRootType` instead + * because getOperationRootType will be removed in v17. + * @param schema - GraphQL schema to use. + * @param operation - The operation definition to inspect. + * @returns The resolved operation root type. + * @example + * ```ts + * import { buildSchema, getOperationRootType } from 'graphql/utilities'; + * import { parse } from 'graphql/language'; + * + * const schema = buildSchema('type Query { name: String }'); + * const operation = parse('{ name }').definitions[0]; + * const rootType = getOperationRootType(schema, operation); * + * rootType.name; // => 'Query' + * ``` * @deprecated Please use `GraphQLSchema.getRootType` instead. Will be removed in v17 */ export function getOperationRootType( diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 90f08fc225..0b72d9947e 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -1,3 +1,11 @@ +/** + * Utilities for building schemas, working with introspection, transforming ASTs, + * and comparing GraphQL types. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + */ + // Produce the GraphQL query recommended for a full schema introspection. export { getIntrospectionQuery } from './getIntrospectionQuery'; diff --git a/src/utilities/introspectionFromSchema.ts b/src/utilities/introspectionFromSchema.ts index e6b4a50db1..f2a117d0f7 100644 --- a/src/utilities/introspectionFromSchema.ts +++ b/src/utilities/introspectionFromSchema.ts @@ -1,3 +1,5 @@ +/** @category Introspection */ + import { invariant } from '../jsutils/invariant'; import { parse } from '../language/parser'; @@ -20,6 +22,59 @@ import { getIntrospectionQuery } from './getIntrospectionQuery'; * * This is the inverse of buildClientSchema. The primary use case is outside * of the server context, for instance when doing schema comparisons. + * @param schema - GraphQL schema to use. + * @param options - Optional configuration for this operation. + * @returns Introspection result data for the schema. + * @example + * ```ts + * // Include schema metadata using the default introspection options. + * import { buildSchema, introspectionFromSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * scalar Url @specifiedBy(url: "https://url.spec.whatwg.org/") + * + * type Query { + * homepage: Url + * } + * `); + * + * const introspection = introspectionFromSchema(schema); + * const urlType = introspection.__schema.types.find((type) => type.name === 'Url'); + * + * urlType.specifiedByURL; // => 'https://url.spec.whatwg.org/' + * ``` + * @example + * ```ts + * // This variant disables optional introspection metadata. + * import { buildSchema, introspectionFromSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * scalar Url @specifiedBy(url: "https://url.spec.whatwg.org/") + * + * type Query { + * homepage: Url + * } + * `); + * + * const introspection = introspectionFromSchema(schema, { + * descriptions: false, + * specifiedByUrl: false, + * directiveIsRepeatable: false, + * schemaDescription: false, + * inputValueDeprecation: false, + * experimentalDirectiveDeprecation: false, + * oneOf: false, + * }); + * const urlType = introspection.__schema.types.find((type) => type.name === 'Url'); + * const deprecatedDirective = introspection.__schema.directives.find( + * (directive) => directive.name === 'deprecated', + * ); + * + * urlType.specifiedByURL; // => undefined + * urlType.description; // => undefined + * introspection.__schema.description; // => undefined + * deprecatedDirective.isRepeatable; // => undefined + * ``` */ export function introspectionFromSchema( schema: GraphQLSchema, diff --git a/src/utilities/lexicographicSortSchema.ts b/src/utilities/lexicographicSortSchema.ts index 26b6908c9f..5ceb9abc78 100644 --- a/src/utilities/lexicographicSortSchema.ts +++ b/src/utilities/lexicographicSortSchema.ts @@ -1,3 +1,5 @@ +/** @category Schema Construction */ + import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { keyValMap } from '../jsutils/keyValMap'; @@ -37,6 +39,40 @@ import { GraphQLSchema } from '../type/schema'; * Sort GraphQLSchema. * * This function returns a sorted copy of the given GraphQLSchema. + * @param schema - GraphQL schema to use. + * @returns A copy of the schema with types, fields, arguments, and values sorted lexicographically. + * @example + * ```ts + * import { buildSchema, lexicographicSortSchema, printSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * zebra: String + * apple: String + * } + * + * enum Episode { + * JEDI + * NEW_HOPE + * EMPIRE + * } + * `); + * + * const sortedSchema = lexicographicSortSchema(schema); + * + * printSchema(sortedSchema); + * // => + * // enum Episode { + * // EMPIRE + * // JEDI + * // NEW_HOPE + * // } + * // + * // type Query { + * // apple: String + * // zebra: String + * // } + * ``` */ export function lexicographicSortSchema(schema: GraphQLSchema): GraphQLSchema { const schemaConfig = schema.toConfig(); diff --git a/src/utilities/printSchema.ts b/src/utilities/printSchema.ts index 6285c8630d..92cdc6d12d 100644 --- a/src/utilities/printSchema.ts +++ b/src/utilities/printSchema.ts @@ -1,3 +1,5 @@ +/** @category Schema Printing */ + import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import type { Maybe } from '../jsutils/Maybe'; @@ -36,6 +38,25 @@ import type { GraphQLSchema } from '../type/schema'; import { astFromValue } from './astFromValue'; +/** + * Prints the schema. + * @param schema - GraphQL schema to use. + * @returns The printed string representation. + * @example + * ```ts + * import { buildSchema, printSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * directive @upper on FIELD_DEFINITION + * + * type Query { + * greeting: String @upper + * } + * `); + * + * printSchema(schema); // => ['directive @upper on FIELD_DEFINITION', '', 'type Query {', ' greeting: String', '}'].join('\n') + * ``` + */ export function printSchema(schema: GraphQLSchema): string { return printFilteredSchema( schema, @@ -44,6 +65,27 @@ export function printSchema(schema: GraphQLSchema): string { ); } +/** + * Prints the introspection schema. + * @param schema - GraphQL schema to use. + * @returns The printed string representation. + * @example + * ```ts + * import { buildSchema, printIntrospectionSchema } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * const printed = printIntrospectionSchema(schema); + * + * printed; // matches /type __Schema/ + * printed; // matches /enum __TypeKind/ + * printed; // does not match /type Query/ + * ``` + */ export function printIntrospectionSchema(schema: GraphQLSchema): string { return printFilteredSchema(schema, isSpecifiedDirective, isIntrospectionType); } @@ -108,6 +150,8 @@ function printSchemaDefinition(schema: GraphQLSchema): Maybe { * ``` * * When using this naming convention, the schema description can be omitted. + * + * @internal */ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { const queryType = schema.getQueryType(); @@ -128,6 +172,28 @@ function isSchemaOfCommonNames(schema: GraphQLSchema): boolean { return true; } +/** + * Prints the type. + * @param type - The GraphQL type to inspect. + * @returns The printed string representation. + * @example + * ```ts + * import { buildSchema, printType } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type User { + * id: ID! + * name: String + * } + * + * type Query { + * viewer: User + * } + * `); + * + * printType(schema.getType('User')); // => ['type User {', ' id: ID!', ' name: String', '}'].join('\n') + * ``` + */ export function printType(type: GraphQLNamedType): string { if (isScalarType(type)) { return printScalar(type); diff --git a/src/utilities/resolveSchemaCoordinate.ts b/src/utilities/resolveSchemaCoordinate.ts index 49aeb6d3b5..4a6d063409 100644 --- a/src/utilities/resolveSchemaCoordinate.ts +++ b/src/utilities/resolveSchemaCoordinate.ts @@ -1,3 +1,5 @@ +/** @category Schema Coordinates */ + import { inspect } from '../jsutils/inspect'; import type { @@ -34,30 +36,36 @@ import type { GraphQLSchema } from '../type/schema'; /** * A resolved schema element may be one of the following kinds: + * + * @internal */ export interface ResolvedNamedType { readonly kind: 'NamedType'; readonly type: GraphQLNamedType; } +/** @internal */ export interface ResolvedField { readonly kind: 'Field'; readonly type: GraphQLObjectType | GraphQLInterfaceType; readonly field: GraphQLField; } +/** @internal */ export interface ResolvedInputField { readonly kind: 'InputField'; readonly type: GraphQLInputObjectType; readonly inputField: GraphQLInputField; } +/** @internal */ export interface ResolvedEnumValue { readonly kind: 'EnumValue'; readonly type: GraphQLEnumType; readonly enumValue: GraphQLEnumValue; } +/** @internal */ export interface ResolvedFieldArgument { readonly kind: 'FieldArgument'; readonly type: GraphQLObjectType | GraphQLInterfaceType; @@ -65,17 +73,20 @@ export interface ResolvedFieldArgument { readonly fieldArgument: GraphQLArgument; } +/** @internal */ export interface ResolvedDirective { readonly kind: 'Directive'; readonly directive: GraphQLDirective; } +/** @internal */ export interface ResolvedDirectiveArgument { readonly kind: 'DirectiveArgument'; readonly directive: GraphQLDirective; readonly directiveArgument: GraphQLArgument; } +/** A schema element resolved from a schema coordinate. */ export type ResolvedSchemaElement = | ResolvedNamedType | ResolvedField @@ -93,6 +104,38 @@ export type ResolvedSchemaElement = * applicable) does not exist. * * https://spec.graphql.org/draft/#sec-Schema-Coordinates.Semantics + * @param schema - GraphQL schema to use. + * @param schemaCoordinate - The schema coordinate to resolve. + * @returns The schema element identified by the coordinate, or undefined if none exists. + * @example + * ```ts + * import { buildSchema, resolveSchemaCoordinate } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * directive @tag(name: String!) on FIELD_DEFINITION + * + * input ReviewInput { + * stars: Int! + * } + * + * enum Episode { + * NEW_HOPE + * } + * + * type Query { + * reviews(input: ReviewInput): [String] @tag(name: "reviews") + * } + * `); + * + * resolveSchemaCoordinate(schema, 'Query').kind; // => 'NamedType' + * resolveSchemaCoordinate(schema, 'Query.reviews').kind; // => 'Field' + * resolveSchemaCoordinate(schema, 'Query.reviews(input:)').kind; // => 'FieldArgument' + * resolveSchemaCoordinate(schema, 'ReviewInput.stars').kind; // => 'InputField' + * resolveSchemaCoordinate(schema, 'Episode.NEW_HOPE').kind; // => 'EnumValue' + * resolveSchemaCoordinate(schema, '@tag').kind; // => 'Directive' + * resolveSchemaCoordinate(schema, '@tag(name:)').kind; // => 'DirectiveArgument' + * resolveSchemaCoordinate(schema, 'Query.missing'); // => undefined + * ``` */ export function resolveSchemaCoordinate( schema: GraphQLSchema, @@ -106,6 +149,8 @@ export function resolveSchemaCoordinate( /** * TypeCoordinate : Name + * + * @internal */ function resolveTypeCoordinate( schema: GraphQLSchema, @@ -125,6 +170,8 @@ function resolveTypeCoordinate( /** * MemberCoordinate : Name . Name + * + * @internal */ function resolveMemberCoordinate( schema: GraphQLSchema, @@ -197,6 +244,8 @@ function resolveMemberCoordinate( /** * ArgumentCoordinate : Name . Name ( Name : ) + * + * @internal */ function resolveArgumentCoordinate( schema: GraphQLSchema, @@ -249,6 +298,8 @@ function resolveArgumentCoordinate( /** * DirectiveCoordinate : \@ Name + * + * @internal */ function resolveDirectiveCoordinate( schema: GraphQLSchema, @@ -268,6 +319,8 @@ function resolveDirectiveCoordinate( /** * DirectiveArgumentCoordinate : \@ Name ( Name : ) + * + * @internal */ function resolveDirectiveArgumentCoordinate( schema: GraphQLSchema, @@ -305,6 +358,26 @@ function resolveDirectiveArgumentCoordinate( /** * Resolves schema coordinate from a parsed SchemaCoordinate node. + * @param schema - GraphQL schema to use. + * @param schemaCoordinate - The schema coordinate to resolve. + * @returns The schema element identified by the parsed coordinate, or undefined if none exists. + * @example + * ```ts + * import { parseSchemaCoordinate } from 'graphql/language'; + * import { buildSchema, resolveASTSchemaCoordinate } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * greeting(name: String): String + * } + * `); + * const coordinate = parseSchemaCoordinate('Query.greeting(name:)'); + * const resolved = resolveASTSchemaCoordinate(schema, coordinate); + * + * resolved.kind; // => 'FieldArgument' + * resolved.field.name; // => 'greeting' + * resolved.fieldArgument.name; // => 'name' + * ``` */ export function resolveASTSchemaCoordinate( schema: GraphQLSchema, diff --git a/src/utilities/separateOperations.ts b/src/utilities/separateOperations.ts index 84a8b774f9..385e4821c5 100644 --- a/src/utilities/separateOperations.ts +++ b/src/utilities/separateOperations.ts @@ -1,3 +1,5 @@ +/** @category AST Utilities */ + import type { ObjMap } from '../jsutils/ObjMap'; import type { @@ -13,6 +15,35 @@ import { visit } from '../language/visitor'; * operations and fragments and returns a collection of AST documents each of * which contains a single operation as well the fragment definitions it * refers to. + * @param documentAST - The parsed GraphQL document AST. + * @returns A map of operation names to documents containing each operation and its referenced fragments. + * @example + * ```ts + * import { parse, print } from 'graphql/language'; + * import { separateOperations } from 'graphql/utilities'; + * + * const document = parse(` + * query GetUser { + * viewer { + * ...UserFields + * } + * } + * + * query GetStatus { + * status + * } + * + * fragment UserFields on User { + * id + * } + * `); + * + * const separated = separateOperations(document); + * + * Object.keys(separated); // => ['GetUser', 'GetStatus'] + * print(separated.GetUser); // matches /fragment UserFields/ + * print(separated.GetStatus); // does not match /fragment UserFields/ + * ``` */ export function separateOperations( documentAST: DocumentNode, diff --git a/src/utilities/stripIgnoredCharacters.ts b/src/utilities/stripIgnoredCharacters.ts index 5eb5c9800c..0011a16053 100644 --- a/src/utilities/stripIgnoredCharacters.ts +++ b/src/utilities/stripIgnoredCharacters.ts @@ -1,3 +1,5 @@ +/** @category AST Utilities */ + import { printBlockString } from '../language/blockString'; import { isPunctuatorTokenKind, Lexer } from '../language/lexer'; import { isSource, Source } from '../language/source'; @@ -62,6 +64,16 @@ import { TokenKind } from '../language/tokenKind'; * ```graphql * """Type description""" type Foo{"""Field description""" bar:String} * ``` + * @param source - The GraphQL source text or source object. + * @returns A semantically equivalent GraphQL source string without ignored characters. + * @example + * ```ts + * import { stripIgnoredCharacters } from 'graphql/utilities'; + * + * const source = stripIgnoredCharacters('query Example { name }'); + * + * source; // => 'query Example{name}' + * ``` */ export function stripIgnoredCharacters(source: string | Source): string { const sourceObj = isSource(source) ? source : new Source(source); @@ -79,6 +91,8 @@ export function stripIgnoredCharacters(source: string | Source): string { * Every two non-punctuator tokens should have space between them. * Also prevent case of non-punctuator token following by spread resulting * in invalid token (e.g. `1...` is invalid Float token). + * + * @internal */ const isNonPunctuator = !isPunctuatorTokenKind(currentToken.kind); if (wasLastAddedTokenNonPunctuator) { diff --git a/src/utilities/typeComparators.ts b/src/utilities/typeComparators.ts index 287be40bfe..f4935b8e1f 100644 --- a/src/utilities/typeComparators.ts +++ b/src/utilities/typeComparators.ts @@ -1,3 +1,5 @@ +/** @category Type Comparisons */ + import type { GraphQLCompositeType, GraphQLType } from '../type/definition'; import { isAbstractType, @@ -10,6 +12,22 @@ import type { GraphQLSchema } from '../type/schema'; /** * Provided two types, return true if the types are equal (invariant). + * @param typeA - The first GraphQL type to compare. + * @param typeB - The second GraphQL type to compare. + * @returns True when both types are equal. + * @example + * ```ts + * import { + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * } from 'graphql/type'; + * import { isEqualType } from 'graphql/utilities'; + * + * isEqualType(GraphQLString, GraphQLString); // => true + * isEqualType(new GraphQLList(GraphQLString), new GraphQLList(GraphQLString)); // => true + * isEqualType(new GraphQLNonNull(GraphQLString), GraphQLString); // => false + * ``` */ export function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean { // Equivalent types are equal. @@ -34,6 +52,40 @@ export function isEqualType(typeA: GraphQLType, typeB: GraphQLType): boolean { /** * Provided a type and a super type, return true if the first type is either * equal or a subset of the second super type (covariant). + * @param schema - GraphQL schema to use. + * @param maybeSubType - The possible subtype to compare. + * @param superType - The possible supertype to compare. + * @returns True when `maybeSubType` is equal to or a subtype of `superType`. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { + * GraphQLNonNull, + * assertInterfaceType, + * assertObjectType, + * } from 'graphql/type'; + * import { isTypeSubTypeOf } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * interface Node { + * id: ID! + * } + * + * type User implements Node { + * id: ID! + * } + * + * type Query { + * node: Node + * } + * `); + * const Node = assertInterfaceType(schema.getType('Node')); + * const User = assertObjectType(schema.getType('User')); + * + * isTypeSubTypeOf(schema, User, Node); // => true + * isTypeSubTypeOf(schema, new GraphQLNonNull(User), Node); // => true + * isTypeSubTypeOf(schema, Node, User); // => false + * ``` */ export function isTypeSubTypeOf( schema: GraphQLSchema, @@ -86,6 +138,39 @@ export function isTypeSubTypeOf( * be visited in a context of another type. * * This function is commutative. + * @param schema - GraphQL schema to use. + * @param typeA - The first GraphQL type to compare. + * @param typeB - The second GraphQL type to compare. + * @returns True when the two composite types can apply to at least one common object type. + * @example + * ```ts + * import { buildSchema } from 'graphql/utilities'; + * import { assertObjectType, assertUnionType } from 'graphql/type'; + * import { doTypesOverlap } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Photo { + * url: String! + * } + * + * type Video { + * url: String! + * } + * + * union Media = Photo | Video + * union StillImage = Photo + * + * type Query { + * media: [Media] + * } + * `); + * const Media = assertUnionType(schema.getType('Media')); + * const StillImage = assertUnionType(schema.getType('StillImage')); + * const Video = assertObjectType(schema.getType('Video')); + * + * doTypesOverlap(schema, Media, StillImage); // => true + * doTypesOverlap(schema, StillImage, Video); // => false + * ``` */ export function doTypesOverlap( schema: GraphQLSchema, diff --git a/src/utilities/typeFromAST.ts b/src/utilities/typeFromAST.ts index 7510df1046..774c071cae 100644 --- a/src/utilities/typeFromAST.ts +++ b/src/utilities/typeFromAST.ts @@ -1,3 +1,5 @@ +/** @category Values */ + import type { ListTypeNode, NamedTypeNode, @@ -16,23 +18,109 @@ import type { GraphQLSchema } from '../type/schema'; * AST node for `[User]`, a GraphQLList instance will be returned, containing * the type called "User" found in the schema. If a type called "User" is not * found in the schema, then undefined will be returned. + * @param schema - GraphQL schema to use. + * @param typeNode - The GraphQL type AST node to resolve. + * @returns The GraphQL type referenced by the AST node, or undefined if it cannot be resolved. + * @example + * ```ts + * import { parseType } from 'graphql/language'; + * import { buildSchema, typeFromAST } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * typeFromAST(schema, parseType('String'))?.toString(); // => 'String' + * typeFromAST(schema, parseType('Missing')); // => undefined + * ``` */ export function typeFromAST( schema: GraphQLSchema, typeNode: NamedTypeNode, ): GraphQLNamedType | undefined; +/** + * Resolves a list type AST node against a schema. + * @param schema - GraphQL schema to use. + * @param typeNode - The list type AST node to resolve. + * @returns The GraphQL list type referenced by the AST node, or undefined if + * it cannot be resolved. + * @example + * ```ts + * import { parseType } from 'graphql/language'; + * import { buildSchema, typeFromAST } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * tags: [String] + * } + * `); + * + * typeFromAST(schema, parseType('[String]'))?.toString(); // => '[String]' + * typeFromAST(schema, parseType('[Missing]')); // => undefined + * ``` + */ export function typeFromAST( schema: GraphQLSchema, typeNode: ListTypeNode, ): GraphQLList | undefined; +/** + * Resolves a non-null type AST node against a schema. + * @param schema - GraphQL schema to use. + * @param typeNode - The non-null type AST node to resolve. + * @returns The GraphQL non-null type referenced by the AST node, or undefined + * if it cannot be resolved. + * @example + * ```ts + * import { parseType } from 'graphql/language'; + * import { buildSchema, typeFromAST } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type Query { + * name: String! + * } + * `); + * + * typeFromAST(schema, parseType('String!'))?.toString(); // => 'String!' + * typeFromAST(schema, parseType('[String!]!'))?.toString(); // => '[String!]!' + * ``` + */ export function typeFromAST( schema: GraphQLSchema, typeNode: NonNullTypeNode, ): GraphQLNonNull | undefined; +/** + * Resolves a type AST node against a schema. + * @param schema - GraphQL schema to use. + * @param typeNode - The GraphQL type AST node to resolve. + * @returns The GraphQL type referenced by the AST node, or undefined if it + * cannot be resolved. + * @example + * ```ts + * import { parseType } from 'graphql/language'; + * import { buildSchema, typeFromAST } from 'graphql/utilities'; + * + * const schema = buildSchema(` + * type User { + * name: String + * } + * + * type Query { + * users: [User!]! + * } + * `); + * + * typeFromAST(schema, parseType('User'))?.toString(); // => 'User' + * typeFromAST(schema, parseType('[User!]!'))?.toString(); // => '[User!]!' + * typeFromAST(schema, parseType('Missing')); // => undefined + * ``` + */ export function typeFromAST( schema: GraphQLSchema, typeNode: TypeNode, ): GraphQLType | undefined; +/** @internal */ export function typeFromAST( schema: GraphQLSchema, typeNode: TypeNode, diff --git a/src/utilities/typedQueryDocumentNode.ts b/src/utilities/typedQueryDocumentNode.ts index 1bd5cf0825..a8b8052750 100644 --- a/src/utilities/typedQueryDocumentNode.ts +++ b/src/utilities/typedQueryDocumentNode.ts @@ -1,11 +1,16 @@ +/** @category Typed Documents */ + import type { DocumentNode, ExecutableDefinitionNode } from '../language/ast'; /** * Wrapper type that contains DocumentNode and types that can be deduced from it. + * @typeParam TResponseData - Typed GraphQL response data shape. + * @typeParam TRequestVariables - Typed GraphQL request variables shape. */ export interface TypedQueryDocumentNode< TResponseData = { [key: string]: any }, TRequestVariables = { [key: string]: any }, > extends DocumentNode { + /** Top-level executable and type-system definitions in this document. */ readonly definitions: ReadonlyArray; // FIXME: remove once TS implements proper way to enforce nominal typing /** diff --git a/src/utilities/valueFromAST.ts b/src/utilities/valueFromAST.ts index 686f568b58..2d43a14d19 100644 --- a/src/utilities/valueFromAST.ts +++ b/src/utilities/valueFromAST.ts @@ -1,3 +1,5 @@ +/** @category Values */ + import { inspect } from '../jsutils/inspect'; import { invariant } from '../jsutils/invariant'; import { keyMap } from '../jsutils/keyMap'; @@ -33,7 +35,44 @@ import { * | Int / Float | Number | * | Enum Value | Unknown | * | NullValue | null | + * @param valueNode - GraphQL value AST node to convert. + * @param type - The GraphQL type to inspect. + * @param variables - Optional runtime variable values keyed by variable name. + * @returns The coerced JavaScript value, or undefined if the AST value cannot be coerced to the type. + * @example + * ```ts + * // Coerce literal values without variables. + * import { parseValue } from 'graphql/language'; + * import { + * GraphQLInputObjectType, + * GraphQLInt, + * GraphQLList, + * GraphQLNonNull, + * GraphQLString, + * } from 'graphql/type'; + * import { valueFromAST } from 'graphql/utilities'; + * + * const ReviewInput = new GraphQLInputObjectType({ + * name: 'ReviewInput', + * fields: { + * stars: { type: new GraphQLNonNull(GraphQLInt) }, + * tags: { type: new GraphQLList(GraphQLString) }, + * }, + * }); + * + * valueFromAST(parseValue('{ stars: 5, tags: ["featured"] }'), ReviewInput); // => { stars: 5, tags: ['featured'] } + * valueFromAST(parseValue('{ stars: "bad" }'), ReviewInput); // => undefined + * ``` + * @example + * ```ts + * // This variant resolves variable references from runtime values. + * import { parseValue } from 'graphql/language'; + * import { GraphQLInt } from 'graphql/type'; + * import { valueFromAST } from 'graphql/utilities'; * + * valueFromAST(parseValue('$stars'), GraphQLInt, { stars: 5 }); // => 5 + * valueFromAST(parseValue('$stars'), GraphQLInt, {}); // => undefined + * ``` */ export function valueFromAST( valueNode: Maybe, diff --git a/src/utilities/valueFromASTUntyped.ts b/src/utilities/valueFromASTUntyped.ts index 05540da3a4..258bfdffa7 100644 --- a/src/utilities/valueFromASTUntyped.ts +++ b/src/utilities/valueFromASTUntyped.ts @@ -1,3 +1,5 @@ +/** @category Values */ + import { keyValMap } from '../jsutils/keyValMap'; import type { Maybe } from '../jsutils/Maybe'; import type { ObjMap } from '../jsutils/ObjMap'; @@ -8,8 +10,8 @@ import { Kind } from '../language/kinds'; /** * Produces a JavaScript value given a GraphQL Value AST. * - * Unlike `valueFromAST()`, no type is provided. The resulting JavaScript value - * will reflect the provided GraphQL value AST. + * Because no GraphQL type is provided, the returned JavaScript value reflects + * the provided GraphQL value AST. * * | GraphQL Value | JavaScript Value | * | -------------------- | ---------------- | @@ -19,7 +21,19 @@ import { Kind } from '../language/kinds'; * | String / Enum | String | * | Int / Float | Number | * | Null | null | + * @param valueNode - GraphQL value AST node to convert. + * @param variables - Optional runtime variable values keyed by variable name. + * @returns JavaScript value represented by the GraphQL value AST. + * @example + * ```ts + * import { parseValue } from 'graphql/language'; + * import { valueFromASTUntyped } from 'graphql/utilities'; + * + * const value = valueFromASTUntyped(parseValue('[1, 2, 3]')); * + * value; // => [1, 2, 3] + * valueFromASTUntyped(parseValue('$name'), { name: 'Ada' }); // => 'Ada' + * ``` */ export function valueFromASTUntyped( valueNode: ValueNode, diff --git a/src/validation/ValidationContext.ts b/src/validation/ValidationContext.ts index 7884031c9d..45f497c01c 100644 --- a/src/validation/ValidationContext.ts +++ b/src/validation/ValidationContext.ts @@ -1,3 +1,5 @@ +/** @category Validation Context */ + import type { Maybe } from '../jsutils/Maybe'; import type { ObjMap } from '../jsutils/ObjMap'; @@ -40,6 +42,8 @@ interface VariableUsage { * An instance of this class is passed as the "this" context to all validators, * allowing access to commonly useful contextual information from within a * validation rule. + * + * @internal */ export class ASTValidationContext { private _ast: DocumentNode; @@ -137,8 +141,10 @@ export class ASTValidationContext { } } +/** @internal */ export type ASTValidationRule = (context: ASTValidationContext) => ASTVisitor; +/** @internal */ export class SDLValidationContext extends ASTValidationContext { private _schema: Maybe; @@ -160,8 +166,10 @@ export class SDLValidationContext extends ASTValidationContext { } } +/** @internal */ export type SDLValidationRule = (context: SDLValidationContext) => ASTVisitor; +/** Validation context passed to query validation rules. */ export class ValidationContext extends ASTValidationContext { private _schema: GraphQLSchema; private _typeInfo: TypeInfo; @@ -175,6 +183,39 @@ export class ValidationContext extends ASTValidationContext { ReadonlyArray >; + /** + * Creates a ValidationContext instance. + * @param schema - Schema used to validate the document. + * @param ast - Document AST being validated. + * @param typeInfo - TypeInfo instance used to track traversal state. + * @param onError - Callback invoked for each validation error. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { GraphQLError } from 'graphql/error'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * const errors = []; + * const context = new ValidationContext( + * schema, + * document, + * new TypeInfo(schema), + * (error) => errors.push(error), + * ); + * + * context.reportError(new GraphQLError('Example validation error.')); + * + * context.getSchema(); // => schema + * errors[0].message; // => 'Example validation error.' + * ``` + */ constructor( schema: GraphQLSchema, ast: DocumentNode, @@ -188,14 +229,72 @@ export class ValidationContext extends ASTValidationContext { this._recursiveVariableUsages = new Map(); } + /** + * Returns the value used by `Object.prototype.toString`. + * @returns The built-in string tag for this object. + */ get [Symbol.toStringTag]() { return 'ValidationContext'; } + /** + * Returns the schema being used by this validation context. + * @returns The schema being validated against. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const context = new ValidationContext( + * schema, + * parse('{ greeting }'), + * new TypeInfo(schema), + * () => {}, + * ); + * + * context.getSchema().getQueryType()?.name; // => 'Query' + * ``` + */ getSchema(): GraphQLSchema { return this._schema; } + /** + * Returns variable usages found directly within this node. + * @param node - The AST node to inspect or visit. + * @returns Variable usages found directly within this node. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting(name: String): String + * } + * `); + * const document = parse('query ($name: String) { greeting(name: $name) }'); + * const operation = document.definitions[0]; + * const context = new ValidationContext( + * schema, + * document, + * new TypeInfo(schema), + * () => {}, + * ); + * + * const usages = context.getVariableUsages(operation); + * + * usages[0].node.name.value; // => 'name' + * String(usages[0].type); // => 'String' + * ``` + */ getVariableUsages(node: NodeWithSelectionSet): ReadonlyArray { let usages = this._variableUsages.get(node); if (!usages) { @@ -221,6 +320,49 @@ export class ValidationContext extends ASTValidationContext { return usages; } + /** + * Returns variable usages for an operation, including variables used by referenced fragments. + * @param operation - Operation definition to inspect. + * @returns Variable usages reachable from the operation. + * @example + * ```ts + * import { parse } from 'graphql/language'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * viewer: User + * } + * + * type User { + * name(prefix: String): String + * } + * `); + * const document = parse(` + * query ($prefix: String) { + * viewer { + * ...UserName + * } + * } + * + * fragment UserName on User { + * name(prefix: $prefix) + * } + * `); + * const operation = document.definitions[0]; + * const context = new ValidationContext( + * schema, + * document, + * new TypeInfo(schema), + * () => {}, + * ); + * + * const usages = context.getRecursiveVariableUsages(operation); + * + * usages.map((usage) => usage.node.name.value); // => ['prefix'] + * ``` + */ getRecursiveVariableUsages( operation: OperationDefinitionNode, ): ReadonlyArray { @@ -235,37 +377,295 @@ export class ValidationContext extends ASTValidationContext { return usages; } + /** + * Returns the current output type at this point in traversal. + * @returns The current output type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let typeName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * Field: () => { + * typeName = String(context.getType()); + * }, + * }), + * ); + * + * typeName; // => 'String' + * ``` + */ getType(): Maybe { return this._typeInfo.getType(); } + /** + * Returns the current parent composite type. + * @returns The current parent composite type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let parentTypeName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * Field: () => { + * parentTypeName = context.getParentType()?.name; + * }, + * }), + * ); + * + * parentTypeName; // => 'Query' + * ``` + */ getParentType(): Maybe { return this._typeInfo.getParentType(); } + /** + * Returns the current input type at this point in traversal. + * @returns The current input type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * reviews(limit: Int): [String] + * } + * `); + * const document = parse('{ reviews(limit: 5) }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let inputTypeName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * Argument: () => { + * inputTypeName = String(context.getInputType()); + * }, + * }), + * ); + * + * inputTypeName; // => 'Int' + * ``` + */ getInputType(): Maybe { return this._typeInfo.getInputType(); } + /** + * Returns the parent input type for the current input position. + * @returns The parent input type, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * input ReviewFilter { + * stars: Int + * } + * + * type Query { + * reviews(filter: ReviewFilter): [String] + * } + * `); + * const document = parse('{ reviews(filter: { stars: 5 }) }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let parentInputTypeName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * ObjectField: () => { + * parentInputTypeName = String(context.getParentInputType()); + * }, + * }), + * ); + * + * parentInputTypeName; // => 'ReviewFilter' + * ``` + */ getParentInputType(): Maybe { return this._typeInfo.getParentInputType(); } + /** + * Returns the current field definition. + * @returns The current field definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let fieldName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * Field: () => { + * fieldName = context.getFieldDef()?.name; + * }, + * }), + * ); + * + * fieldName; // => 'greeting' + * ``` + */ getFieldDef(): Maybe> { return this._typeInfo.getFieldDef(); } + /** + * Returns the current directive definition. + * @returns The current directive definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ greeting @include(if: true) }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let directiveName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * Directive: () => { + * directiveName = context.getDirective()?.name; + * }, + * }), + * ); + * + * directiveName; // => 'include' + * ``` + */ getDirective(): Maybe { return this._typeInfo.getDirective(); } + /** + * Returns the current argument definition. + * @returns The current argument definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * reviews(limit: Int): [String] + * } + * `); + * const document = parse('{ reviews(limit: 5) }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let argumentName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * Argument: () => { + * argumentName = context.getArgument()?.name; + * }, + * }), + * ); + * + * argumentName; // => 'limit' + * ``` + */ getArgument(): Maybe { return this._typeInfo.getArgument(); } + /** + * Returns the current enum value definition. + * @returns The current enum value definition, if known. + * @example + * ```ts + * import { parse, visit } from 'graphql/language'; + * import { buildSchema, TypeInfo, visitWithTypeInfo } from 'graphql/utilities'; + * import { ValidationContext } from 'graphql/validation'; + * + * const schema = buildSchema(` + * enum Sort { + * NEWEST + * OLDEST + * } + * + * type Query { + * reviews(sort: Sort): [String] + * } + * `); + * const document = parse('{ reviews(sort: OLDEST) }'); + * const typeInfo = new TypeInfo(schema); + * const context = new ValidationContext(schema, document, typeInfo, () => {}); + * let enumValueName; + * + * visit( + * document, + * visitWithTypeInfo(typeInfo, { + * EnumValue: () => { + * enumValueName = context.getEnumValue()?.name; + * }, + * }), + * ); + * + * enumValueName; // => 'OLDEST' + * ``` + */ getEnumValue(): Maybe { return this._typeInfo.getEnumValue(); } } +/** A function that creates an AST visitor for validating a GraphQL document. */ export type ValidationRule = (context: ValidationContext) => ASTVisitor; diff --git a/src/validation/index.ts b/src/validation/index.ts index 587479e351..7a2980e101 100644 --- a/src/validation/index.ts +++ b/src/validation/index.ts @@ -1,3 +1,10 @@ +/** + * Validate GraphQL documents and schemas with the specified validation rules. + * + * These exports are also available from the root `graphql` package. + * @packageDocumentation + */ + export { validate } from './validate'; export { ValidationContext } from './ValidationContext'; diff --git a/src/validation/rules/ExecutableDefinitionsRule.ts b/src/validation/rules/ExecutableDefinitionsRule.ts index 8f82a6797b..80920aee06 100644 --- a/src/validation/rules/ExecutableDefinitionsRule.ts +++ b/src/validation/rules/ExecutableDefinitionsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import { Kind } from '../../language/kinds'; @@ -13,6 +15,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * operation or fragment definitions. * * See https://spec.graphql.org/draft/#sec-Executable-Definitions + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { ExecutableDefinitionsRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * type Extra { field: String } + * `); + * const invalidErrors = validate(schema, invalidDocument, [ExecutableDefinitionsRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name } + * `); + * const validErrors = validate(schema, validDocument, [ExecutableDefinitionsRule]); + * + * validErrors; // => [] + * ``` */ export function ExecutableDefinitionsRule( context: ASTValidationContext, diff --git a/src/validation/rules/FieldsOnCorrectTypeRule.ts b/src/validation/rules/FieldsOnCorrectTypeRule.ts index 9182f9c4a1..cad5323e84 100644 --- a/src/validation/rules/FieldsOnCorrectTypeRule.ts +++ b/src/validation/rules/FieldsOnCorrectTypeRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { didYouMean } from '../../jsutils/didYouMean'; import { naturalCompare } from '../../jsutils/naturalCompare'; import { suggestionList } from '../../jsutils/suggestionList'; @@ -28,6 +30,33 @@ import type { ValidationContext } from '../ValidationContext'; * parent type, or are an allowed meta field such as __typename. * * See https://spec.graphql.org/draft/#sec-Field-Selections + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { FieldsOnCorrectTypeRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { missing } + * `); + * const invalidErrors = validate(schema, invalidDocument, [FieldsOnCorrectTypeRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name } + * `); + * const validErrors = validate(schema, validDocument, [FieldsOnCorrectTypeRule]); + * + * validErrors; // => [] + * ``` */ export function FieldsOnCorrectTypeRule( context: ValidationContext, @@ -71,6 +100,8 @@ export function FieldsOnCorrectTypeRule( * Go through all of the implementations of type, as well as the interfaces that * they implement. If any of those types include the provided field, suggest them, * sorted by how often the type is referenced. + * + * @internal */ function getSuggestedTypeNames( schema: GraphQLSchema, @@ -130,6 +161,8 @@ function getSuggestedTypeNames( /** * For the field name provided, determine if there are any similar field names * that may be the result of a typo. + * + * @internal */ function getSuggestedFieldNames( type: GraphQLOutputType, diff --git a/src/validation/rules/FragmentsOnCompositeTypesRule.ts b/src/validation/rules/FragmentsOnCompositeTypesRule.ts index fb71f63836..0a8732be57 100644 --- a/src/validation/rules/FragmentsOnCompositeTypesRule.ts +++ b/src/validation/rules/FragmentsOnCompositeTypesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import { print } from '../../language/printer'; @@ -17,6 +19,33 @@ import type { ValidationContext } from '../ValidationContext'; * type condition must also be a composite type. * * See https://spec.graphql.org/draft/#sec-Fragments-On-Composite-Types + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { FragmentsOnCompositeTypesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * fragment Bad on String { length } + * `); + * const invalidErrors = validate(schema, invalidDocument, [FragmentsOnCompositeTypesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * fragment Good on Query { name } + * `); + * const validErrors = validate(schema, validDocument, [FragmentsOnCompositeTypesRule]); + * + * validErrors; // => [] + * ``` */ export function FragmentsOnCompositeTypesRule( context: ValidationContext, diff --git a/src/validation/rules/KnownArgumentNamesRule.ts b/src/validation/rules/KnownArgumentNamesRule.ts index 332b21c1ca..b3ee4bbd00 100644 --- a/src/validation/rules/KnownArgumentNamesRule.ts +++ b/src/validation/rules/KnownArgumentNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { didYouMean } from '../../jsutils/didYouMean'; import { suggestionList } from '../../jsutils/suggestionList'; @@ -21,6 +23,33 @@ import type { * * See https://spec.graphql.org/draft/#sec-Argument-Names * See https://spec.graphql.org/draft/#sec-Directives-Are-In-Valid-Locations + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { KnownArgumentNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: String): String + * } + * `); + * + * const invalidDocument = parse(` + * { field(unknown: "1") } + * `); + * const invalidErrors = validate(schema, invalidDocument, [KnownArgumentNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { field(arg: "1") } + * `); + * const validErrors = validate(schema, validDocument, [KnownArgumentNamesRule]); + * + * validErrors; // => [] + * ``` */ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor { return { @@ -47,9 +76,7 @@ export function KnownArgumentNamesRule(context: ValidationContext): ASTVisitor { }; } -/** - * @internal - */ +/** @internal */ export function KnownArgumentNamesOnDirectivesRule( context: ValidationContext | SDLValidationContext, ): ASTVisitor { diff --git a/src/validation/rules/KnownDirectivesRule.ts b/src/validation/rules/KnownDirectivesRule.ts index 162b9e9680..16510e6e86 100644 --- a/src/validation/rules/KnownDirectivesRule.ts +++ b/src/validation/rules/KnownDirectivesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { inspect } from '../../jsutils/inspect'; import { invariant } from '../../jsutils/invariant'; @@ -23,6 +25,33 @@ import type { * schema and legally positioned. * * See https://spec.graphql.org/draft/#sec-Directives-Are-Defined + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { KnownDirectivesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { name @unknown } + * `); + * const invalidErrors = validate(schema, invalidDocument, [KnownDirectivesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name @include(if: true) } + * `); + * const validErrors = validate(schema, validDocument, [KnownDirectivesRule]); + * + * validErrors; // => [] + * ``` */ export function KnownDirectivesRule( context: ValidationContext | SDLValidationContext, diff --git a/src/validation/rules/KnownFragmentNamesRule.ts b/src/validation/rules/KnownFragmentNamesRule.ts index c37403f752..854330da81 100644 --- a/src/validation/rules/KnownFragmentNamesRule.ts +++ b/src/validation/rules/KnownFragmentNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTVisitor } from '../../language/visitor'; @@ -11,6 +13,33 @@ import type { ValidationContext } from '../ValidationContext'; * to fragments defined in the same document. * * See https://spec.graphql.org/draft/#sec-Fragment-spread-target-defined + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { KnownFragmentNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { ...Missing } + * `); + * const invalidErrors = validate(schema, invalidDocument, [KnownFragmentNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * fragment NameFields on Query { name } query { ...NameFields } + * `); + * const validErrors = validate(schema, validDocument, [KnownFragmentNamesRule]); + * + * validErrors; // => [] + * ``` */ export function KnownFragmentNamesRule(context: ValidationContext): ASTVisitor { return { diff --git a/src/validation/rules/KnownTypeNamesRule.ts b/src/validation/rules/KnownTypeNamesRule.ts index fadc080c35..654d3d6569 100644 --- a/src/validation/rules/KnownTypeNamesRule.ts +++ b/src/validation/rules/KnownTypeNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { didYouMean } from '../../jsutils/didYouMean'; import { suggestionList } from '../../jsutils/suggestionList'; @@ -26,6 +28,33 @@ import type { * variable definitions and fragment conditions) are defined by the type schema. * * See https://spec.graphql.org/draft/#sec-Fragment-Spread-Type-Existence + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { KnownTypeNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * fragment Bad on Missing { name } + * `); + * const invalidErrors = validate(schema, invalidDocument, [KnownTypeNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * fragment Good on Query { name } + * `); + * const validErrors = validate(schema, validDocument, [KnownTypeNamesRule]); + * + * validErrors; // => [] + * ``` */ export function KnownTypeNamesRule( context: ValidationContext | SDLValidationContext, diff --git a/src/validation/rules/LoneAnonymousOperationRule.ts b/src/validation/rules/LoneAnonymousOperationRule.ts index 291a494c76..e1fab81f01 100644 --- a/src/validation/rules/LoneAnonymousOperationRule.ts +++ b/src/validation/rules/LoneAnonymousOperationRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import { Kind } from '../../language/kinds'; @@ -12,6 +14,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * (the query short-hand) that it contains only that one operation definition. * * See https://spec.graphql.org/draft/#sec-Lone-Anonymous-Operation + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { LoneAnonymousOperationRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * query { name } query Other { name } + * `); + * const invalidErrors = validate(schema, invalidDocument, [LoneAnonymousOperationRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name } + * `); + * const validErrors = validate(schema, validDocument, [LoneAnonymousOperationRule]); + * + * validErrors; // => [] + * ``` */ export function LoneAnonymousOperationRule( context: ASTValidationContext, diff --git a/src/validation/rules/LoneSchemaDefinitionRule.ts b/src/validation/rules/LoneSchemaDefinitionRule.ts index 4eeb8cdcba..cbacceda02 100644 --- a/src/validation/rules/LoneSchemaDefinitionRule.ts +++ b/src/validation/rules/LoneSchemaDefinitionRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTVisitor } from '../../language/visitor'; @@ -8,6 +10,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Lone Schema definition * * A GraphQL document is only valid if it contains only one schema definition. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { LoneSchemaDefinitionRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * schema { query: Query } schema { query: Query } type Query { name: String } + * `; + * + * LoneSchemaDefinitionRule.name; // => 'LoneSchemaDefinitionRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * schema { query: Query } type Query { name: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function LoneSchemaDefinitionRule( context: SDLValidationContext, diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 0c2dbd3879..1d543987c4 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTNode } from '../../language/ast'; @@ -8,12 +10,44 @@ import type { ASTValidationContext } from '../ValidationContext'; const MAX_LISTS_DEPTH = 3; +/** + * Implements the max introspection depth validation rule. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { MaxIntrospectionDepthRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { __schema { types { fields { type { fields { type { fields { name } } } } } } } } + * `); + * const invalidErrors = validate(schema, invalidDocument, [MaxIntrospectionDepthRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { __schema { queryType { name } } } + * `); + * const validErrors = validate(schema, validDocument, [MaxIntrospectionDepthRule]); + * + * validErrors; // => [] + * ``` + */ export function MaxIntrospectionDepthRule( context: ASTValidationContext, ): ASTVisitor { /** * Counts the depth of list fields in "__Type" recursively and * returns `true` if the limit has been reached. + * + * @internal */ function checkDepth( node: ASTNode, diff --git a/src/validation/rules/NoFragmentCyclesRule.ts b/src/validation/rules/NoFragmentCyclesRule.ts index 448b1cf496..10843dce6b 100644 --- a/src/validation/rules/NoFragmentCyclesRule.ts +++ b/src/validation/rules/NoFragmentCyclesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import type { ObjMap } from '../../jsutils/ObjMap'; import { GraphQLError } from '../../error/GraphQLError'; @@ -17,6 +19,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data. * * See https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { NoFragmentCyclesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * fragment A on Query { ...B } fragment B on Query { ...A } query { ...A } + * `); + * const invalidErrors = validate(schema, invalidDocument, [NoFragmentCyclesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * fragment A on Query { name } query { ...A } + * `); + * const validErrors = validate(schema, validDocument, [NoFragmentCyclesRule]); + * + * validErrors; // => [] + * ``` */ export function NoFragmentCyclesRule( context: ASTValidationContext, diff --git a/src/validation/rules/NoUndefinedVariablesRule.ts b/src/validation/rules/NoUndefinedVariablesRule.ts index 3d499b5dcc..dabfe0ba2f 100644 --- a/src/validation/rules/NoUndefinedVariablesRule.ts +++ b/src/validation/rules/NoUndefinedVariablesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTVisitor } from '../../language/visitor'; @@ -11,6 +13,33 @@ import type { ValidationContext } from '../ValidationContext'; * and via fragment spreads, are defined by that operation. * * See https://spec.graphql.org/draft/#sec-All-Variable-Uses-Defined + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { NoUndefinedVariablesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: ID): String + * } + * `); + * + * const invalidDocument = parse(` + * query ($id: ID) { field(arg: $missing) } + * `); + * const invalidErrors = validate(schema, invalidDocument, [NoUndefinedVariablesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * query ($id: ID) { field(arg: $id) } + * `); + * const validErrors = validate(schema, validDocument, [NoUndefinedVariablesRule]); + * + * validErrors; // => [] + * ``` */ export function NoUndefinedVariablesRule( context: ValidationContext, diff --git a/src/validation/rules/NoUnusedFragmentsRule.ts b/src/validation/rules/NoUnusedFragmentsRule.ts index aebf34535d..3ddea2e8bc 100644 --- a/src/validation/rules/NoUnusedFragmentsRule.ts +++ b/src/validation/rules/NoUnusedFragmentsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { @@ -15,6 +17,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * within operations, or spread within other fragments spread within operations. * * See https://spec.graphql.org/draft/#sec-Fragments-Must-Be-Used + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { NoUnusedFragmentsRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * fragment Unused on Query { name } query { name } + * `); + * const invalidErrors = validate(schema, invalidDocument, [NoUnusedFragmentsRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * fragment Used on Query { name } query { ...Used } + * `); + * const validErrors = validate(schema, validDocument, [NoUnusedFragmentsRule]); + * + * validErrors; // => [] + * ``` */ export function NoUnusedFragmentsRule( context: ASTValidationContext, diff --git a/src/validation/rules/NoUnusedVariablesRule.ts b/src/validation/rules/NoUnusedVariablesRule.ts index 5083af4f28..3501660377 100644 --- a/src/validation/rules/NoUnusedVariablesRule.ts +++ b/src/validation/rules/NoUnusedVariablesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { VariableDefinitionNode } from '../../language/ast'; @@ -12,6 +14,34 @@ import type { ValidationContext } from '../ValidationContext'; * are used, either directly or within a spread fragment. * * See https://spec.graphql.org/draft/#sec-All-Variables-Used + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { NoUnusedVariablesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: ID): String + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * query ($id: ID) { name } + * `); + * const invalidErrors = validate(schema, invalidDocument, [NoUnusedVariablesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * query ($id: ID) { field(arg: $id) } + * `); + * const validErrors = validate(schema, validDocument, [NoUnusedVariablesRule]); + * + * validErrors; // => [] + * ``` */ export function NoUnusedVariablesRule(context: ValidationContext): ASTVisitor { let variableDefs: Array = []; diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index 8397a35b80..0df368b4e2 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { inspect } from '../../jsutils/inspect'; import type { Maybe } from '../../jsutils/Maybe'; import type { ObjMap } from '../../jsutils/ObjMap'; @@ -55,6 +57,38 @@ function reasonMessage(reason: ConflictReasonMessage): string { * without ambiguity. * * See https://spec.graphql.org/draft/#sec-Field-Selection-Merging + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { OverlappingFieldsCanBeMergedRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * dog: Dog + * } + * + * type Dog { + * name: String + * barkVolume: Int + * } + * `); + * + * const invalidDocument = parse(` + * { dog { value: barkVolume value: name } } + * `); + * const invalidErrors = validate(schema, invalidDocument, [OverlappingFieldsCanBeMergedRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { dog { barkVolume name } } + * `); + * const validErrors = validate(schema, validDocument, [OverlappingFieldsCanBeMergedRule]); + * + * validErrors; // => [] + * ``` */ export function OverlappingFieldsCanBeMergedRule( context: ValidationContext, @@ -113,6 +147,10 @@ type FragmentNames = Array; type FieldsAndFragmentNames = readonly [NodeAndDefCollection, FragmentNames]; /** + * Find all conflicts found "within" a selection set, including those found + * via spreading in fragments. Called when visiting each SelectionSet in the + * GraphQL Document. + * * Algorithm: * * Conflicts occur when two fields exist in a query which will produce the same @@ -128,7 +166,7 @@ type FieldsAndFragmentNames = readonly [NodeAndDefCollection, FragmentNames]; * A) Each selection set represented in the document first compares "within" its * collected set of fields, finding any conflicts between every pair of * overlapping fields. - * Note: This is the *only time* that a the fields "within" a set are compared + * Note: This is the *only time* that the fields "within" a set are compared * to each other. After this only fields "between" sets are compared. * * B) Also, if any fragment is referenced in a selection set, then a @@ -140,14 +178,14 @@ type FieldsAndFragmentNames = readonly [NodeAndDefCollection, FragmentNames]; * * D) When comparing "between" a set of fields and a referenced fragment, first * a comparison is made between each field in the original set of fields and - * each field in the the referenced set of fields. + * each field in the referenced set of fields. * * E) Also, if any fragment is referenced in the referenced selection set, * then a comparison is made "between" the original set of fields and the * referenced fragment (recursively referring to step D). * * F) When comparing "between" two fragments, first a comparison is made between - * each field in the first referenced set of fields and each field in the the + * each field in the first referenced set of fields and each field in the * second referenced set of fields. * * G) Also, any fragments referenced by the first must be compared to the @@ -165,11 +203,8 @@ type FieldsAndFragmentNames = readonly [NodeAndDefCollection, FragmentNames]; * J) Also, if two fragments are referenced in both selection sets, then a * comparison is made "between" the two fragments. * + * @internal */ - -// Find all conflicts found "within" a selection set, including those found -// via spreading in fragments. Called when visiting each SelectionSet in the -// GraphQL Document. function findConflictsWithinSelectionSet( context: ValidationContext, cachedFieldsAndFragmentNames: Map, @@ -846,6 +881,8 @@ function subfieldConflicts( * * Provides a third argument for has/set to allow flagging the pair as * weakly or strongly present within the collection. + * + * @internal */ class OrderedPairSet { _data: Map>; @@ -876,6 +913,8 @@ class OrderedPairSet { /** * A way to keep track of pairs of similar things when the ordering of the pair * does not matter. + * + * @internal */ class PairSet { _orderedPairSet: OrderedPairSet; diff --git a/src/validation/rules/PossibleFragmentSpreadsRule.ts b/src/validation/rules/PossibleFragmentSpreadsRule.ts index fe738e5559..7e180926ca 100644 --- a/src/validation/rules/PossibleFragmentSpreadsRule.ts +++ b/src/validation/rules/PossibleFragmentSpreadsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { inspect } from '../../jsutils/inspect'; import type { Maybe } from '../../jsutils/Maybe'; @@ -19,6 +21,41 @@ import type { ValidationContext } from '../ValidationContext'; * A fragment spread is only valid if the type condition could ever possibly * be true: if there is a non-empty intersection of the possible parent types, * and possible types which pass the type condition. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { PossibleFragmentSpreadsRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * dog: Dog + * } + * + * type Dog { + * barkVolume: Int + * } + * + * type Cat { + * meowVolume: Int + * } + * `); + * + * const invalidDocument = parse(` + * { dog { ... on Cat { meowVolume } } } + * `); + * const invalidErrors = validate(schema, invalidDocument, [PossibleFragmentSpreadsRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { dog { ... on Dog { barkVolume } } } + * `); + * const validErrors = validate(schema, validDocument, [PossibleFragmentSpreadsRule]); + * + * validErrors; // => [] + * ``` */ export function PossibleFragmentSpreadsRule( context: ValidationContext, diff --git a/src/validation/rules/PossibleTypeExtensionsRule.ts b/src/validation/rules/PossibleTypeExtensionsRule.ts index 57d16b473f..eace134590 100644 --- a/src/validation/rules/PossibleTypeExtensionsRule.ts +++ b/src/validation/rules/PossibleTypeExtensionsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { didYouMean } from '../../jsutils/didYouMean'; import { inspect } from '../../jsutils/inspect'; import { invariant } from '../../jsutils/invariant'; @@ -27,6 +29,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Possible type extension * * A type extension is only valid if the type is defined and has the same kind. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { PossibleTypeExtensionsRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * extend type Missing { name: String } type Query { name: String } + * `; + * + * PossibleTypeExtensionsRule.name; // => 'PossibleTypeExtensionsRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * type Query { name: String } extend type Query { other: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function PossibleTypeExtensionsRule( context: SDLValidationContext, diff --git a/src/validation/rules/ProvidedRequiredArgumentsRule.ts b/src/validation/rules/ProvidedRequiredArgumentsRule.ts index b111dcee1b..accafd30b0 100644 --- a/src/validation/rules/ProvidedRequiredArgumentsRule.ts +++ b/src/validation/rules/ProvidedRequiredArgumentsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { inspect } from '../../jsutils/inspect'; import { keyMap } from '../../jsutils/keyMap'; import type { ObjMap } from '../../jsutils/ObjMap'; @@ -23,6 +25,33 @@ import type { * * A field or directive is only valid if all required (non-null without a * default value) field arguments have been provided. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { ProvidedRequiredArgumentsRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(required: String!): String + * } + * `); + * + * const invalidDocument = parse(` + * { field } + * `); + * const invalidErrors = validate(schema, invalidDocument, [ProvidedRequiredArgumentsRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { field(required: "x") } + * `); + * const validErrors = validate(schema, validDocument, [ProvidedRequiredArgumentsRule]); + * + * validErrors; // => [] + * ``` */ export function ProvidedRequiredArgumentsRule( context: ValidationContext, @@ -59,9 +88,7 @@ export function ProvidedRequiredArgumentsRule( }; } -/** - * @internal - */ +/** @internal */ export function ProvidedRequiredArgumentsOnDirectivesRule( context: ValidationContext | SDLValidationContext, ): ASTVisitor { diff --git a/src/validation/rules/ScalarLeafsRule.ts b/src/validation/rules/ScalarLeafsRule.ts index 966143c58b..8ff9a7831e 100644 --- a/src/validation/rules/ScalarLeafsRule.ts +++ b/src/validation/rules/ScalarLeafsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { inspect } from '../../jsutils/inspect'; import { GraphQLError } from '../../error/GraphQLError'; @@ -14,6 +16,33 @@ import type { ValidationContext } from '../ValidationContext'; * * A GraphQL document is valid only if all leaf fields (fields without * sub selections) are of scalar or enum types. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { ScalarLeafsRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { name { length } } + * `); + * const invalidErrors = validate(schema, invalidDocument, [ScalarLeafsRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name } + * `); + * const validErrors = validate(schema, validDocument, [ScalarLeafsRule]); + * + * validErrors; // => [] + * ``` */ export function ScalarLeafsRule(context: ValidationContext): ASTVisitor { return { diff --git a/src/validation/rules/SingleFieldSubscriptionsRule.ts b/src/validation/rules/SingleFieldSubscriptionsRule.ts index 21cb1abaf6..0e689a9d59 100644 --- a/src/validation/rules/SingleFieldSubscriptionsRule.ts +++ b/src/validation/rules/SingleFieldSubscriptionsRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import type { ObjMap } from '../../jsutils/ObjMap'; import { GraphQLError } from '../../error/GraphQLError'; @@ -20,6 +22,38 @@ import type { ValidationContext } from '../ValidationContext'; * that root field is not an introspection field. * * See https://spec.graphql.org/draft/#sec-Single-root-field + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { SingleFieldSubscriptionsRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * + * type Subscription { + * a: String + * b: String + * } + * `); + * + * const invalidDocument = parse(` + * subscription { a b } + * `); + * const invalidErrors = validate(schema, invalidDocument, [SingleFieldSubscriptionsRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * subscription { a } + * `); + * const validErrors = validate(schema, validDocument, [SingleFieldSubscriptionsRule]); + * + * validErrors; // => [] + * ``` */ export function SingleFieldSubscriptionsRule( context: ValidationContext, diff --git a/src/validation/rules/UniqueArgumentDefinitionNamesRule.ts b/src/validation/rules/UniqueArgumentDefinitionNamesRule.ts index 2348276338..c71312121f 100644 --- a/src/validation/rules/UniqueArgumentDefinitionNamesRule.ts +++ b/src/validation/rules/UniqueArgumentDefinitionNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { groupBy } from '../../jsutils/groupBy'; import { GraphQLError } from '../../error/GraphQLError'; @@ -16,6 +18,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * * A GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments. * A GraphQL Directive is only valid if all its arguments are uniquely named. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { UniqueArgumentDefinitionNamesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * type Query { field(arg: String, arg: Int): String } + * `; + * + * UniqueArgumentDefinitionNamesRule.name; // => 'UniqueArgumentDefinitionNamesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * type Query { field(arg: String): String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function UniqueArgumentDefinitionNamesRule( context: SDLValidationContext, diff --git a/src/validation/rules/UniqueArgumentNamesRule.ts b/src/validation/rules/UniqueArgumentNamesRule.ts index 19667efaa7..a55ae366c8 100644 --- a/src/validation/rules/UniqueArgumentNamesRule.ts +++ b/src/validation/rules/UniqueArgumentNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { groupBy } from '../../jsutils/groupBy'; import { GraphQLError } from '../../error/GraphQLError'; @@ -14,6 +16,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * uniquely named. * * See https://spec.graphql.org/draft/#sec-Argument-Names + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { UniqueArgumentNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: String): String + * } + * `); + * + * const invalidDocument = parse(` + * { field(arg: "1", arg: "2") } + * `); + * const invalidErrors = validate(schema, invalidDocument, [UniqueArgumentNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { field(arg: "1") } + * `); + * const validErrors = validate(schema, validDocument, [UniqueArgumentNamesRule]); + * + * validErrors; // => [] + * ``` */ export function UniqueArgumentNamesRule( context: ASTValidationContext, diff --git a/src/validation/rules/UniqueDirectiveNamesRule.ts b/src/validation/rules/UniqueDirectiveNamesRule.ts index ade517ddce..1dd5d3591b 100644 --- a/src/validation/rules/UniqueDirectiveNamesRule.ts +++ b/src/validation/rules/UniqueDirectiveNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTVisitor } from '../../language/visitor'; @@ -8,6 +10,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Unique directive names * * A GraphQL document is only valid if all defined directives have unique names. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { UniqueDirectiveNamesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * directive @tag on FIELD directive @tag on QUERY type Query { name: String } + * `; + * + * UniqueDirectiveNamesRule.name; // => 'UniqueDirectiveNamesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * directive @tag on FIELD type Query { name: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function UniqueDirectiveNamesRule( context: SDLValidationContext, diff --git a/src/validation/rules/UniqueDirectivesPerLocationRule.ts b/src/validation/rules/UniqueDirectivesPerLocationRule.ts index 9ceeb8ecb0..535a4940ae 100644 --- a/src/validation/rules/UniqueDirectivesPerLocationRule.ts +++ b/src/validation/rules/UniqueDirectivesPerLocationRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import { Kind } from '../../language/kinds'; @@ -21,6 +23,33 @@ import type { * a given location are uniquely named. * * See https://spec.graphql.org/draft/#sec-Directives-Are-Unique-Per-Location + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { UniqueDirectivesPerLocationRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { name @include(if: true) @include(if: false) } + * `); + * const invalidErrors = validate(schema, invalidDocument, [UniqueDirectivesPerLocationRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name @include(if: true) } + * `); + * const validErrors = validate(schema, validDocument, [UniqueDirectivesPerLocationRule]); + * + * validErrors; // => [] + * ``` */ export function UniqueDirectivesPerLocationRule( context: ValidationContext | SDLValidationContext, diff --git a/src/validation/rules/UniqueEnumValueNamesRule.ts b/src/validation/rules/UniqueEnumValueNamesRule.ts index 2bdf8749a2..b0adf4d9e5 100644 --- a/src/validation/rules/UniqueEnumValueNamesRule.ts +++ b/src/validation/rules/UniqueEnumValueNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { @@ -14,6 +16,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Unique enum value names * * A GraphQL enum type is only valid if all its values are uniquely named. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { UniqueEnumValueNamesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * enum Status { ACTIVE ACTIVE } type Query { status: Status } + * `; + * + * UniqueEnumValueNamesRule.name; // => 'UniqueEnumValueNamesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * enum Status { ACTIVE INACTIVE } type Query { status: Status } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function UniqueEnumValueNamesRule( context: SDLValidationContext, diff --git a/src/validation/rules/UniqueFieldDefinitionNamesRule.ts b/src/validation/rules/UniqueFieldDefinitionNamesRule.ts index 52f6527d64..dc59ddaf16 100644 --- a/src/validation/rules/UniqueFieldDefinitionNamesRule.ts +++ b/src/validation/rules/UniqueFieldDefinitionNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { @@ -20,6 +22,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Unique field definition names * * A GraphQL complex type is only valid if all its fields are uniquely named. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { UniqueFieldDefinitionNamesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * type Query { name: String name: String } + * `; + * + * UniqueFieldDefinitionNamesRule.name; // => 'UniqueFieldDefinitionNamesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * type Query { name: String other: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function UniqueFieldDefinitionNamesRule( context: SDLValidationContext, diff --git a/src/validation/rules/UniqueFragmentNamesRule.ts b/src/validation/rules/UniqueFragmentNamesRule.ts index 3b4311e9c8..e5adcebcf1 100644 --- a/src/validation/rules/UniqueFragmentNamesRule.ts +++ b/src/validation/rules/UniqueFragmentNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTVisitor } from '../../language/visitor'; @@ -10,6 +12,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * A GraphQL document is only valid if all defined fragments have unique names. * * See https://spec.graphql.org/draft/#sec-Fragment-Name-Uniqueness + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { UniqueFragmentNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * fragment A on Query { name } fragment A on Query { name } query { ...A } + * `); + * const invalidErrors = validate(schema, invalidDocument, [UniqueFragmentNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * fragment A on Query { name } query { ...A } + * `); + * const validErrors = validate(schema, validDocument, [UniqueFragmentNamesRule]); + * + * validErrors; // => [] + * ``` */ export function UniqueFragmentNamesRule( context: ASTValidationContext, diff --git a/src/validation/rules/UniqueInputFieldNamesRule.ts b/src/validation/rules/UniqueInputFieldNamesRule.ts index c1916a73b3..b4b20e1874 100644 --- a/src/validation/rules/UniqueInputFieldNamesRule.ts +++ b/src/validation/rules/UniqueInputFieldNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { invariant } from '../../jsutils/invariant'; import type { ObjMap } from '../../jsutils/ObjMap'; @@ -15,6 +17,37 @@ import type { ASTValidationContext } from '../ValidationContext'; * uniquely named. * * See https://spec.graphql.org/draft/#sec-Input-Object-Field-Uniqueness + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { UniqueInputFieldNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * input Filter { + * name: String + * } + * + * type Query { + * search(filter: Filter): String + * } + * `); + * + * const invalidDocument = parse(` + * { search(filter: { name: "a", name: "b" }) } + * `); + * const invalidErrors = validate(schema, invalidDocument, [UniqueInputFieldNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { search(filter: { name: "a" }) } + * `); + * const validErrors = validate(schema, validDocument, [UniqueInputFieldNamesRule]); + * + * validErrors; // => [] + * ``` */ export function UniqueInputFieldNamesRule( context: ASTValidationContext, diff --git a/src/validation/rules/UniqueOperationNamesRule.ts b/src/validation/rules/UniqueOperationNamesRule.ts index 6df98be8c7..2c8409c72c 100644 --- a/src/validation/rules/UniqueOperationNamesRule.ts +++ b/src/validation/rules/UniqueOperationNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { ASTVisitor } from '../../language/visitor'; @@ -10,6 +12,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * A GraphQL document is only valid if all defined operations have unique names. * * See https://spec.graphql.org/draft/#sec-Operation-Name-Uniqueness + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { UniqueOperationNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * query Same { name } query Same { name } + * `); + * const invalidErrors = validate(schema, invalidDocument, [UniqueOperationNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * query One { name } query Two { name } + * `); + * const validErrors = validate(schema, validDocument, [UniqueOperationNamesRule]); + * + * validErrors; // => [] + * ``` */ export function UniqueOperationNamesRule( context: ASTValidationContext, diff --git a/src/validation/rules/UniqueOperationTypesRule.ts b/src/validation/rules/UniqueOperationTypesRule.ts index f8ac6871ec..b36d7b2403 100644 --- a/src/validation/rules/UniqueOperationTypesRule.ts +++ b/src/validation/rules/UniqueOperationTypesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { @@ -12,6 +14,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Unique operation types * * A GraphQL document is only valid if it has only one type per operation. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { UniqueOperationTypesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * schema { query: Query query: Other } type Query { name: String } type Other { name: String } + * `; + * + * UniqueOperationTypesRule.name; // => 'UniqueOperationTypesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * schema { query: Query } type Query { name: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function UniqueOperationTypesRule( context: SDLValidationContext, diff --git a/src/validation/rules/UniqueTypeNamesRule.ts b/src/validation/rules/UniqueTypeNamesRule.ts index a1f6588b11..c66ae043f9 100644 --- a/src/validation/rules/UniqueTypeNamesRule.ts +++ b/src/validation/rules/UniqueTypeNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { TypeDefinitionNode } from '../../language/ast'; @@ -9,6 +11,26 @@ import type { SDLValidationContext } from '../ValidationContext'; * Unique type names * * A GraphQL document is only valid if all defined types have unique names. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema } from 'graphql'; + * import { UniqueTypeNamesRule } from 'graphql/validation'; + * + * const invalidSDL = ` + * type Query { name: String } type Query { other: String } + * `; + * + * UniqueTypeNamesRule.name; // => 'UniqueTypeNamesRule' + * buildSchema(invalidSDL); // throws an error + * + * const validSDL = ` + * type Query { name: String } type Other { name: String } + * `; + * + * buildSchema(validSDL); // does not throw + * ``` */ export function UniqueTypeNamesRule(context: SDLValidationContext): ASTVisitor { const knownTypeNames = Object.create(null); diff --git a/src/validation/rules/UniqueVariableNamesRule.ts b/src/validation/rules/UniqueVariableNamesRule.ts index 3c9f76d885..c90df4c0af 100644 --- a/src/validation/rules/UniqueVariableNamesRule.ts +++ b/src/validation/rules/UniqueVariableNamesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { groupBy } from '../../jsutils/groupBy'; import { GraphQLError } from '../../error/GraphQLError'; @@ -10,6 +12,33 @@ import type { ASTValidationContext } from '../ValidationContext'; * Unique variable names * * A GraphQL operation is only valid if all its variables are uniquely named. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { UniqueVariableNamesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: ID): String + * } + * `); + * + * const invalidDocument = parse(` + * query ($id: ID, $id: ID) { field(arg: $id) } + * `); + * const invalidErrors = validate(schema, invalidDocument, [UniqueVariableNamesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * query ($id: ID) { field(arg: $id) } + * `); + * const validErrors = validate(schema, validDocument, [UniqueVariableNamesRule]); + * + * validErrors; // => [] + * ``` */ export function UniqueVariableNamesRule( context: ASTValidationContext, diff --git a/src/validation/rules/ValuesOfCorrectTypeRule.ts b/src/validation/rules/ValuesOfCorrectTypeRule.ts index 3a7f4f235a..c2a1322f1c 100644 --- a/src/validation/rules/ValuesOfCorrectTypeRule.ts +++ b/src/validation/rules/ValuesOfCorrectTypeRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { didYouMean } from '../../jsutils/didYouMean'; import { inspect } from '../../jsutils/inspect'; import { keyMap } from '../../jsutils/keyMap'; @@ -35,6 +37,33 @@ import type { ValidationContext } from '../ValidationContext'; * expected at their position. * * See https://spec.graphql.org/draft/#sec-Values-of-Correct-Type + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { ValuesOfCorrectTypeRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * count(limit: Int): Int + * } + * `); + * + * const invalidDocument = parse(` + * { count(limit: "many") } + * `); + * const invalidErrors = validate(schema, invalidDocument, [ValuesOfCorrectTypeRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { count(limit: 1) } + * `); + * const validErrors = validate(schema, validDocument, [ValuesOfCorrectTypeRule]); + * + * validErrors; // => [] + * ``` */ export function ValuesOfCorrectTypeRule( context: ValidationContext, @@ -118,6 +147,8 @@ export function ValuesOfCorrectTypeRule( /** * Any value literal may be a valid representation of a Scalar, depending on * that scalar type. + * + * @internal */ function isValidValueNode(context: ValidationContext, node: ValueNode): void { // Report any error at the full type expected by the location. diff --git a/src/validation/rules/VariablesAreInputTypesRule.ts b/src/validation/rules/VariablesAreInputTypesRule.ts index 58d535ce81..5d97c68e00 100644 --- a/src/validation/rules/VariablesAreInputTypesRule.ts +++ b/src/validation/rules/VariablesAreInputTypesRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { GraphQLError } from '../../error/GraphQLError'; import type { VariableDefinitionNode } from '../../language/ast'; @@ -17,6 +19,37 @@ import type { ValidationContext } from '../ValidationContext'; * input types (scalar, enum, or input object). * * See https://spec.graphql.org/draft/#sec-Variables-Are-Input-Types + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { VariablesAreInputTypesRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: ID): String + * } + * + * type User { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * query ($user: User) { field(arg: "1") } + * `); + * const invalidErrors = validate(schema, invalidDocument, [VariablesAreInputTypesRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * query ($id: ID) { field(arg: $id) } + * `); + * const validErrors = validate(schema, validDocument, [VariablesAreInputTypesRule]); + * + * validErrors; // => [] + * ``` */ export function VariablesAreInputTypesRule( context: ValidationContext, diff --git a/src/validation/rules/VariablesInAllowedPositionRule.ts b/src/validation/rules/VariablesInAllowedPositionRule.ts index 3f4cb51c27..32c641ab79 100644 --- a/src/validation/rules/VariablesInAllowedPositionRule.ts +++ b/src/validation/rules/VariablesInAllowedPositionRule.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + import { inspect } from '../../jsutils/inspect'; import type { Maybe } from '../../jsutils/Maybe'; @@ -26,6 +28,33 @@ import type { ValidationContext } from '../ValidationContext'; * Variable usages must be compatible with the arguments they are passed to. * * See https://spec.graphql.org/draft/#sec-All-Variable-Usages-are-Allowed + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { VariablesInAllowedPositionRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * field(arg: ID!): String + * } + * `); + * + * const invalidDocument = parse(` + * query ($id: String) { field(arg: $id) } + * `); + * const invalidErrors = validate(schema, invalidDocument, [VariablesInAllowedPositionRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * query ($id: ID!) { field(arg: $id) } + * `); + * const validErrors = validate(schema, validDocument, [VariablesInAllowedPositionRule]); + * + * validErrors; // => [] + * ``` */ export function VariablesInAllowedPositionRule( context: ValidationContext, @@ -95,8 +124,13 @@ export function VariablesInAllowedPositionRule( /** * Returns true if the variable is allowed in the location it was found, - * which includes considering if default values exist for either the variable + * including considering if default values exist for either the variable * or the location at which it is located. + * + * OneOf Input Object Type fields are considered separately above to + * provide a more descriptive error message. + * + * @internal */ function allowedVariableUsage( schema: GraphQLSchema, diff --git a/src/validation/rules/custom/NoDeprecatedCustomRule.ts b/src/validation/rules/custom/NoDeprecatedCustomRule.ts index e06ac2e789..f58110e45f 100644 --- a/src/validation/rules/custom/NoDeprecatedCustomRule.ts +++ b/src/validation/rules/custom/NoDeprecatedCustomRule.ts @@ -1,3 +1,5 @@ +/** @category Custom Rules */ + import { invariant } from '../../../jsutils/invariant'; import { GraphQLError } from '../../../error/GraphQLError'; @@ -17,6 +19,46 @@ import type { ValidationContext } from '../../ValidationContext'; * Note: This rule is optional and is not part of the Validation section of the GraphQL * Specification. The main purpose of this rule is detection of deprecated usages and not * necessarily to forbid their use when querying a service. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { + * GraphQLObjectType, + * GraphQLSchema, + * GraphQLString, + * parse, + * validate, + * } from 'graphql'; + * import { NoDeprecatedCustomRule } from 'graphql/validation'; + * + * const schema = new GraphQLSchema({ + * query: new GraphQLObjectType({ + * name: 'Query', + * fields: { + * name: { type: GraphQLString }, + * oldName: { + * type: GraphQLString, + * deprecationReason: 'Use name instead.', + * }, + * }, + * }), + * }); + * + * const invalidDocument = parse(` + * { oldName } + * `); + * const invalidErrors = validate(schema, invalidDocument, [NoDeprecatedCustomRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name } + * `); + * const validErrors = validate(schema, validDocument, [NoDeprecatedCustomRule]); + * + * validErrors; // => [] + * ``` */ export function NoDeprecatedCustomRule(context: ValidationContext): ASTVisitor { return { diff --git a/src/validation/rules/custom/NoSchemaIntrospectionCustomRule.ts b/src/validation/rules/custom/NoSchemaIntrospectionCustomRule.ts index 257d58d723..d4025b7810 100644 --- a/src/validation/rules/custom/NoSchemaIntrospectionCustomRule.ts +++ b/src/validation/rules/custom/NoSchemaIntrospectionCustomRule.ts @@ -1,3 +1,5 @@ +/** @category Custom Rules */ + import { GraphQLError } from '../../../error/GraphQLError'; import type { FieldNode } from '../../../language/ast'; @@ -17,6 +19,33 @@ import type { ValidationContext } from '../../ValidationContext'; * Note: This rule is optional and is not part of the Validation section of the * GraphQL Specification. This rule effectively disables introspection, which * does not reflect best practices and should only be done if absolutely necessary. + * @param context - The validation context used while checking the document. + * @returns A visitor that reports validation errors for this rule. + * @example + * ```ts + * import { buildSchema, parse, validate } from 'graphql'; + * import { NoSchemaIntrospectionCustomRule } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * name: String + * } + * `); + * + * const invalidDocument = parse(` + * { __schema { queryType { name } } } + * `); + * const invalidErrors = validate(schema, invalidDocument, [NoSchemaIntrospectionCustomRule]); + * + * invalidErrors.length; // => 1 + * + * const validDocument = parse(` + * { name } + * `); + * const validErrors = validate(schema, validDocument, [NoSchemaIntrospectionCustomRule]); + * + * validErrors; // => [] + * ``` */ export function NoSchemaIntrospectionCustomRule( context: ValidationContext, diff --git a/src/validation/specifiedRules.ts b/src/validation/specifiedRules.ts index c312c9839c..15c742bb42 100644 --- a/src/validation/specifiedRules.ts +++ b/src/validation/specifiedRules.ts @@ -1,3 +1,5 @@ +/** @category Validation Rules */ + // Spec Section: "Executable Definitions" import { ExecutableDefinitionsRule } from './rules/ExecutableDefinitionsRule'; // Spec Section: "Field Selections on Objects, Interfaces, and Unions Types" @@ -111,9 +113,7 @@ export const specifiedRules: ReadonlyArray = Object.freeze([ ...recommendedRules, ]); -/** - * @internal - */ +/** @internal */ export const specifiedSDLRules: ReadonlyArray = Object.freeze([ LoneSchemaDefinitionRule, diff --git a/src/validation/validate.ts b/src/validation/validate.ts index a2ab30001f..ca8624fe14 100644 --- a/src/validation/validate.ts +++ b/src/validation/validate.ts @@ -1,3 +1,5 @@ +/** @category Validation */ + import { devAssert } from '../jsutils/devAssert'; import { mapValue } from '../jsutils/mapValue'; import type { Maybe } from '../jsutils/Maybe'; @@ -24,6 +26,18 @@ const QueryDocumentKeysToValidate = mapValue( (keys: ReadonlyArray) => keys.filter((key) => key !== 'description'), ); +/** + * Options used when validating a GraphQL document. + * @internal + */ +export interface ValidationOptions { + /** + * Maximum number of validation errors before validation stops. + * @internal + */ + maxErrors?: number; +} + /** * Implements the "Validation" section of the spec. * @@ -33,24 +47,76 @@ const QueryDocumentKeysToValidate = mapValue( * A list of specific validation rules may be provided. If not provided, the * default list of rules defined by the GraphQL specification will be used. * - * Each validation rules is a function which returns a visitor + * Each validation rule is a function that returns a visitor * (see the language/visitor API). Visitor methods are expected to return * GraphQLErrors, or Arrays of GraphQLErrors when invalid. * * Validate will stop validation after a `maxErrors` limit has been reached. * Attackers can send pathologically invalid queries to induce a DoS attack, - * so by default `maxErrors` set to 100 errors. + * so `maxErrors` defaults to 100 errors. * * Optionally a custom TypeInfo instance may be provided. If not provided, one * will be created from the provided schema. + * @param schema - Schema to validate against. + * @param documentAST - Document AST to validate. + * @param rules - Validation rules to apply. + * @param options - Validation options, including error limits. + * @param typeInfo - TypeInfo instance to update during traversal. + * @returns Validation errors, or an empty array when the document is valid. + * @example + * ```ts + * // Validate with the default specified rules. + * import { parse } from 'graphql/language'; + * import { buildSchema } from 'graphql/utilities'; + * import { validate } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * + * validate(schema, parse('{ greeting }')); // => [] + * + * const errors = validate(schema, parse('{ missing }')); + * errors[0].message; // => 'Cannot query field "missing" on type "Query".' + * ``` + * @example + * ```ts + * // This variant uses a custom rule list, TypeInfo, and validation options. + * import { parse } from 'graphql/language'; + * import { buildSchema, TypeInfo } from 'graphql/utilities'; + * import { FieldsOnCorrectTypeRule, validate } from 'graphql/validation'; + * + * const schema = buildSchema(` + * type Query { + * greeting: String + * } + * `); + * const document = parse('{ missingOne missingTwo }'); + * + * const errors = validate( + * schema, + * document, + * [FieldsOnCorrectTypeRule], + * { maxErrors: 1 }, + * new TypeInfo(schema), + * ); + * + * errors.length; // => 2 + * errors[1].message; // => 'Too many validation errors, error limit reached. Validation aborted.' + * ``` */ export function validate( schema: GraphQLSchema, documentAST: DocumentNode, rules: ReadonlyArray = specifiedRules, - options?: { maxErrors?: number }, + options?: ValidationOptions, - /** @deprecated will be removed in 17.0.0 */ + /** + * TypeInfo instance used to track traversal state during validation. + * @deprecated will be removed in 17.0.0 + */ typeInfo: TypeInfo = new TypeInfo(schema), ): ReadonlyArray { const maxErrors = options?.maxErrors ?? 100; @@ -98,9 +164,7 @@ export function validate( return errors; } -/** - * @internal - */ +/** @internal */ export function validateSDL( documentAST: DocumentNode, schemaToExtend?: Maybe, diff --git a/src/version.ts b/src/version.ts index 09e9977b16..b12d3b0679 100644 --- a/src/version.ts +++ b/src/version.ts @@ -3,11 +3,13 @@ /** * A string containing the version of the GraphQL.js library + * @category Version */ export const version = '16.14.0' as string; /** * An object containing the components of the GraphQL.js version string + * @category Version */ export const versionInfo = Object.freeze({ major: 16 as number, diff --git a/tsdoc.json b/tsdoc.json new file mode 100644 index 0000000000..45d100a88b --- /dev/null +++ b/tsdoc.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "tagDefinitions": [ + { + "tagName": "@category", + "syntaxKind": "block" + } + ], + "supportForTags": { + "@deprecated": true, + "@defaultValue": true, + "@example": true, + "@link": true, + "@param": true, + "@packageDocumentation": true, + "@remarks": true, + "@returns": true, + "@typeParam": true, + "@category": true, + "@internal": true + } +}