Skip to content

Commit de93775

Browse files
author
Cole Turner
authored
feat(GraphQLQueryRequest): support alias in projections (#563)
* feat(GraphQLQueryRequest): support alias in projections * fix typespec on tests
1 parent 4bc381f commit de93775

File tree

12 files changed

+121
-19
lines changed

12 files changed

+121
-19
lines changed

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ class ClientApiGenerator(private val config: CodeGenConfig, private val document
466466
val javaType = TypeSpec.classBuilder(clazzName)
467467
.addOptionalGeneratedAnnotation(config)
468468
.addModifiers(Modifier.PUBLIC)
469-
.superclass(ParameterizedTypeName.get(className, ClassName.get(getPackageName(), parent.name), ClassName.get(getPackageName(), root.name)))
469+
.superclass(ParameterizedTypeName.get(className, ClassName.get(getPackageName(), clazzName), ClassName.get(getPackageName(), parent.name), ClassName.get(getPackageName(), root.name)))
470470
.addMethod(
471471
MethodSpec.constructorBuilder()
472472
.addModifiers(Modifier.PUBLIC)

graphql-dgs-codegen-core/src/main/kotlin/com/netflix/graphql/dgs/codegen/generators/java/ClientApiGeneratorv2.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -211,14 +211,14 @@ class ClientApiGeneratorv2(private val config: CodeGenConfig, private val docume
211211
private fun createRootProjection(type: TypeDefinition<*>, prefix: String): CodeGenResult {
212212
val clazzName = "${prefix}ProjectionRoot"
213213
val className = ClassName.get(BaseSubProjectionNode::class.java)
214-
val parentJavaType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?")))
215-
val rootJavaType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?")))
214+
val parentJavaType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"), TypeVariableName.get("?")))
215+
val rootJavaType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"), TypeVariableName.get("?")))
216216
val javaType = TypeSpec.classBuilder(clazzName)
217217
.addOptionalGeneratedAnnotation(config)
218218
.addTypeVariable(parentJavaType)
219219
.addTypeVariable(rootJavaType)
220220
.addModifiers(Modifier.PUBLIC)
221-
.superclass(ParameterizedTypeName.get(className, TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT")))
221+
.superclass(ParameterizedTypeName.get(className, ClassName.get(getPackageName(), clazzName), TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT")))
222222
.addMethod(
223223
MethodSpec.constructorBuilder()
224224
.addModifiers(Modifier.PUBLIC)
@@ -333,14 +333,14 @@ class ClientApiGeneratorv2(private val config: CodeGenConfig, private val docume
333333
private fun createEntitiesRootProjection(federatedTypes: List<ObjectTypeDefinition>): CodeGenResult {
334334
val clazzName = "EntitiesProjectionRoot"
335335
val className = ClassName.get(BaseSubProjectionNode::class.java)
336-
val parentType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?")))
337-
val rootType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?")))
336+
val parentType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"), TypeVariableName.get("?")))
337+
val rootType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"), TypeVariableName.get("?")))
338338
val javaType = TypeSpec.classBuilder(clazzName)
339339
.addOptionalGeneratedAnnotation(config)
340340
.addTypeVariable(parentType)
341341
.addTypeVariable(rootType)
342342
.addModifiers(Modifier.PUBLIC)
343-
.superclass(ParameterizedTypeName.get(className, TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT")))
343+
.superclass(ParameterizedTypeName.get(className, ClassName.get(getPackageName(), clazzName), TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT")))
344344
.addMethod(
345345
MethodSpec.constructorBuilder()
346346
.addModifiers(Modifier.PUBLIC)
@@ -480,14 +480,14 @@ class ClientApiGeneratorv2(private val config: CodeGenConfig, private val docume
480480
val clazzName = "${prefix}Projection"
481481
if (generatedClasses.contains(clazzName)) return null else generatedClasses.add(clazzName)
482482

483-
val parentJavaType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?")))
484-
val rootJavaType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?")))
483+
val parentJavaType = TypeVariableName.get("PARENT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"), TypeVariableName.get("?")))
484+
val rootJavaType = TypeVariableName.get("ROOT").withBounds(ParameterizedTypeName.get(className, TypeVariableName.get("?"), TypeVariableName.get("?"), TypeVariableName.get("?")))
485485
val javaType = TypeSpec.classBuilder(clazzName)
486486
.addOptionalGeneratedAnnotation(config)
487487
.addTypeVariable(parentJavaType)
488488
.addTypeVariable(rootJavaType)
489489
.addModifiers(Modifier.PUBLIC)
490-
.superclass(ParameterizedTypeName.get(className, TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT")))
490+
.superclass(ParameterizedTypeName.get(className, ClassName.get(getPackageName(), clazzName), TypeVariableName.get("PARENT"), TypeVariableName.get("ROOT")))
491491
.addMethod(
492492
MethodSpec.constructorBuilder()
493493
.addModifiers(Modifier.PUBLIC)

graphql-dgs-codegen-core/src/test/kotlin/com/netflix/graphql/dgs/codegen/clientapi/ClientApiGenFragmentTest.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ class ClientApiGenFragmentTest {
127127
.doesNotContain("duration")
128128

129129
val superclass = codeGenResult.clientProjections[3].typeSpec.superclass as ParameterizedTypeName
130-
assertThat(superclass.typeArguments[1]).extracting("simpleName").isEqualTo("SearchProjectionRoot")
130+
assertThat(superclass.typeArguments[1]).extracting("simpleName").isEqualTo("Search_ShowProjection")
131+
assertThat(superclass.typeArguments[2]).extracting("simpleName").isEqualTo("SearchProjectionRoot")
131132

132133
assertCompilesJava(
133134
codeGenResult.clientProjections + codeGenResult.javaQueryTypes + codeGenResult.javaEnumTypes + codeGenResult.javaDataTypes + codeGenResult.javaInterfaces
@@ -225,7 +226,8 @@ class ClientApiGenFragmentTest {
225226
assertThat(codeGenResult.clientProjections[3].typeSpec.initializerBlock.isEmpty).isFalse
226227

227228
val superclass = codeGenResult.clientProjections[3].typeSpec.superclass as ParameterizedTypeName
228-
assertThat(superclass.typeArguments[1]).extracting("simpleName").isEqualTo("SearchProjectionRoot")
229+
assertThat(superclass.typeArguments[1]).extracting("simpleName").isEqualTo("Search_ResultProjection")
230+
assertThat(superclass.typeArguments[2]).extracting("simpleName").isEqualTo("SearchProjectionRoot")
229231

230232
val searchResult = codeGenResult.javaInterfaces[0].typeSpec
231233

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/BaseProjectionNode.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@ abstract class BaseProjectionNode(
3333
) {
3434

3535
val fields: MutableMap<String, Any?> = LinkedHashMap()
36+
val aliases: MutableMap<String, FieldAlias> = LinkedHashMap()
3637
val fragments: MutableList<BaseProjectionNode> = LinkedList()
3738
val inputArguments: MutableMap<String, List<InputArgument>> = LinkedHashMap()
3839

3940
data class InputArgument(val name: String, val value: Any?)
41+
data class FieldAlias(val fieldName: String, val value: Any?)
4042
}

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/BaseSubProjectionNode.kt

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,60 @@ package com.netflix.graphql.dgs.client.codegen
1818

1919
import java.util.*
2020

21-
abstract class BaseSubProjectionNode<T, R>(
21+
abstract class BaseSubProjectionNode<S, T, R>(
2222
val parent: T,
2323
val root: R,
2424
schemaType: Optional<String> = Optional.empty()
2525
) : BaseProjectionNode(schemaType) {
2626

2727
constructor(parent: T, root: R) : this(parent, root, schemaType = Optional.empty())
28-
2928
fun parent(): T {
3029
return parent
3130
}
3231

3332
fun root(): R {
3433
return root
3534
}
35+
36+
fun alias(alias: String, assigner: (node: S) -> Any): S {
37+
// Avoid alias against existing field name
38+
if (this.javaClass.kotlin.members.any { it.name.equals(alias, ignoreCase = true) }) {
39+
throw AssertionError("Tried to specify alias $alias which already exists as field.")
40+
}
41+
42+
// Stash our current set of fields
43+
val currentFields = fields.toMap()
44+
val currentInputArguments = inputArguments.toMap()
45+
fields.clear()
46+
inputArguments.clear()
47+
48+
// Track the aliased field
49+
@Suppress("UNCHECKED_CAST")
50+
assigner(this as S)
51+
52+
// Constraints on how aliases are assigned
53+
if (fields.isEmpty()) {
54+
throw AssertionError("Tried to initialize alias but did not call any fields.")
55+
}
56+
57+
if (fields.size > 1) {
58+
throw AssertionError("Tried to call multiple fields while initializing alias.")
59+
}
60+
61+
val fieldName = fields.keys.first()
62+
val fieldValue = fields.values.first()
63+
val fieldArguments = mapOf(alias to inputArguments[fieldName]?.let { it }.orEmpty())
64+
65+
// Restore our fields stash
66+
fields.clear()
67+
fields.putAll(currentFields)
68+
inputArguments.clear()
69+
inputArguments.putAll(currentInputArguments)
70+
71+
// Layer in our new state
72+
aliases[alias] = FieldAlias(fieldName = fieldName, value = fieldValue)
73+
inputArguments.putAll(fieldArguments)
74+
75+
return this
76+
}
3677
}

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class GraphQLMultiQueryRequest(
5757
}
5858

5959
if (request.projection != null) {
60-
val selectionSet = if (request.projection is BaseSubProjectionNode<*, *>) {
60+
val selectionSet = if (request.projection is BaseSubProjectionNode<*, *, *>) {
6161
request.projectionSerializer.toSelectionSet(request.projection.root() as BaseProjectionNode)
6262
} else {
6363
request.projectionSerializer.toSelectionSet(request.projection)

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class GraphQLQueryRequest @JvmOverloads constructor(
6969
}
7070

7171
if (projection != null) {
72-
val selectionSetFromProjection = if (projection is BaseSubProjectionNode<*, *> && projection.root() != null) {
72+
val selectionSetFromProjection = if (projection is BaseSubProjectionNode<*, *, *> && projection.root() != null) {
7373
projectionSerializer.toSelectionSet(projection.root() as BaseProjectionNode)
7474
} else {
7575
projectionSerializer.toSelectionSet(projection)

graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/ProjectionSerializer.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,30 @@ class ProjectionSerializer(private val inputValueSerializer: InputValueSerialize
5151
selectionSet.selection(fieldSelection.build())
5252
}
5353

54+
for ((fieldName, fieldAlias) in projection.aliases) {
55+
val fieldSelection = Field.newField()
56+
.name(fieldAlias.fieldName)
57+
.alias(fieldName)
58+
.arguments(
59+
projection.inputArguments[fieldName].orEmpty().map { (argName, values) ->
60+
Argument(argName, inputValueSerializer.toValue(values))
61+
}
62+
)
63+
if (fieldAlias.value is BaseProjectionNode) {
64+
val fieldSelectionSet = toSelectionSet(fieldAlias.value)
65+
if (fieldSelectionSet.selections.isNotEmpty()) {
66+
fieldSelection.selectionSet(fieldSelectionSet)
67+
}
68+
} else if (fieldAlias.value != null) {
69+
fieldSelection.selectionSet(
70+
SelectionSet.newSelectionSet()
71+
.selection(Field.newField(fieldAlias.value.toString()).build())
72+
.build()
73+
)
74+
}
75+
selectionSet.selection(fieldSelection.build())
76+
}
77+
5478
for (fragment in projection.fragments) {
5579
val typeCondition = fragment.schemaType.map { TypeName(it) }
5680
.orElseGet {

graphql-dgs-codegen-shared-core/src/test/java/com/netflix/graphql/dgs/client/codegen/exampleprojection/ShowsProjectionRoot.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public ShowsProjectionRoot releaseYear() {
5757
return this;
5858
}
5959

60-
public static class Shows_ReviewsProjection extends BaseSubProjectionNode<ShowsProjectionRoot, ShowsProjectionRoot> {
60+
public static class Shows_ReviewsProjection extends BaseSubProjectionNode<Shows_ReviewsProjection, ShowsProjectionRoot, ShowsProjectionRoot> {
6161
public Shows_ReviewsProjection(ShowsProjectionRoot parent, ShowsProjectionRoot root) {
6262
super(parent, root);
6363
}

graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/ProjectionSerializerTest.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,37 @@ internal class ProjectionSerializerTest {
102102
""".trimMargin()
103103
)
104104
}
105+
106+
@Test
107+
fun `Projection supports aliases`() {
108+
// given
109+
val root = EntitiesProjectionRoot()
110+
.onMovie(Optional.empty())
111+
.moveId().title().releaseYear()
112+
.alias("colesReviews") { it.reviews("Cole", 10).username().score() }
113+
.reviews(username = "Foo", score = 10).username().score()
114+
.root()
115+
// when
116+
val serialized = ProjectionSerializer(InputValueSerializer()).serialize(root)
117+
// then
118+
assertThat(serialized).isEqualTo(
119+
"""{
120+
| ... on EntitiesMovieKey {
121+
| __typename
122+
| moveId
123+
| title
124+
| releaseYear
125+
| reviews(username: "Foo", score: 10) {
126+
| username
127+
| score
128+
| }
129+
| colesReviews: reviews(username: "Cole", score: 10) {
130+
| username
131+
| score
132+
| }
133+
| }
134+
|}
135+
""".trimMargin()
136+
)
137+
}
105138
}

0 commit comments

Comments
 (0)