Skip to content

__fulfilled meta field #3216

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/__testUtils__/kitchenSinkQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,9 @@ fragment frag on Friend @onFragmentDefinition {
query {
__typename
}

query Fulfilled {
__fulfilled
a: __fulfilled(label: "a")
}
`;
9 changes: 9 additions & 0 deletions src/execution/__tests__/union-interface-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ describe('Execute: Union and intersection types', () => {
name
pets {
__typename
__fulfilled
... on Dog {
isDog: __fulfilled(label: "Dog")
name
barks
}
Expand All @@ -284,11 +286,14 @@ describe('Execute: Union and intersection types', () => {
pets: [
{
__typename: 'Cat',
__fulfilled: true,
name: 'Garfield',
meows: false,
},
{
__typename: 'Dog',
__fulfilled: true,
isDog: true,
name: 'Odie',
barks: true,
},
Expand Down Expand Up @@ -341,6 +346,7 @@ describe('Execute: Union and intersection types', () => {
}

... on Mammal {
isMammal: __fulfilled(label: "Mammal")
mother {
__typename
... on Dog {
Expand All @@ -364,11 +370,13 @@ describe('Execute: Union and intersection types', () => {
friends: [
{
__typename: 'Person',
isMammal: true,
name: 'Liz',
mother: null,
},
{
__typename: 'Dog',
isMammal: true,
name: 'Odie',
barks: true,
mother: { __typename: 'Dog', name: "Odie's Mom", barks: true },
Expand Down Expand Up @@ -396,6 +404,7 @@ describe('Execute: Union and intersection types', () => {
}

fragment CatMeows on Cat {
catMeowsFulfilled: __fulfilled(label: "CatMeows")
meows
}
`);
Expand Down
15 changes: 9 additions & 6 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
} from '../type/definition';
import { assertValidSchema } from '../type/validate';
import {
FulfilledMetaFieldDef,
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
Expand Down Expand Up @@ -1016,12 +1017,12 @@ export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =

/**
* This method looks up the field on the given type definition.
* It has special casing for the three introspection fields,
* __schema, __type 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.
* It has special casing for the four introspection fields,
* __schema, __type, __typename and __fulfilled. __typename and __fulfilled
* are special because they 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.
*
* @internal
*/
Expand All @@ -1044,6 +1045,8 @@ export function getFieldDef(
return TypeMetaFieldDef;
} else if (fieldName === TypeNameMetaFieldDef.name) {
return TypeNameMetaFieldDef;
} else if (fieldName === FulfilledMetaFieldDef.name) {
return FulfilledMetaFieldDef;
}
return parentType.getFields()[fieldName];
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export {
__EnumValue,
__TypeKind,
/** Meta-field definitions. */
FulfilledMetaFieldDef,
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
Expand Down
5 changes: 5 additions & 0 deletions src/language/__tests__/printer-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,11 @@ describe('Printer: Query document', () => {
{
__typename
}

query Fulfilled {
__fulfilled
a: __fulfilled(label: "a")
}
`),
);
});
Expand Down
22 changes: 22 additions & 0 deletions src/language/__tests__/visitor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,28 @@ describe('Visitor', () => {
['leave', 'Field', 0, undefined],
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['leave', 'OperationDefinition', 5, undefined],
['enter', 'OperationDefinition', 6, undefined],
['enter', 'Name', 'name', 'OperationDefinition'],
['leave', 'Name', 'name', 'OperationDefinition'],
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['enter', 'Field', 0, undefined],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['leave', 'Field', 0, undefined],
['enter', 'Field', 1, undefined],
['enter', 'Name', 'alias', 'Field'],
['leave', 'Name', 'alias', 'Field'],
['enter', 'Name', 'name', 'Field'],
['leave', 'Name', 'name', 'Field'],
['enter', 'Argument', 0, undefined],
['enter', 'Name', 'name', 'Argument'],
['leave', 'Name', 'name', 'Argument'],
['enter', 'StringValue', 'value', 'Argument'],
['leave', 'StringValue', 'value', 'Argument'],
['leave', 'Argument', 0, undefined],
['leave', 'Field', 1, undefined],
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
['leave', 'OperationDefinition', 6, undefined],
['leave', 'Document', undefined, undefined],
]);
});
Expand Down
1 change: 1 addition & 0 deletions src/type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ export {
/** "Enum" of Type Kinds */
TypeKind,
/** Meta-field definitions. */
FulfilledMetaFieldDef,
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
Expand Down
21 changes: 21 additions & 0 deletions src/type/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,27 @@ export const TypeNameMetaFieldDef: GraphQLField<unknown, unknown> = {
astNode: undefined,
};

export const FulfilledMetaFieldDef: GraphQLField<unknown, unknown> = {
name: '__fulfilled',
type: new GraphQLNonNull(GraphQLBoolean),
description: 'If the current selection was included at runtime.',
args: [
{
name: 'label',
description: undefined,
type: GraphQLString,
defaultValue: undefined,
deprecationReason: undefined,
extensions: undefined,
astNode: undefined,
},
],
resolve: (_source, _args, _context, _info) => true,
deprecationReason: undefined,
extensions: undefined,
astNode: undefined,
};

export const introspectionTypes: ReadonlyArray<GraphQLNamedType> =
Object.freeze([
__Schema,
Expand Down
4 changes: 4 additions & 0 deletions src/utilities/TypeInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
getNamedType,
} from '../type/definition';
import {
FulfilledMetaFieldDef,
SchemaMetaFieldDef,
TypeMetaFieldDef,
TypeNameMetaFieldDef,
Expand Down Expand Up @@ -327,6 +328,9 @@ function getFieldDef(
if (name === TypeNameMetaFieldDef.name && isCompositeType(parentType)) {
return TypeNameMetaFieldDef;
}
if (name === FulfilledMetaFieldDef.name && isCompositeType(parentType)) {
return FulfilledMetaFieldDef;
}
if (isObjectType(parentType) || isInterfaceType(parentType)) {
return parentType.getFields()[name];
}
Expand Down
65 changes: 65 additions & 0 deletions src/utilities/__tests__/TypeInfo-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,4 +457,69 @@ describe('visitWithTypeInfo', () => {
['leave', 'SelectionSet', null, 'Human', 'Human'],
]);
});

it('supports traversals of __fulfilled meta-field', () => {
const humanType = testSchema.getType('Human');
invariant(humanType != null);

const typeInfo = new TypeInfo(testSchema, humanType);

const ast = parse('{ __fulfilled, pets { __fulfilled(label: "pet") } }');
const operationNode = ast.definitions[0];
invariant(operationNode.kind === 'OperationDefinition');

const visited: Array<any> = [];
visit(
operationNode.selectionSet,
visitWithTypeInfo(typeInfo, {
enter(node) {
const parentType = typeInfo.getParentType();
const type = typeInfo.getType();
visited.push([
'enter',
node.kind,
node.kind === 'Name' ? node.value : null,
String(parentType),
String(type),
]);
},
leave(node) {
const parentType = typeInfo.getParentType();
const type = typeInfo.getType();
visited.push([
'leave',
node.kind,
node.kind === 'Name' ? node.value : null,
String(parentType),
String(type),
]);
},
}),
);

expect(visited).to.deep.equal([
['enter', 'SelectionSet', null, 'Human', 'Human'],
['enter', 'Field', null, 'Human', 'Boolean!'],
['enter', 'Name', '__fulfilled', 'Human', 'Boolean!'],
['leave', 'Name', '__fulfilled', 'Human', 'Boolean!'],
['leave', 'Field', null, 'Human', 'Boolean!'],
['enter', 'Field', null, 'Human', '[Pet]'],
['enter', 'Name', 'pets', 'Human', '[Pet]'],
['leave', 'Name', 'pets', 'Human', '[Pet]'],
['enter', 'SelectionSet', null, 'Pet', '[Pet]'],
['enter', 'Field', null, 'Pet', 'Boolean!'],
['enter', 'Name', '__fulfilled', 'Pet', 'Boolean!'],
['leave', 'Name', '__fulfilled', 'Pet', 'Boolean!'],
['enter', 'Argument', null, 'Pet', 'Boolean!'],
['enter', 'Name', 'label', 'Pet', 'Boolean!'],
['leave', 'Name', 'label', 'Pet', 'Boolean!'],
['enter', 'StringValue', null, 'Pet', 'Boolean!'],
['leave', 'StringValue', null, 'Pet', 'Boolean!'],
['leave', 'Argument', null, 'Pet', 'Boolean!'],
['leave', 'Field', null, 'Pet', 'Boolean!'],
['leave', 'SelectionSet', null, 'Pet', '[Pet]'],
['leave', 'Field', null, 'Human', '[Pet]'],
['leave', 'SelectionSet', null, 'Human', 'Human'],
]);
});
});