diff --git a/src/__tests__/starWarsIntrospection-test.ts b/src/__tests__/starWarsIntrospection-test.ts index 224c506c19..ce08f208ee 100644 --- a/src/__tests__/starWarsIntrospection-test.ts +++ b/src/__tests__/starWarsIntrospection-test.ts @@ -362,5 +362,47 @@ describe('Star Wars Introspection Tests', () => { }, }); }); + + it('Allows querying the schema for directives', () => { + const data = queryStarWars(` + { + __directive(name: "skip") { + name + args { + name + type { + kind + name + ofType { + kind + name + } + } + defaultValue + } + } + } + `); + + expect(data).to.deep.equal({ + __directive: { + name: 'skip', + args: [ + { + name: 'if', + type: { + kind: 'NON_NULL', + name: null, + ofType: { + kind: 'SCALAR', + name: 'Boolean', + }, + }, + defaultValue: null, + }, + ], + }, + }); + }); }); }); diff --git a/src/index.ts b/src/index.ts index 27ec4301d5..f3be80c809 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,6 +84,7 @@ export { // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, + DirectiveMetaFieldDef, TypeNameMetaFieldDef, // Predicates isSchema, diff --git a/src/type/__tests__/introspection-test.ts b/src/type/__tests__/introspection-test.ts index c914570713..c1d75208ab 100644 --- a/src/type/__tests__/introspection-test.ts +++ b/src/type/__tests__/introspection-test.ts @@ -1656,6 +1656,32 @@ describe('Introspection', () => { }); }); + it('fails as expected on the __directive root field without an arg', () => { + const schema = buildSchema(` + type Query { + someField: String + } + `); + + const source = ` + { + __directive { + name + } + } + `; + + expectJSON(graphqlSync({ schema, source })).toDeepEqual({ + errors: [ + { + message: + 'Argument ".__directive(name:)" of type "String!" is required, but it was not provided.', + locations: [{ line: 3, column: 9 }], + }, + ], + }); + }); + it('exposes descriptions', () => { const schema = buildSchema(` """Enum description""" diff --git a/src/type/__tests__/schema-test.ts b/src/type/__tests__/schema-test.ts index 5bd86e030a..6dc4409d07 100644 --- a/src/type/__tests__/schema-test.ts +++ b/src/type/__tests__/schema-test.ts @@ -18,6 +18,7 @@ import { } from '../definition.js'; import { GraphQLDirective } from '../directives.js'; import { + DirectiveMetaFieldDef, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, @@ -413,6 +414,7 @@ describe('Type System: Schema', () => { expectField(catOrDog, '__typename').to.equal(TypeNameMetaFieldDef); expectField(queryType, '__type').to.equal(TypeMetaFieldDef); + expectField(queryType, '__directive').to.equal(DirectiveMetaFieldDef); expectField(queryType, '__schema').to.equal(SchemaMetaFieldDef); }); diff --git a/src/type/index.ts b/src/type/index.ts index dfbca75ad0..e9042d19ab 100644 --- a/src/type/index.ts +++ b/src/type/index.ts @@ -182,6 +182,7 @@ export { // Meta-field definitions. SchemaMetaFieldDef, TypeMetaFieldDef, + DirectiveMetaFieldDef, TypeNameMetaFieldDef, } from './introspection.js'; diff --git a/src/type/introspection.ts b/src/type/introspection.ts index b72b162cbb..998717df6d 100644 --- a/src/type/introspection.ts +++ b/src/type/introspection.ts @@ -546,6 +546,28 @@ export const TypeMetaFieldDef: GraphQLField = { astNode: undefined, }; +export const DirectiveMetaFieldDef: GraphQLField = { + name: '__directive', + type: __Directive, + description: 'Request information for a single directive.', + args: [ + { + name: 'name', + description: undefined, + type: new GraphQLNonNull(GraphQLString), + defaultValue: undefined, + deprecationReason: undefined, + extensions: Object.create(null), + astNode: undefined, + }, + ], + resolve: (_source, { name }, _context, { schema }) => + schema.getDirective(name), + deprecationReason: undefined, + extensions: Object.create(null), + astNode: undefined, +}; + export const TypeNameMetaFieldDef: GraphQLField = { name: '__typename', type: new GraphQLNonNull(GraphQLString), diff --git a/src/type/schema.ts b/src/type/schema.ts index 694454fae5..abc95ae19b 100644 --- a/src/type/schema.ts +++ b/src/type/schema.ts @@ -32,6 +32,7 @@ import type { GraphQLDirective } from './directives.js'; import { isDirective, specifiedDirectives } from './directives.js'; import { __Schema, + DirectiveMetaFieldDef, SchemaMetaFieldDef, TypeMetaFieldDef, TypeNameMetaFieldDef, @@ -340,13 +341,14 @@ export class GraphQLSchema { /** * This method looks up the field on the given type definition. * It has special casing for the three introspection fields, `__schema`, - * `__type` and `__typename`. + * `__type`, `__directive` and `__typename`. * * `__typename` is special because it can always be queried as a field, even * in situations where no other fields are allowed, like on a Union. * - * `__schema` and `__type` could get automatically added to the query type, - * but that would require mutating type definitions, which would cause issues. + * `__schema`, `__type`, and `__directive` could get automatically added to the + * query type, but that would require mutating type definitions, which would + * cause issues. */ getField( parentType: GraphQLCompositeType, @@ -361,6 +363,10 @@ export class GraphQLSchema { return this.getQueryType() === parentType ? TypeMetaFieldDef : undefined; + case DirectiveMetaFieldDef.name: + return this.getQueryType() === parentType + ? DirectiveMetaFieldDef + : undefined; case TypeNameMetaFieldDef.name: return TypeNameMetaFieldDef; } diff --git a/src/utilities/__tests__/TypeInfo-test.ts b/src/utilities/__tests__/TypeInfo-test.ts index cc129287c3..e2d9c78bd0 100644 --- a/src/utilities/__tests__/TypeInfo-test.ts +++ b/src/utilities/__tests__/TypeInfo-test.ts @@ -160,6 +160,7 @@ describe('visitWithTypeInfo', () => { { __typename __type(name: "Cat") { __typename } + __directive(name: "skip") { __typename } __schema { __typename # in object type } @@ -174,6 +175,7 @@ describe('visitWithTypeInfo', () => { } pet { __type # unknown + __directive # unknown __schema # unknown } } @@ -195,6 +197,8 @@ describe('visitWithTypeInfo', () => { ['QueryRoot', '__typename'], ['QueryRoot', '__type'], ['__Type', '__typename'], + ['QueryRoot', '__directive'], + ['__Directive', '__typename'], ['QueryRoot', '__schema'], ['__Schema', '__typename'], ['QueryRoot', 'humanOrAlien'], @@ -206,6 +210,7 @@ describe('visitWithTypeInfo', () => { ['QueryRoot', 'pet'], ['Pet', undefined], ['Pet', undefined], + ['Pet', undefined], ]); }); diff --git a/src/validation/rules/MaxIntrospectionDepthRule.ts b/src/validation/rules/MaxIntrospectionDepthRule.ts index 5cde22f130..090b7a99eb 100644 --- a/src/validation/rules/MaxIntrospectionDepthRule.ts +++ b/src/validation/rules/MaxIntrospectionDepthRule.ts @@ -77,7 +77,11 @@ export function MaxIntrospectionDepthRule( return { Field(node) { - if (node.name.value === '__schema' || node.name.value === '__type') { + if ( + node.name.value === '__schema' || + node.name.value === '__type' || + node.name.value === '__directive' + ) { if (checkDepth(node)) { context.reportError( new GraphQLError('Maximum introspection depth exceeded', {