diff --git a/src/index.d.ts b/src/index.d.ts index 14a1551b4f8..27bf598a8d0 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -414,7 +414,7 @@ export { DangerousChangeType, findBreakingChanges, findDangerousChanges, - // Report all deprecated usage within a GraphQL document. + // @deprecated: Report all deprecated usage within a GraphQL document. findDeprecatedUsages, } from './utilities/index'; diff --git a/src/index.js b/src/index.js index 8e2d9f4dd49..33dc53d9dcd 100644 --- a/src/index.js +++ b/src/index.js @@ -414,7 +414,7 @@ export { DangerousChangeType, findBreakingChanges, findDangerousChanges, - // Report all deprecated usage within a GraphQL document. + // @deprecated: Report all deprecated usage within a GraphQL document. findDeprecatedUsages, } from './utilities/index'; diff --git a/src/utilities/__tests__/findDeprecatedUsages-test.js b/src/utilities/__tests__/findDeprecatedUsages-test.js index f49c67589fb..225b21976e4 100644 --- a/src/utilities/__tests__/findDeprecatedUsages-test.js +++ b/src/utilities/__tests__/findDeprecatedUsages-test.js @@ -1,5 +1,7 @@ // @flow strict +/* eslint-disable import/no-deprecated */ + import { expect } from 'chai'; import { describe, it } from 'mocha'; diff --git a/src/utilities/findDeprecatedUsages.d.ts b/src/utilities/findDeprecatedUsages.d.ts index 6beb244617a..80ff6a3e5d9 100644 --- a/src/utilities/findDeprecatedUsages.d.ts +++ b/src/utilities/findDeprecatedUsages.d.ts @@ -3,7 +3,7 @@ import { DocumentNode } from '../language/ast'; import { GraphQLSchema } from '../type/schema'; /** - * A validation rule which reports deprecated usages. + * @deprecated: A validation rule which reports deprecated usages. * * Returns a list of GraphQLError instances describing each deprecated use. */ diff --git a/src/utilities/findDeprecatedUsages.js b/src/utilities/findDeprecatedUsages.js index e63003b7e5f..1175af34eb4 100644 --- a/src/utilities/findDeprecatedUsages.js +++ b/src/utilities/findDeprecatedUsages.js @@ -11,7 +11,7 @@ import { type GraphQLSchema } from '../type/schema'; import { TypeInfo, visitWithTypeInfo } from './TypeInfo'; /** - * A validation rule which reports deprecated usages. + * @deprecated: A validation rule which reports deprecated usages. * * Returns a list of GraphQLError instances describing each deprecated use. */ diff --git a/src/utilities/index.d.ts b/src/utilities/index.d.ts index fadc54c67ce..658a69ac0d7 100644 --- a/src/utilities/index.d.ts +++ b/src/utilities/index.d.ts @@ -112,5 +112,5 @@ export { DangerousChange, } from './findBreakingChanges'; -// Report all deprecated usage within a GraphQL document. +// @deprecated: Report all deprecated usage within a GraphQL document. export { findDeprecatedUsages } from './findDeprecatedUsages'; diff --git a/src/utilities/index.js b/src/utilities/index.js index 994948393a3..eb7c1566965 100644 --- a/src/utilities/index.js +++ b/src/utilities/index.js @@ -111,5 +111,5 @@ export { } from './findBreakingChanges'; export type { BreakingChange, DangerousChange } from './findBreakingChanges'; -// Report all deprecated usage within a GraphQL document. +// @deprecated: Report all deprecated usage within a GraphQL document. export { findDeprecatedUsages } from './findDeprecatedUsages'; diff --git a/src/validation/ValidationContext.d.ts b/src/validation/ValidationContext.d.ts index 72492bdfcc3..b0e0b06a250 100644 --- a/src/validation/ValidationContext.d.ts +++ b/src/validation/ValidationContext.d.ts @@ -18,6 +18,7 @@ import { GraphQLCompositeType, GraphQLField, GraphQLArgument, + GraphQLEnumValue, } from '../type/definition'; import { TypeInfo } from '../utilities/TypeInfo'; @@ -90,6 +91,8 @@ export class ValidationContext extends ASTValidationContext { getDirective(): Maybe; getArgument(): Maybe; + + getEnumValue(): Maybe; } export type ValidationRule = (context: ValidationContext) => ASTVisitor; diff --git a/src/validation/ValidationContext.js b/src/validation/ValidationContext.js index ecb9854f4a6..3b2886095d5 100644 --- a/src/validation/ValidationContext.js +++ b/src/validation/ValidationContext.js @@ -23,6 +23,7 @@ import { type GraphQLCompositeType, type GraphQLField, type GraphQLArgument, + type GraphQLEnumValue, } from '../type/definition'; import { TypeInfo, visitWithTypeInfo } from '../utilities/TypeInfo'; @@ -243,6 +244,10 @@ export class ValidationContext extends ASTValidationContext { getArgument(): ?GraphQLArgument { return this._typeInfo.getArgument(); } + + getEnumValue(): ?GraphQLEnumValue { + return this._typeInfo.getEnumValue(); + } } export type ValidationRule = (ValidationContext) => ASTVisitor; diff --git a/src/validation/__tests__/ProhibitDeprecatedFieldsRule-test.js b/src/validation/__tests__/ProhibitDeprecatedFieldsRule-test.js new file mode 100644 index 00000000000..982aff4dec8 --- /dev/null +++ b/src/validation/__tests__/ProhibitDeprecatedFieldsRule-test.js @@ -0,0 +1,135 @@ +// @flow strict + +import { describe, it } from 'mocha'; + +import { buildSchema } from '../../utilities/buildASTSchema'; + +import { ProhibitDeprecatedFieldsRule } from '../rules/optional/ProhibitDeprecatedFieldsRule'; + +import { expectValidationErrorsWithSchema } from './harness'; + +const schema = buildSchema(` + enum EnumType { + NORMAL_VALUE + DEPRECATED_VALUE @deprecated(reason: "Some enum reason.") + DEPRECATED_VALUE_WITH_NO_REASON @deprecated + } + + type Query { + normalField(enumArg: [EnumType]): String + deprecatedField: String @deprecated(reason: "Some field reason.") + deprecatedFieldWithNoReason: String @deprecated + } +`); + +function expectErrors(queryStr) { + return expectValidationErrorsWithSchema( + schema, + ProhibitDeprecatedFieldsRule, + queryStr, + ); +} + +function expectValid(queryStr) { + expectErrors(queryStr).to.deep.equal([]); +} + +describe('Validate: prohibit deprecated fields', () => { + it('ignores fields and enum values that are not deprecated', () => { + expectValid(` + { + normalField(enumArg: [NORMAL_VALUE]) + } + `); + }); + + it('ignores unknown fields and enum values', () => { + expectValid(` + fragment UnknownFragment on UnknownType { + unknownField(unknownArg: UNKNOWN_VALUE) + } + `); + }); + + it('reports error when a deprecated field is selected', () => { + expectErrors(` + { + normalField + deprecatedField + } + `).to.deep.equal([ + { + message: + 'The field Query.deprecatedField is deprecated. Some field reason.', + locations: [{ line: 4, column: 9 }], + }, + ]); + }); + + it('reports error when a deprecated field with default reason is selected', () => { + expectErrors(` + { + normalField + deprecatedFieldWithNoReason + } + `).to.deep.equal([ + { + message: + 'The field Query.deprecatedFieldWithNoReason is deprecated. No longer supported', + locations: [{ line: 4, column: 9 }], + }, + ]); + }); + + it('reports error when a deprecated enum value is used', () => { + expectErrors(` + { + normalField(enumArg: [NORMAL_VALUE, DEPRECATED_VALUE]) + } + `).to.deep.equal([ + { + message: + 'The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason.', + locations: [{ line: 3, column: 45 }], + }, + ]); + }); + + it('reports error when a deprecated enum value with default reason is used', () => { + expectErrors(` + { + normalField(enumArg: [DEPRECATED_VALUE_WITH_NO_REASON]) + } + `).to.deep.equal([ + { + message: + 'The enum value "EnumType.DEPRECATED_VALUE_WITH_NO_REASON" is deprecated. No longer supported', + locations: [{ line: 3, column: 31 }], + }, + ]); + }); + + it('reports error when a deprecated field is selected or an enum value is used inside a fragment', () => { + expectErrors(` + { + ...QueryFragment + } + + fragment QueryFragment on Query { + deprecatedField + normalField(enumArg: [NORMAL_VALUE, DEPRECATED_VALUE]) + } + `).to.deep.equal([ + { + message: + 'The field Query.deprecatedField is deprecated. Some field reason.', + locations: [{ line: 7, column: 9 }], + }, + { + message: + 'The enum value "EnumType.DEPRECATED_VALUE" is deprecated. Some enum reason.', + locations: [{ line: 8, column: 45 }], + }, + ]); + }); +}); diff --git a/src/validation/index.d.ts b/src/validation/index.d.ts index 79317bff030..82e2f01d3bb 100644 --- a/src/validation/index.d.ts +++ b/src/validation/index.d.ts @@ -90,3 +90,6 @@ export { UniqueEnumValueNamesRule } from './rules/UniqueEnumValueNamesRule'; export { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNamesRule'; export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule'; export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule'; + +// Optional rules not defined by the GraphQL Specification +export { ProhibitDeprecatedFieldsRule } from './rules/optional/ProhibitDeprecatedFieldsRule'; diff --git a/src/validation/index.js b/src/validation/index.js index 906b31e43c2..030cc72ad7b 100644 --- a/src/validation/index.js +++ b/src/validation/index.js @@ -94,3 +94,6 @@ export { UniqueEnumValueNamesRule } from './rules/UniqueEnumValueNamesRule'; export { UniqueFieldDefinitionNamesRule } from './rules/UniqueFieldDefinitionNamesRule'; export { UniqueDirectiveNamesRule } from './rules/UniqueDirectiveNamesRule'; export { PossibleTypeExtensionsRule } from './rules/PossibleTypeExtensionsRule'; + +// Optional rules not defined by the GraphQL Specification +export { ProhibitDeprecatedFieldsRule } from './rules/optional/ProhibitDeprecatedFieldsRule'; diff --git a/src/validation/rules/optional/ProhibitDeprecatedFieldsRule.d.ts b/src/validation/rules/optional/ProhibitDeprecatedFieldsRule.d.ts new file mode 100644 index 00000000000..e47513332db --- /dev/null +++ b/src/validation/rules/optional/ProhibitDeprecatedFieldsRule.d.ts @@ -0,0 +1,15 @@ +import { ASTVisitor } from '../../../language/visitor'; +import { ValidationContext } from '../../ValidationContext'; + +/** + * Prohibit deprecated fields + * + * A GraphQL document is only valid if all selected fields and all used enum + * values have not been deprecated. + * + * Note: This rule is optional and is not part of the Validation section of the + * GraphQL Specification. + */ +export function ProhibitDeprecatedFieldsRule( + context: ValidationContext, +): ASTVisitor; diff --git a/src/validation/rules/optional/ProhibitDeprecatedFieldsRule.js b/src/validation/rules/optional/ProhibitDeprecatedFieldsRule.js new file mode 100644 index 00000000000..22e8e68d99c --- /dev/null +++ b/src/validation/rules/optional/ProhibitDeprecatedFieldsRule.js @@ -0,0 +1,52 @@ +// @flow strict + +import { GraphQLError } from '../../../error/GraphQLError'; + +import { type EnumValueNode, type FieldNode } from '../../../language/ast'; +import { type ASTVisitor } from '../../../language/visitor'; + +import { getNamedType } from '../../../type/definition'; + +import { type ValidationContext } from '../../ValidationContext'; + +/** + * Prohibit deprecated fields + * + * A GraphQL document is only valid if all selected fields and all used enum + * values have not been deprecated. + * + * Note: This rule is optional and is not part of the Validation section of the + * GraphQL Specification. + */ +export function ProhibitDeprecatedFieldsRule( + context: ValidationContext, +): ASTVisitor { + return { + Field(node: FieldNode) { + const fieldDef = context.getFieldDef(); + const parentType = context.getParentType(); + if (parentType && fieldDef && fieldDef.deprecationReason != null) { + context.reportError( + new GraphQLError( + `The field ${parentType.name}.${fieldDef.name} is deprecated. ` + + fieldDef.deprecationReason, + [node], + ), + ); + } + }, + EnumValue(node: EnumValueNode) { + const type = getNamedType(context.getInputType()); + const enumValue = context.getEnumValue(); + if (type && enumValue?.deprecationReason != null) { + context.reportError( + new GraphQLError( + `The enum value "${type.name}.${enumValue.name}" is deprecated. ` + + enumValue.deprecationReason, + node, + ), + ); + } + }, + }; +}