Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use operationName if provided by GraphQLRequest (fixes #81) #203

Merged
merged 2 commits into from
Jan 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions kgraphql-ktor/src/main/kotlin/com/apurebase/kgraphql/KtorFeature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,19 @@ package com.apurebase.kgraphql
import com.apurebase.kgraphql.schema.Schema
import com.apurebase.kgraphql.schema.dsl.SchemaBuilder
import com.apurebase.kgraphql.schema.dsl.SchemaConfigurationDSL
import io.ktor.server.application.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.application.Application
import io.ktor.server.application.ApplicationCall
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.routing.*
import io.ktor.util.*
import java.nio.charset.Charset
import kotlinx.coroutines.coroutineScope
import kotlinx.serialization.json.*
import kotlinx.serialization.json.Json.Default.decodeFromString

class GraphQL(val schema: Schema) {

class Configuration: SchemaConfigurationDSL() {
class Configuration : SchemaConfigurationDSL() {
fun schema(block: SchemaBuilder.() -> Unit) {
schemaBlock = block
}
Expand All @@ -47,7 +42,7 @@ class GraphQL(val schema: Schema) {
}


companion object Feature: Plugin<Application, Configuration, GraphQL> {
companion object Feature : Plugin<Application, Configuration, GraphQL> {
override val key = AttributeKey<GraphQL>("KGraphQL")

private val rootFeature = FeatureInstance("KGraphQL")
Expand All @@ -57,7 +52,7 @@ class GraphQL(val schema: Schema) {
}
}

class FeatureInstance(featureKey: String = "KGraphQL"): Plugin<Application, Configuration, GraphQL> {
class FeatureInstance(featureKey: String = "KGraphQL") : Plugin<Application, Configuration, GraphQL> {

override val key = AttributeKey<GraphQL>(featureKey)

Expand All @@ -77,12 +72,20 @@ class GraphQL(val schema: Schema) {
val ctx = context {
config.contextSetup?.invoke(this, call)
}
val result = schema.execute(request.query, request.variables.toString(), ctx)
val result =
schema.execute(
request.query,
request.variables.toString(),
ctx,
operationName = request.operationName
)
call.respondText(result, contentType = ContentType.Application.Json)
}
if (config.playground) get {
@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
val playgroundHtml = KtorGraphQLConfiguration::class.java.classLoader.getResource("playground.html").readBytes()
val playgroundHtml =
KtorGraphQLConfiguration::class.java.classLoader.getResource("playground.html")
.readBytes()
call.respondBytes(playgroundHtml, contentType = ContentType.Text.Html)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import com.apurebase.kgraphql.Context
import com.apurebase.kgraphql.GraphQLError
import com.apurebase.kgraphql.configuration.SchemaConfiguration
import com.apurebase.kgraphql.request.CachingDocumentParser
import com.apurebase.kgraphql.request.VariablesJson
import com.apurebase.kgraphql.schema.introspection.__Schema
import com.apurebase.kgraphql.request.Parser
import com.apurebase.kgraphql.request.VariablesJson
import com.apurebase.kgraphql.schema.execution.*
import com.apurebase.kgraphql.schema.execution.Executor.*
import com.apurebase.kgraphql.schema.execution.Executor.DataLoaderPrepared
import com.apurebase.kgraphql.schema.execution.Executor.Parallel
import com.apurebase.kgraphql.schema.introspection.__Schema
import com.apurebase.kgraphql.schema.model.ast.NameNode
import com.apurebase.kgraphql.schema.structure.LookupSchema
import com.apurebase.kgraphql.schema.structure.RequestInterpreter
Expand All @@ -19,10 +20,10 @@ import kotlin.reflect.KClass
import kotlin.reflect.KType
import kotlin.reflect.jvm.jvmErasure

class DefaultSchema (
override val configuration: SchemaConfiguration,
internal val model : SchemaModel
) : Schema , __Schema by model, LookupSchema {
class DefaultSchema(
override val configuration: SchemaConfiguration,
internal val model: SchemaModel
) : Schema, __Schema by model, LookupSchema {

companion object {
val OPERATION_NAME_PARAM = NameNode("operationName", null)
Expand All @@ -35,11 +36,17 @@ class DefaultSchema (
DataLoaderPrepared -> DataLoaderPreparedRequestExecutor(this)
}

private val requestInterpreter : RequestInterpreter = RequestInterpreter(model)
private val requestInterpreter: RequestInterpreter = RequestInterpreter(model)

private val cacheParser: CachingDocumentParser by lazy { CachingDocumentParser(configuration.documentParserCacheMaximumSize) }

override suspend fun execute(request: String, variables: String?, context: Context, options: ExecutionOptions): String = coroutineScope {
override suspend fun execute(
request: String,
variables: String?,
context: Context,
options: ExecutionOptions,
operationName: String?,
): String = coroutineScope {
val parsedVariables = variables
?.let { VariablesJson.Defined(configuration.objectMapper, variables) }
?: VariablesJson.Empty()
Expand All @@ -53,7 +60,7 @@ class DefaultSchema (
val executor = options.executor?.let(this@DefaultSchema::getExecutor) ?: defaultRequestExecutor

executor.suspendExecute(
plan = requestInterpreter.createExecutionPlan(document, parsedVariables, options),
plan = requestInterpreter.createExecutionPlan(document, operationName, parsedVariables, options),
variables = parsedVariables,
context = context
)
Expand Down
10 changes: 6 additions & 4 deletions kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/Schema.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ interface Schema : __Schema {
@Language("graphql") request: String,
variables: String? = null,
context: Context = Context(emptyMap()),
options: ExecutionOptions = ExecutionOptions()
) : String
options: ExecutionOptions = ExecutionOptions(),
operationName: String? = null
): String

fun executeBlocking(
@Language("graphql") request: String,
variables: String? = null,
context: Context = Context(emptyMap()),
options: ExecutionOptions = ExecutionOptions()
) = runBlocking { execute(request, variables, context, options) }
options: ExecutionOptions = ExecutionOptions(),
operationName: String? = null,
) = runBlocking { execute(request, variables, context, options, operationName) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import kotlin.reflect.KType

class SchemaProxy(
override val configuration: SchemaConfiguration,
var proxiedSchema : LookupSchema? = null
): LookupSchema {
var proxiedSchema: LookupSchema? = null
) : LookupSchema {

companion object {
const val ILLEGAL_STATE_MESSAGE = "Missing proxied __Schema instance"
Expand Down Expand Up @@ -49,7 +49,13 @@ class SchemaProxy(

override fun inputTypeByName(name: String): Type? = inputTypeByName(name)

override suspend fun execute(request: String, variables: String?, context: Context, options: ExecutionOptions): String {
return getProxied().execute(request, variables, context, options)
override suspend fun execute(
request: String,
variables: String?,
context: Context,
options: ExecutionOptions,
operationName: String?
): String {
return getProxied().execute(request, variables, context, options, operationName)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
// prevent stack overflow
private val fragmentsStack = Stack<String>()
fun get(node: FragmentSpreadNode): Execution.Fragment? {
if(fragmentsStack.contains(node.name.value)) throw GraphQLError(
if (fragmentsStack.contains(node.name.value)) throw GraphQLError(
"Fragment spread circular references are not allowed",
node
)
Expand All @@ -46,7 +46,12 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
}
}

fun createExecutionPlan(document: DocumentNode, variables: VariablesJson, options: ExecutionOptions): ExecutionPlan {
fun createExecutionPlan(
document: DocumentNode,
requestedOperationName: String?,
variables: VariablesJson,
options: ExecutionOptions
): ExecutionPlan {
val test = document.definitions.filterIsInstance<ExecutableDefinitionNode>()

val operation = test.filterIsInstance<OperationDefinitionNode>().let { operations ->
Expand All @@ -58,8 +63,10 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
if (it.size != operations.size) throw GraphQLError("anonymous operation must be the only defined operation")
}.joinToString(prefix = "[", postfix = "]")

val operationName = variables.get(String::class, String::class.starProjectedType, OPERATION_NAME_PARAM)
?: throw GraphQLError("Must provide an operation name from: $operationNamesFound")
val operationName = requestedOperationName ?: (
variables.get(String::class, String::class.starProjectedType, OPERATION_NAME_PARAM)
?: throw GraphQLError("Must provide an operation name from: $operationNamesFound")
)

operations.firstOrNull { it.name?.value == operationName }
?: throw GraphQLError("Must provide an operation name from: $operationNamesFound, found $operationName")
Expand All @@ -69,8 +76,11 @@ class RequestInterpreter(val schemaModel: SchemaModel) {

val root = when (operation.operation) {
OperationTypeNode.QUERY -> schemaModel.query
OperationTypeNode.MUTATION -> schemaModel.mutation ?: throw GraphQLError("Mutations are not supported on this schema")
OperationTypeNode.SUBSCRIPTION -> schemaModel.subscription ?: throw GraphQLError("Subscriptions are not supported on this schema")
OperationTypeNode.MUTATION -> schemaModel.mutation
?: throw GraphQLError("Mutations are not supported on this schema")

OperationTypeNode.SUBSCRIPTION -> schemaModel.subscription
?: throw GraphQLError("Subscriptions are not supported on this schema")
}

val fragmentDefinitionNode = test.filterIsInstance<FragmentDefinitionNode>()
Expand All @@ -79,7 +89,7 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
val name = fragmentDef.name!!.value

if (fragmentDefinitionNode.count { it.name!!.value == name } > 1) {
throw GraphQLError("There can be only one fragment named $name.", fragmentDef )
throw GraphQLError("There can be only one fragment named $name.", fragmentDef)
}

name to (type to fragmentDef.selectionSet)
Expand All @@ -100,7 +110,12 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
private fun handleReturnType(ctx: InterpreterContext, type: Type, requestNode: FieldNode) =
handleReturnType(ctx, type, requestNode.selectionSet, requestNode.name)

private fun handleReturnType(ctx: InterpreterContext, type: Type, selectionSet: SelectionSetNode?, propertyName: NameNode? = null): List<Execution> {
private fun handleReturnType(
ctx: InterpreterContext,
type: Type,
selectionSet: SelectionSetNode?,
propertyName: NameNode? = null
): List<Execution> {
val children = mutableListOf<Execution>()

if (!selectionSet?.selections.isNullOrEmpty()) {
Expand All @@ -120,15 +135,22 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
private fun handleReturnTypeChildOrFragment(node: SelectionNode, returnType: Type, ctx: InterpreterContext) =
returnType.unwrapped().handleSelectionFieldOrFragment(node, ctx)

private fun findFragmentType(fragment: FragmentNode, ctx: InterpreterContext, enclosingType: Type): Execution.Fragment = when(fragment) {
private fun findFragmentType(
fragment: FragmentNode,
ctx: InterpreterContext,
enclosingType: Type
): Execution.Fragment = when (fragment) {
is FragmentSpreadNode -> {
ctx.get(fragment) ?: throw throwUnknownFragmentTypeEx(fragment)
}

is InlineFragmentNode -> {
val type =if (fragment.directives?.isNotEmpty() == true) {
val type = if (fragment.directives?.isNotEmpty() == true) {
enclosingType
} else {
schemaModel.queryTypesByName[fragment.typeCondition?.name?.value] ?: throw throwUnknownFragmentTypeEx(fragment)
schemaModel.queryTypesByName[fragment.typeCondition?.name?.value] ?: throw throwUnknownFragmentTypeEx(
fragment
)
}
Execution.Fragment(
selectionNode = fragment,
Expand All @@ -139,17 +161,23 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
}
}

private fun Type.handleSelectionFieldOrFragment(node: SelectionNode, ctx: InterpreterContext): Execution = when (node) {
is FragmentNode -> findFragmentType(node, ctx, this)
is FieldNode -> handleSelection(node, ctx)
}
private fun Type.handleSelectionFieldOrFragment(node: SelectionNode, ctx: InterpreterContext): Execution =
when (node) {
is FragmentNode -> findFragmentType(node, ctx, this)
is FieldNode -> handleSelection(node, ctx)
}

private fun Type.handleSelection(node: FieldNode, ctx: InterpreterContext, variables: List<VariableDefinitionNode>? = null): Execution.Node {
private fun Type.handleSelection(
node: FieldNode,
ctx: InterpreterContext,
variables: List<VariableDefinitionNode>? = null
): Execution.Node {
return when (val field = this[node.name.value]) {
null -> throw GraphQLError(
"Property ${node.name.value} on $name does not exist",
node
)

is Field.Union<*> -> handleUnion(field, node, ctx)
else -> {
validatePropertyArguments(this, field, node)
Expand All @@ -168,31 +196,40 @@ class RequestInterpreter(val schemaModel: SchemaModel) {
}
}

private fun <T> handleUnion(field: Field.Union<T>, selectionNode: FieldNode, ctx: InterpreterContext): Execution.Union {
private fun <T> handleUnion(
field: Field.Union<T>,
selectionNode: FieldNode,
ctx: InterpreterContext
): Execution.Union {
validateUnionRequest(field, selectionNode)

val unionMembersChildren: Map<Type, List<Execution>> = field.returnType.possibleTypes.associateWith { possibleType ->
val selections = selectionNode.selectionSet?.selections
val unionMembersChildren: Map<Type, List<Execution>> =
field.returnType.possibleTypes.associateWith { possibleType ->
val selections = selectionNode.selectionSet?.selections

val a = selections?.filterIsInstance<FragmentSpreadNode>()?.firstOrNull {
ctx.fragments[it.name.value]?.first?.name == possibleType.name
}
val a = selections?.filterIsInstance<FragmentSpreadNode>()?.firstOrNull {
ctx.fragments[it.name.value]?.first?.name == possibleType.name
}

if (a != null) return@associateWith handleReturnType(ctx, possibleType, ctx.fragments.getValue(a.name.value).second)
if (a != null) return@associateWith handleReturnType(
ctx,
possibleType,
ctx.fragments.getValue(a.name.value).second
)

val b = selections?.filterIsInstance<InlineFragmentNode>()?.find {
possibleType.name == it.typeCondition?.name?.value
}
val b = selections?.filterIsInstance<InlineFragmentNode>()?.find {
possibleType.name == it.typeCondition?.name?.value
}

if (b != null) return@associateWith handleReturnType(ctx, possibleType, b.selectionSet)
if (b != null) return@associateWith handleReturnType(ctx, possibleType, b.selectionSet)

throw GraphQLError(
"Missing type argument for type ${possibleType.name}",
selectionNode
)
}
throw GraphQLError(
"Missing type argument for type ${possibleType.name}",
selectionNode
)
}

return Execution.Union (
return Execution.Union(
node = selectionNode,
unionField = field,
memberChildren = unionMembersChildren,
Expand Down