diff --git a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequest.kt b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequest.kt new file mode 100644 index 000000000..ea06c5f4b --- /dev/null +++ b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequest.kt @@ -0,0 +1,80 @@ +/* + * + * Copyright 2020 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.client.codegen + +import graphql.language.* + +class GraphQLMultiQueryRequest( + private val requests: List +) { + + fun serialize(): String { + if (requests.isEmpty()) throw AssertionError("Request must have at least one query") + + val operationDef = OperationDefinition.newOperationDefinition() + requests[0].query.name?.let { operationDef.name(it) } + requests[0].query.getOperationType()?.let { operationDef.operation(OperationDefinition.Operation.valueOf(it.uppercase())) } + + val queryType = requests[0].query.getOperationType().toString() + val variableDefinitions = mutableListOf() + val selectionList: MutableList = mutableListOf() + + for (request in this.requests) { + val query = request.query + // Graphql only supports multiple mutations or multiple queries. Not a combination of the two. + // Graphql does not support multiple subscriptions in one request http://spec.graphql.org/June2018/#sec-Single-root-field + if (!query.getOperationType().equals(queryType) || queryType == OperationDefinition.Operation.SUBSCRIPTION.name) { + throw AssertionError("Request has to have exclusively queries or mutations in a multi operation request") + } + + if (request.query.variableDefinitions.isNotEmpty()) { + variableDefinitions.addAll(request.query.variableDefinitions) + } + + val selection = Field.newField(request.query.getOperationName()) + if (query.input.isNotEmpty()) { + selection.arguments( + query.input.map { (name, value) -> + Argument(name, request.inputValueSerializer.toValue(value)) + } + ) + } + + if (request.projection != null) { + val selectionSet = if (request.projection is BaseSubProjectionNode<*, *>) { + request.projectionSerializer.toSelectionSet(request.projection.root() as BaseProjectionNode) + } else { + request.projectionSerializer.toSelectionSet(request.projection) + } + if (selectionSet.selections.isNotEmpty()) { + selection.selectionSet(selectionSet) + } + } + if (query.queryAlias.isNotEmpty()) { + selection.alias(query.queryAlias) + } + + selectionList.add(selection) + } + + operationDef.selectionSet(SelectionSet.newSelectionSet(selectionList.map(Field.Builder::build).toList()).build()) + + return AstPrinter.printAst(operationDef.build()) + } +} diff --git a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQuery.kt b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQuery.kt index 8e4b3dfb3..8b1f8cb76 100644 --- a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQuery.kt +++ b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQuery.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Netflix, Inc. + * Copyright 2022 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import graphql.language.VariableDefinition abstract class GraphQLQuery(val operation: String, val name: String?) { val input: MutableMap = mutableMapOf() val variableDefinitions = mutableListOf() + var queryAlias: String = "" constructor(operation: String) : this(operation, null) constructor() : this("query") diff --git a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt index 3869cc7f4..3aa742e4f 100644 --- a/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt +++ b/graphql-dgs-codegen-shared-core/src/main/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Netflix, Inc. + * Copyright 2022 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,15 +24,15 @@ import graphql.language.SelectionSet import graphql.schema.Coercing class GraphQLQueryRequest( - private val query: GraphQLQuery, - private val projection: BaseProjectionNode?, + val query: GraphQLQuery, + val projection: BaseProjectionNode?, scalars: Map, Coercing<*, *>>? ) { constructor(query: GraphQLQuery) : this(query, null, null) constructor(query: GraphQLQuery, projection: BaseProjectionNode?) : this(query, projection, null) - private val inputValueSerializer = InputValueSerializer(scalars ?: emptyMap()) - private val projectionSerializer = ProjectionSerializer(inputValueSerializer) + val inputValueSerializer = InputValueSerializer(scalars ?: emptyMap()) + val projectionSerializer = ProjectionSerializer(inputValueSerializer) fun serialize(): String { val operationDef = OperationDefinition.newOperationDefinition() diff --git a/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequestTest.kt b/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequestTest.kt new file mode 100644 index 000000000..69362ed47 --- /dev/null +++ b/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLMultiQueryRequestTest.kt @@ -0,0 +1,150 @@ +/* + * + * Copyright 2022 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.netflix.graphql.dgs.client.codegen + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class GraphQLMultiQueryRequestTest { + + @Test + fun testSerializeInputClassWithProjectionAndMultipleQueries() { + val query = TestGraphQLQuery().apply { + input["movie"] = Movie(1234, "testMovie") + } + val query2 = TestGraphQLQuery().apply { + input["actors"] = "actorA" + input["movies"] = listOf("movie1", "movie2") + } + + query.queryAlias = "alias1" + query2.queryAlias = "alias2" + + val multiRequest = GraphQLMultiQueryRequest( + listOf( + GraphQLQueryRequest(query, MovieProjection().name().movieId()), + GraphQLQueryRequest(query2, MovieProjection().name()) + ) + ) + + val result = multiRequest.serialize() + GraphQLQueryRequestTest.assertValidQuery(result) + Assertions.assertThat(result).isEqualTo( + """query { + | alias1: test(movie: {movieId : 1234, name : "testMovie"}) { + | name + | movieId + | } + | alias2: test(actors: "actorA", movies: ["movie1", "movie2"]) { + | name + | } + |} + """.trimMargin() + ) + } + + @Test + fun testSerializeInputClassWithProjectionAndMultipleMutations() { + val query = TestGraphQLMutation().apply { + input["movie"] = Movie(1234, "testMovie") + } + val query2 = TestGraphQLMutation().apply { + input["actors"] = "actorA" + input["movies"] = listOf("movie1", "movie2") + } + val query3 = TestGraphQLMutation().apply { + input["actors"] = "actorA" + input["movies"] = listOf("movie1", "movie2") + } + + query.queryAlias = "alias1" + query2.queryAlias = "alias2" + query3.queryAlias = "alias3" + + val multiRequest = GraphQLMultiQueryRequest( + listOf( + GraphQLQueryRequest(query), + GraphQLQueryRequest(query2), + GraphQLQueryRequest(query3) + ) + ) + + val result = multiRequest.serialize() + GraphQLQueryRequestTest.assertValidQuery(result) + Assertions.assertThat(result).isEqualTo( + """mutation { + | alias1: testMutation(movie: {movieId : 1234, name : "testMovie"}) + | alias2: testMutation(actors: "actorA", movies: ["movie1", "movie2"]) + | alias3: testMutation(actors: "actorA", movies: ["movie1", "movie2"]) + |} + """.trimMargin() + ) + } + + @Test + fun testSerializeInputClassWithProjectionAndMultipleMutations_MismatchOperationType() { + val query = TestGraphQLMutation().apply { + input["movie"] = Movie(1234, "testMovie") + } + + val query2 = TestGraphQLQuery().apply { + input["actors"] = "actorA" + input["movies"] = listOf("movie1", "movie2") + } + + val multiRequest = GraphQLMultiQueryRequest( + listOf( + GraphQLQueryRequest(query), + GraphQLQueryRequest(query2) + ) + ) + + assertThrows { + multiRequest.serialize() + } + } + + @Test + fun testSerializeInputClassWithProjectionAndSingleQueriesAndAlias() { + val query = TestGraphQLQuery().apply { + input["movie"] = Movie(1234, "testMovie") + } + + query.queryAlias = "alias1" + + val multiRequest = GraphQLMultiQueryRequest( + listOf( + GraphQLQueryRequest(query, MovieProjection().name().movieId()) + ) + ) + + val result = multiRequest.serialize() + GraphQLQueryRequestTest.assertValidQuery(result) + Assertions.assertThat(result).isEqualTo( + """query { + | alias1: test(movie: {movieId : 1234, name : "testMovie"}) { + | name + | movieId + | } + |} + """.trimMargin() + ) + } +} diff --git a/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequestTest.kt b/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequestTest.kt index 9063a142c..f53c40f2a 100644 --- a/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequestTest.kt +++ b/graphql-dgs-codegen-shared-core/src/test/kotlin/com/netflix/graphql/dgs/client/codegen/GraphQLQueryRequestTest.kt @@ -332,14 +332,16 @@ class GraphQLQueryRequestTest { /** * Assert that the GraphQL query is syntactically valid. */ - private fun assertValidQuery(query: String) { - val doc = try { - Parser().parseDocument(query) - } catch (exc: InvalidSyntaxException) { - throw AssertionError("The query failed to parse: ${exc.localizedMessage}") + companion object AssertValidQueryCompanion { + fun assertValidQuery(query: String) { + val doc = try { + Parser().parseDocument(query) + } catch (exc: InvalidSyntaxException) { + throw AssertionError("The query failed to parse: ${exc.localizedMessage}") + } + doc.getFirstDefinitionOfType(OperationDefinition::class.java) + .orElseThrow { AssertionError("No operation definition found in document") } } - doc.getFirstDefinitionOfType(OperationDefinition::class.java) - .orElseThrow { AssertionError("No operation definition found in document") } } }