Skip to content

Commit

Permalink
Enhancing SelectionSet annotation to ignore fields in the return type #…
Browse files Browse the repository at this point in the history
  • Loading branch information
AmandeepSingh97 authored Sep 21, 2021
1 parent 3778422 commit f457d11
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 20 deletions.
34 changes: 33 additions & 1 deletion docs/END_USER_DOCUMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,18 @@ query {

#### 4.5.2 Using Mocca SelectionSet annotation

There might be cases when the application has a DTO it could use as return type for a GraphQL operation, however, not all its fields are really required to be returned from the server. In this case, the annotation `com.paypal.mocca.client.annotation.SelectionSet` can be used to customize the selection set, letting the application specify exactly what the data it would like to receive.
There might be cases when the application has a DTO it could use as return type for a GraphQL operation, however, not all its fields are really required to be returned from the server. In this case, the annotation `com.paypal.mocca.client.annotation.SelectionSet` can be used to customize the selection set, letting the application specify exactly what the data it would like to receive or ignore.

Mocca `SelectionSet` supports two options:

- value
- ignore

`SelectionSet` behaviour:

- If annotation is present and its `value` attribute is set, Mocca automatic selection set resolution is turned OFF, and `SelectionSet` `value` is used to define the selection set. In this case if `ignore` value is also set, then that is not used by Mocca, and a warning is logged.
- If annotation is present, its value attribute is NOT set, but `ignore` is, then Mocca automatic selection set resolution is turned ON, and `SelectionSet` `ignore` is used to pick which response DTO fields to ignore from the selection set.
- If annotation is present and both `value` and `ignore` attributes are NOT set, then a `MoccaException` is thrown.

It is important to mention though that, when `SelectionSet` annotation is present, although Mocca won't resolve automatically the selection set using the return type, still application has to make sure all fields in the provided custom selection set exist in the DTO used in the return type.

Expand Down Expand Up @@ -429,6 +440,27 @@ query {
}
```

The example below shows an application using `Book` DTO as return type, but ignoring only the `title` field. Notice multiple values can be set to `SelectionSet` `ignore` as an array of `String` values.

##### GraphQL operation definition in the client API

``` java
@Query
@SelectionSet(ignore="title")
Book getBookById(int bookId);
```

##### Sample GraphQL query generated by Mocca

``` GraphQL
query {
getBookById(bookId: 1000) {
id
Author
}
}
```

## 5 Code generation

Code generation for a GraphQL service in Java can be done for both server side and client side. There are few code generation tools available on opensource out of which [Apollo Code Generator](https://the-guild.dev/blog/graphql-codegen-java) and [Netflix DGS framework](https://netflix.github.io/dgs/generating-code-from-schema/) ones are popular.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,22 +366,30 @@ private List<String> getNextIgnoreFields(String prefix, final List<String> ignor
* @param responseType the return type set in the GraphQL operation method, necessary to dynamically set the selection set
*/
private void writeSelectionSet(final ByteArrayOutputStream requestPayload, final String operationName, final SelectionSet selectionSet, Type responseType) {
if (selectionSet != null && (selectionSet.value().equals(SelectionSet.UNDEFINED) || selectionSet.value().trim().isEmpty())) {
throw new MoccaException("A com.paypal.mocca.client.annotation.SelectionSet annotation with an undefined value is present at the method related to operation "
+ operationName + ". Please, set its value, or remove the annotation, letting Mocca use the return type to automatically set the selection set.");
} else if (selectionSet != null) {
// Adding selection set using the selection set annotation

if (selectionSet != null && isUndefinedOrNullOrEmpty(selectionSet.value()) && isUndefinedOrNullOrEmpty(selectionSet.ignore())) {
throw new MoccaException("A com.paypal.mocca.client.annotation.SelectionSet annotation with undefined value and ignore fields is present at the method related to operation "
+ operationName + ". Please, set its value or ignore fields, or remove the annotation, letting Mocca use the return type to automatically set the selection set.");
} else if (selectionSet != null && !isUndefinedOrNullOrEmpty(selectionSet.value())) {
// Adding selection set using the selection set annotation with value
if(!isUndefinedOrNullOrEmpty(selectionSet.ignore())) {
logger.warn("Value and ignore fields are both set in the com.paypal.mocca.client.annotation.SelectionSet annotation at "
+ operationName + ". Value will be used to generate the selection set and no fields will be ignored.");
}
writeUserProvidedSelectionSet(requestPayload, selectionSet);
} else if(selectionSet != null && !isUndefinedOrNullOrEmpty(selectionSet.ignore())) {
// Adding selection set using the ignore values in the selection set annotation
writeResponseTypeSelectionSet(requestPayload, responseType, Arrays.asList(selectionSet.ignore()));
} else if (responseType != null) {
// Adding selection set using the response type
writeResponseTypeSelectionSet(requestPayload, responseType);
// Adding selection set using the response type and empty ignore list
writeResponseTypeSelectionSet(requestPayload, responseType, Collections.emptyList());
} else {
logger.debug("Response type is null and a custom selection set was not provided, so a selection set will not be written to the GraphQL operation");
}
}

/*
* Writes the selection set of the GraphQL request message using the user provided SelectionSet annotation as reference.
* Writes the selection set of the GraphQL request message using the user provided SelectionSet annotation with Value attribute set as reference.
*
* @param requestPayload the output stream object used to write the selection set, based on the other parameters
* @param selectionSet the SelectionSet annotation set in the GraphQL operation method, necessary to set the selection set
Expand All @@ -407,14 +415,15 @@ private void writeUserProvidedSelectionSet(final ByteArrayOutputStream requestPa
*
* @param requestPayload the output stream object used to write the selection set, based on the other parameters
* @param responseType the return type set in the GraphQL operation method, necessary to dynamically set the selection set
* @param ignoreFields the list of fields which have to be ignored in selection set generation
* @throws MoccaException if a cycle is found or any error happens when writing the selection set
*/
private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPayload, final Type responseType) {
private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPayload, final Type responseType, List<String> ignoreFields) {

// This is necessary to detect cycles and prevent stack overflow
Set<Type> seenPojoTypes = new HashSet<>();

writeResponseTypeSelectionSet(requestPayload, responseType, seenPojoTypes);
writeResponseTypeSelectionSet(requestPayload, responseType, seenPojoTypes, ignoreFields);
}

/*
Expand All @@ -424,9 +433,10 @@ private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPa
* @param requestPayload the output stream object used to write the selection set, based on the other parameters
* @param responseType the return type set in the GraphQL operation method, necessary to dynamically set the selection set
* @param seenPojoTypes to detect cycles and prevent stack overflow
* @param ignoreFields the list of fields which have to be ignored in selection set generation
* @throws MoccaException if a cycle is found or any error happens when writing the selection set
*/
private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPayload, final Type responseType, Set<Type> seenPojoTypes) throws MoccaException {
private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPayload, final Type responseType, Set<Type> seenPojoTypes, List<String> ignoreFields) throws MoccaException {
try {

// Retrieving type out of parameterized types if necessary
Expand All @@ -450,6 +460,7 @@ private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPa
PropertyDescriptor[] pds = info.getPropertyDescriptors();
List<String> selectionSet = Arrays.stream(pds)
.filter(pd -> !pd.getReadMethod().getReturnType().equals(Class.class))
.filter(pd -> !ignoreFields.contains(pd.getName()))
// Parameterized types should not be treated as Java Beans and
// shouldn't be serialized. The exception though is Lists
// and Optionals, which should have their parameter types
Expand All @@ -462,12 +473,15 @@ private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPa
.map(pd -> new Tuple<>(pd.getName(), pd.getReadMethod().getGenericReturnType()))
.map(e -> {
Type type = e.value;
final List<String> nextIgnoreFields = getNextIgnoreFields(e.key + ".", ignoreFields);
if (isPojo(type)) {
return writeSelectionSetPojo(e.key, type, seenPojoTypes);
return writeSelectionSetPojo(e.key, type, seenPojoTypes, nextIgnoreFields);
} else if (isParameterizedType(type)) {
// Here we know it is either an Optional or a List
final Type typeParameter = getInnerType(type);
if (isPojo(typeParameter)) return writeSelectionSetPojo(e.key, typeParameter, seenPojoTypes);
if (isPojo(typeParameter)) {
return writeSelectionSetPojo(e.key, typeParameter, seenPojoTypes, nextIgnoreFields);
}
}
// If this line is reached, that means this is not a POJO and serialization
// (for this particular field at least) in the selection set should end here.
Expand All @@ -488,11 +502,12 @@ private void writeResponseTypeSelectionSet(final ByteArrayOutputStream requestPa
*
* @param fieldName the element (GraphQL field name and type) whose String representation should be returned
* @param type the particular type to be used to create the String representation of the given element
* @param ignoreFields the list of fields which have to be ignored in selection set generation
* @return the String representation of a POJO in GraphQL SelectionSet notation
*/
private String writeSelectionSetPojo(final String fieldName, final Type type, Set<Type> seenPojoTypes) {
private String writeSelectionSetPojo(final String fieldName, final Type type, Set<Type> seenPojoTypes, List<String> ignoreFields) {
ByteArrayOutputStream complexVariable = new ByteArrayOutputStream();
writeResponseTypeSelectionSet(complexVariable, type, seenPojoTypes);
writeResponseTypeSelectionSet(complexVariable, type, seenPojoTypes, ignoreFields);
return fieldName + complexVariable.toString();
}

Expand All @@ -507,4 +522,12 @@ private void write(ByteArrayOutputStream outputStream, final String data) throws
outputStream.write(data.getBytes());
}

}
private boolean isUndefinedOrNullOrEmpty(String value) {
return value==null || value.equals(SelectionSet.UNDEFINED) || value.trim().isEmpty();
}

private boolean isUndefinedOrNullOrEmpty(String[] value) {
return value==null || value.length==0;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@
* <br>
* When {@code SelectionSet} annotation is present, although Mocca won't resolve automatically the selection set using the return type, still the application has to make sure all fields in the provided custom selection set exist in the DTO used in the return type.
* <br>
* {@code SelectionSet} would then behave like this:
* <ol>
* <li>If annotation is present and its value attribute is set, Mocca automatic selection set resolution is turned off, and {@code SelectionSet} value is used to define the selection set. In this case if ignore value is also set, then that is not used by Mocca, and a warning is logged.</li>
* <li>If annotation is present, its value attribute is NOT set, but ignore is, then Mocca automatic selection set resolution is turned ON, and {@code SelectionSet} ignore is used to pick which response DTO fields to ignore from the selection set.</li>
* <li>If annotation is present and both value and ignore attributes are NOT set, then a {@link MoccaException} is thrown.</li>
* </ol>
*
* See a client API example below. Notice the given selection set must be wrapped around curly braces.
* <pre><code>
* import com.paypal.mocca.client.MoccaClient;
Expand All @@ -27,6 +34,10 @@
* List&#60;Book&#62; getBooks(String variables);
*
* &#064;Query
* &#064;SelectionSet(ignore="title")
* List&#60;Book&#62; getBookById(int bookId);
*
* &#064;Query
* Book getBook(long id);
*
* &#064;Mutation
Expand All @@ -53,4 +64,18 @@
*/
String value() default UNDEFINED;

}
/**
* When the Response type is a POJO (following Java bean conventions), sometimes it is populated with certain properties
* the application would like to be ignored by Mocca when generating the GraphQL Selection Set.
* In order to do so, specify here an array containing all fields to be ignored in the Selection Set.
* The name of a property in an inner POJO can be specified using the outer field name followed by dot.
* If the request type is not a POJO, or if {@link #value()} is set, then {@code ignore} will have no effect.
* If the property set to be ignored doesn't exist, then it has no effect, the Selection Set is generated normally as
* if that ignore value had not been set.
* If both {@link #value()} and {@code ignore} attributes are set, then the {@code ignore} value is not used by Mocca, and a warning is logged.
*
* @return an array containing all fields to be ignored in the return type, in case it is a DTO
*/
String[] ignore() default UNDEFINED;

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.paypal.mocca.client.sample.SampleRequestDTO;
import com.paypal.mocca.client.sample.SampleResponseDTO;
import com.paypal.mocca.client.sample.SuperComplexSampleType;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import java.io.IOException;
Expand Down Expand Up @@ -143,6 +144,30 @@ public void complexResponseCustomSelectionSetLessFieldsTest() throws IOException
selectionSet, "{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar complexField stringVar}}\"}");
}

@Test(dataProvider = "ignoreSelectionSetCases")
public void customSelectionWithIgnoreFieldsTest(String expectedReq, String... ignoreArr) throws IOException {
SampleRequestDTO sampleRequestDTO = new SampleRequestDTO("foo", "bar");
SelectionSet selectionSet = newSelectionSet(null, ignoreArr);
List<MoccaSerializer.Variable> variables = Collections.singletonList(
new MoccaSerializer.Variable(sampleRequestDTO, SampleRequestDTO.class, newVar("sampleRequest"))
);

requestTest(variables, ComplexSampleType.class,"getABeer", OperationType.Query,
selectionSet, expectedReq);
}

@Test(dataProvider = "valueWithIgnoreSelectionSetCases")
public void customSelectionWithValueAndIgnoreFieldsTest(String expectedReq, String value, String... ignoreArr) throws IOException {
SampleRequestDTO sampleRequestDTO = new SampleRequestDTO("foo", "bar");
SelectionSet selectionSet = newSelectionSet(value, ignoreArr);
List<MoccaSerializer.Variable> variables = Collections.singletonList(
new MoccaSerializer.Variable(sampleRequestDTO, SampleRequestDTO.class, newVar("sampleRequest"))
);

requestTest(variables, ComplexSampleType.class,"getABeer", OperationType.Query,
selectionSet, expectedReq);
}

@Test
public void complexWithStringListRequestTest() throws IOException {
SuperComplexSampleType.SuperComplexField superComplexField =
Expand Down Expand Up @@ -271,10 +296,49 @@ private Var newVar(String value, String... ignore) {
};
}

private SelectionSet newSelectionSet(String value) {
private SelectionSet newSelectionSet(String value, String... ignore) {
return new SelectionSet(){
@Override public Class<? extends Annotation> annotationType() { return SelectionSet.class; }
@Override public String value() { return value;}
@Override public String[] ignore() { return ignore;}
};
}

@DataProvider(name = "ignoreSelectionSetCases")
public static Object[][] ignoreSelectionSetCases() {
return new Object[][]{
// Ignore one value of Complex type and 1 outer boolean var
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {complexField {innerBooleanVar innerIntVar} intVar stringVar}}\"}",
"booleanVar", "complexField.innerStringVar"},
// ignore the entire complex Type
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar intVar stringVar}}\"}",
"complexField"},
// ignore everything
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {}}\"}",
"complexField", "intVar", "stringVar", "booleanVar"},
// ignore nothing
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar complexField {innerBooleanVar innerIntVar innerStringVar} intVar stringVar}}\"}",
""},
// ignore Complex type as a whole and also a field
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar intVar stringVar}}\"}",
"complexField.innerStringVar", "complexField"},
// ignore a field Not present in the response Type and an inner ComplexField value
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar complexField {innerBooleanVar innerIntVar} intVar stringVar}}\"}",
"complexField.innerStringVar", "complexField.someFieldNotPresent"}
};
}

// Testcases for Custom Selection set with Value and Ignore fields both set. Ignore attribute should NOT be used
@DataProvider(name = "valueWithIgnoreSelectionSetCases")
public static Object[][] ignoreWithValueSelectionSetCases() {
return new Object[][]{

// here innerBooleanVar is not a part of the requested SelectionSet
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar complexField {innerStringVar} intVar stringVar}}\"}",
"{booleanVar complexField {innerStringVar} intVar stringVar}", "innerBooleanVar"},
// here complexField.innerStringVar is a part of the requested SelectionSet
{"{ \"query\" : \"query{getABeer(sampleRequest: {bar: \\\"bar\\\", foo: \\\"foo\\\"}) {booleanVar complexField {innerStringVar} intVar stringVar}}\"}",
"{booleanVar complexField {innerStringVar} intVar stringVar}", "complexField.innerStringVar"},
};
}
}
}

0 comments on commit f457d11

Please sign in to comment.