Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- `apollo-graphql`
- <First `apollo-graphql` related entry goes here>
- `apollo-language-server`
- <First `apollo-language-server` related entry goes here>
- Fix issue where fragment definitions only included in `@client` fields would not be stripped ((AP-682)(https://golinks.io/AP-682), [#1454](https://github.com/apollographql/apollo-tooling/pull/1454))
- `apollo-tools`
- <First `apollo-tools` related entry goes here>
- `vscode-apollo`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import gql from "graphql-tag";
import { print } from "graphql";
import { withTypenameFieldAddedWhereNeeded } from "../graphql";
import { parse, print } from "graphql";
import {
withTypenameFieldAddedWhereNeeded,
removeDirectiveAnnotatedFields
} from "../graphql";

describe("withTypenameFieldAddedWhereNeeded", () => {
it("properly adds __typename to each selectionSet", () => {
Expand Down Expand Up @@ -73,3 +76,117 @@ describe("withTypenameFieldAddedWhereNeeded", () => {
`);
});
});

describe("removeDirectiveAnnotatedFields", () => {
it("should be a function", () => {
Comment thread
justinanastos marked this conversation as resolved.
Outdated
expect(typeof removeDirectiveAnnotatedFields).toBe("function");
});

it("should remove fields with matching directives", () => {
expect(
print(
removeDirectiveAnnotatedFields(
parse(`query Query { fieldToKeep fieldToRemove @client }`),
["client"]
)
)
).toMatchInlineSnapshot(`
"query Query {
fieldToKeep
}
"
`);
});

it("should remove object fields with matching directives", () => {
Comment thread
justinanastos marked this conversation as resolved.
Outdated
expect(
print(
removeDirectiveAnnotatedFields(
parse(`
query Query {
fieldToKeep
fieldToRemove @client {
childField
}
}
`),
["client"]
)
)
).toMatchInlineSnapshot(`
"query Query {
fieldToKeep
}
"
`);
});

it("should remove fragments that become unused when antecendant directives are removed", () => {
Comment thread
justinanastos marked this conversation as resolved.
expect(
print(
removeDirectiveAnnotatedFields(
parse(`
fragment ClientObjectFragment on ClientObject {
string
number
}

fragment LaunchTile on Launch {
__typename
id
isBooked
rocket {
id
name
}
mission {
name
missionPatch
}
}

query LaunchDetails($launchId: ID!) {
launch(id: $launchId) {
isInCart @client
clientObject @client {
...ClientObjectFragment
}
site
rocket {
type
}
...LaunchTile
}
}
`),
["client"]
)
)
).toMatchInlineSnapshot(`
"fragment LaunchTile on Launch {
__typename
id
isBooked
rocket {
id
name
}
mission {
name
missionPatch
}
}

query LaunchDetails($launchId: ID!) {
launch(id: $launchId) {
site
rocket {
type
}
...LaunchTile
}
}
"
`);
});
});
37 changes: 33 additions & 4 deletions packages/apollo-language-server/src/utilities/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,22 @@ export function removeDirectives(ast: ASTNode, directiveNames: string[]) {
}

// remove fields where a given directive is found
export function removeDirectiveAnnotatedFields(
ast: ASTNode,
export function removeDirectiveAnnotatedFields<AST extends ASTNode>(
Comment thread
justinanastos marked this conversation as resolved.
ast: AST,
directiveNames: string[]
) {
): AST {
if (!directiveNames.length) return ast;
return visit(ast, {

/**
* List of names of all the `FragmentDefinition`s that we are going to keep.
*
* We store the names that we want to _keep_ instead of fields we want to _delete_ because it's possible
* that we're removing a `Fragment` from a `@client` field that is used in a non-`@client` field (if it's a
* fragment on an interface, for example).
*/
const fragmentNamesToKeep = new Set<string>();

ast = visit(ast, {
Field(node: FieldNode): FieldNode | null {
if (
node.directives &&
Expand All @@ -103,6 +113,18 @@ export function removeDirectiveAnnotatedFields(
)
)
return null;

// We're keeping this field. Save the names of all fragment spreads that are in this field's selection
// set. This is intentionally _not_ recursive because descendents of this field may a directive that we
// want to remove; so we only know that this field's selection set is safe to keep.
if (node.selectionSet) {
node.selectionSet.selections.forEach(selection => {
Comment thread
justinanastos marked this conversation as resolved.
Outdated
if (selection.kind === "FragmentSpread") {
fragmentNamesToKeep.add(selection.name.value);
}
});
}

return node;
},
OperationDefinition: {
Expand All @@ -112,6 +134,13 @@ export function removeDirectiveAnnotatedFields(
}
}
});

// Remove all `FragmentDefinitions`s that have names _not_ stored in `fragmentNamesToKeep`.
return visit(ast, {
FragmentDefinition(node) {
return fragmentNamesToKeep.has(node.name.value) ? node : null;
}
});
}

const typenameField = {
Expand Down