Skip to content

Commit 1297e55

Browse files
author
Carlo Feliciano Aureus
committed
Accept @authzPolicy directive
1 parent a7fa55a commit 1297e55

File tree

8 files changed

+755
-4
lines changed

8 files changed

+755
-4
lines changed

src/main/java/com/intuit/graphql/orchestrator/schema/type/conflict/resolver/XtextTypeConflictResolver.java

+18
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
import static com.intuit.graphql.orchestrator.resolverdirective.FieldResolverDirectiveUtil.RESOLVER_ARGUMENT_INPUT_NAME;
44
import static com.intuit.graphql.orchestrator.utils.FederationConstants.FEDERATION_EXTENDS_DIRECTIVE;
55
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.checkFieldsCompatibility;
6+
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.checkInputObjectTypeCompatibility;
67
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isEntity;
78
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInaccessible;
9+
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isInputObjectType;
810
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.isScalarType;
911
import static com.intuit.graphql.orchestrator.utils.XtextTypeUtils.toDescriptiveString;
1012
import static com.intuit.graphql.orchestrator.utils.XtextUtils.definitionContainsDirective;
1113

1214
import com.intuit.graphql.graphQL.EnumTypeDefinition;
15+
import com.intuit.graphql.graphQL.InputObjectTypeDefinition;
1316
import com.intuit.graphql.graphQL.InterfaceTypeDefinition;
1417
import com.intuit.graphql.graphQL.ObjectTypeDefinition;
1518
import com.intuit.graphql.graphQL.TypeDefinition;
@@ -38,6 +41,13 @@ public void resolve(final TypeDefinition conflictingType, final TypeDefinition e
3841
}
3942

