Skip to content

Add support for defer and stream directives (Feedback is welcome) #2839

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

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
Implement support for @stream directive
# Conflicts:
#	src/execution/execute.ts
#	src/validation/index.d.ts
#	src/validation/index.ts
  • Loading branch information
robrichard committed Jun 23, 2022
commit 2abfffe8511dba23325ffbadcfa78b63c2981cd2
1,192 changes: 1,192 additions & 0 deletions src/execution/__tests__/stream-test.ts

Large diffs are not rendered by default.

360 changes: 341 additions & 19 deletions src/execution/execute.ts

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions src/validation/__tests__/DeferStreamDirectiveLabelRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,63 @@ describe('Validate: Defer/Stream directive on root field', () => {
},
]);
});
it('Defer and stream with no label', () => {
expectValid(`
{
dog {
...dogFragment @defer
}
pets @stream(initialCount: 0) @stream {
name
}
}
fragment dogFragment on Dog {
name
}
`);
});
it('Stream with variable label', () => {
expectErrors(`
query ($label: String!) {
dog {
...dogFragment @defer
}
pets @stream(initialCount: 0) @stream(label: $label) {
name
}
}
fragment dogFragment on Dog {
name
}
`).toDeepEqual([
{
message:
'Directive "stream"\'s label argument must be a static string.',
locations: [{ line: 6, column: 39 }],
},
]);
});
it('Defer and stream with the same label', () => {
expectErrors(`
{
dog {
...dogFragment @defer(label: "MyLabel")
}
pets @stream(initialCount: 0) @stream(label: "MyLabel") {
name
}
}
fragment dogFragment on Dog {
name
}
`).toDeepEqual([
{
message: 'Defer/Stream directive label argument must be unique.',
locations: [
{ line: 4, column: 26 },
{ line: 6, column: 39 },
],
},
]);
});
});
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 }],
},
]);
});
});
108 changes: 108 additions & 0 deletions src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,114 @@ 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', () => {
expectValid(`
fragment conflictingArgs on Dog {
name @stream
name @stream
}
`);
});

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 @@ -63,6 +63,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
Loading