Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,26 @@ const schema = buildSchema(`
sender: String
}

type SubscriptionRoot {
interface Root {
rootField: Message
}

type SubscriptionRoot implements Root {
subscriptionField: Message
subscriptionListField: [Message]
rootField: Message
}

type MutationRoot {
type MutationRoot implements Root {
mutationField: Message
mutationListField: [Message]
rootField: Message
}

type QueryRoot {
type QueryRoot implements Root {
message: Message
messages: [Message]
rootField: Message
}

schema {
Expand Down Expand Up @@ -76,6 +83,13 @@ describe('Validate: Defer/Stream directive on root field', () => {
expectErrors(`
mutation {
...rootFragment @defer
...otherFragment
}
fragment otherFragment on MutationRoot {
...rootFragment
mutationListField {
body
}
}
fragment rootFragment on MutationRoot {
mutationField {
Expand Down Expand Up @@ -107,7 +121,26 @@ describe('Validate: Defer/Stream directive on root field', () => {
},
]);
});

it('Defer fragment spread on root mutation field interface', () => {
expectErrors(`
mutation {
...rootFragment
}
fragment rootFragment on Root {
... @defer {
rootField {
body
}
}
}
`).toDeepEqual([
{
message:
'Defer directive cannot be used on root mutation type "MutationRoot".',
locations: [{ line: 6, column: 13 }],
},
]);
});
it('Defer fragment spread on nested mutation field', () => {
expectValid(`
mutation {
Expand All @@ -120,6 +153,26 @@ describe('Validate: Defer/Stream directive on root field', () => {
`);
});

it('Defer fragment spread on root subscription field interface', () => {
expectErrors(`
subscription {
...rootFragment
}
fragment rootFragment on Root {
... @defer {
rootField {
body
}
}
}
`).toDeepEqual([
{
message:
'Defer directive cannot be used on root subscription type "SubscriptionRoot".',
locations: [{ line: 6, column: 13 }],
},
]);
});
it('Defer fragment spread on root subscription field', () => {
expectErrors(`
subscription {
Expand Down
144 changes: 110 additions & 34 deletions src/validation/rules/DeferStreamDirectiveOnRootFieldRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,18 @@

import { GraphQLError } from '../../error/GraphQLError.ts';

import type {
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
OperationDefinitionNode,
OperationTypeNode,
SelectionSetNode,
} from '../../language/ast.ts';
import { Kind } from '../../language/kinds.ts';
import type { ASTVisitor } from '../../language/visitor.ts';

import type { GraphQLObjectType } from '../../type/definition.ts';
import {
GraphQLDeferDirective,
GraphQLStreamDirective,
Expand Down Expand Up @@ -47,46 +57,112 @@ export function DeferStreamDirectiveOnRootFieldRule(
context: ValidationContext,
): ASTVisitor {
return {
Directive(node) {
const mutationType = context.getSchema().getMutationType();
const subscriptionType = context.getSchema().getSubscriptionType();
const parentType = context.getParentType();
if (parentType && node.name.value === GraphQLDeferDirective.name) {
if (mutationType && parentType === mutationType) {
context.reportError(
new GraphQLError(
`Defer directive cannot be used on root mutation type "${parentType}".`,
{ nodes: node },
),
);
}
if (subscriptionType && parentType === subscriptionType) {
context.reportError(
new GraphQLError(
`Defer directive cannot be used on root subscription type "${parentType}".`,
{ nodes: node },
),
);
OperationDefinition(node: OperationDefinitionNode) {
const document = context.getDocument();
const fragments = new Map<string, FragmentDefinitionNode>();

for (const definition of document.definitions) {
if (definition.kind === Kind.FRAGMENT_DEFINITION) {
fragments.set(definition.name.value, definition);
}
}
if (parentType && node.name.value === GraphQLStreamDirective.name) {
if (mutationType && parentType === mutationType) {
context.reportError(
new GraphQLError(
`Stream directive cannot be used on root mutation type "${parentType}".`,
{ nodes: node },
),
);
}
if (subscriptionType && parentType === subscriptionType) {
if (node.operation !== 'subscription' && node.operation !== 'mutation') {
return;
}
const schema = context.getSchema();
const rootType = schema.getRootType(node.operation);
if (rootType) {
forbidDeferStream({
context,
operationType: node.operation,
rootType,
fragments,
selectionSet: node.selectionSet,
visitedFragments: new Set(),
});
}
},
};
}

function forbidDeferStream({
context,
operationType,
rootType,
fragments,
selectionSet,
visitedFragments,
}: {
context: ValidationContext;
operationType: OperationTypeNode;
rootType: GraphQLObjectType;
fragments: Map<string, FragmentDefinitionNode>;
selectionSet: SelectionSetNode;
visitedFragments: Set<string>;
}) {
for (const selection of selectionSet.selections) {
if (selection.kind === 'Field') {
const stream = selection.directives?.find(
(d) => d.name.value === GraphQLStreamDirective.name,
);
if (stream) {
context.reportError(
new GraphQLError(
`Stream directive cannot be used on root ${operationType} type "${rootType}".`,
{ nodes: stream },
),
);
}
} else if (selection.kind === 'FragmentSpread') {
const fragmentName = selection.name.value;
if (visitedFragments.has(fragmentName)) {
continue;
}
const fragment = fragments.get(fragmentName);
if (fragment) {
const defer = getDeferDirective(selection);
if (defer !== undefined) {
context.reportError(
new GraphQLError(
`Stream directive cannot be used on root subscription type "${parentType}".`,
{ nodes: node },
`Defer directive cannot be used on root ${operationType} type "${rootType}".`,
{ nodes: defer },
),
);
}
forbidDeferStream({
context,
operationType,
rootType,
fragments,
selectionSet: fragment.selectionSet,
visitedFragments,
});
}
},
};
visitedFragments.add(fragmentName);
} else if (selection.kind === 'InlineFragment') {
const defer = getDeferDirective(selection);
if (defer !== undefined) {
context.reportError(
new GraphQLError(
`Defer directive cannot be used on root ${operationType} type "${rootType}".`,
{ nodes: defer },
),
);
}
forbidDeferStream({
context,
operationType,
rootType,
fragments,
selectionSet: selection.selectionSet,
visitedFragments,
});
}
}
}

function getDeferDirective(fragment: FragmentSpreadNode | InlineFragmentNode) {
return fragment.directives?.find(
(d) => d.name.value === GraphQLDeferDirective.name,
);
}
Loading