4043
private void checkSameType(final TypeDefinition conflictingType, final TypeDefinition existingType) {
44+
boolean isInputObjectTypeComparison = isInputObjectType(conflictingType) || isInputObjectType(existingType);
45+
if (isInputObjectTypeComparison) {
46+
// need before checkFieldsCompatibility which supports fieldContainers. InputObjectTypeComparison does not have field containers
47+
checkInputObjectTypeCompatibility(existingType, conflictingType);
48+
return;
49+
}
50+
4151
if (!(isSameType(conflictingType, existingType) && isScalarType(conflictingType))) {
4252
throw new TypeConflictException(
4353
String.format("Type %s is conflicting with existing type %s", toDescriptiveString(conflictingType),
@@ -52,6 +62,7 @@ private void checkSharedType(final TypeDefinition conflictingType, final TypeDef
5262
boolean entityComparison = conflictingTypeisEntity && existingTypeIsEntity;
5363
boolean isInaccessibleComparison = isInaccessible(conflictingType) || isInaccessible(existingType);
5464
boolean baseExtensionComparison = definitionContainsDirective(existingType, FEDERATION_EXTENDS_DIRECTIVE) || definitionContainsDirective(conflictingType, FEDERATION_EXTENDS_DIRECTIVE);
65+
boolean isInputObjectTypeComparison = isInputObjectType(conflictingType) || isInputObjectType(existingType);
5566

5667
if(!isInaccessibleComparison) {
5768
if(isEntity(conflictingType) != isEntity(existingType)) {
@@ -67,6 +78,13 @@ private void checkSharedType(final TypeDefinition conflictingType, final TypeDef
6778
toDescriptiveString(existingType)));
6879
}
6980

81+
if (isInputObjectTypeComparison) {
82+
// need before checkFieldsCompatibility which supports fieldContainers. InputObjectTypeComparison does not have field containers
83+
checkInputObjectTypeCompatibility(existingType, conflictingType);
84+
return;
85+
86+
}
87+
7088
if(!(conflictingType instanceof UnionTypeDefinition || isScalarType(conflictingType) || conflictingType instanceof EnumTypeDefinition)) {
7189
checkFieldsCompatibility(existingType, conflictingType, existingTypeIsEntity, conflictingTypeisEntity,federatedComparison);
7290
}

src/main/java/com/intuit/graphql/orchestrator/utils/XtextTypeUtils.java

+56
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,58 @@ public static String toDescriptiveString(ArgumentsDefinition argumentsDefinition
211211
return StringUtils.EMPTY;
212212
}
213213

214+
public static void checkInputObjectTypeCompatibility(TypeDefinition existingType, TypeDefinition incomingType) {
215+
if (!(existingType instanceof InputObjectTypeDefinition)) {
216+
throw new TypeConflictException(
217+
format("Type %s is conflicting with Input Type %s. Both types must be of the same type",
218+
toDescriptiveString(existingType),
219+
toDescriptiveString(incomingType)
220+
)
221+
);
222+
}
223+
224+
if (!(incomingType instanceof InputObjectTypeDefinition)) {
225+
throw new TypeConflictException(
226+
format("Type %s is conflicting with Input Type %s. Both types must be of the same type",
227+
toDescriptiveString(incomingType),
228+
toDescriptiveString(existingType)
229+
)
230+
);
231+
}
232+
233+
234+
InputObjectTypeDefinition existingTypeDefinition = (InputObjectTypeDefinition) existingType;
235+
InputObjectTypeDefinition incomingTypeDefinition = (InputObjectTypeDefinition) incomingType;
236+
237+
if (existingTypeDefinition.getInputValueDefinition().size() != incomingTypeDefinition.getInputValueDefinition().size()) {
238+
throw new TypeConflictException(
239+
format("Type %s is conflicting with Input Type %s. Both types must be of the same size",
240+
toDescriptiveString(incomingType),
241+
toDescriptiveString(existingType)
242+
)
243+
);
244+
}
245+
246+
incomingTypeDefinition.getInputValueDefinition()
247+
.forEach(incomingInputValueDefinition -> {
248+
boolean found = existingTypeDefinition.getInputValueDefinition()
249+
.stream()
250+
.anyMatch(existingTnputValueDefinition -> StringUtils.equals(incomingInputValueDefinition.getName(),
251+
existingTnputValueDefinition.getName()));
252+
253+
if (!found) {
254+
throw new TypeConflictException(
255+
format("Type %s is conflicting with Input Type %s. Both types much have the same InputValueDefinition",
256+
toDescriptiveString(incomingType),
257+
toDescriptiveString(existingType)
258+
)
259+
);
260+
}
261+
262+
});
263+
//
264+
}
265+
214266
public static void checkFieldsCompatibility(final TypeDefinition existingTypeDefinition, final TypeDefinition conflictingTypeDefinition,
215267
boolean existingTypeIsEntity, boolean conflictingTypeisEntity, boolean federatedComparison) {
216268
List<FieldDefinition> existingFieldDefinitions = getFieldDefinitions(existingTypeDefinition);
@@ -291,6 +343,10 @@ public static boolean isValidInputType(NamedType namedType) {
291343
return true;
292344
}
293345

346+
public static boolean isInputObjectType(TypeDefinition typeDefinition) {
347+
return typeDefinition instanceof InputObjectTypeDefinition;
348+
}
349+
294350
public static boolean isEntity(final TypeDefinition type) {
295351
return definitionContainsDirective(type, FEDERATION_KEY_DIRECTIVE);
296352
}

src/main/java/com/intuit/graphql/orchestrator/xtext/XtextResourceSetBuilder.java

+21-4
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ public class XtextResourceSetBuilder {
3636
private Map<String, String> files = new ConcurrentHashMap<>();
3737
private boolean isFederatedResourceSet = false;
3838

39-
public static final String FEDERATION_DIRECTIVES = getFederationDirectives();
39+
public static final String FEDERATION_DIRECTIVES = getDirectiveDefinitions("federation_built_in_directives.graphqls");
40+
public static final String AUTHZPOLICY_DIRECTIVES = getDirectiveDefinitions("authzpolicy_directive_definition.graphqls");
4041

4142
private XtextResourceSetBuilder() {
4243
}
@@ -79,13 +80,17 @@ public XtextResourceSet build() {
7980

8081
if(isFederatedResourceSet) {
8182
String content = FEDERATION_DIRECTIVES + "\n" + StringUtils.join(files.values(), "\n");
82-
83+
content = addAuthzPolicyDirectiveDefinition(content);
8384
try {
8485
createGraphqlResourceFromString(content, "appended_federation");
8586
} catch (IOException e) {
8687
throw new SchemaParseException("Unable to parse file: appended federation file", e);
8788
}
8889
} else {
90+
if (isUsingAuthzPolicy(files)) {
91+
files.put("authzpolicy_directive_definition.graphqls", AUTHZPOLICY_DIRECTIVES);
92+
}
93+
8994
files.forEach((fileName, content) -> {
9095
try {
9196
createGraphqlResourceFromString(content, fileName);
@@ -102,6 +107,18 @@ public XtextResourceSet build() {
102107
return graphqlResourceSet;
103108
}
104109

110+
private boolean isUsingAuthzPolicy(Map<String, String> files) {
111+
return files.values().stream()
112+
.anyMatch(content -> StringUtils.contains(content, "@authzPolicy"));
113+
}
114+
115+
public String addAuthzPolicyDirectiveDefinition(String content) {
116+
if (StringUtils.contains(content, "@authzPolicy")) {
117+
return content + "\n" + AUTHZPOLICY_DIRECTIVES;
118+
}
119+
return content;
120+
}
121+
105122
private XtextResource createResourceFrom(InputStream input, URI uri, Injector injector) throws IOException {
106123
XtextResource resource = (XtextResource) (injector.getInstance(IResourceFactory.class).createResource(uri));
107124
resource.load(input, null);
@@ -143,12 +160,12 @@ public static XtextResourceSet singletonSet(String fileName, String file) {
143160
.build();
144161
}
145162

146-
private static String getFederationDirectives() {
163+
private static String getDirectiveDefinitions(String file) {
147164
String directives = "";
148165
try {
149166
directives = IOUtils.toString(
150167
XtextResourceSetBuilder.class.getClassLoader()
151-
.getResourceAsStream( "federation_built_in_directives.graphqls"),
168+
.getResourceAsStream( file),
152169
Charset.defaultCharset()
153170
);
154171
} catch (IOException ex) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
directive @authzPolicy(id: String, ruleInputs:[RuleInput]!) on FIELD_DEFINITION
2+
input RuleInput { key: String! value: [String]!}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package com.intuit.graphql.orchestrator.integration.authzpolicy
2+
3+
import com.intuit.graphql.orchestrator.ServiceProvider
4+
import com.intuit.graphql.orchestrator.datafetcher.ServiceDataFetcher
5+
import com.intuit.graphql.orchestrator.schema.Operation
6+
import com.intuit.graphql.orchestrator.stitching.StitchingException
7+
import graphql.schema.FieldCoordinates
8+
import graphql.schema.GraphQLCodeRegistry
9+
import graphql.schema.GraphQLSchema
10+
import graphql.schema.StaticDataFetcher
11+
import helpers.BaseIntegrationTestSpecification
12+
import spock.lang.Subject
13+
14+
import static com.intuit.graphql.orchestrator.TestHelper.getResourceAsString
15+
import static graphql.Scalars.GraphQLInt
16+
17+
class AuthzPolicyWithNestedLevelStitchingSpec extends BaseIntegrationTestSpecification {
18+
19+
def v4osService, turboService
20+
21+
def mockServiceResponse = new HashMap()
22+
23+
def conflictingSchema = """
24+
type Query {
25+
root: RootType
26+
topLevelField: NestedType @authzPolicy(ruleInput: [{key:"foo",value:"a"}])
27+
}
28+
29+
type RootType {
30+
nestedField: NestedType
31+
}
32+
33+
type NestedType {
34+
fieldA: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}])
35+
fieldB: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}])
36+
}
37+
"""
38+
39+
def conflictingSchema2 = """
40+
type Query {
41+
root: RootType
42+
}
43+
44+
type RootType {
45+
nestedField: NestedType
46+
}
47+
48+
type NestedType {
49+
fieldC: String @authzPolicy(ruleInput: [{key:"foo",value:"b"}])
50+
fieldD: String @authzPolicy(ruleInput: [{key:"foo",value:"c"}])
51+
}
52+
"""
53+
ServiceProvider topLevelDeprecatedService1
54+
ServiceProvider topLevelDeprecatedService2
55+
56+
57+
@Subject
58+
def specUnderTest
59+
60+
def "Test Nested Stitching"() {
61+
given:
62+
v4osService = createSimpleMockService("V4OS", getResourceAsString("nested/v4os/schema.graphqls"), mockServiceResponse)
63+
turboService = createSimpleMockService("TURBO", getResourceAsString("nested/turbo/schema.graphqls"), mockServiceResponse)
64+
65+
when:
66+
specUnderTest = createGraphQLOrchestrator([v4osService, turboService])
67+
68+
then:
69+
GraphQLSchema result = specUnderTest.schema
70+
71+
def consumer = result?.getQueryType()?.getFieldDefinition("consumer")
72+
consumer != null
73+
74+
def consumerType = result?.getQueryType()?.getFieldDefinition("consumer")?.type
75+
consumerType != null
76+
consumerType.name == "ConsumerType"
77+
consumerType.description == "[V4OS,TURBO]"
78+
79+
def financialProfile = consumerType?.getFieldDefinition("financialProfile")
80+
financialProfile != null
81+
financialProfile.type?.name == "FinancialProfileType"
82+
83+
def turboExperiences = consumerType?.getFieldDefinition("turboExperiences")
84+
turboExperiences != null
85+
turboExperiences.type?.name == "ExperienceType"
86+
87+
def financeField = consumerType?.getFieldDefinition("finance")
88+
financeField != null
89+
financeField.type?.name == "FinanceType"
90+
financeField.type?.getFieldDefinition("fieldFinance")?.type == GraphQLInt
91+
financeField.type?.getFieldDefinition("fieldTurbo")?.type == GraphQLInt
92+
93+
//DataFetchers
94+
final GraphQLCodeRegistry codeRegistry = specUnderTest.runtimeGraph.getCodeRegistry().build()
95+
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("Query", "consumer"), consumer) instanceof StaticDataFetcher
96+
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "finance"), financeField) instanceof StaticDataFetcher
97+
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "financialProfile"), financeField) instanceof ServiceDataFetcher
98+
codeRegistry?.getDataFetcher(FieldCoordinates.coordinates("ConsumerType", "turboExperiences"), turboExperiences) instanceof ServiceDataFetcher
99+
}
100+
101+
def "Nested Type Description With Namespace And Empty Description"() {
102+
given:
103+
def bSchema = "schema { query: Query } type Query { a: A } \"\n" +\
104+
" \"type A { b: B @adapter(service: 'foo') } type B {d: D}\"\n" +\
105+
" \"type D { field: String}\"\n" +\
106+
" \"directive @adapter(service:String!) on FIELD_DEFINITION"
107+
108+
def bbSchema = "schema { query: Query } type Query { a: A } \"\n" +\
109+
" \"type A { bbc: BB } type BB {cc: String}"
110+
111+
def abcSchema = "schema { query: Query } type Query { a: A } \"\n" +\
112+
" \"type A { bbcd: C } type C {cc: String}"
113+
114+
def secondSchema = "schema { query: Query } type Query { a: A } " +\
115+
"type A { bbbb: BAB } type BAB {fieldBB: String}"
116+
117+
def ambcSchema = "schema { query: Query } type Query { a: A } \"\n" +\
118+
" \"type A { bba: CDD } type CDD {ccdd: String}"
119+
120+
def ttbbSchema = "schema { query: Query } type Query { a: A } \"\n" +\
121+
" \"type A { bbab: BBD } type BBD {cc: String}"
122+
123+
def bService = createSimpleMockService("SVC_b", bSchema, mockServiceResponse)
124+
def bbService = createSimpleMockService("SVC_bb", bbSchema, mockServiceResponse)
125+
def abcService = createSimpleMockService("SVC_abc", abcSchema, mockServiceResponse)
126+
def secondService = createSimpleMockService("SVC_Second", secondSchema, mockServiceResponse)
127+
def ambcService = createSimpleMockService("AMBC", ambcSchema, mockServiceResponse)
128+
def ttbbService = createSimpleMockService("TTBB", ttbbSchema, mockServiceResponse)
129+
130+
when:
131+
specUnderTest = createGraphQLOrchestrator([bService, bbService ,abcService,secondService, ambcService, ttbbService])
132+
133+
134+
then:
135+
def aType = specUnderTest.runtimeGraph.getOperation(Operation.QUERY)?.getFieldDefinition("a")?.type
136+
137+
aType.description.contains("SVC_abc")
138+
aType.description.contains("SVC_bb")
139+
aType.description.contains("AMBC")
140+
aType.description.contains("TTBB")
141+
aType.description.contains("SVC_Second")
142+
aType.description.contains("SVC_b")
143+
}
144+
145+
def "Nested Type Description With Namespace And Description"() {
146+
given:
147+
String schema1 = "schema { query: Query } type Query { a: A } " +\
148+
"type A { b: B @adapter(service: 'foo') } type B {d: D}" +\
149+
"type D { field: String}" +\
150+
"directive @adapter(service:String!) on FIELD_DEFINITION"
151+
152+
String schema2 = "schema { query: Query } type Query { a: A } " +\
153+
"\"description for schema2\"type A { bbc: BB } type BB {cc: String}"
154+
155+
String schema3 = "schema { query: Query } type Query { a: A } " +\
156+
"\"description for schema3\"type A { bbcd: C } type C {cc: String}"
157+
158+
String schema4 = "schema { query: Query } type Query { a: A } " +\
159+
"type A { bbbb: BAB } type BAB {fieldBB: String}"
160+
161+
def service1 = createSimpleMockService("SVC_b", schema1, mockServiceResponse)
162+
def service2 = createSimpleMockService("SVC_bb", schema2, mockServiceResponse)
163+
def service3 = createSimpleMockService("SVC_abc", schema3, mockServiceResponse)
164+
def service4 = createSimpleMockService("SVC_Second", schema4, mockServiceResponse)
165+
166+
when:
167+
specUnderTest = createGraphQLOrchestrator([service1, service2, service3, service4])
168+
169+
then:
170+
def aType = specUnderTest?.runtimeGraph?.getOperation(Operation.QUERY)?.getFieldDefinition("a")?.type
171+
172+
aType.description.contains("SVC_abc")
173+
aType.description.contains("SVC_bb")
174+
aType.description.contains("SVC_Second")
175+
aType.description.contains("SVC_b")
176+
aType.description.contains("description for schema3")
177+
aType.description.contains("description for schema2")
178+
}
179+
180+
def "deprecated fields can not be referenced again"() {
181+
given:
182+
topLevelDeprecatedService1 = createSimpleMockService("test1", conflictingSchema, new HashMap<String, Object>())
183+
topLevelDeprecatedService2 = createSimpleMockService("test2", conflictingSchema2, new HashMap<String, Object>())
184+
185+
when:
186+
specUnderTest = createGraphQLOrchestrator([topLevelDeprecatedService1, topLevelDeprecatedService2])
187+
188+
then:
189+
def exception = thrown(StitchingException)
190+
exception.message == "FORBIDDEN: Subgraphs [test2,test1] are reusing type NestedType with different field definitions."
191+
}
192+
}

0 commit comments

Comments
 (0)