Skip to content

Commit 519cd49

Browse files
committed
Introspection related changes
1 parent 6d3b630 commit 519cd49

7 files changed

Lines changed: 199 additions & 6 deletions

File tree

src/type/__tests__/directive-test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,22 @@ describe('Type System: Directive', () => {
7373
});
7474
});
7575

76+
it('defines a deprecated directive', () => {
77+
const directive = new GraphQLDirective({
78+
name: 'Foo',
79+
locations: [DirectiveLocation.QUERY],
80+
deprecationReason: 'Some reason',
81+
});
82+
83+
expect(directive).to.deep.include({
84+
name: 'Foo',
85+
args: [],
86+
isRepeatable: false,
87+
locations: ['QUERY'],
88+
deprecationReason: 'Some reason',
89+
});
90+
});
91+
7692
it('can be stringified, JSON.stringified and Object.toStringified', () => {
7793
const directive = new GraphQLDirective({
7894
name: 'Foo',

src/type/__tests__/introspection-test.ts

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,17 @@ describe('Introspection', () => {
156156
},
157157
{
158158
name: 'directives',
159-
args: [],
159+
args: [
160+
{
161+
name: 'includeDeprecated',
162+
type: {
163+
kind: 'SCALAR',
164+
name: 'Boolean',
165+
ofType: null,
166+
},
167+
defaultValue: 'false',
168+
},
169+
],
160170
type: {
161171
kind: 'NON_NULL',
162172
name: null,
@@ -805,6 +815,32 @@ describe('Introspection', () => {
805815
isDeprecated: false,
806816
deprecationReason: null,
807817
},
818+
{
819+
name: 'isDeprecated',
820+
args: [],
821+
type: {
822+
kind: 'NON_NULL',
823+
name: null,
824+
ofType: {
825+
kind: 'SCALAR',
826+
name: 'Boolean',
827+
ofType: null,
828+
},
829+
},
830+
isDeprecated: false,
831+
deprecationReason: null,
832+
},
833+
{
834+
name: 'deprecationReason',
835+
args: [],
836+
type: {
837+
kind: 'SCALAR',
838+
name: 'String',
839+
ofType: null,
840+
},
841+
isDeprecated: false,
842+
deprecationReason: null,
843+
},
808844
],
809845
inputFields: null,
810846
interfaces: [],
@@ -1760,4 +1796,70 @@ describe('Introspection', () => {
17601796
});
17611797
expect(result).to.not.have.property('errors');
17621798
});
1799+
1800+
it('identifies deprecated directives', () => {
1801+
const schema = buildSchema(`
1802+
type Query {
1803+
someField: String
1804+
}
1805+
directive @isNotDeprecated on FIELD_DEFINITION
1806+
directive @isDeprecated @deprecated(reason: "No longer supported") on FIELD_DEFINITION
1807+
`);
1808+
1809+
const source = `
1810+
{
1811+
__schema {
1812+
directives(includeDeprecated: true) {
1813+
name
1814+
isDeprecated
1815+
deprecationReason
1816+
}
1817+
}
1818+
}
1819+
`;
1820+
1821+
expect(graphqlSync({ schema, source })).to.deep.equal({
1822+
data: {
1823+
__schema: {
1824+
directives: [
1825+
{
1826+
name: 'isNotDeprecated',
1827+
isDeprecated: false,
1828+
deprecationReason: null,
1829+
},
1830+
{
1831+
name: 'isDeprecated',
1832+
isDeprecated: true,
1833+
deprecationReason: 'No longer supported',
1834+
},
1835+
{
1836+
name: 'include',
1837+
isDeprecated: false,
1838+
deprecationReason: null,
1839+
},
1840+
{
1841+
name: 'skip',
1842+
isDeprecated: false,
1843+
deprecationReason: null,
1844+
},
1845+
{
1846+
name: 'deprecated',
1847+
isDeprecated: false,
1848+
deprecationReason: null,
1849+
},
1850+
{
1851+
name: 'specifiedBy',
1852+
isDeprecated: false,
1853+
deprecationReason: null,
1854+
},
1855+
{
1856+
name: 'oneOf',
1857+
isDeprecated: false,
1858+
deprecationReason: null,
1859+
},
1860+
],
1861+
},
1862+
},
1863+
});
1864+
});
17631865
});

src/type/directives.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import { isObjectLike } from '../jsutils/isObjectLike';
55
import type { Maybe } from '../jsutils/Maybe';
66
import { toObjMap } from '../jsutils/toObjMap';
77

8-
import type { DirectiveDefinitionNode } from '../language/ast';
8+
import type {
9+
DirectiveDefinitionNode,
10+
DirectiveExtensionNode,
11+
} from '../language/ast';
912
import { DirectiveLocation } from '../language/directiveLocation';
1013

1114
import { assertName } from './assertName';
@@ -62,6 +65,7 @@ export class GraphQLDirective {
6265
deprecationReason: Maybe<string>;
6366
extensions: Readonly<GraphQLDirectiveExtensions>;
6467
astNode: Maybe<DirectiveDefinitionNode>;
68+
extensionASTNodes: ReadonlyArray<DirectiveExtensionNode>;
6569

6670
constructor(config: Readonly<GraphQLDirectiveConfig>) {
6771
this.name = assertName(config.name);
@@ -71,6 +75,7 @@ export class GraphQLDirective {
7175
this.deprecationReason = config.deprecationReason;
7276
this.extensions = toObjMap(config.extensions);
7377
this.astNode = config.astNode;
78+
this.extensionASTNodes = config.extensionASTNodes ?? [];
7479

7580
devAssert(
7681
Array.isArray(config.locations),
@@ -100,6 +105,7 @@ export class GraphQLDirective {
100105
deprecationReason: this.deprecationReason,
101106
extensions: this.extensions,
102107
astNode: this.astNode,
108+
extensionASTNodes: this.extensionASTNodes,
103109
};
104110
}
105111

@@ -121,12 +127,14 @@ export interface GraphQLDirectiveConfig {
121127
deprecationReason?: Maybe<string>;
122128
extensions?: Maybe<Readonly<GraphQLDirectiveExtensions>>;
123129
astNode?: Maybe<DirectiveDefinitionNode>;
130+
extensionASTNodes?: Maybe<ReadonlyArray<DirectiveExtensionNode>>;
124131
}
125132

126133
interface GraphQLDirectiveNormalizedConfig extends GraphQLDirectiveConfig {
127134
args: GraphQLFieldConfigArgumentMap;
128135
isRepeatable: boolean;
129136
extensions: Readonly<GraphQLDirectiveExtensions>;
137+
extensionASTNodes: ReadonlyArray<DirectiveExtensionNode>;
130138
}
131139

132140
/**

src/type/introspection.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,15 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({
7272
type: new GraphQLNonNull(
7373
new GraphQLList(new GraphQLNonNull(__Directive)),
7474
),
75-
resolve: (schema) => schema.getDirectives(),
75+
args: {
76+
includeDeprecated: { type: GraphQLBoolean, defaultValue: false },
77+
},
78+
resolve: (schema, { includeDeprecated }) =>
79+
includeDeprecated
80+
? schema.getDirectives()
81+
: schema
82+
.getDirectives()
83+
.filter((directive) => directive.deprecationReason == null),
7684
},
7785
} as GraphQLFieldConfigMap<GraphQLSchema, unknown>),
7886
});
@@ -117,6 +125,14 @@ export const __Directive: GraphQLObjectType = new GraphQLObjectType({
117125
: field.args.filter((arg) => arg.deprecationReason == null);
118126
},
119127
},
128+
isDeprecated: {
129+
type: new GraphQLNonNull(GraphQLBoolean),
130+
resolve: (directive) => directive.deprecationReason != null,
131+
},
132+
deprecationReason: {
133+
type: GraphQLString,
134+
resolve: (directive) => directive.deprecationReason,
135+
},
120136
} as GraphQLFieldConfigMap<GraphQLDirective, unknown>),
121137
});
122138

src/utilities/__tests__/extendSchema-test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,5 +1318,20 @@ describe('extendSchema', () => {
13181318
extend schema @foo
13191319
`);
13201320
});
1321+
1322+
it('extend directive to make it deprecated', () => {
1323+
const schema = buildSchema('directive @isDeprecated on FIELD_DEFINITION');
1324+
const extendAST = parse(`
1325+
extend directive @isDeprecated @deprecated(reason: "use another directive")
1326+
`);
1327+
const extendedSchema = extendSchema(schema, extendAST);
1328+
1329+
const someDirective = assertDirective(
1330+
extendedSchema.getDirective('isDeprecated'),
1331+
);
1332+
expect(someDirective).to.include({
1333+
deprecationReason: 'use another directive',
1334+
});
1335+
});
13211336
});
13221337
});

src/utilities/__tests__/printSchema-test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -717,7 +717,7 @@ describe('Type System Printer', () => {
717717
subscriptionType: __Type
718718
719719
"""A list of all directives supported by this server."""
720-
directives: [__Directive!]!
720+
directives(includeDeprecated: Boolean = false): [__Directive!]!
721721
}
722722
723723
"""
@@ -821,6 +821,8 @@ describe('Type System Printer', () => {
821821
isRepeatable: Boolean!
822822
locations: [__DirectiveLocation!]!
823823
args(includeDeprecated: Boolean = false): [__InputValue!]!
824+
isDeprecated: Boolean!
825+
deprecationReason: String
824826
}
825827
826828
"""

src/utilities/extendSchema.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Maybe } from '../jsutils/Maybe';
77

88
import type {
99
DirectiveDefinitionNode,
10+
DirectiveExtensionNode,
1011
DocumentNode,
1112
EnumTypeDefinitionNode,
1213
EnumTypeExtensionNode,
@@ -138,6 +139,7 @@ export function extendSchemaImpl(
138139
// Collect the type definitions and extensions found in the document.
139140
const typeDefs: Array<TypeDefinitionNode> = [];
140141
const typeExtensionsMap = Object.create(null);
142+
const directiveExtensionsMap = Object.create(null);
141143

142144
// New directives and types are separate because a directives and types can
143145
// have the same name. For example, a type named "skip".
@@ -162,6 +164,14 @@ export function extendSchemaImpl(
162164
: [def];
163165
} else if (def.kind === Kind.DIRECTIVE_DEFINITION) {
164166
directiveDefs.push(def);
167+
} else if (def.kind === Kind.DIRECTIVE_EXTENSION) {
168+
const extendedDirectiveName = def.name.value;
169+
const existingDirectiveExtensions =
170+
directiveExtensionsMap[extendedDirectiveName];
171+
directiveExtensionsMap[extendedDirectiveName] =
172+
existingDirectiveExtensions
173+
? existingDirectiveExtensions.concat([def])
174+
: [def];
165175
}
166176
}
167177

@@ -170,6 +180,7 @@ export function extendSchemaImpl(
170180
if (
171181
Object.keys(typeExtensionsMap).length === 0 &&
172182
typeDefs.length === 0 &&
183+
Object.keys(directiveExtensionsMap).length === 0 &&
173184
directiveDefs.length === 0 &&
174185
schemaExtensions.length === 0 &&
175186
schemaDef == null
@@ -187,6 +198,11 @@ export function extendSchemaImpl(
187198
typeMap[name] = stdTypeMap[name] ?? buildType(typeNode);
188199
}
189200

201+
const directiveMap = Object.create(null);
202+
for (const existingDirective of schemaConfig.directives) {
203+
directiveMap[existingDirective.name] = extendDirective(existingDirective);
204+
}
205+
190206
const operationTypes = {
191207
// Get the extended root operation types.
192208
query: schemaConfig.query && replaceNamedType(schemaConfig.query),
@@ -199,12 +215,13 @@ export function extendSchemaImpl(
199215
};
200216

201217
// Then produce and return a Schema config with these types.
218+
const directives: Array<GraphQLDirective> = Object.values(directiveMap);
202219
return {
203220
description: schemaDef?.description?.value,
204221
...operationTypes,
205222
types: Object.values(typeMap),
206223
directives: [
207-
...schemaConfig.directives.map(replaceDirective),
224+
...directives.map(replaceDirective),
208225
...directiveDefs.map(buildDirective),
209226
],
210227
extensions: Object.create(null),
@@ -415,6 +432,20 @@ export function extendSchemaImpl(
415432
return opTypes;
416433
}
417434

435+
function extendDirective(directive: GraphQLDirective): GraphQLDirective {
436+
const config = directive.toConfig();
437+
const extensions = directiveExtensionsMap[config.name] ?? [];
438+
const deprecatedReason = extensions
439+
.map((ext: DirectiveExtensionNode) => getDeprecationReason(ext))
440+
.find((reason: Maybe<string>) => reason != null);
441+
442+
return new GraphQLDirective({
443+
...config,
444+
deprecationReason: deprecatedReason,
445+
extensionASTNodes: config.extensionASTNodes.concat(extensions),
446+
});
447+
}
448+
418449
function getNamedType(node: NamedTypeNode): GraphQLNamedType {
419450
const name = node.name.value;
420451
const type = stdTypeMap[name] ?? typeMap[name];
@@ -443,6 +474,7 @@ export function extendSchemaImpl(
443474
locations: node.locations.map(({ value }) => value),
444475
isRepeatable: node.repeatable,
445476
args: buildArgumentMap(node.arguments),
477+
deprecationReason: getDeprecationReason(node),
446478
astNode: node,
447479
});
448480
}
@@ -667,7 +699,9 @@ function getDeprecationReason(
667699
node:
668700
| EnumValueDefinitionNode
669701
| FieldDefinitionNode
670-
| InputValueDefinitionNode,
702+
| InputValueDefinitionNode
703+
| DirectiveDefinitionNode
704+
| DirectiveExtensionNode,
671705
): Maybe<string> {
672706
const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node);
673707
// @ts-expect-error validated by `getDirectiveValues`

0 commit comments

Comments
 (0)