-
Notifications
You must be signed in to change notification settings - Fork 254
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prevent over-eager merging of fields which have differing directive a…
…pplications (#2713) Fix over-eager merging of fields with different directive applications Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the @Skip condition: ```graphql query Test($skipField: Boolean!) { hello @Skip(if: $skipField) { world } hello { goodbye } } ``` This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is.
- Loading branch information
1 parent
aa5bd59
commit 35179f0
Showing
4 changed files
with
239 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
--- | ||
"@apollo/query-planner": patch | ||
"@apollo/federation-internals": patch | ||
--- | ||
|
||
Fix over-eager merging of fields with different directive applications | ||
|
||
Previously, the following query would incorrectly combine the selection set of `hello`, with both fields ending up under the @skip condition: | ||
```graphql | ||
query Test($skipField: Boolean!) { | ||
hello @skip(if: $skipField) { | ||
world | ||
} | ||
hello { | ||
goodbye | ||
} | ||
} | ||
``` | ||
|
||
This change identifies those two selections on `hello` as unique while constructing our operation representation so they aren't merged at all, leaving it to the subgraph to handle the operation as-is. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 192 additions & 0 deletions
192
query-planner-js/src/__tests__/buildPlan.directiveMerging.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import { operationFromDocument } from '@apollo/federation-internals'; | ||
import gql from 'graphql-tag'; | ||
import { composeAndCreatePlanner } from './testHelper'; | ||
|
||
describe('merging @skip / @include directives', () => { | ||
const subgraph1 = { | ||
name: 'S1', | ||
typeDefs: gql` | ||
type Query { | ||
hello: Hello! | ||
extraFieldToPreventSkipIncludeNodes: String! | ||
} | ||
type Hello { | ||
world: String! | ||
goodbye: String! | ||
} | ||
`, | ||
}; | ||
|
||
const [api, queryPlanner] = composeAndCreatePlanner(subgraph1); | ||
|
||
it('with fragment', () => { | ||
const operation = operationFromDocument( | ||
api, | ||
gql` | ||
query Test($skipField: Boolean!) { | ||
...ConditionalSkipFragment | ||
hello { | ||
world | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
fragment ConditionalSkipFragment on Query { | ||
hello @skip(if: $skipField) { | ||
goodbye | ||
} | ||
} | ||
`, | ||
); | ||
|
||
const plan = queryPlanner.buildQueryPlan(operation); | ||
expect(plan).toMatchInlineSnapshot(` | ||
QueryPlan { | ||
Fetch(service: "S1") { | ||
{ | ||
hello @skip(if: $skipField) { | ||
goodbye | ||
} | ||
hello { | ||
world | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
it('without fragment', () => { | ||
const operation = operationFromDocument( | ||
api, | ||
gql` | ||
query Test($skipField: Boolean!) { | ||
hello @skip(if: $skipField) { | ||
world | ||
} | ||
hello { | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
`, | ||
); | ||
|
||
const plan = queryPlanner.buildQueryPlan(operation); | ||
expect(plan).toMatchInlineSnapshot(` | ||
QueryPlan { | ||
Fetch(service: "S1") { | ||
{ | ||
hello @skip(if: $skipField) { | ||
world | ||
} | ||
hello { | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
it('multiple applications identical', () => { | ||
const operation = operationFromDocument( | ||
api, | ||
gql` | ||
query Test($skipField: Boolean!, $includeField: Boolean!) { | ||
hello @skip(if: $skipField) @include(if: $includeField) { | ||
world | ||
} | ||
hello @skip(if: $skipField) @include(if: $includeField) { | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
`, | ||
); | ||
|
||
const plan = queryPlanner.buildQueryPlan(operation); | ||
expect(plan).toMatchInlineSnapshot(` | ||
QueryPlan { | ||
Fetch(service: "S1") { | ||
{ | ||
hello @skip(if: $skipField) @include(if: $includeField) { | ||
world | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
it('multiple applications differing order', () => { | ||
const operation = operationFromDocument( | ||
api, | ||
gql` | ||
query Test($skipField: Boolean!, $includeField: Boolean!) { | ||
hello @skip(if: $skipField) @include(if: $includeField) { | ||
world | ||
} | ||
hello @include(if: $includeField) @skip(if: $skipField) { | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
`, | ||
); | ||
|
||
const plan = queryPlanner.buildQueryPlan(operation); | ||
expect(plan).toMatchInlineSnapshot(` | ||
QueryPlan { | ||
Fetch(service: "S1") { | ||
{ | ||
hello @include(if: $includeField) @skip(if: $skipField) { | ||
world | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
}, | ||
} | ||
`); | ||
}); | ||
|
||
it('multiple applications differing quantity', () => { | ||
const operation = operationFromDocument( | ||
api, | ||
gql` | ||
query Test($skipField: Boolean!, $includeField: Boolean!) { | ||
hello @skip(if: $skipField) @include(if: $includeField) { | ||
world | ||
} | ||
hello @include(if: $includeField) { | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
`, | ||
); | ||
|
||
const plan = queryPlanner.buildQueryPlan(operation); | ||
expect(plan).toMatchInlineSnapshot(` | ||
QueryPlan { | ||
Fetch(service: "S1") { | ||
{ | ||
hello @skip(if: $skipField) @include(if: $includeField) { | ||
world | ||
} | ||
hello @include(if: $includeField) { | ||
goodbye | ||
} | ||
extraFieldToPreventSkipIncludeNodes | ||
} | ||
}, | ||
} | ||
`); | ||
}); | ||
}); |