Skip to content

Commit c9bbcda

Browse files
committed
Add anonymizer to make anonymous version of a schema and queries for that schema
1 parent aa0dacb commit c9bbcda

3 files changed

Lines changed: 378 additions & 0 deletions

File tree

src/main/java/graphql/Directives.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,12 @@ private static Description createDescription(String s) {
119119
return new Description(s, null, false);
120120
}
121121

122+
public static boolean isBuiltInDirective(GraphQLDirective graphQLDirective) {
123+
//TODO: This should be working by just `==` but it doesn't: we create somehow new directives of the build ins
124+
return SpecifiedByDirective.getName().equals(graphQLDirective.getName()) ||
125+
DeprecatedDirective.getName().equals(graphQLDirective.getName()) ||
126+
SkipDirective == graphQLDirective ||
127+
IncludeDirective == graphQLDirective;
128+
}
129+
122130
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
package graphql.util;
2+
3+
import graphql.Assert;
4+
import graphql.Directives;
5+
import graphql.analysis.QueryTraverser;
6+
import graphql.analysis.QueryVisitor;
7+
import graphql.analysis.QueryVisitorFieldEnvironment;
8+
import graphql.analysis.QueryVisitorFragmentSpreadEnvironment;
9+
import graphql.analysis.QueryVisitorInlineFragmentEnvironment;
10+
import graphql.introspection.Introspection;
11+
import graphql.language.AstPrinter;
12+
import graphql.language.AstTransformer;
13+
import graphql.language.Document;
14+
import graphql.language.Field;
15+
import graphql.language.FragmentDefinition;
16+
import graphql.language.FragmentSpread;
17+
import graphql.language.Node;
18+
import graphql.language.NodeVisitorStub;
19+
import graphql.language.TypeName;
20+
import graphql.parser.Parser;
21+
import graphql.schema.GraphQLArgument;
22+
import graphql.schema.GraphQLDirective;
23+
import graphql.schema.GraphQLEnumType;
24+
import graphql.schema.GraphQLEnumValueDefinition;
25+
import graphql.schema.GraphQLFieldDefinition;
26+
import graphql.schema.GraphQLInputObjectField;
27+
import graphql.schema.GraphQLInputObjectType;
28+
import graphql.schema.GraphQLInterfaceType;
29+
import graphql.schema.GraphQLNamedSchemaElement;
30+
import graphql.schema.GraphQLObjectType;
31+
import graphql.schema.GraphQLScalarType;
32+
import graphql.schema.GraphQLSchema;
33+
import graphql.schema.GraphQLSchemaElement;
34+
import graphql.schema.GraphQLType;
35+
import graphql.schema.GraphQLTypeVisitorStub;
36+
import graphql.schema.GraphQLUnionType;
37+
import graphql.schema.SchemaTransformer;
38+
import graphql.schema.idl.ScalarInfo;
39+
40+
import java.util.ArrayList;
41+
import java.util.Collections;
42+
import java.util.LinkedHashMap;
43+
import java.util.List;
44+
import java.util.Map;
45+
import java.util.concurrent.atomic.AtomicInteger;
46+
47+
import static graphql.util.TreeTransformerUtil.changeNode;
48+
49+
public class Anonymizer {
50+
51+
public static class AnonymizeResult {
52+
private GraphQLSchema schema;
53+
private List<String> queries;
54+
55+
public AnonymizeResult(GraphQLSchema schema, List<String> queries) {
56+
this.schema = schema;
57+
this.queries = queries;
58+
}
59+
60+
public GraphQLSchema getSchema() {
61+
return schema;
62+
}
63+
64+
public List<String> getQueries() {
65+
return queries;
66+
}
67+
}
68+
69+
public static AnonymizeResult anonymizeSchemaAndQueries(GraphQLSchema schema, List<String> queries) {
70+
return anonymizeSchemaAndQueries(schema, queries, Collections.emptyMap());
71+
}
72+
73+
public static AnonymizeResult anonymizeSchemaAndQueries(GraphQLSchema schema, List<String> queries, Map<String, Object> variables) {
74+
75+
AtomicInteger objectCounter = new AtomicInteger(1);
76+
AtomicInteger inputObjectCounter = new AtomicInteger(1);
77+
AtomicInteger inputObjectFieldCounter = new AtomicInteger(1);
78+
AtomicInteger fieldCounter = new AtomicInteger(1);
79+
AtomicInteger scalarCounter = new AtomicInteger(1);
80+
AtomicInteger directiveCounter = new AtomicInteger(1);
81+
AtomicInteger argumentCounter = new AtomicInteger(1);
82+
AtomicInteger interfaceCounter = new AtomicInteger(1);
83+
AtomicInteger unionCounter = new AtomicInteger(1);
84+
AtomicInteger enumCounter = new AtomicInteger(1);
85+
AtomicInteger enumValueCounter = new AtomicInteger(1);
86+
Map<GraphQLNamedSchemaElement, String> newNameMap = new LinkedHashMap<>();
87+
88+
SchemaTransformer schemaTransformer = new SchemaTransformer();
89+
GraphQLSchema newSchema = schemaTransformer.transform(schema, new GraphQLTypeVisitorStub() {
90+
@Override
91+
public TraversalControl visitGraphQLArgument(GraphQLArgument graphQLArgument, TraverserContext<GraphQLSchemaElement> context) {
92+
String newName = "argument" + argumentCounter.getAndIncrement();
93+
newNameMap.put(graphQLArgument, newName);
94+
GraphQLArgument newElement = graphQLArgument.transform(builder -> {
95+
builder.name(newName);
96+
});
97+
return changeNode(context, newElement);
98+
}
99+
100+
@Override
101+
public TraversalControl visitGraphQLInterfaceType(GraphQLInterfaceType graphQLInterfaceType, TraverserContext<GraphQLSchemaElement> context) {
102+
if (Introspection.isIntrospectionTypes(graphQLInterfaceType)) {
103+
return TraversalControl.ABORT;
104+
}
105+
String newName = "Interface" + interfaceCounter.getAndIncrement();
106+
newNameMap.put(graphQLInterfaceType, newName);
107+
GraphQLInterfaceType newElement = graphQLInterfaceType.transform(builder -> {
108+
builder.name(newName);
109+
});
110+
return changeNode(context, newElement);
111+
}
112+
113+
@Override
114+
public TraversalControl visitGraphQLEnumType(GraphQLEnumType graphQLEnumType, TraverserContext<GraphQLSchemaElement> context) {
115+
if (Introspection.isIntrospectionTypes(graphQLEnumType)) {
116+
return TraversalControl.ABORT;
117+
}
118+
String newName = "Enum" + enumCounter.getAndIncrement();
119+
newNameMap.put(graphQLEnumType, newName);
120+
GraphQLEnumType newElement = graphQLEnumType.transform(builder -> {
121+
builder.name(newName);
122+
});
123+
return changeNode(context, newElement);
124+
}
125+
126+
@Override
127+
public TraversalControl visitGraphQLEnumValueDefinition(GraphQLEnumValueDefinition enumValueDefinition, TraverserContext<GraphQLSchemaElement> context) {
128+
String newName = "EnumValue" + enumValueCounter.getAndIncrement();
129+
newNameMap.put(enumValueDefinition, newName);
130+
GraphQLEnumValueDefinition newElement = enumValueDefinition.transform(builder -> {
131+
builder.name(newName);
132+
});
133+
return changeNode(context, newElement);
134+
}
135+
136+
@Override
137+
public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition graphQLFieldDefinition, TraverserContext<GraphQLSchemaElement> context) {
138+
String newName = "field" + fieldCounter.getAndIncrement();
139+
newNameMap.put(graphQLFieldDefinition, newName);
140+
GraphQLFieldDefinition newElement = graphQLFieldDefinition.transform(builder -> {
141+
builder.name(newName);
142+
});
143+
return changeNode(context, newElement);
144+
}
145+
146+
@Override
147+
public TraversalControl visitGraphQLDirective(GraphQLDirective graphQLDirective, TraverserContext<GraphQLSchemaElement> context) {
148+
if (Directives.isBuiltInDirective(graphQLDirective)) {
149+
return TraversalControl.ABORT;
150+
}
151+
String newName = "Directive" + directiveCounter.getAndIncrement();
152+
newNameMap.put(graphQLDirective, newName);
153+
GraphQLDirective newElement = graphQLDirective.transform(builder -> {
154+
builder.name(newName);
155+
});
156+
return changeNode(context, newElement);
157+
}
158+
159+
@Override
160+
public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField graphQLInputObjectField, TraverserContext<GraphQLSchemaElement> context) {
161+
String newName = "InputField" + inputObjectFieldCounter.getAndIncrement();
162+
newNameMap.put(graphQLInputObjectField, newName);
163+
GraphQLInputObjectField newElement = graphQLInputObjectField.transform(builder -> {
164+
builder.name(newName);
165+
});
166+
return changeNode(context, newElement);
167+
}
168+
169+
@Override
170+
public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType graphQLInputObjectType, TraverserContext<GraphQLSchemaElement> context) {
171+
if (Introspection.isIntrospectionTypes(graphQLInputObjectType)) {
172+
return TraversalControl.ABORT;
173+
}
174+
String newName = "InputObject" + inputObjectCounter.getAndIncrement();
175+
newNameMap.put(graphQLInputObjectType, newName);
176+
GraphQLInputObjectType newElement = graphQLInputObjectType.transform(builder -> {
177+
builder.name(newName);
178+
});
179+
return changeNode(context, newElement);
180+
}
181+
182+
183+
@Override
184+
public TraversalControl visitGraphQLObjectType(GraphQLObjectType graphQLObjectType, TraverserContext<GraphQLSchemaElement> context) {
185+
if (Introspection.isIntrospectionTypes(graphQLObjectType)) {
186+
return TraversalControl.ABORT;
187+
}
188+
String newName = "Object" + objectCounter.getAndIncrement();
189+
newNameMap.put(graphQLObjectType, newName);
190+
GraphQLObjectType newElement = graphQLObjectType.transform(builder -> {
191+
builder.name(newName);
192+
});
193+
return changeNode(context, newElement);
194+
}
195+
196+
@Override
197+
public TraversalControl visitGraphQLScalarType(GraphQLScalarType graphQLScalarType, TraverserContext<GraphQLSchemaElement> context) {
198+
if (ScalarInfo.isGraphqlSpecifiedScalar(graphQLScalarType)) {
199+
return TraversalControl.ABORT;
200+
}
201+
String newName = "Scalar" + scalarCounter.getAndIncrement();
202+
newNameMap.put(graphQLScalarType, newName);
203+
GraphQLScalarType newElement = graphQLScalarType.transform(builder -> {
204+
builder.name(newName);
205+
});
206+
return changeNode(context, newElement);
207+
}
208+
209+
@Override
210+
public TraversalControl visitGraphQLUnionType(GraphQLUnionType graphQLUnionType, TraverserContext<GraphQLSchemaElement> context) {
211+
if (Introspection.isIntrospectionTypes(graphQLUnionType)) {
212+
return TraversalControl.ABORT;
213+
}
214+
String newName = "Union" + unionCounter.getAndIncrement();
215+
newNameMap.put(graphQLUnionType, newName);
216+
GraphQLUnionType newElement = graphQLUnionType.transform(builder -> {
217+
builder.name(newName);
218+
});
219+
return changeNode(context, newElement);
220+
}
221+
});
222+
223+
List<String> newQueries = new ArrayList<>();
224+
for (String query : queries) {
225+
String newQuery = rewriteQuery(query, schema, newNameMap, variables);
226+
newQueries.add(newQuery);
227+
}
228+
AnonymizeResult result = new AnonymizeResult(newSchema, newQueries);
229+
return result;
230+
}
231+
232+
private static String rewriteQuery(String query, GraphQLSchema schema, Map<GraphQLNamedSchemaElement, String> newNames, Map<String, Object> variables) {
233+
AtomicInteger fragmentCounter = new AtomicInteger(1);
234+
Map<Node, String> nodeToNewName = new LinkedHashMap<>();
235+
Document document = new Parser().parseDocument(query);
236+
QueryTraverser queryTraverser = QueryTraverser.newQueryTraverser().document(document).schema(schema).variables(variables).build();
237+
queryTraverser.visitDepthFirst(new QueryVisitor() {
238+
@Override
239+
public void visitField(QueryVisitorFieldEnvironment queryVisitorFieldEnvironment) {
240+
String newName = Assert.assertNotNull(newNames.get(queryVisitorFieldEnvironment.getFieldDefinition()));
241+
nodeToNewName.put(queryVisitorFieldEnvironment.getField(), newName);
242+
}
243+
244+
@Override
245+
public void visitInlineFragment(QueryVisitorInlineFragmentEnvironment queryVisitorInlineFragmentEnvironment) {
246+
247+
}
248+
249+
@Override
250+
public void visitFragmentSpread(QueryVisitorFragmentSpreadEnvironment queryVisitorFragmentSpreadEnvironment) {
251+
FragmentDefinition fragmentDefinition = queryVisitorFragmentSpreadEnvironment.getFragmentDefinition();
252+
String newName;
253+
if (!nodeToNewName.containsKey(fragmentDefinition)) {
254+
newName = "Fragment" + fragmentCounter.getAndIncrement();
255+
nodeToNewName.put(fragmentDefinition, newName);
256+
} else {
257+
newName = nodeToNewName.get(fragmentDefinition);
258+
}
259+
nodeToNewName.put(queryVisitorFragmentSpreadEnvironment.getFragmentSpread(), newName);
260+
}
261+
262+
});
263+
264+
AstTransformer astTransformer = new AstTransformer();
265+
Document newDocument = (Document) astTransformer.transform(document, new NodeVisitorStub() {
266+
267+
@Override
268+
public TraversalControl visitField(Field node, TraverserContext<Node> context) {
269+
String newName = Assert.assertNotNull(nodeToNewName.get(node));
270+
return changeNode(context, node.transform(builder -> builder.name(newName)));
271+
}
272+
273+
@Override
274+
public TraversalControl visitFragmentDefinition(FragmentDefinition node, TraverserContext<Node> context) {
275+
String newName = Assert.assertNotNull(nodeToNewName.get(node));
276+
GraphQLType currentCondition = Assert.assertNotNull(schema.getType(node.getTypeCondition().getName()));
277+
String newCondition = newNames.get(currentCondition);
278+
return changeNode(context, node.transform(builder -> builder.name(newName).typeCondition(new TypeName(newCondition))));
279+
}
280+
281+
@Override
282+
public TraversalControl visitFragmentSpread(FragmentSpread node, TraverserContext<Node> context) {
283+
String newName = Assert.assertNotNull(nodeToNewName.get(node));
284+
return changeNode(context, node.transform(builder -> builder.name(newName)));
285+
}
286+
});
287+
return AstPrinter.printAstCompact(newDocument)
288+
;
289+
}
290+
291+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package graphql.util
2+
3+
import graphql.TestUtil
4+
import graphql.schema.idl.SchemaPrinter
5+
import spock.lang.Specification
6+
7+
class AnonymizerTest extends Specification {
8+
9+
def "simple schema and query"() {
10+
given:
11+
def schema = TestUtil.schema("""
12+
type Query {
13+
foo: Foo
14+
}
15+
type Foo {
16+
bar1: String
17+
bar2: ID
18+
}
19+
""")
20+
def query = "{foo{bar1 bar2}}"
21+
22+
when:
23+
def result = Anonymizer.anonymizeSchemaAndQueries(schema, [query])
24+
def newSchema = new SchemaPrinter(SchemaPrinter.Options.defaultOptions().includeDirectiveDefinitions(false)).print(result.schema)
25+
def newQuery = result.queries[0]
26+
27+
then:
28+
newSchema == """schema {
29+
query: Object1
30+
}
31+
32+
type Object1 {
33+
field1: Object2
34+
}
35+
36+
type Object2 {
37+
field2: String
38+
field3: ID
39+
}
40+
"""
41+
newQuery == "query {field1 {field2 field3}}"
42+
}
43+
44+
def "query with fragments"() {
45+
given:
46+
def schema = TestUtil.schema("""
47+
type Query {
48+
foo: Foo
49+
}
50+
type Foo {
51+
bar1: String
52+
bar2: ID
53+
}
54+
""")
55+
def query = "{...MyFragment foo {bar1 bar2}} fragment MyFragment on Query{foo {bar1 bar2 }}"
56+
57+
when:
58+
def result = Anonymizer.anonymizeSchemaAndQueries(schema, [query])
59+
def newSchema = new SchemaPrinter(SchemaPrinter.Options.defaultOptions().includeDirectiveDefinitions(false)).print(result.schema)
60+
def newQuery = result.queries[0]
61+
62+
then:
63+
newSchema == """schema {
64+
query: Object1
65+
}
66+
67+
type Object1 {
68+
field1: Object2
69+
}
70+
71+
type Object2 {
72+
field2: String
73+
field3: ID
74+
}
75+
"""
76+
newQuery == "query {...Fragment1 field1 {field2 field3}} fragment Fragment1 on Object1 {field1 {field2 field3}}"
77+
78+
}
79+
}

0 commit comments

Comments
 (0)