Skip to content

Commit

Permalink
Implement support for @stream directive
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/execution/execute.ts
#	src/validation/index.d.ts
#	src/validation/index.ts
  • Loading branch information
robrichard committed Dec 30, 2021
1 parent 78a31b3 commit b2df00a
Show file tree
Hide file tree
Showing 11 changed files with 1,696 additions and 23 deletions.
991 changes: 991 additions & 0 deletions src/execution/__tests__/stream-test.ts

Large diffs are not rendered by default.

312 changes: 291 additions & 21 deletions src/execution/execute.ts

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const schema = buildSchema(`
type QueryRoot {
message: Message
messages: [Message]
}
schema {
Expand Down Expand Up @@ -167,4 +168,91 @@ describe('Validate: Defer/Stream directive on root field', () => {
}
`);
});
it('Stream field on root query field', () => {
expectValid(`
{
messages @stream {
name
}
}
`);
});
it('Stream field on fragment on root query field', () => {
expectValid(`
{
...rootFragment
}
fragment rootFragment on QueryType {
messages @stream {
name
}
}
`);
});
it('Stream field on root mutation field', () => {
expectErrors(`
mutation {
mutationListField @stream {
name
}
}
`).toDeepEqual([
{
message:
'Stream directive cannot be used on root mutation type "MutationRoot".',
locations: [{ line: 3, column: 27 }],
},
]);
});
it('Stream field on fragment on root mutation field', () => {
expectErrors(`
mutation {
...rootFragment
}
fragment rootFragment on MutationRoot {
mutationListField @stream {
name
}
}
`).toDeepEqual([
{
message:
'Stream directive cannot be used on root mutation type "MutationRoot".',
locations: [{ line: 6, column: 27 }],
},
]);
});
it('Stream field on root subscription field', () => {
expectErrors(`
subscription {
subscriptionListField @stream {
name
}
}
`).toDeepEqual([
{
message:
'Stream directive cannot be used on root subscription type "SubscriptionRoot".',
locations: [{ line: 3, column: 31 }],
},
]);
});
it('Stream field on fragment on root subscription field', () => {
expectErrors(`
subscription {
...rootFragment
}
fragment rootFragment on SubscriptionRoot {
subscriptionListField @stream {
name
}
}
`).toDeepEqual([
{
message:
'Stream directive cannot be used on root subscription type "SubscriptionRoot".',
locations: [{ line: 6, column: 31 }],
},
]);
});
});
117 changes: 117 additions & 0 deletions src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,123 @@ describe('Validate: Overlapping fields can be merged', () => {
`);
});

it('Same stream directives supported', () => {
expectValid(`
fragment differentDirectivesWithDifferentAliases on Dog {
name @stream(label: "streamLabel", initialCount: 1)
name @stream(label: "streamLabel", initialCount: 1)
}
`);
});

it('different stream directive label', () => {
expectErrors(`
fragment conflictingArgs on Dog {
name @stream(label: "streamLabel", initialCount: 1)
name @stream(label: "anotherLabel", initialCount: 1)
}
`).toDeepEqual([
{
message:
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 3, column: 9 },
{ line: 4, column: 9 },
],
},
]);
});

it('different stream directive initialCount', () => {
expectErrors(`
fragment conflictingArgs on Dog {
name @stream(label: "streamLabel", initialCount: 1)
name @stream(label: "streamLabel", initialCount: 2)
}
`).toDeepEqual([
{
message:
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 3, column: 9 },
{ line: 4, column: 9 },
],
},
]);
});

it('different stream directive first missing args', () => {
expectErrors(`
fragment conflictingArgs on Dog {
name @stream
name @stream(label: "streamLabel", initialCount: 1)
}
`).toDeepEqual([
{
message:
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 3, column: 9 },
{ line: 4, column: 9 },
],
},
]);
});

it('different stream directive second missing args', () => {
expectErrors(`
fragment conflictingArgs on Dog {
name @stream(label: "streamLabel", initialCount: 1)
name @stream
}
`).toDeepEqual([
{
message:
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 3, column: 9 },
{ line: 4, column: 9 },
],
},
]);
});

it('mix of stream and no stream', () => {
expectErrors(`
fragment conflictingArgs on Dog {
name @stream
name
}
`).toDeepEqual([
{
message:
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 3, column: 9 },
{ line: 4, column: 9 },
],
},
]);
});

it('different stream directive both missing args', () => {
expectErrors(`
fragment conflictingArgs on Dog {
name @stream
name @stream
}
`).toDeepEqual([
{
message:
'Fields "name" conflict because they have differing stream directives. Use different aliases on the fields to fetch both if this was intentional.',
locations: [
{ line: 3, column: 9 },
{ line: 4, column: 9 },
],
},
]);
});

it('Same aliases with different field targets', () => {
expectErrors(`
fragment sameAliasesWithDifferentFieldTargets on Dog {
Expand Down
79 changes: 79 additions & 0 deletions src/validation/__tests__/StreamDirectiveOnListFieldRule-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { describe, it } from 'mocha';

import { StreamDirectiveOnListFieldRule } from '../rules/StreamDirectiveOnListFieldRule';

import { expectValidationErrors } from './harness';

function expectErrors(queryStr: string) {
return expectValidationErrors(StreamDirectiveOnListFieldRule, queryStr);
}

function expectValid(queryStr: string) {
expectErrors(queryStr).toDeepEqual([]);
}

describe('Validate: Stream directive on list field', () => {
it('Stream on list field', () => {
expectValid(`
fragment objectFieldSelection on Human {
pets @stream(initialCount: 0) {
name
}
}
`);
});

it('Stream on non-null list field', () => {
expectValid(`
fragment objectFieldSelection on Human {
relatives @stream(initialCount: 0) {
name
}
}
`);
});

it("Doesn't validate other directives on list fields", () => {
expectValid(`
fragment objectFieldSelection on Human {
pets @include(if: true) {
name
}
}
`);
});

it("Doesn't validate other directives on non-list fields", () => {
expectValid(`
fragment objectFieldSelection on Human {
pets {
name @include(if: true)
}
}
`);
});

it("Doesn't validate misplaced stream directives", () => {
expectValid(`
fragment objectFieldSelection on Human {
... @stream(initialCount: 0) {
name
}
}
`);
});

it('reports errors when stream is used on non-list field', () => {
expectErrors(`
fragment objectFieldSelection on Human {
name @stream(initialCount: 0)
}
`).toDeepEqual([
{
message:
'Stream directive cannot be used on non-list field "name" on type "Human".',
locations: [{ line: 3, column: 14 }],
},
]);
});
});
2 changes: 1 addition & 1 deletion src/validation/__tests__/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const testSchema: GraphQLSchema = buildSchema(`
type Human {
name(surname: Boolean): String
pets: [Pet]
relatives: [Human]
relatives: [Human]!
}
enum FurColor {
Expand Down
3 changes: 3 additions & 0 deletions src/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ export { ScalarLeafsRule } from './rules/ScalarLeafsRule';
/** Spec Section: "Subscriptions with Single Root Field" */
export { SingleFieldSubscriptionsRule } from './rules/SingleFieldSubscriptionsRule';

/** Spec Section: "Stream Directives Are Used On List Fields" */
export { StreamDirectiveOnListFieldRule } from './rules/StreamDirectiveOnListFieldRule';

/** Spec Section: "Argument Uniqueness" */
export { UniqueArgumentNamesRule } from './rules/UniqueArgumentNamesRule';

Expand Down
23 changes: 22 additions & 1 deletion src/validation/rules/DeferStreamDirectiveOnRootFieldRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { GraphQLError } from '../../error/GraphQLError';

import type { ASTVisitor } from '../../language/visitor';

import { GraphQLDeferDirective } from '../../type/directives';
import {
GraphQLDeferDirective,
GraphQLStreamDirective,
} from '../../type/directives';

import type { ValidationContext } from '../ValidationContext';

Expand Down Expand Up @@ -37,6 +40,24 @@ export function DeferStreamDirectiveOnRootFieldRule(
);
}
}
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.name}".`,
node,
),
);
}
if (subscriptionType && parentType === subscriptionType) {
context.reportError(
new GraphQLError(
`Stream directive cannot be used on root subscription type "${parentType.name}".`,
node,
),
);
}
}
},
};
}
Loading

0 comments on commit b2df00a

Please sign in to comment.