Skip to content

Commit 442e57b

Browse files
committed
Expand @deprecated to Object types
See [RFC](graphql/graphql-spec#997)
1 parent 7a609a2 commit 442e57b

12 files changed

+310
-12
lines changed

src/type/__tests__/definition-test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,20 @@ describe('Type System: Objects', () => {
186186
});
187187
});
188188

189+
it('defines a deprecated object type', () => {
190+
const DeprecatedType = new GraphQLObjectType({
191+
name: 'foo',
192+
fields: {
193+
bar: {
194+
type: ScalarType,
195+
},
196+
},
197+
deprecationReason: 'A terrible reason',
198+
});
199+
200+
expect(DeprecatedType.deprecationReason).to.equal('A terrible reason');
201+
});
202+
189203
it('accepts an Object type with a field function', () => {
190204
const objType = new GraphQLObjectType({
191205
name: 'SomeObject',

src/type/__tests__/introspection-test.ts

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,17 @@ describe('Introspection', () => {
9696
},
9797
{
9898
name: 'types',
99-
args: [],
99+
args: [
100+
{
101+
name: 'includeDeprecated',
102+
type: {
103+
kind: 'SCALAR',
104+
name: 'Boolean',
105+
ofType: null,
106+
},
107+
defaultValue: 'false',
108+
},
109+
],
100110
type: {
101111
kind: 'NON_NULL',
102112
name: null,
@@ -286,7 +296,17 @@ describe('Introspection', () => {
286296
},
287297
{
288298
name: 'possibleTypes',
289-
args: [],
299+
args: [
300+
{
301+
name: 'includeDeprecated',
302+
type: {
303+
kind: 'SCALAR',
304+
name: 'Boolean',
305+
ofType: null,
306+
},
307+
defaultValue: 'false',
308+
},
309+
],
290310
type: {
291311
kind: 'LIST',
292312
name: null,
@@ -372,6 +392,28 @@ describe('Introspection', () => {
372392
isDeprecated: false,
373393
deprecationReason: null,
374394
},
395+
{
396+
name: 'isDeprecated',
397+
args: [],
398+
type: {
399+
kind: 'SCALAR',
400+
name: 'Boolean',
401+
ofType: null,
402+
},
403+
isDeprecated: false,
404+
deprecationReason: null,
405+
},
406+
{
407+
name: 'deprecationReason',
408+
args: [],
409+
type: {
410+
kind: 'SCALAR',
411+
name: 'String',
412+
ofType: null,
413+
},
414+
isDeprecated: false,
415+
deprecationReason: null,
416+
},
375417
],
376418
inputFields: null,
377419
interfaces: [],
@@ -956,6 +998,7 @@ describe('Introspection', () => {
956998
'ARGUMENT_DEFINITION',
957999
'INPUT_FIELD_DEFINITION',
9581000
'ENUM_VALUE',
1001+
'OBJECT',
9591002
],
9601003
args: [
9611004
{
@@ -1231,6 +1274,153 @@ describe('Introspection', () => {
12311274
});
12321275
});
12331276

1277+
it('identifies deprecated objects', () => {
1278+
const schema = buildSchema(`
1279+
type Query {
1280+
dragon: [Dragon]
1281+
}
1282+
type Dragon @deprecated(reason: "No longer known to exist") {
1283+
name: String
1284+
}
1285+
`);
1286+
1287+
const source = `
1288+
{
1289+
__schema {
1290+
types(includeDeprecated: true) {
1291+
name
1292+
isDeprecated
1293+
deprecationReason
1294+
}
1295+
}
1296+
dragon: __type(name: "Dragon") {
1297+
name
1298+
isDeprecated
1299+
deprecationReason
1300+
}
1301+
}
1302+
`;
1303+
1304+
interface IntrospectionResponse {
1305+
__schema: {
1306+
types: [
1307+
{ name: string; isDeprecated: boolean; deprecationReason: string },
1308+
];
1309+
};
1310+
dragon: { name: string; isDeprecated: boolean; deprecationReason: string };
1311+
}
1312+
1313+
const resp = graphqlSync({ schema, source })
1314+
.data as unknown as IntrospectionResponse;
1315+
1316+
expect(resp.__schema.types).to.deep.include.members([
1317+
{
1318+
name: 'Dragon',
1319+
isDeprecated: true,
1320+
deprecationReason: 'No longer known to exist',
1321+
},
1322+
]);
1323+
expect(resp.dragon).to.deep.equal({
1324+
name: 'Dragon',
1325+
isDeprecated: true,
1326+
deprecationReason: 'No longer known to exist',
1327+
});
1328+
});
1329+
1330+
it('respects the includeDeprecated parameter for types', () => {
1331+
const schema = buildSchema(`
1332+
type Query {
1333+
dragon: [Dragon]
1334+
}
1335+
type Dragon @deprecated(reason: "No longer known to exist") {
1336+
name: String
1337+
}
1338+
`);
1339+
1340+
const source = `
1341+
{
1342+
__schema {
1343+
trueTypes: types(includeDeprecated: true) {
1344+
name
1345+
}
1346+
falseTypes: types(includeDeprecated: false) {
1347+
name
1348+
}
1349+
omittedTypes: types {
1350+
name
1351+
}
1352+
}
1353+
}
1354+
`;
1355+
1356+
interface IntrospectionResponse {
1357+
__schema: {
1358+
trueTypes: [{ name: string }];
1359+
falseTypes: [{ name: string }];
1360+
omittedTypes: [{ name: string }];
1361+
};
1362+
}
1363+
1364+
const response = graphqlSync({ schema, source })
1365+
.data as unknown as IntrospectionResponse;
1366+
expect(response.__schema.trueTypes).to.deep.include.members([
1367+
{ name: 'Dragon' },
1368+
]);
1369+
expect(response.__schema.falseTypes).to.not.deep.include.members([
1370+
{ name: 'Dragon' },
1371+
]);
1372+
expect(response.__schema.omittedTypes).to.not.deep.include.members([
1373+
{ name: 'Dragon' },
1374+
]);
1375+
});
1376+
1377+
it('respects the includeDeprecated parameter for possibleTypes', () => {
1378+
const schema = buildSchema(`
1379+
type Query {
1380+
animals: [Animal]
1381+
}
1382+
1383+
interface Animal {
1384+
name: String
1385+
}
1386+
1387+
type Dog implements Animal {
1388+
name: String
1389+
}
1390+
1391+
type Dragon implements Animal @deprecated(reason: "No longer known to exist") {
1392+
name: String
1393+
}
1394+
`);
1395+
1396+
const source = `
1397+
{
1398+
animal: __type(name: "Animal") {
1399+
trueTypes: possibleTypes(includeDeprecated: true) {
1400+
name
1401+
}
1402+
falseTypes: possibleTypes(includeDeprecated: false) {
1403+
name
1404+
}
1405+
omittedTypes: possibleTypes {
1406+
name
1407+
}
1408+
}
1409+
}
1410+
`;
1411+
1412+
const result = graphqlSync({ schema, source });
1413+
const animal = result.data?.animal;
1414+
// @ts-expect-error
1415+
expect(animal.trueTypes).to.deep.include.members([{ name: 'Dragon' }]);
1416+
// @ts-expect-error
1417+
expect(animal.falseTypes).to.not.deep.include.members([{ name: 'Dragon' }]);
1418+
// @ts-expect-error
1419+
expect(animal.omittedTypes).to.not.deep.include.members([
1420+
{ name: 'Dragon' },
1421+
]);
1422+
});
1423+
12341424
it('identifies deprecated args', () => {
12351425
const schema = buildSchema(`
12361426
type Query {

src/type/definition.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,10 @@ export type GraphQLNamedOutputType =
444444
| GraphQLUnionType
445445
| GraphQLEnumType;
446446

447+
export function getDeprecationReason(type: GraphQLNamedType) {
448+
return 'deprecationReason' in type ? type.deprecationReason : undefined;
449+
}
450+
447451
export function isNamedType(type: unknown): type is GraphQLNamedType {
448452
return (
449453
isScalarType(type) ||
@@ -707,6 +711,7 @@ export class GraphQLObjectType<TSource = any, TContext = any> {
707711
extensions: Readonly<GraphQLObjectTypeExtensions<TSource, TContext>>;
708712
astNode: Maybe<ObjectTypeDefinitionNode>;
709713
extensionASTNodes: ReadonlyArray<ObjectTypeExtensionNode>;
714+
deprecationReason: Maybe<string>;
710715

711716
private _fields: ThunkObjMap<GraphQLField<TSource, TContext>>;
712717
private _interfaces: ThunkReadonlyArray<GraphQLInterfaceType>;
@@ -718,6 +723,7 @@ export class GraphQLObjectType<TSource = any, TContext = any> {
718723
this.extensions = toObjMap(config.extensions);
719724
this.astNode = config.astNode;
720725
this.extensionASTNodes = config.extensionASTNodes ?? [];
726+
this.deprecationReason = config.deprecationReason;
721727

722728
this._fields = () => defineFieldMap(config);
723729
this._interfaces = () => defineInterfaces(config);
@@ -751,6 +757,7 @@ export class GraphQLObjectType<TSource = any, TContext = any> {
751757
extensions: this.extensions,
752758
astNode: this.astNode,
753759
extensionASTNodes: this.extensionASTNodes,
760+
deprecationReason: this.deprecationReason,
754761
};
755762
}
756763

@@ -853,6 +860,7 @@ export interface GraphQLObjectTypeConfig<TSource, TContext> {
853860
extensions?: Maybe<Readonly<GraphQLObjectTypeExtensions<TSource, TContext>>>;
854861
astNode?: Maybe<ObjectTypeDefinitionNode>;
855862
extensionASTNodes?: Maybe<ReadonlyArray<ObjectTypeExtensionNode>>;
863+
deprecationReason?: Maybe<string>;
856864
}
857865

858866
interface GraphQLObjectTypeNormalizedConfig<TSource, TContext>

src/type/directives.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,7 @@ export const GraphQLDeprecatedDirective: GraphQLDirective =
220220
DirectiveLocation.ARGUMENT_DEFINITION,
221221
DirectiveLocation.INPUT_FIELD_DEFINITION,
222222
DirectiveLocation.ENUM_VALUE,
223+
DirectiveLocation.OBJECT,
223224
],
224225
args: {
225226
reason: {

src/type/introspection.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
GraphQLType,
1616
} from './definition.js';
1717
import {
18+
getDeprecationReason,
1819
GraphQLEnumType,
1920
GraphQLList,
2021
GraphQLNonNull,
@@ -46,8 +47,14 @@ export const __Schema: GraphQLObjectType = new GraphQLObjectType({
4647
types: {
4748
description: 'A list of all types supported by this server.',
4849
type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(__Type))),
49-
resolve(schema) {
50-
return Object.values(schema.getTypeMap());
50+
args: {
51+
includeDeprecated: { type: GraphQLBoolean, defaultValue: false },
52+
},
53+
resolve(schema, { includeDeprecated }) {
54+
const types = Object.values(schema.getTypeMap());
55+
return includeDeprecated
56+
? types
57+
: types.filter((type) => getDeprecationReason(type) == null);
5158
},
5259
},
5360
queryType: {
@@ -282,9 +289,15 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({
282289
},
283290
possibleTypes: {
284291
type: new GraphQLList(new GraphQLNonNull(__Type)),
285-
resolve(type, _args, _context, { schema }) {
292+
args: {
293+
includeDeprecated: { type: GraphQLBoolean, defaultValue: false },
294+
},
295+
resolve(type, { includeDeprecated }, _context, { schema }) {
286296
if (isAbstractType(type)) {
287-
return schema.getPossibleTypes(type);
297+
const possibleTypes = schema.getPossibleTypes(type);
298+
return includeDeprecated
299+
? possibleTypes
300+
: possibleTypes.filter((t) => t.deprecationReason == null);
288301
}
289302
},
290303
},
@@ -323,6 +336,18 @@ export const __Type: GraphQLObjectType = new GraphQLObjectType({
323336
type: __Type,
324337
resolve: (type) => ('ofType' in type ? type.ofType : undefined),
325338
},
339+
isDeprecated: {
340+
type: GraphQLBoolean,
341+
resolve: (type) =>
342+
'deprecationReason' in type
343+
? type.deprecationReason != null
344+
: undefined,
345+
},
346+
deprecationReason: {
347+
type: GraphQLString,
348+
resolve: (type) =>
349+
'deprecationReason' in type ? type.deprecationReason : undefined,
350+
},
326351
} as GraphQLFieldConfigMap<GraphQLType, unknown>),
327352
});
328353

src/utilities/__tests__/buildASTSchema-test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -634,6 +634,10 @@ describe('Schema Builder', () => {
634634
field4(oldArg: String @deprecated(reason: "Why not?"), arg: String): String
635635
field5(arg: MyInput): String
636636
}
637+
638+
type DeprecatedObject @deprecated(reason: "It ain't") {
639+
fields1: Boolean
640+
}
637641
`;
638642
expect(cycleSDL(sdl)).to.equal(sdl);
639643

@@ -662,6 +666,13 @@ describe('Schema Builder', () => {
662666
deprecationReason: 'Because I said so',
663667
});
664668

669+
const deprecatedObject = assertObjectType(
670+
schema.getType('DeprecatedObject'),
671+
);
672+
expect(deprecatedObject).to.include({
673+
deprecationReason: "It ain't",
674+
});
675+
665676
const inputFields = assertInputObjectType(
666677
schema.getType('MyInput'),
667678
).getFields();

0 commit comments

Comments
 (0)