Skip to content

Commit

Permalink
Merge pull request #426 from manymotes/multi_query_request
Browse files Browse the repository at this point in the history
Multi query request
  • Loading branch information
srinivasankavitha authored Jul 20, 2022
2 parents 9228915 + 6c27073 commit 0c9f695
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -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<GraphQLQueryRequest>
) {

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<VariableDefinition>()
val selectionList: MutableList<Field.Builder> = 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())
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -21,6 +21,7 @@ import graphql.language.VariableDefinition
abstract class GraphQLQuery(val operation: String, val name: String?) {
val input: MutableMap<String, Any> = mutableMapOf()
val variableDefinitions = mutableListOf<VariableDefinition>()
var queryAlias: String = ""

constructor(operation: String) : this(operation, null)
constructor() : this("query")
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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<Class<*>, 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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AssertionError> {
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()
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
}
}

Expand Down

0 comments on commit 0c9f695

Please sign in to comment.