diff --git a/.gitignore b/.gitignore index 6617a70a..1fd6a3dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/vcs.xml +.idea/inspectionProfiles # Created by https://www.gitignore.io/api/java,gradle,intellij+all # Edit at https://www.gitignore.io/?templates=java,gradle,intellij+all diff --git a/.idea/.name b/.idea/.name index 3e7982f2..0f31176c 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -KGraphQL +core \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4bfbe46d..355ea664 100644 --- a/build.gradle +++ b/build.gradle @@ -1,8 +1,9 @@ plugins { - id "org.jetbrains.kotlin.jvm" version "1.3.50" + id "org.jetbrains.kotlin.jvm" version "1.3.61" id "com.github.ben-manes.versions" version "0.24.0" id "com.jfrog.bintray" version "1.8.4" id "maven-publish" + id "jacoco" } allprojects { @@ -26,9 +27,12 @@ allprojects { testCompile "io.netty:netty-all:$netty_version" - testCompile "junit:junit:$junit_version" testCompile "org.hamcrest:hamcrest:$hamcrest_version" testCompile "org.amshove.kluent:kluent:$kluent_version" + testCompile "org.junit.jupiter:junit-jupiter-api:$junit_version" + testCompile "org.junit.jupiter:junit-jupiter-params:$junit_version" + testRuntime "org.junit.jupiter:junit-jupiter-engine:$junit_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-debug:$coroutine_version" } task sourcesJar(type: Jar, dependsOn: classes) { @@ -75,5 +79,12 @@ allprojects { } } } + + + + + test { + useJUnitPlatform() + } } diff --git a/gradle.properties b/gradle.properties index d034a47d..82d82768 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,17 +1,18 @@ # KGraphQL version -version=0.9.2 +version=0.10.0 # Dependencies kotlin_version=1.3.50 coroutine_version=1.3.2 jackson_version=2.9.7 caffeine_version=2.8.0 - +serialization_version=0.14.0 +kDataLoader_version=0.1.1 # Test-Dependencies kotlin_html_version=0.6.12 netty_version=4.1.42.Final -junit_version=4.12 +junit_version=5.5.2 kluent_version=1.56 hamcrest_version=2.2 diff --git a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt index 6821d694..4d006fe4 100644 --- a/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt +++ b/kgraphql-ktor/src/test/kotlin/com/apurebase/kgraphql/KtorFeatureTest.kt @@ -1,7 +1,7 @@ package com.apurebase.kgraphql import org.amshove.kluent.shouldEqual -import org.junit.Test +import org.junit.jupiter.api.Test class KtorFeatureTest { @Test diff --git a/kgraphql/build.gradle b/kgraphql/build.gradle index f46ee0cd..aaa2ac3a 100644 --- a/kgraphql/build.gradle +++ b/kgraphql/build.gradle @@ -1,3 +1,7 @@ dependencies { compile "com.github.ben-manes.caffeine:caffeine:$caffeine_version" + compile "de.nidomiro:KDataLoader:$kDataLoader_version" + + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compile "org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serialization_version" // JVM dependency } diff --git a/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/FunctionExecutionBenchmark.kt b/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/FunctionExecutionBenchmark.kt index 4ee51872..fc811c58 100644 --- a/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/FunctionExecutionBenchmark.kt +++ b/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/FunctionExecutionBenchmark.kt @@ -2,7 +2,7 @@ package com.apurebase.kgraphql import com.apurebase.kgraphql.schema.model.FunctionWrapper import kotlinx.coroutines.runBlocking -import org.junit.Test +import org.junit.jupiter.api.Test import org.openjdk.jmh.annotations.* import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.TimeUnit diff --git a/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/ParallelExecutionBenchmark.kt b/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/ParallelExecutionBenchmark.kt index 6b070087..073373db 100644 --- a/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/ParallelExecutionBenchmark.kt +++ b/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/ParallelExecutionBenchmark.kt @@ -5,7 +5,7 @@ import org.openjdk.jmh.annotations.* import java.util.concurrent.ThreadLocalRandom import java.util.concurrent.TimeUnit import kotlinx.coroutines.* -import org.junit.Test +import org.junit.jupiter.api.Test @State(Scope.Benchmark) @Warmup(iterations = 5) diff --git a/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/SimpleExecutionOverheadBenchmark.kt b/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/SimpleExecutionOverheadBenchmark.kt index a84d16cd..ad7e47d6 100644 --- a/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/SimpleExecutionOverheadBenchmark.kt +++ b/kgraphql/src/jmh/kotlin/com/apurebase/kgraphql/SimpleExecutionOverheadBenchmark.kt @@ -7,7 +7,7 @@ import com.apurebase.kgraphql.schema.Schema import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import kotlinx.coroutines.runBlocking -import org.junit.Test +import org.junit.jupiter.api.Test import org.openjdk.jmh.annotations.* import java.util.concurrent.TimeUnit diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/Extensions.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/Extensions.kt index c0299fec..45f0d42b 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/Extensions.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/Extensions.kt @@ -1,5 +1,8 @@ package com.apurebase.kgraphql +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlin.coroutines.EmptyCoroutineContext import kotlin.reflect.KClass import kotlin.reflect.KParameter import kotlin.reflect.KType @@ -30,3 +33,34 @@ internal fun KType.getIterableElementType(): KType? { internal fun not(boolean: Boolean) = !boolean + + +internal suspend fun Collection.toMapAsync( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + block: suspend (T) -> R +): Map = coroutineScope { + val channel = Channel>() + val jobs = map { item -> + launch(dispatcher) { + try { + val res = block(item) + channel.send(item to res) + } catch (e: Exception) { + channel.close(e) + } + } + } + val resultMap = mutableMapOf() + repeat(size) { + try { + val (item, result) = channel.receive() + resultMap[item] = result + } catch (e: Exception) { + jobs.forEach { job: Job -> job.cancel() } + throw e + } + } + + channel.close() + resultMap +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt index d9fec594..70b89a60 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/configuration/SchemaConfiguration.kt @@ -1,5 +1,6 @@ package com.apurebase.kgraphql.configuration +import com.apurebase.kgraphql.schema.execution.Executor import com.fasterxml.jackson.databind.ObjectMapper import kotlinx.coroutines.CoroutineDispatcher @@ -11,5 +12,8 @@ data class SchemaConfiguration ( val objectMapper: ObjectMapper, val useDefaultPrettyPrinter: Boolean, //execution - val coroutineDispatcher: CoroutineDispatcher + val coroutineDispatcher: CoroutineDispatcher, + + val executor: Executor, + val timeout: Long? ) diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt index adc23523..4ae8c16f 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/DefaultSchema.kt @@ -4,10 +4,10 @@ import com.apurebase.kgraphql.Context import com.apurebase.kgraphql.configuration.SchemaConfiguration import com.apurebase.kgraphql.request.CachingDocumentParser import com.apurebase.kgraphql.request.VariablesJson -import com.apurebase.kgraphql.schema.execution.ParallelRequestExecutor -import com.apurebase.kgraphql.schema.execution.RequestExecutor import com.apurebase.kgraphql.schema.introspection.__Schema import com.apurebase.kgraphql.request.Parser +import com.apurebase.kgraphql.schema.execution.* +import com.apurebase.kgraphql.schema.execution.Executor.* import com.apurebase.kgraphql.schema.model.ast.NameNode import com.apurebase.kgraphql.schema.structure.LookupSchema import com.apurebase.kgraphql.schema.structure.RequestInterpreter @@ -26,7 +26,10 @@ class DefaultSchema ( val OPERATION_NAME_PARAM = NameNode("operationName", null) } - private val requestExecutor : RequestExecutor = ParallelRequestExecutor(this) + private val requestExecutor : RequestExecutor = when (configuration.executor) { + Parallel -> ParallelRequestExecutor(this) + DataLoaderPrepared -> DataLoaderPreparedRequestExecutor(this) + } private val requestInterpreter : RequestInterpreter = RequestInterpreter(model) diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt new file mode 100644 index 00000000..94cd25b2 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/DataLoaderPropertyDSL.kt @@ -0,0 +1,63 @@ +package com.apurebase.kgraphql.schema.dsl + +import com.apurebase.kgraphql.Context +import com.apurebase.kgraphql.schema.model.FunctionWrapper +import com.apurebase.kgraphql.schema.model.InputValueDef +import com.apurebase.kgraphql.schema.model.PropertyDef +import nidomiro.kdataloader.BatchLoader +import nidomiro.kdataloader.dsl.dataLoaderFactory +import kotlin.reflect.KType + +class DataLoaderPropertyDSL( + val name: String, + val returnType: KType, + private val block : DataLoaderPropertyDSL.() -> Unit +): LimitedAccessItemDSL(), ResolverDSL.Target { + + internal var dataLoader: BatchLoader? = null + internal var prepareWrapper: FunctionWrapper? = null + + private val inputValues = mutableListOf>() + + fun loader(block: BatchLoader) { + dataLoader = block + } + + fun prepare(block: suspend (T) -> K) { + prepareWrapper = FunctionWrapper.on(block, true) + } + + fun prepare(block: suspend (T, E) -> K) { + prepareWrapper = FunctionWrapper.on(block, true) + } + + fun accessRule(rule: (T, Context) -> Exception?){ + val accessRuleAdapter: (T?, Context) -> Exception? = { parent, ctx -> + if (parent != null) rule(parent, ctx) else IllegalArgumentException("Unexpected null parent of kotlin property") + } + this.accessRuleBlock = accessRuleAdapter + } + + fun toKQLProperty(): PropertyDef.DataLoadedFunction { + block() + requireNotNull(prepareWrapper) + requireNotNull(dataLoader) + + return PropertyDef.DataLoadedFunction( + name = name, + description = description, + accessRule = accessRuleBlock, + deprecationReason = deprecationReason, + isDeprecated = isDeprecated, + inputValues = inputValues, + returnType = returnType, + prepare = prepareWrapper!!, + loader = dataLoaderFactory(dataLoader!!) + ) + } + + override fun addInputValues(inputValues: Collection>) { + this.inputValues.addAll(inputValues) + } + +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt index 15de5f13..4b397fca 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/PropertyDSL.kt @@ -62,4 +62,4 @@ class PropertyDSL(val name : String, block : PropertyDSL.() -> override fun addInputValues(inputValues: Collection>) { this.inputValues.addAll(inputValues) } -} \ No newline at end of file +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt index 5ccca132..1bf8994a 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaBuilder.kt @@ -15,6 +15,7 @@ import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.module.SimpleModule +import kotlinx.coroutines.runBlocking import kotlin.reflect.KClass /** @@ -27,7 +28,10 @@ class SchemaBuilder internal constructor() { private var configuration = SchemaConfigurationDSL() fun build(): Schema { - return SchemaCompilation(configuration.build(), model.toSchemaDefinition()).perform() + // TODO: [runBlocking] is a temp fix + return runBlocking { + SchemaCompilation(configuration.build(), model.toSchemaDefinition()).perform() + } } fun configure(block: SchemaConfigurationDSL.() -> Unit){ @@ -187,13 +191,9 @@ class SchemaBuilder internal constructor() { model.addInputObject(TypeDef.Input(input.name, kClass, input.description)) } - inline fun inputType(noinline block : InputTypeDSL.() -> Unit) { + inline fun inputType(noinline block : InputTypeDSL.() -> Unit = {}) { inputType(T::class, block) } - - inline fun inputType() { - inputType(T::class) {} - } } inline fun SchemaConfigurationDSL.appendMapper(scalar: ScalarDSL, kClass: KClass) { diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt index c8afcb97..5a1a9809 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/SchemaConfigurationDSL.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.apurebase.kgraphql.configuration.SchemaConfiguration +import com.apurebase.kgraphql.schema.execution.Executor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -15,17 +16,21 @@ class SchemaConfigurationDSL { var documentParserCacheMaximumSize: Long = 1000L var acceptSingleValueAsArray: Boolean = true var coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default + var executor: Executor = Executor.Parallel + var timeout: Long? = null internal fun update(block: SchemaConfigurationDSL.() -> Unit) = block() internal fun build(): SchemaConfiguration { objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, acceptSingleValueAsArray) return SchemaConfiguration( - useCachingDocumentParser, - documentParserCacheMaximumSize, - objectMapper, - useDefaultPrettyPrinter, - coroutineDispatcher + useCachingDocumentParser, + documentParserCacheMaximumSize, + objectMapper, + useDefaultPrettyPrinter, + coroutineDispatcher, + executor, + timeout ) } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt index 0d2c39d6..09feefd0 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/InputTypeDSL.kt @@ -8,4 +8,4 @@ import kotlin.reflect.KClass class InputTypeDSL(val kClass: KClass) : ItemDSL() { var name = kClass.defaultKQLTypeName() -} \ No newline at end of file +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/TypeDSL.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/TypeDSL.kt index 56e75cfe..e42177cc 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/TypeDSL.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/dsl/types/TypeDSL.kt @@ -2,16 +2,15 @@ package com.apurebase.kgraphql.schema.dsl.types import com.apurebase.kgraphql.defaultKQLTypeName import com.apurebase.kgraphql.schema.SchemaException -import com.apurebase.kgraphql.schema.dsl.ItemDSL -import com.apurebase.kgraphql.schema.dsl.KotlinPropertyDSL -import com.apurebase.kgraphql.schema.dsl.PropertyDSL -import com.apurebase.kgraphql.schema.dsl.UnionPropertyDSL +import com.apurebase.kgraphql.schema.dsl.* import com.apurebase.kgraphql.schema.model.FunctionWrapper import com.apurebase.kgraphql.schema.model.PropertyDef import com.apurebase.kgraphql.schema.model.Transformation import com.apurebase.kgraphql.schema.model.TypeDef import kotlin.reflect.KClass import kotlin.reflect.KProperty1 +import kotlin.reflect.full.createType +import kotlin.reflect.typeOf open class TypeDSL( @@ -29,6 +28,8 @@ open class TypeDSL( internal val describedKotlinProperties = mutableMapOf, PropertyDef.Kotlin>() + val dataloadedExtensionProperties = mutableSetOf>() + fun transformation(kProperty: KProperty1, function: suspend (R, E) -> R) { transformationProperties.add(Transformation(kProperty, FunctionWrapper.on(function, true))) } @@ -61,6 +62,12 @@ open class TypeDSL( transformationProperties.add(Transformation(kProperty, FunctionWrapper.on(function, true))) } + @UseExperimental(ExperimentalStdlibApi::class) + inline fun dataProperty(name: String, noinline block: DataLoaderPropertyDSL.() -> Unit) { + dataloadedExtensionProperties.add( + DataLoaderPropertyDSL(name, typeOf(), block).toKQLProperty() + ) + } fun property(kProperty: KProperty1, block : KotlinPropertyDSL.() -> Unit){ val dsl = KotlinPropertyDSL(kProperty, block) @@ -95,6 +102,7 @@ open class TypeDSL( kClass = kClass, kotlinProperties = describedKotlinProperties.toMap(), extensionProperties = extensionProperties.toList(), + dataloadExtensionProperties = dataloadedExtensionProperties.toList(), unionProperties = unionProperties.toList(), transformations = transformationProperties.associateBy { it.kProperty }, description = description diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DataLoaderPreparedRequestExecutor.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DataLoaderPreparedRequestExecutor.kt new file mode 100644 index 00000000..e50e0bc0 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DataLoaderPreparedRequestExecutor.kt @@ -0,0 +1,425 @@ +package com.apurebase.kgraphql.schema.execution + +import com.apurebase.kgraphql.Context +import com.apurebase.kgraphql.ExecutionException +import com.apurebase.kgraphql.request.Variables +import com.apurebase.kgraphql.request.VariablesJson +import com.apurebase.kgraphql.schema.DefaultSchema +import com.apurebase.kgraphql.schema.introspection.TypeKind +import com.apurebase.kgraphql.schema.model.FunctionWrapper +import com.apurebase.kgraphql.schema.model.TypeDef +import com.apurebase.kgraphql.schema.model.ast.ArgumentNodes +import com.apurebase.kgraphql.schema.scalar.serializeScalar +import com.apurebase.kgraphql.schema.structure.Field +import com.apurebase.kgraphql.schema.structure.InputValue +import com.apurebase.kgraphql.schema.structure.Type +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.serialization.json.* +import nidomiro.kdataloader.DataLoader +import nidomiro.kdataloader.factories.DataLoaderFactory +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong +import kotlin.reflect.KProperty1 + + +class DataLoaderPreparedRequestExecutor(val schema: DefaultSchema) : RequestExecutor, CoroutineScope { + + private val argumentsHandler = ArgumentsHandler(schema) + private val dispatcher = schema.configuration.coroutineDispatcher + override val coroutineContext = Job() + + inner class ExecutionContext( + val variables: Variables, + val requestContext: Context, + private val dataCounters: ConcurrentHashMap, AtomicLong> = ConcurrentHashMap(), + private val loaders: ConcurrentHashMap, DataLoader> = ConcurrentHashMap() + ) : Mutex by Mutex() { + + + suspend fun get(loader: DataLoader<*, *>): Long = withLock { + dataCounters[loader]?.get() ?: throw IllegalArgumentException("Something went wrong with execution") + } + suspend fun add(loader: DataLoader<*, *>, check: Pair<*, String>, count: Long) = withLock { + if (dataCounters[loader] == null) { + /** TODO: There shouldn't be a need for Atomic here as we are using [withLock] */ + dataCounters[loader] = AtomicLong(count) + } else { + val counter = dataCounters[loader]!! + counter.getAndUpdate { + it + count + } + } + } + + suspend fun load(builder: DeferredJsonMap, loaderfactory: DataLoaderFactory, node: Execution.Node, preparedValue: Any?): Deferred { + val loader = loaders.getOrPut(loaderfactory) { loaderfactory.constructNew() } + add(loader, preparedValue to node.selectionNode.fullPath, 1) // parentCount) + + val value = loader.loadAsync(preparedValue) + + // TODO: Create unit tests with nested different dataLoaders + builder.deferredLaunch { + val count = get(loader) + val stats = loader.createStatisticsSnapshot() + if (stats.objectsRequested >= count) { + loader.dispatch() + } // else if (stats.objectsRequested > count) throw TODO("This should never happen!!!") + } + + + return value + } + } + + + private suspend fun DeferredJsonMap.writeOperation( + ctx: ExecutionContext, + node: Execution.Node, + operation: FunctionWrapper + ) { + node.field.checkAccess(null, ctx.requestContext) + val result: T? = operation.invoke( + funName = node.field.name, + receiver = null, + inputValues = node.field.arguments, + args = node.arguments, + executionNode = node, + ctx = ctx + ) + + + applyKeyToElement(ctx, result, node, node.field.returnType, 1) + } + + private fun Any?.toPrimitive(node: Execution.Node, returnType: Type): JsonElement = when { + this == null -> createNullNode(node, returnType.unwrapList()) + this is Collection<*> || this is Array<*> -> when (this) { + is Array<*> -> this.toList() + else -> this as Collection<*> + }.map { it.toPrimitive(node, returnType.unwrapList()) }.let(::JsonArray) + this is String -> JsonPrimitive(this) + this is Int -> JsonPrimitive(this) + this is Float -> JsonPrimitive(this) + this is Double -> JsonPrimitive(this) + this is Boolean -> JsonPrimitive(this) + this is Long -> JsonPrimitive(this) + returnType.unwrapped() is Type.Enum<*> -> JsonPrimitive(toString()) + else -> throw TODO("Whaa? -> $this") + } + + private suspend fun DeferredJsonMap.applyKeyToElement( + ctx: ExecutionContext, + value: T?, + node: Execution.Node, + returnType: Type, + parentCount: Long + ) { + return when { + value == null -> node.aliasOrKey toValue createNullNode(node, returnType) + value is Collection<*> || value is Array<*> -> { + if (returnType.isList()) { + val values = when (value) { + is Array<*> -> value.toList() + else -> value as Collection<*> + } + + if (node.children.isEmpty()) { + node.aliasOrKey toDeferredArray { + values.map { addValue(it.toPrimitive(node, returnType)) } + } + } else { + node.aliasOrKey toDeferredArray { + values.map { v -> + addDeferredObj { + when { + v == null -> createNullNode(node, returnType) +// node.children.isEmpty() -> createSimpleValueNode(returnType.unwrapList(), v, node) + node.children.isNotEmpty() -> this@addDeferredObj.applyObjectProperties( + ctx = ctx, + value = v, + node = node, + type = returnType.unwrapList(), + parentCount = values.size.toLong() + ) + else -> throw TODO("Unknown error!") + } + } + } + } + } + } else { + throw ExecutionException("Invalid collection value for non collection property", node) + } + } + value is String -> node.aliasOrKey toValue JsonPrimitive(value) + value is Int -> node.aliasOrKey toValue JsonPrimitive(value) + value is Float -> node.aliasOrKey toValue JsonPrimitive(value) + value is Double -> node.aliasOrKey toValue JsonPrimitive(value) + value is Boolean -> node.aliasOrKey toValue JsonPrimitive(value) + value is Long -> node.aliasOrKey toValue JsonPrimitive(value) + value is Deferred<*> -> { + deferredLaunch { + applyKeyToElement(ctx, value.await(), node, returnType, parentCount) + } + } + node.children.isNotEmpty() -> node.aliasOrKey toDeferredObj { + applyObjectProperties(ctx, value, node, returnType, parentCount) + } + node is Execution.Union -> node.aliasOrKey toDeferredObj { + applyObjectProperties(ctx, value, node.memberExecution(returnType), returnType, parentCount) + } + else -> node.aliasOrKey toValue createSimpleValueNode(returnType, value, node) + } + } + + private fun createSimpleValueNode(returnType: Type, value: T, node: Execution.Node): JsonElement { + return when (val unwrapped = returnType.unwrapped()) { + is Type.Scalar<*> -> { + serializeScalar(unwrapped, value, node) + } + is Type.Enum<*> -> JsonPrimitive(value.toString()) + is TypeDef.Object<*> -> throw ExecutionException("Cannot handle object return type, schema structure exception", node) + else -> throw ExecutionException("Invalid Type: ${returnType.name}", node) + } + } + + + private suspend fun DeferredJsonMap.applyObjectProperties( + ctx: ExecutionContext, + value: T, + node: Execution.Node, + type: Type, + parentCount: Long + ) { + node.children.map { child -> + when (child) { + is Execution.Fragment -> handleFragment(ctx, value, child) + else -> applyProperty(ctx, value, child, type, parentCount) + } + } + } + + private suspend fun DeferredJsonMap.handleFragment(ctx: ExecutionContext, value: T, container: Execution.Fragment) { + if (!shouldInclude(ctx, container)) return + + val expectedType = container.condition.type + + if (expectedType.kind == TypeKind.OBJECT || expectedType.kind == TypeKind.INTERFACE) { + if (expectedType.isInstance(value)) { + container.elements.map { child -> + when (child) { + is Execution.Fragment -> handleFragment(ctx, value, child) + else -> applyProperty(ctx, value, child, expectedType, container.elements.size.toLong()) + } + } + } + } else { + throw IllegalStateException("fragments can be specified on object types, interfaces, and unions") + } + } + + @Suppress("UNCHECKED_CAST") + private suspend fun DeferredJsonMap.applyProperty( + ctx: ExecutionContext, + value: T, + child: Execution, type: Type, + parentCount: Long + ) { + when (child) { + is Execution.Union -> { + val field = type.unwrapped()[child.key] + ?: throw IllegalStateException("Execution unit ${child.key} is not contained by operation return type") + if (field is Field.Union<*>) { + createUnionOperationNode(ctx, value, child, field as Field.Union, parentCount) + } else { + throw ExecutionException("Unexpected non-union field for union execution node", child) + } + } + is Execution.Node -> { + val field = type.unwrapped()[child.key] + ?: throw IllegalStateException("Execution unit ${child.key} is not contained by operation return type") + createPropertyNodeAsync(ctx, value, child, field, parentCount) + } + else -> throw UnsupportedOperationException("Whatever this is isn't supported!") + } + } + + private suspend fun DeferredJsonMap.createUnionOperationNode(ctx: ExecutionContext, parent: T, node: Execution.Union, unionProperty: Field.Union, parentCount: Long) { + node.field.checkAccess(parent, ctx.requestContext) + + val operationResult: Any? = unionProperty.invoke( + funName = unionProperty.name, + receiver = parent, + inputValues = node.field.arguments, + args = node.arguments, + executionNode = node, + ctx = ctx + ) + + val returnType = unionProperty.returnType.possibleTypes.find { it.isInstance(operationResult) } + + if (returnType == null && !unionProperty.nullable) { + val expectedOneOf = unionProperty.type.possibleTypes!!.joinToString { it.name.toString() } + throw ExecutionException( + "Unexpected type of union property value, expected one of: [$expectedOneOf]." + + " value was $operationResult", node + ) + } + + applyKeyToElement(ctx, operationResult, node, returnType ?: unionProperty.returnType, parentCount) + } + + @Suppress("UNCHECKED_CAST") + private suspend fun DeferredJsonMap.createPropertyNodeAsync( + ctx: ExecutionContext, + parentValue: T, + node: Execution.Node, + field: Field, + parentCount: Long + ) { + // TODO: Check include directive + node.field.checkAccess(parentValue, ctx.requestContext) + if (!shouldInclude(ctx, node)) return + + + when (field) { + is Field.Kotlin<*, *> -> { + val rawValue = try { + (field.kProperty as KProperty1).get(parentValue) + } catch(e: NullPointerException) { + throw e + } + val value: Any? = field.transformation?.invoke( + funName = field.name, + receiver = rawValue, + inputValues = field.arguments, + args = node.arguments, + executionNode = node, + ctx = ctx + ) ?: rawValue + + applyKeyToElement(ctx, value, node, field.returnType, parentCount) + } + is Field.Function<*, *> -> { + handleFunctionProperty(ctx, parentValue, node, field, parentCount) + } + is Field.DataLoader<*, *, *> -> { + field as Field.DataLoader + handleDataPropertyAsync(ctx, parentValue, node, field, parentCount) + } + else -> throw TODO("Only Kotlin Fields are supported!") + } + } + + private suspend fun DeferredJsonMap.handleDataPropertyAsync( + ctx: ExecutionContext, + parentValue: T, + node: Execution.Node, + field: Field.DataLoader, + parentCount: Long + ) { + val preparedValue = field.kql.prepare.invoke( + funName = field.name, + receiver = parentValue, + inputValues = field.arguments, + args = node.arguments, + executionNode = node, + ctx = ctx + ) // ?: TODO("Nullable prepare functions isn't supported") + + + val value = ctx.load(this, field.loader as DataLoaderFactory, node, preparedValue) + + + applyKeyToElement(ctx, value, node, field.returnType, parentCount) + } + + private suspend fun DeferredJsonMap.handleFunctionProperty( + ctx: ExecutionContext, + parentValue: T, + node: Execution.Node, + field: Field.Function<*, *>, + parentCount: Long + ) { + val deferred = CompletableDeferred() + deferredLaunch { + try { + val res = field.invoke( + funName = field.name, + receiver = parentValue, + inputValues = field.arguments, + args = node.arguments, + executionNode = node, + ctx = ctx + ) + deferred.complete(res) + } catch (e: Throwable) { + deferred.completeExceptionally(e) + } + } + + applyKeyToElement(ctx, deferred, node, field.returnType, parentCount) + } + + override suspend fun suspendExecute(plan: ExecutionPlan, variables: VariablesJson, context: Context) = deferredJsonBuilder(dispatcher, schema.configuration.timeout) { + val ctx = ExecutionContext(Variables(schema, variables, plan.firstOrNull { it.variables != null }?.variables), context) + + + "data" toDeferredObj { + plan.forEach { node -> + if (shouldInclude(ctx, node)) writeOperation(ctx, node, node.field as Field.Function<*, *>) + } + } + }.toString() + + private fun createNullNode(node: Execution.Node, returnType: Type): JsonNull = if (returnType !is Type.NonNull) { + JsonNull + } else { + throw ExecutionException("null result for non-nullable operation ${node.field}", node) + } + + override fun execute(plan: ExecutionPlan, variables: VariablesJson, context: Context) = runBlocking { + suspendExecute(plan, variables, context) + } + + private suspend fun shouldInclude(ctx: ExecutionContext, executionNode: Execution): Boolean { + if (executionNode.directives?.isEmpty() == true) return true + return executionNode.directives?.map { (directive, arguments) -> + directive.execution.invoke( + funName = directive.name, + inputValues = directive.arguments, + receiver = null, + args = arguments, + executionNode = executionNode, + ctx = ctx + )?.include + ?: throw ExecutionException("Illegal directive implementation returning null result", executionNode) + }?.reduce { acc, b -> acc && b } ?: true + } + + internal suspend operator fun FunctionWrapper.invoke( + funName: String, + receiver: Any?, + inputValues: List>, + args: ArgumentNodes?, + executionNode: Execution, + ctx: ExecutionContext + ): T? { + val transformedArgs = argumentsHandler.transformArguments( + funName, + inputValues, + args, + ctx.variables, + executionNode, + ctx.requestContext + ) + + return when { + hasReceiver -> invoke(receiver, *transformedArgs.toTypedArray()) + else -> invoke(*transformedArgs.toTypedArray()) + } + } +} + diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DeferredJsonArray.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DeferredJsonArray.kt new file mode 100644 index 00000000..4e3794c2 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DeferredJsonArray.kt @@ -0,0 +1,55 @@ +package com.apurebase.kgraphql.schema.execution + +import kotlinx.coroutines.* +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement + +class DeferredJsonArray internal constructor( + private val dispatcher: CoroutineDispatcher +): CoroutineScope { + + private val job = SupervisorJob() + override val coroutineContext = (dispatcher + job) + + private val deferredArray = mutableListOf>() + private var completedArray: List? = null + + fun addValue(element: JsonElement) { + addDeferredValue(CompletableDeferred(element)) + } + + fun addDeferredValue(element: Deferred) { + deferredArray.add(element) + } + + suspend fun addDeferredObj(block: suspend DeferredJsonMap.() -> Unit) { + val map = DeferredJsonMap(dispatcher) + block(map) + addDeferredValue(map.asDeferred()) + } + + // TODO: Add support for this within the [DataLoaderPreparedRequestExecutor] + suspend fun addDeferredArray(block: suspend DeferredJsonArray.() -> Unit) { + val array = DeferredJsonArray(dispatcher) + block(array) + addDeferredValue(array.asDeferred()) + } + + fun asDeferred() : Deferred { + return async(coroutineContext, start = CoroutineStart.LAZY) { + awaitAll() + build() + } + } + + suspend fun awaitAll() { + check(completedArray == null) { "The deferred tree has already been awaited!" } + completedArray = deferredArray.awaitAll() + job.complete() + } + + fun build(): JsonArray { + checkNotNull(completedArray) { "The deferred tree has not been awaited!" } + return JsonArray(completedArray!!) + } +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DeferredJsonMap.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DeferredJsonMap.kt new file mode 100644 index 00000000..ed88e4f4 --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/DeferredJsonMap.kt @@ -0,0 +1,75 @@ +package com.apurebase.kgraphql.schema.execution + +import kotlinx.coroutines.* +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject + +class DeferredJsonMap internal constructor( + private val dispatcher: CoroutineDispatcher +): CoroutineScope { + + internal val job = Job() + override val coroutineContext = (dispatcher + job) + + private val deferredMap = mutableMapOf>() + private val unDefinedDeferredMap = mutableMapOf() + private val moreJobs = mutableListOf>() + private var completedMap: Map? = null + + infix fun String.toValue(element: JsonElement) { + this toDeferredValue CompletableDeferred(element) + } + + infix fun String.toDeferredValue(element: Deferred) { +// require(deferredMap[this] == null) { "Key '$this' is already registered in builder" } + deferredMap[this] = element + } + + suspend infix fun String.toDeferredObj(block: suspend DeferredJsonMap.() -> Unit) { + if (unDefinedDeferredMap[this] != null) { + val prevMap = unDefinedDeferredMap.getValue(this) + block(prevMap) + } else { + val map = DeferredJsonMap(dispatcher) + block(map) + unDefinedDeferredMap[this] = map + } + } + + suspend infix fun String.toDeferredArray(block: suspend DeferredJsonArray.() -> Unit) { + val array = DeferredJsonArray(dispatcher) + block(array) + this@toDeferredArray toDeferredValue array.asDeferred() + } + + fun asDeferred() : Deferred { + return async(coroutineContext, start = CoroutineStart.LAZY) { + awaitAll() + build() + } + } + + suspend fun awaitAll() { + check(completedMap == null) { "The deferred tree has already been awaited!" } + + (moreJobs + deferredMap.values).awaitAll() + unDefinedDeferredMap.map { (key, map) -> + key toDeferredValue map.asDeferred() + } + (moreJobs + deferredMap.values).awaitAll() + completedMap = deferredMap.mapValues { it.value.await() } + + job.complete() + } + + fun deferredLaunch(lazy: Boolean = true, block: suspend DeferredJsonMap.() -> Unit) { + moreJobs.add(async(job, start = if (lazy) CoroutineStart.LAZY else CoroutineStart.DEFAULT) { + block(this@DeferredJsonMap) + }) + } + + fun build(): JsonObject { + checkNotNull(completedMap) { "The deferred tree has not been awaited!" } + return JsonObject(completedMap!!) + } +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Executor.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Executor.kt new file mode 100644 index 00000000..2714461c --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Executor.kt @@ -0,0 +1,14 @@ +package com.apurebase.kgraphql.schema.execution + +enum class Executor { + Parallel, + + /** + * **This is in experimental state** + * + * * Subscriptions are not supported + * * Ordering of object fields are not guaranteed + * * Some configuration options are not taken into account when using this executor + */ + DataLoaderPrepared +} diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt index 7468d924..a9650ef7 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/ParallelRequestExecutor.kt @@ -13,12 +13,12 @@ import com.apurebase.kgraphql.schema.scalar.serializeScalar import com.apurebase.kgraphql.schema.structure.Field import com.apurebase.kgraphql.schema.structure.InputValue import com.apurebase.kgraphql.schema.structure.Type +import com.apurebase.kgraphql.toMapAsync import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.node.JsonNodeFactory import com.fasterxml.jackson.databind.node.NullNode import com.fasterxml.jackson.databind.node.ObjectNode import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* import kotlin.coroutines.CoroutineContext import kotlin.reflect.KProperty1 @@ -51,7 +51,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro val root = jsonNodeFactory.objectNode() val data = root.putObject("data") - val resultMap = plan.toMapAsync { + val resultMap = plan.toMapAsync(dispatcher) { val ctx = ExecutionContext(Variables(schema, variables, it.variables), context) if (determineInclude(ctx, it)) writeOperation( isSubscription = plan.isSubscription, @@ -74,33 +74,6 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro suspendExecute(plan, variables, context) } - private suspend fun Collection.toMapAsync(block: suspend (T) -> R): Map = coroutineScope { - val channel = Channel>() - val jobs = map { item -> - launch(dispatcher) { - try { - val res = block(item) - channel.send(item to res) - } catch (e: Exception) { - channel.close(e) - } - } - } - val resultMap = mutableMapOf() - repeat(size) { - try { - val (item, result) = channel.receive() - resultMap[item] = result - } catch (e: Exception) { - jobs.forEach { job: Job -> job.cancel() } - throw e - } - } - - channel.close() - resultMap - } - private suspend fun writeOperation(isSubscription: Boolean, ctx: ExecutionContext, node: Execution.Node, operation: FunctionWrapper): JsonNode { node.field.checkAccess(null, ctx.requestContext) val operationResult: T? = operation.invoke( @@ -153,7 +126,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro else -> value as Collection<*> } if (returnType.isList()) { - val valuesMap = values.toMapAsync { + val valuesMap = values.toMapAsync(dispatcher) { createNode(ctx, it, node, returnType.unwrapList()) } values.fold(jsonNodeFactory.arrayNode(values.size)) { array, v -> @@ -171,7 +144,9 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro value is Long -> jsonNodeFactory.numberNode(value) //big decimal etc? - node.children.isNotEmpty() -> createObjectNode(ctx, value, node, returnType) + node.children.isNotEmpty() -> { + createObjectNode(ctx, value, node, returnType) + } node is Execution.Union -> { createObjectNode(ctx, value, node.memberExecution(returnType), returnType) } @@ -206,7 +181,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro when (child) { is Execution.Fragment -> objectNode.setAll(handleFragment(ctx, value, child)) else -> { - val ( key, jsonNode) = handleProperty(ctx, value, child, type) + val (key, jsonNode) = handleProperty(ctx, value, child, type, node.children.size) objectNode.set(key, jsonNode) } } @@ -214,7 +189,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro return objectNode } - private suspend fun handleProperty(ctx: ExecutionContext, value: T, child: Execution, type: Type): Pair { + private suspend fun handleProperty(ctx: ExecutionContext, value: T, child: Execution, type: Type, childrenSize: Int): Pair { when (child) { //Union is subclass of Node so check it first is Execution.Union -> { @@ -229,7 +204,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro is Execution.Node -> { val field = type.unwrapped()[child.key] ?: throw IllegalStateException("Execution unit ${child.key} is not contained by operation return type") - return child.aliasOrKey to createPropertyNode(ctx, value, child, field) + return child.aliasOrKey to createPropertyNode(ctx, value, child, field, childrenSize) } else -> { throw UnsupportedOperationException("Handling containers is not implemented yet") @@ -247,7 +222,8 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro return container.elements.flatMap { child -> when (child) { is Execution.Fragment -> handleFragment(ctx, value, child).toList() - else -> listOf(handleProperty(ctx, value, child, expectedType)) + // TODO: Should not be 1 + else -> listOf(handleProperty(ctx, value, child, expectedType, 1)) } }.fold(mutableMapOf()) { map, entry -> map.merge(entry.first, entry.second) } } @@ -259,7 +235,7 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro return emptyMap() } - private suspend fun createPropertyNode(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field): JsonNode? { + private suspend fun createPropertyNode(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field, parentTimes: Int): JsonNode? { val include = determineInclude(ctx, node) node.field.checkAccess(parentValue, ctx.requestContext) @@ -280,6 +256,9 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro is Field.Function<*, *> -> { return handleFunctionProperty(ctx, parentValue, node, field) } + is Field.DataLoader<*, *, *> -> { + return handleDataProperty(ctx, parentValue, node, field, parentTimes) + } else -> { throw Exception("Unexpected field type: $field, should be Field.Kotlin or Field.Function") } @@ -289,6 +268,29 @@ class ParallelRequestExecutor(val schema: DefaultSchema) : RequestExecutor, Coro } } + private suspend fun handleDataProperty(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field.DataLoader<*, *, *>, parentTimes: Int): JsonNode { + val preparedValue = field.kql.prepare.invoke( + funName = field.name, + receiver = parentValue, + inputValues = field.arguments, + args = node.arguments, + executionNode = node, + ctx = ctx + ) ?: TODO("Nullable prepare functions isn't supported") + + val valueDeferred = CompletableDeferred() +// ctx.loadValue( +// ctx.dataLoaders.getValue(field), +// preparedValue, +// valueDeferred, +// parentTimes +// ) + println("Waiting for key: $preparedValue - [parent: $parentValue]") + val value = valueDeferred.await() + println("Loaded key: $preparedValue | $value - [parent: $parentValue]") + return createNode(ctx, value, node, field.returnType) + } + private suspend fun handleFunctionProperty(ctx: ExecutionContext, parentValue: T, node: Execution.Node, field: Field.Function<*, *>): JsonNode { val result = field.invoke( funName = field.name, diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Utils.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Utils.kt new file mode 100644 index 00000000..9491804e --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/execution/Utils.kt @@ -0,0 +1,23 @@ +package com.apurebase.kgraphql.schema.execution + +import kotlinx.coroutines.* +import kotlinx.serialization.json.JsonObject + +suspend fun deferredJsonBuilder( + dispatcher: CoroutineDispatcher = Dispatchers.Default, + timeout: Long? = null, + init: suspend DeferredJsonMap.() -> Unit +): JsonObject { + val block: suspend () -> JsonObject = { + try { + val builder = DeferredJsonMap(dispatcher) + builder.init() + builder.awaitAll() + builder.build() + } catch (e: CancellationException) { + throw e.cause ?: e + } + } + return timeout?.let { withTimeout(it) { block() } } ?: block() +} + diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/DataLoader.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/DataLoader.kt new file mode 100644 index 00000000..ba413fcc --- /dev/null +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/DataLoader.kt @@ -0,0 +1,73 @@ +package com.apurebase.kgraphql.schema.model + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ObsoleteCoroutinesApi +import kotlinx.coroutines.channels.actor +import java.util.* +import java.util.concurrent.atomic.AtomicInteger + +class DataLoader(private val batchLoader: suspend (List) -> Map) { + + inner class DataScope(totalTimes: Int, scope: CoroutineScope) : CoroutineScope by scope { + private val actor = dataActor(totalTimes, batchLoader) + + suspend fun load(key: K, valueResult: CompletableDeferred) { + actor.send(Add(key, valueResult)) + } + + } + + fun begin(totalTimes: Int, scope: CoroutineScope): DataScope { + return DataScope(totalTimes, scope) + } +} + +sealed class DataActor +class Add(val key: K, val result: CompletableDeferred) : DataActor() +class Increment(val count: Int) : DataActor() + +@ObsoleteCoroutinesApi +fun CoroutineScope.dataActor(totalTimes: Int, batchLoader: suspend (List) -> Map) = actor { + var counter = totalTimes + + log("Starting dataActor with totalCount: $counter") + + val cache = mutableMapOf() + val promiseMap = mutableMapOf>>() + + suspend fun doJoin() { + val toLoad = promiseMap + .map { it.key } + .filterNot { cache.containsKey(it) } + .toList() + if (toLoad.isNotEmpty()) { + batchLoader(toLoad).forEach { (key, value) -> + cache[key] = value + } + } + promiseMap.forEach { (key, promises) -> + var promise: CompletableDeferred? = promises.pop() + while (promise != null) { + promise.complete(cache[key]) + promise = if (promises.isNotEmpty()) promises.pop() else null + } + } + promiseMap.clear() + } + + for (msg in channel) { + when (msg) { + is Add<*, *> -> { + msg as Add + if (!promiseMap.containsKey(msg.key)) promiseMap[msg.key] = Stack() + promiseMap[msg.key]?.add(msg.result) ?: throw TODO("Couldn't find any '${msg.key}' in map") + log("$counter") + if (--counter == 0) doJoin() + } + } + } +} + + +fun log(str: String) = println("DATALOADER: $str") diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt index bca920da..388d176c 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/PropertyDef.kt @@ -1,7 +1,9 @@ package com.apurebase.kgraphql.schema.model import com.apurebase.kgraphql.Context +import nidomiro.kdataloader.BatchLoader import kotlin.reflect.KProperty1 +import kotlin.reflect.KType interface PropertyDef : Depreciable, DescribedDef { @@ -19,6 +21,23 @@ interface PropertyDef : Depreciable, DescribedDef { inputValues : List> = emptyList() ) : BaseOperationDef(name, resolver, inputValues, accessRule), PropertyDef + /** + * [T] -> The Parent Type + * [K] -> The Key that'll be passed to the dataLoader + * [R] -> The return type + */ + open class DataLoadedFunction( + override val name: String, + val loader: nidomiro.kdataloader.factories.DataLoaderFactory, + val prepare: FunctionWrapper, + val returnType: KType, + override val description: String? = null, + override val isDeprecated: Boolean = false, + override val deprecationReason: String? = null, + override val accessRule: ((T?, Context) -> Exception?)? = null, + val inputValues: List> = emptyList() + ): PropertyDef + open class Kotlin ( val kProperty: KProperty1, override val description: String? = null, diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt index cbd7e809..6838f066 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/TypeDef.kt @@ -22,6 +22,7 @@ interface TypeDef { override val kClass: KClass, val kotlinProperties: Map, PropertyDef.Kotlin> = emptyMap(), val extensionProperties : List> = emptyList(), + val dataloadExtensionProperties: List> = emptyList(), val unionProperties : List> = emptyList(), val transformations : Map, Transformation> = emptyMap(), description : String? = null diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/SelectionNode.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/SelectionNode.kt index 4d324338..4f856053 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/SelectionNode.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/model/ast/SelectionNode.kt @@ -4,6 +4,8 @@ import com.apurebase.kgraphql.schema.model.ast.TypeNode.NamedTypeNode sealed class SelectionNode(val parent: SelectionNode?): ASTNode() { + abstract val fullPath: String + class FieldNode( parent: SelectionNode?, val alias: NameNode?, @@ -24,9 +26,14 @@ sealed class SelectionNode(val parent: SelectionNode?): ASTNode() { } val aliasOrName get() = alias ?: name + + override val fullPath get() = (parent?.fullPath?.let { "$it." } ?: "") + aliasOrName.value + } sealed class FragmentNode(parent: SelectionNode?, val directives: List?): SelectionNode(parent) { + override val fullPath get () = parent?.fullPath?.let {"$it."} ?: "" + /** * ...FragmentName */ @@ -59,7 +66,6 @@ sealed class SelectionNode(val parent: SelectionNode?): ASTNode() { _loc = loc return this } - } } diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt index 03e6b582..a24df8b5 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/scalar/Coercion.kt @@ -14,6 +14,8 @@ import com.apurebase.kgraphql.schema.model.ast.ValueNode import com.apurebase.kgraphql.schema.model.ast.ValueNode.* import com.apurebase.kgraphql.GraphQLError import com.apurebase.kgraphql.schema.structure.Type +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonPrimitive private typealias JsonValueNode = com.fasterxml.jackson.databind.node.ValueNode @@ -70,3 +72,23 @@ fun serializeScalar(jsonNodeFactory: JsonNodeFactory, scalar: Type.Scalar<*> else -> throw ExecutionException("Unsupported coercion for scalar type ${scalar.name}", executionNode) } +@Suppress("UNCHECKED_CAST") +fun serializeScalar(scalar: Type.Scalar<*>, value: T, executionNode: Execution): JsonElement = when (scalar.coercion) { + is StringScalarCoercion<*> -> { + JsonPrimitive((scalar.coercion as StringScalarCoercion).serialize(value)) + } + is IntScalarCoercion<*> -> { + JsonPrimitive((scalar.coercion as IntScalarCoercion).serialize(value)) + } + is DoubleScalarCoercion<*> -> { + JsonPrimitive((scalar.coercion as DoubleScalarCoercion).serialize(value)) + } + is LongScalarCoercion<*> -> { + JsonPrimitive((scalar.coercion as LongScalarCoercion).serialize(value)) + } + is BooleanScalarCoercion<*> -> { + JsonPrimitive((scalar.coercion as BooleanScalarCoercion).serialize(value)) + } + else -> throw ExecutionException("Unsupported coercion for scalar type ${scalar.name}", executionNode) +} + diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Field.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Field.kt index cebfc5c1..a99ed758 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Field.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/Field.kt @@ -3,6 +3,7 @@ package com.apurebase.kgraphql.schema.structure import com.apurebase.kgraphql.Context import com.apurebase.kgraphql.schema.introspection.* import com.apurebase.kgraphql.schema.model.* +import nidomiro.kdataloader.DataLoader import kotlin.reflect.full.findAnnotation @@ -41,6 +42,22 @@ sealed class Field : __Field { } } + class DataLoader( + val kql: PropertyDef.DataLoadedFunction, + val loader: nidomiro.kdataloader.factories.DataLoaderFactory, + override val returnType: Type, + override val arguments: List> + ): Field() { + override val isDeprecated = kql.isDeprecated + override val deprecationReason = kql.deprecationReason + override val description = kql.description + override val name = kql.name + + override fun checkAccess(parent: Any?, ctx: Context) { + kql.accessRule?.invoke(parent as T?, ctx)?.let { throw it } + } + } + class Kotlin( kql : PropertyDef.Kotlin, override val returnType: Type, diff --git a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt index be338f21..34cdd549 100644 --- a/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt +++ b/kgraphql/src/main/kotlin/com/apurebase/kgraphql/schema/structure/SchemaCompilation.kt @@ -19,6 +19,10 @@ import com.apurebase.kgraphql.schema.model.QueryDef import com.apurebase.kgraphql.schema.model.SchemaDefinition import com.apurebase.kgraphql.schema.model.Transformation import com.apurebase.kgraphql.schema.model.TypeDef +import nidomiro.kdataloader.DataLoader +import nidomiro.kdataloader.DataLoaderOptions +import nidomiro.kdataloader.SimpleDataLoaderImpl +import nidomiro.kdataloader.factories.DataLoaderFactory import kotlin.reflect.KClass import kotlin.reflect.KProperty1 import kotlin.reflect.KType @@ -50,7 +54,7 @@ class SchemaCompilation( INPUT, QUERY } - fun perform(): DefaultSchema { + suspend fun perform(): DefaultSchema { val queryType = handleQueries() val mutationType = handleMutations() val subscriptionType = handleSubscriptions() @@ -107,52 +111,52 @@ class SchemaCompilation( } } - private fun handlePartialDirective(directive: Directive.Partial) : Directive { + private suspend fun handlePartialDirective(directive: Directive.Partial) : Directive { val inputValues = handleInputValues(directive.name, directive.execution, emptyList()) return directive.toDirective(inputValues) } - private fun handleQueries() : Type { + private suspend fun handleQueries() : Type { return Type.OperationObject( name = "Query", description = "Query object", - fields = definition.queries.map(this::handleOperation) + introspectionSchemaQuery() + introspectionTypeQuery() + fields = definition.queries.map { handleOperation(it) } + introspectionSchemaQuery() + introspectionTypeQuery() ) } - private fun handleMutations() : Type { + private suspend fun handleMutations() : Type { return Type.OperationObject("Mutation", "Mutation object", definition.mutations.map { handleOperation(it) }) } - private fun handleSubscriptions() : Type { + private suspend fun handleSubscriptions() : Type { return Type.OperationObject("Subscription", "Subscription object", definition.subscriptions.map { handleOperation(it) }) } @Suppress("USELESS_CAST") // We are casting as __Schema so we don't get proxied types. https://github.com/aPureBase/KGraphQL/issues/45 - private fun introspectionSchemaQuery() = handleOperation( + private suspend fun introspectionSchemaQuery() = handleOperation( QueryDef("__schema", FunctionWrapper.on<__Schema> { schemaProxy as __Schema }) ) - private fun introspectionTypeQuery() = handleOperation( + private suspend fun introspectionTypeQuery() = handleOperation( QueryDef("__type", FunctionWrapper.on { name : String -> schemaProxy.findTypeByName(name) }) ) - private fun handleOperation(operation : BaseOperationDef<*, *>) : Field { + private suspend fun handleOperation(operation : BaseOperationDef<*, *>) : Field { val returnType = handlePossiblyWrappedType(operation.kFunction.returnType, TypeCategory.QUERY) val inputValues = handleInputValues(operation.name, operation, operation.inputValues) return Field.Function(operation, returnType, inputValues) } - private fun handleUnionProperty(unionProperty: PropertyDef.Union<*>) : Field { + private suspend fun handleUnionProperty(unionProperty: PropertyDef.Union<*>) : Field { val inputValues = handleInputValues(unionProperty.name, unionProperty, unionProperty.inputValues) val type = handleUnionType(unionProperty.union) return Field.Union(unionProperty, unionProperty.nullable, type, inputValues) } - private fun handlePossiblyWrappedType(kType : KType, typeCategory: TypeCategory) : Type = when { + private suspend fun handlePossiblyWrappedType(kType : KType, typeCategory: TypeCategory) : Type = when { kType.isIterable() -> handleCollectionType(kType, typeCategory) kType.jvmErasure == Context::class && typeCategory == TypeCategory.INPUT -> contextType kType.jvmErasure == Context::class && typeCategory == TypeCategory.QUERY -> throw SchemaException("Context type cannot be part of schema") @@ -160,7 +164,7 @@ class SchemaCompilation( else -> handleSimpleType(kType, typeCategory) } - private fun handleCollectionType(kType: KType, typeCategory: TypeCategory): Type { + private suspend fun handleCollectionType(kType: KType, typeCategory: TypeCategory): Type { val type = when { kType.getIterableElementType() != null -> kType.getIterableElementType() kType.arguments.size == 1 -> kType.arguments.first().type @@ -171,7 +175,7 @@ class SchemaCompilation( return applyNullability(kType, nullableListType) } - private fun handleSimpleType(kType: KType, typeCategory: TypeCategory): Type { + private suspend fun handleSimpleType(kType: KType, typeCategory: TypeCategory): Type { val simpleType = handleRawType(kType.jvmErasure, typeCategory) return applyNullability(kType, simpleType) } @@ -184,7 +188,7 @@ class SchemaCompilation( } } - private fun handleRawType(kClass: KClass<*>, typeCategory: TypeCategory) : Type { + private suspend fun handleRawType(kClass: KClass<*>, typeCategory: TypeCategory) : Type { if(kClass == Context::class) throw SchemaException("Context type cannot be part of schema") @@ -193,18 +197,31 @@ class SchemaCompilation( TypeCategory.INPUT -> inputTypeProxies } - val typeCreator = when(typeCategory){ - TypeCategory.QUERY -> this::handleObjectType - TypeCategory.INPUT -> this::handleInputType - } return cachedInstances[kClass] - ?: enums[kClass] - ?: scalars[kClass] - ?: typeCreator (kClass) + ?: enums[kClass] + ?: scalars[kClass] + ?: when(typeCategory) { + TypeCategory.QUERY -> handleObjectType(kClass) + TypeCategory.INPUT -> handleInputType(kClass) + } } - private fun handleObjectType(kClass: KClass<*>) : Type { + private suspend fun handleDataloadOperation( + operation: PropertyDef.DataLoadedFunction + ): Field { + val returnType = handlePossiblyWrappedType(operation.returnType, TypeCategory.QUERY) + val inputValues = handleInputValues(operation.name, operation.prepare, operation.inputValues) + + return Field.DataLoader( + kql = operation, + loader = operation.loader, + returnType = returnType, + arguments = inputValues + ) + } + + private suspend fun handleObjectType(kClass: KClass<*>) : Type { assertValidObjectType(kClass) val objectDefs = definition.objects.filter { it.kClass.isSuperclassOf(kClass) } val objectDef = objectDefs.find { it.kClass == kClass } ?: TypeDef.Object(kClass.defaultKQLTypeName(), kClass) @@ -234,6 +251,10 @@ class SchemaCompilation( .flatMap(TypeDef.Object<*>::extensionProperties) .map { property -> handleOperation(property) } + val dataloadExtensionFields = objectDefs + .flatMap(TypeDef.Object<*>::dataloadExtensionProperties) + .map { property -> handleDataloadOperation(property) } + val unionFields = objectDefs .flatMap(TypeDef.Object<*>::unionProperties) .map { property -> handleUnionProperty(property) } @@ -247,7 +268,7 @@ class SchemaCompilation( PropertyDef.Function ("__typename", FunctionWrapper.on(typenameResolver, true)) ) - val declaredFields = kotlinFields + extensionFields + unionFields + val declaredFields = kotlinFields + extensionFields + unionFields + dataloadExtensionFields if(declaredFields.isEmpty()){ throw SchemaException("An Object type must define one or more fields. Found none on type ${objectDef.name}") @@ -262,13 +283,13 @@ class SchemaCompilation( return typeProxy } - private fun handleInputType(kClass: KClass<*>) : Type { + private suspend fun handleInputType(kClass: KClass<*>) : Type { assertValidObjectType(kClass) val inputObjectDef = definition.inputObjects.find { it.kClass == kClass } ?: TypeDef.Input(kClass.defaultKQLTypeName(), kClass) val objectType = Type.Input(inputObjectDef) val typeProxy = TypeProxy(objectType) - inputTypeProxies.put(kClass, typeProxy) + inputTypeProxies[kClass] = typeProxy val fields = kClass.memberProperties.map { property -> handleKotlinInputProperty(property) } @@ -276,7 +297,7 @@ class SchemaCompilation( return typeProxy } - private fun handleInputValues(operationName : String, operation: FunctionWrapper<*>, inputValues: List>) : List> { + private suspend fun handleInputValues(operationName : String, operation: FunctionWrapper<*>, inputValues: List>) : List> { val invalidInputValues = inputValues .map { it.name } .filterNot { it in operation.argumentsDescriptor.keys } @@ -292,7 +313,7 @@ class SchemaCompilation( } } - private fun handleUnionType(union : TypeDef.Union) : Type.Union { + private suspend fun handleUnionType(union : TypeDef.Union) : Type.Union { val possibleTypes = union.members.map { handleRawType(it, TypeCategory.QUERY) } @@ -307,12 +328,12 @@ class SchemaCompilation( return unionType } - private fun handleKotlinInputProperty(kProperty: KProperty1<*, *>) : InputValue<*> { + private suspend fun handleKotlinInputProperty(kProperty: KProperty1<*, *>) : InputValue<*> { val type = handlePossiblyWrappedType(kProperty.returnType, TypeCategory.INPUT) return InputValue(InputValueDef(kProperty.returnType.jvmErasure, kProperty.name), type) } - private fun handleKotlinProperty ( + private suspend fun handleKotlinProperty ( kProperty: KProperty1, kqlProperty: PropertyDef.Kotlin<*, *>?, transformation: Transformation<*, *>? diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/DataLoaderTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/DataLoaderTest.kt new file mode 100644 index 00000000..c49586b2 --- /dev/null +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/DataLoaderTest.kt @@ -0,0 +1,395 @@ +package com.apurebase.kgraphql + +import com.apurebase.kgraphql.schema.DefaultSchema +import com.apurebase.kgraphql.schema.dsl.SchemaBuilder +import com.apurebase.kgraphql.schema.execution.Executor +import kotlinx.coroutines.delay +import nidomiro.kdataloader.ExecutionResult +import org.amshove.kluent.shouldEqual +import org.hamcrest.CoreMatchers +import org.hamcrest.MatcherAssert +import org.junit.jupiter.api.Assertions.assertTimeoutPreemptively +import org.junit.jupiter.api.RepeatedTest +import java.time.Duration.ofSeconds +import java.util.concurrent.atomic.AtomicInteger + +// This is just for safety, so when the tests fail and +// end up in an endless waiting state, they'll fail after this amount +val timeout = ofSeconds(10)!! +const val repeatTimes = 2 + +class DataLoaderTest { + + data class Person(val id: Int, val firstName: String, val lastName: String) + + private val jogvan = Person(1, "Jógvan", "Olsen") + private val beinisson = Person(2, "Høgni", "Beinisson") + private val juul = Person(3, "Høgni", "Juul") + private val otherOne = Person(4, "The other one", "??") + + val allPeople = listOf(jogvan, beinisson, juul, otherOne) + + private val colleagues = mapOf( + jogvan.id to listOf(beinisson, juul), + beinisson.id to listOf(jogvan, juul, otherOne), + juul.id to listOf(beinisson, jogvan), + otherOne.id to listOf(beinisson) + ) + + private val boss = mapOf( + jogvan.id to juul, + juul.id to beinisson, + beinisson.id to otherOne + ) + + data class Tree(val id: Int, val value: String) + + data class ABC(val value: String, val personId: Int? = null) + + data class AtomicProperty( + val loader: AtomicInteger = AtomicInteger(), + val prepare: AtomicInteger = AtomicInteger() + ) + + data class AtomicCounters( + val abcB: AtomicProperty = AtomicProperty(), + val abcChildren: AtomicProperty = AtomicProperty(), + val treeChild: AtomicProperty = AtomicProperty() + ) + + fun schema( + block: SchemaBuilder.() -> Unit = {} + ): Pair { + val counters = AtomicCounters() + + val schema = defaultSchema { + configure { + useDefaultPrettyPrinter = true + executor = Executor.DataLoaderPrepared + } + + query("people") { + resolver { -> allPeople } + } + + type { + property("fullName") { + resolver { "${it.firstName} ${it.lastName}" } + } + + dataProperty("respondsTo") { +// setReturnType { jogvan as Person? } + prepare { it.id } + loader { keys -> + println("== Running [respondsTo] loader with keys: $keys ==") + keys.map { ExecutionResult.Success(boss[it]) } + } + } + dataProperty>("colleagues") { +// setReturnType { listOf() } + prepare { it.id } + loader { keys -> + println("== Running [colleagues] loader with keys: $keys ==") + keys.map { ExecutionResult.Success(colleagues[it] ?: listOf()) } + } + } + } + + query("tree") { + resolver { -> + listOf( + Tree(1, "Fisk"), + Tree(2, "Fisk!") + ) + } + } + + query("abc") { + resolver { -> + (1..3).map { ABC("Testing $it", if (it == 2) null else it) } + } + } + + type { + + dataProperty("B") { +// setReturnType { 25 } + loader { keys -> + println("== Running [B] loader with keys: $keys ==") + counters.abcB.loader.incrementAndGet() + keys.map { + ExecutionResult.Success(it.map(Char::toInt).fold(0) { a, b -> a + b }) + } + } + prepare { parent: ABC -> + counters.abcB.prepare.incrementAndGet() + parent.value + } + } + + property("simpleChild") { + resolver { + delay(10) + Thread.sleep(10) + delay(10) + ABC("NewChild!") + } + } + + dataProperty("person") { +// setReturnType { null as Person? } + prepare { it.personId } + loader { personIds -> + personIds.map { + ExecutionResult.Success( + if (it == null || it < 1) null else allPeople[it - 1] + ) + } + } + } + + dataProperty>("children") { +// setReturnType { listOf() } + loader { keys -> + println("== Running [children] loader with keys: $keys ==") + counters.abcChildren.loader.incrementAndGet() + keys.map { + val (a1, a2) = when (it) { + "Testing 1" -> "Hello" to "World" + "Testing 2" -> "Fizz" to "Buzz" + "Testing 3" -> "Jógvan" to "Høgni" + else -> "${it}Nest-0" to "${it}Nest-1" + } + ExecutionResult.Success(listOf(ABC(a1), ABC(a2))) + } + } + prepare { parent -> + counters.abcChildren.prepare.incrementAndGet() + parent.value + } + } + } + + + type { + dataProperty("child") { +// setReturnType { Tree(0, "") } + loader { keys -> + println("== Running [child] loader with keys: $keys ==") + counters.treeChild.loader.incrementAndGet() + keys.map { num -> ExecutionResult.Success(Tree(10 + num, "Fisk - $num")) } + } + + prepare { parent, buzz: Int -> + counters.treeChild.prepare.incrementAndGet() + parent.id + buzz + } + } + } + + block(this) + } + + return schema to counters + } + + @RepeatedTest(repeatTimes) + fun `Nested array loaders`() { + assertTimeoutPreemptively(timeout) { + val (schema) = schema() + val query = """ + { + people { + fullName + colleagues { + fullName + respondsTo { + fullName + } + } + } + } + """.trimIndent() + + val result = schema.executeBlocking(query).also(::println).deserialize() + + } + } + + @RepeatedTest(repeatTimes) + fun `Old basic resolvers in new executor`() { + assertTimeoutPreemptively(timeout) { + val (schema) = schema() + val query = """ + { + abc { + value + simpleChild { + value + } + } + } + """.trimIndent() + + val result = schema.executeBlocking(query).also(::println).deserialize() + + MatcherAssert.assertThat(result.extract("data/abc[0]/simpleChild/value"), CoreMatchers.equalTo("NewChild!")) + } + } + + @RepeatedTest(repeatTimes) + fun `Very basic new Level executor`() { + assertTimeoutPreemptively(timeout) { + val (schema) = schema() + + val query = """ + { + people { + id + fullName + respondsTo { + fullName + respondsTo { + fullName + } + } + } + } + """.trimIndent() + + val result = schema.executeBlocking(query).also(::println).deserialize() + } + } + + @RepeatedTest(repeatTimes) + fun `dataloader with nullable prepare keys`() { + assertTimeoutPreemptively(timeout) { + val (schema) = schema() + + val query = """ + { + abc { + value + personId + person { + id + fullName + } + } + } + """.trimIndent() + val result = schema.executeBlocking(query).also(::println).deserialize() + + result.extract("data/abc[0]/person/fullName") shouldEqual "${jogvan.firstName} ${jogvan.lastName}" + extractOrNull(result, "data/abc[1]/person") shouldEqual null + result.extract("data/abc[2]/person/fullName") shouldEqual "${juul.firstName} ${juul.lastName}" + } + } + + @RepeatedTest(repeatTimes) + fun `Basic dataloader test`() { + assertTimeoutPreemptively(timeout) { + val (schema) = schema() + + val query = """ + { + people { + ...PersonInfo + respondsTo { ...PersonInfo } + colleagues { ...PersonInfo } + } + } + fragment PersonInfo on Person { + id + fullName + } + """.trimIndent() + val result = schema.executeBlocking(query).also(::println).deserialize() + + + result.extract("data/people[0]/respondsTo/fullName") shouldEqual "${juul.firstName} ${juul.lastName}" + result.extract("data/people[1]/colleagues[0]/fullName") shouldEqual "${jogvan.firstName} ${jogvan.lastName}" + } + } + + @RepeatedTest(2) + fun `basic data loader`() { + assertTimeoutPreemptively(timeout) { + val (schema, counters) = schema() + + val query = """ + { + tree { # <-- 2 + id + child(buzz: 3) { + id + value + } + } + } + """.trimIndent() + + val result = schema.executeBlocking(query).also(::println).deserialize() + counters.treeChild.prepare.get() shouldEqual 2 + counters.treeChild.loader.get() shouldEqual 1 + + + result.extract("data/tree[1]/id") shouldEqual 2 + result.extract("data/tree[0]/child/id") shouldEqual 14 + result.extract("data/tree[1]/child/id") shouldEqual 15 + } + } + + @RepeatedTest(repeatTimes) + fun `data loader cache per request only`() { + assertTimeoutPreemptively(timeout) { + val (schema, counters) = schema() + + val query = """ + { + first: tree { id, child(buzz: 3) { id, value } } + second: tree { id, child(buzz: 3) { id, value } } + } + """.trimIndent() + + val result = schema.executeBlocking(query).also(::println).deserialize() + + counters.treeChild.prepare.get() shouldEqual 4 + counters.treeChild.loader.get() shouldEqual 1 + + result.extract("data/first[1]/id") shouldEqual 2 + result.extract("data/first[0]/child/id") shouldEqual 14 + result.extract("data/first[1]/child/id") shouldEqual 15 + result.extract("data/second[1]/child/id") shouldEqual 15 + + } + } + + @RepeatedTest(repeatTimes) + fun `multiple layers of dataLoaders`() { + assertTimeoutPreemptively(timeout) { + val (schema) = schema() + + val query = """ + { + abc { + value + B + children { + value + B + children { + value + B + } + } + } + } + """.trimIndent() + + val result = schema.executeBlocking(query).also(::println).deserialize() + +// throw TODO("Assert results") + } + } +} diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/access/AccessRulesTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/access/AccessRulesTest.kt index 58547571..69ad2950 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/access/AccessRulesTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/access/AccessRulesTest.kt @@ -3,7 +3,7 @@ package com.apurebase.kgraphql.access import com.apurebase.kgraphql.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class AccessRulesTest { @@ -63,7 +63,7 @@ class AccessRulesTest { @Test fun `reject property resolver access rule`() { expect("ILLEGAL ACCESS") { - schema.executeBlocking("{black_mamba {item}}", context { +"LAKERS" }) + schema.executeBlocking("{black_mamba {item}}", context { +"LAKERS" }).also(::println) } } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt index 966b2acc..66498840 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/BaseSchemaTest.kt @@ -1,7 +1,7 @@ package com.apurebase.kgraphql.integration import com.apurebase.kgraphql.* -import org.junit.After +import org.junit.jupiter.api.AfterEach import java.io.ByteArrayInputStream @@ -278,8 +278,12 @@ abstract class BaseSchemaTest { } } - @After + @AfterEach fun cleanup() = createdActors.clear() - fun execute(query: String, variables : String? = null) = deserialize(testedSchema.executeBlocking(query, variables)) + fun execute(query: String, variables : String? = null) = testedSchema + .executeBlocking(query, variables) + .also(::println) + .deserialize() + } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/EnumTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/EnumTest.kt index 3a565054..45db900e 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/EnumTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/EnumTest.kt @@ -6,7 +6,7 @@ import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class EnumTest : BaseSchemaTest() { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/LongScalarTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/LongScalarTest.kt index dbf83ce7..eec2a9e4 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/LongScalarTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/LongScalarTest.kt @@ -3,7 +3,7 @@ package com.apurebase.kgraphql.integration import com.apurebase.kgraphql.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class LongScalarTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/MutationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/MutationTest.kt index 8226a037..8e9ef58a 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/MutationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/MutationTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class MutationTest : BaseSchemaTest() { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/ParallelExecutionTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/ParallelExecutionTest.kt index fd6e83d3..f1743d9e 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/ParallelExecutionTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/ParallelExecutionTest.kt @@ -7,7 +7,7 @@ import com.apurebase.kgraphql.GraphQLError import kotlinx.coroutines.delay import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.random.Random class ParallelExecutionTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt index 38d2799b..27ccc94e 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/integration/QueryTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class QueryTest : BaseSchemaTest() { @Test diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/MapMergeTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/MapMergeTest.kt index 8fb0ab23..3d11f2c8 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/MapMergeTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/MapMergeTest.kt @@ -6,7 +6,7 @@ import com.apurebase.kgraphql.expect import com.apurebase.kgraphql.schema.execution.merge import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class MapMergeTest { private val jsonNodeFactory = JsonNodeFactory.instance diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/ObjectNodeMergeTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/ObjectNodeMergeTest.kt index e844d216..87ce7505 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/ObjectNodeMergeTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/merge/ObjectNodeMergeTest.kt @@ -6,7 +6,7 @@ import com.apurebase.kgraphql.expect import com.apurebase.kgraphql.schema.execution.merge import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class ObjectNodeMergeTest { private val jsonNodeFactory = JsonNodeFactory.instance diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/DedentBlockStringTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/DedentBlockStringTest.kt index 43bd427a..0a04dffb 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/DedentBlockStringTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/DedentBlockStringTest.kt @@ -4,7 +4,7 @@ import com.apurebase.kgraphql.schema.structure.dedentBlockStringValue import com.apurebase.kgraphql.schema.structure.getBlockStringIndentation import com.apurebase.kgraphql.schema.structure.printBlockString import org.amshove.kluent.shouldEqual -import org.junit.Test +import org.junit.jupiter.api.Test class DedentBlockStringTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/LexerTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/LexerTest.kt index d235aa3a..04a826ac 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/LexerTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/LexerTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.schema.model.ast.Token import com.apurebase.kgraphql.schema.model.ast.TokenKindEnum.* import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.* -import org.junit.Test +import org.junit.jupiter.api.Test internal class LexerTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt index c9aa68ec..602ad013 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/request/ParserTest.kt @@ -14,7 +14,7 @@ import com.apurebase.kgraphql.ResourceFiles.kitchenSinkQuery import com.apurebase.kgraphql.d import com.apurebase.kgraphql.schema.model.ast.* import org.amshove.kluent.* -import org.junit.Test +import org.junit.jupiter.api.Test class ParserTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt index 518b8596..33bc6423 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaBuilderTest.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test import java.util.* /** @@ -228,7 +228,7 @@ class SchemaBuilderTest { } } - deserialize(schema.executeBlocking("{actor{favDishes(size: 2)}}")) + schema.executeBlocking("{actor{favDishes(size: 2)}}").also(::println).deserialize() } @Test diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaInheritanceTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaInheritanceTest.kt index 0477fede..daaf5818 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaInheritanceTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/schema/SchemaInheritanceTest.kt @@ -6,7 +6,7 @@ import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.invoking import org.amshove.kluent.shouldThrow import org.amshove.kluent.withMessage -import org.junit.Test +import org.junit.jupiter.api.Test import java.util.* diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/ContextSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/ContextSpecificationTest.kt index fdb9d847..88fe592f 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/ContextSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/ContextSpecificationTest.kt @@ -6,7 +6,7 @@ import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.extract import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert -import org.junit.Test +import org.junit.jupiter.api.Test class ContextSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt index 82bd03bb..db578fb1 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DeprecationSpecificationTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.extract import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class DeprecationSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt index 6be49c63..273364f1 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/DocumentationSpecificationTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.extract import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class DocumentationSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt index 5b1d1849..ea2fd858 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/introspection/IntrospectionSpecificationTest.kt @@ -8,7 +8,7 @@ import org.amshove.kluent.* import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.collection.IsEmptyCollection.empty -import org.junit.Test +import org.junit.jupiter.api.Test class IntrospectionSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/ArgumentsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/ArgumentsSpecificationTest.kt index 5f495b53..544a3dc0 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/ArgumentsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/ArgumentsSpecificationTest.kt @@ -5,15 +5,21 @@ import com.apurebase.kgraphql.Specification import com.apurebase.kgraphql.defaultSchema import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.executeEqualQueries +import com.apurebase.kgraphql.schema.execution.Executor import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.6 Arguments") class ArgumentsSpecificationTest { val age = 432 val schema = defaultSchema { + + configure { + executor = Executor.Parallel + } + query("actor") { resolver { -> Actor("Boguś Linda", age) } } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldAliasSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldAliasSpecificationTest.kt index 80ce2d38..6cb943a3 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldAliasSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldAliasSpecificationTest.kt @@ -7,7 +7,7 @@ import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.extract import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.7 Field Alias") class FieldAliasSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldsSpecificationTest.kt index 72cd5fbe..1f8c8ee1 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FieldsSpecificationTest.kt @@ -7,7 +7,7 @@ import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.extract import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.5 Fields") class FieldsSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FragmentsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FragmentsSpecificationTest.kt index bef08cd7..b752d4d7 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FragmentsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/FragmentsSpecificationTest.kt @@ -12,7 +12,7 @@ import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers.* import org.hamcrest.MatcherAssert import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.8 Fragments") class FragmentsSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/InputValuesSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/InputValuesSpecificationTest.kt index 768d8dfc..ffe86e6b 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/InputValuesSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/InputValuesSpecificationTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.9 Input Values") class InputValuesSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/OperationsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/OperationsSpecificationTest.kt index c8c1fef6..ab9b8f64 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/OperationsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/OperationsSpecificationTest.kt @@ -4,11 +4,13 @@ import com.apurebase.kgraphql.Specification import com.apurebase.kgraphql.defaultSchema import com.apurebase.kgraphql.executeEqualQueries import com.apurebase.kgraphql.expect +import com.apurebase.kgraphql.schema.DefaultSchema import com.apurebase.kgraphql.schema.SchemaException import com.apurebase.kgraphql.schema.dsl.operations.subscribe import com.apurebase.kgraphql.schema.dsl.operations.unsubscribe +import com.apurebase.kgraphql.schema.execution.Executor import org.amshove.kluent.shouldEqual -import org.junit.Test +import org.junit.jupiter.api.Test data class Actor(var name : String? = "", var age: Int? = 0) data class Actress(var name : String? = "", var age: Int? = 0) @@ -18,7 +20,10 @@ class OperationsSpecificationTest { var subscriptionResult = "" - val schema = defaultSchema { + fun newSchema(executor: Executor = Executor.DataLoaderPrepared) = defaultSchema { + configure { + this@configure.executor = executor + } query("fizz") { resolver{ -> "buzz"}.withArgs { } @@ -54,7 +59,7 @@ class OperationsSpecificationTest { @Test fun `unnamed and named queries are equivalent`(){ - executeEqualQueries(schema, + executeEqualQueries(newSchema(), mapOf("data" to mapOf("fizz" to "buzz")), "{fizz}", "query {fizz}", @@ -64,7 +69,7 @@ class OperationsSpecificationTest { @Test fun `unnamed and named mutations are equivalent`(){ - executeEqualQueries(schema, + executeEqualQueries(newSchema(), mapOf("data" to mapOf("createActor" to mapOf("name" to "Kurt Russel"))), "mutation {createActor(name : \"Kurt Russel\"){name}}", "mutation KURT {createActor(name : \"Kurt Russel\"){name}}" @@ -73,6 +78,7 @@ class OperationsSpecificationTest { @Test fun `handle subscription`(){ + val schema = newSchema(Executor.Parallel) schema.executeBlocking("subscription {subscriptionActor(subscription : \"mySubscription\"){name}}") subscriptionResult = "" @@ -99,7 +105,7 @@ class OperationsSpecificationTest { @Test fun `Subscription return type must be the same as the publisher's`(){ expect("Subscription return type must be the same as the publisher's"){ - schema.executeBlocking("subscription {subscriptionActress(subscription : \"mySubscription\"){age}}") + newSchema(Executor.Parallel).executeBlocking("subscription {subscriptionActress(subscription : \"mySubscription\"){age}}") } } } diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/QueryDocumentSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/QueryDocumentSpecificationTest.kt index a07511f1..b1031458 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/QueryDocumentSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/QueryDocumentSpecificationTest.kt @@ -8,7 +8,7 @@ import org.amshove.kluent.shouldThrow import org.amshove.kluent.withMessage import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.2 Query Document") class QueryDocumentSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SelectionSetsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SelectionSetsSpecificationTest.kt index 4d89ac2c..73d39c5b 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SelectionSetsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SelectionSetsSpecificationTest.kt @@ -7,7 +7,7 @@ import com.apurebase.kgraphql.deserialize import com.apurebase.kgraphql.extract import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.4 Selection Sets") class SelectionSetsSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SourceTextSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SourceTextSpecificationTest.kt index 1e4475ad..6f227696 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SourceTextSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/SourceTextSpecificationTest.kt @@ -8,7 +8,7 @@ import org.amshove.kluent.shouldThrow import org.amshove.kluent.withMessage import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("2.1. Source Text") class SourceTextSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt index 6a1bc007..dc94163f 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/language/VariablesSpecificationTest.kt @@ -1,14 +1,11 @@ package com.apurebase.kgraphql.specification.language -import com.apurebase.kgraphql.Specification -import com.apurebase.kgraphql.assertNoErrors -import com.apurebase.kgraphql.expect -import com.apurebase.kgraphql.extract +import com.apurebase.kgraphql.* import com.apurebase.kgraphql.integration.BaseSchemaTest import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Ignore -import org.junit.Test +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test @Specification("2.10 Variables") class VariablesSpecificationTest : BaseSchemaTest() { @@ -53,7 +50,7 @@ class VariablesSpecificationTest : BaseSchemaTest() { } @Test - @Ignore("I don't think this should actually be supported?") + @Disabled("I don't think this should actually be supported?") fun `query with variables and default value pointing to another variable`(){ val map = execute( query = "mutation(\$name: String = \"Boguś Linda\", \$age : Int = \$defaultAge, \$defaultAge : Int!) " + diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/DirectivesSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/DirectivesSpecificationTest.kt index 796a163b..5a2f4453 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/DirectivesSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/DirectivesSpecificationTest.kt @@ -6,7 +6,7 @@ import org.amshove.kluent.shouldEqual import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.2 Directives") class DirectivesSpecificationTest : BaseSchemaTest() { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt index 29ffa6e9..8c7057c7 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/EnumsSpecificationTest.kt @@ -7,7 +7,7 @@ import org.amshove.kluent.shouldThrow import org.amshove.kluent.withMessage import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.1.5 Enums") diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InputObjectsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InputObjectsSpecificationTest.kt index 47094a22..d32e1420 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InputObjectsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InputObjectsSpecificationTest.kt @@ -8,7 +8,7 @@ import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.CoreMatchers.startsWith import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test class InputObjectsSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InterfacesSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InterfacesSpecificationTest.kt index 71b4dc0b..3dfd856e 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InterfacesSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/InterfacesSpecificationTest.kt @@ -7,7 +7,7 @@ import org.amshove.kluent.shouldThrow import org.amshove.kluent.withMessage import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.1.3 Interfaces") class InterfacesSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt index 94f38593..8cc992c3 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ListsSpecificationTest.kt @@ -11,7 +11,7 @@ import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.1.7 Lists") class ListsSpecificationTest{ diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/NonNullSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/NonNullSpecificationTest.kt index 873662c8..1f5abdd9 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/NonNullSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/NonNullSpecificationTest.kt @@ -5,7 +5,7 @@ import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.* import org.hamcrest.CoreMatchers.nullValue import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.1.8 Non-null") class NonNullSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ObjectsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ObjectsSpecificationTest.kt index c8ab331a..65e6fe42 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ObjectsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ObjectsSpecificationTest.kt @@ -5,19 +5,29 @@ import com.apurebase.kgraphql.KGraphQL import com.apurebase.kgraphql.Specification import com.apurebase.kgraphql.expect import com.apurebase.kgraphql.schema.SchemaException +import com.apurebase.kgraphql.schema.dsl.SchemaBuilder +import com.apurebase.kgraphql.schema.execution.Executor import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.Matchers.equalTo import org.hamcrest.Matchers.greaterThan -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.1.2 Objects") class ObjectsSpecificationTest { data class Underscore(val __field : Int) + // TODO: We want to have [Executor.DataLoaderPrepared] working with these tests before reaching stable release of that executor! + fun schema(executor: Executor = Executor.Parallel, block: SchemaBuilder.() -> Unit) = KGraphQL.schema { + configure { + this@configure.executor = executor + } + block() + } + @Test fun `All fields defined within an Object type must not have a name which begins with __`(){ expect("Illegal name '__field'. Names starting with '__' are reserved for introspection system"){ - KGraphQL.schema { + schema { query("underscore") { resolver { -> Underscore(0) } } @@ -35,7 +45,7 @@ class ObjectsSpecificationTest { @Test fun `fields are conceptually ordered in the same order in which they were encountered during query execution`(){ - val schema = KGraphQL.schema { + val schema = schema { // TODO: Update this test to use the new [DataLoaderPrepared] executor! query("many") { resolver { -> ManyFields() } } type{ property("name") { @@ -65,7 +75,7 @@ class ObjectsSpecificationTest { @Test fun `fragment spread fields occur before the following fields`(){ - val schema = KGraphQL.schema { + val schema = schema { query("many") { resolver { -> ManyFields() } } } @@ -80,7 +90,7 @@ class ObjectsSpecificationTest { @Test fun `fragments for which the type does not apply does not affect ordering`(){ - val schema = KGraphQL.schema { + val schema = schema { query("many") { resolver { -> ManyFields() } } type() } @@ -99,7 +109,7 @@ class ObjectsSpecificationTest { @Test fun `If a field is queried multiple times in a selection, it is ordered by the first time it is encountered`(){ - val schema = KGraphQL.schema { + val schema = schema { query("many") { resolver { -> ManyFields() } } } @@ -129,7 +139,7 @@ class ObjectsSpecificationTest { @Test fun `All arguments defined within a field must not have a name which begins with __`(){ expect("Illegal name '__id'. Names starting with '__' are reserved for introspection system"){ - KGraphQL.schema { + schema { query("many") { resolver { __id : String -> ManyFields(__id) } } } } @@ -140,13 +150,13 @@ class ObjectsSpecificationTest { @Test fun `An Object type must define one or more fields` (){ expect("An Object type must define one or more fields. Found none on type Empty"){ - KGraphQL.schema { type() } + schema { type() } } } @Test fun `field resolution order does not affect response field order`(){ - val schema = KGraphQL.schema { + val schema = schema { type { property("long"){ resolver { @@ -180,7 +190,7 @@ class ObjectsSpecificationTest { @Test fun `operation resolution order does not affect response field order`(){ - val schema = KGraphQL.schema { + val schema = schema { query("long"){ resolver { Thread.sleep(100) diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt index 3265d187..52331c96 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/ScalarsSpecificationTest.kt @@ -10,7 +10,7 @@ import com.apurebase.kgraphql.schema.scalar.StringScalarCoercion import org.amshove.kluent.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test +import org.junit.jupiter.api.Test import java.time.LocalDate import java.util.* diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/TypeSystemSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/TypeSystemSpecificationTest.kt index b19b2243..c850c717 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/TypeSystemSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/TypeSystemSpecificationTest.kt @@ -4,7 +4,7 @@ import com.apurebase.kgraphql.KGraphQL.Companion.schema import com.apurebase.kgraphql.Specification import com.apurebase.kgraphql.expect import com.apurebase.kgraphql.schema.SchemaException -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3 Type System") class TypeSystemSpecificationTest { diff --git a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/UnionsSpecificationTest.kt b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/UnionsSpecificationTest.kt index 7a524efa..99103dff 100644 --- a/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/UnionsSpecificationTest.kt +++ b/kgraphql/src/test/kotlin/com/apurebase/kgraphql/specification/typesystem/UnionsSpecificationTest.kt @@ -7,7 +7,7 @@ import com.apurebase.kgraphql.GraphQLError import org.amshove.kluent.* import org.hamcrest.CoreMatchers import org.hamcrest.MatcherAssert -import org.junit.Test +import org.junit.jupiter.api.Test @Specification("3.1.4 Unions")