diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index 9be8f3bf8..0969c5738 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -13,6 +13,9 @@ val slf4jVersion = "1.7.25" dependencies { api("com.graphql-java:graphql-java:$graphqlJavaVersion") implementation("org.slf4j:slf4j-api:$slf4jVersion") + implementation("com.graphql-java:graphql-java-extended-scalars:21.0") { + exclude("com.graphql-java", "graphql-java") + } api(kotlin("stdlib")) api(kotlin("reflect")) diff --git a/lib/src/main/java/graphql/nadel/dsl/RemoteArgumentSource.kt b/lib/src/main/java/graphql/nadel/dsl/RemoteArgumentSource.kt index bf62333ac..60d54b73c 100644 --- a/lib/src/main/java/graphql/nadel/dsl/RemoteArgumentSource.kt +++ b/lib/src/main/java/graphql/nadel/dsl/RemoteArgumentSource.kt @@ -1,13 +1,17 @@ package graphql.nadel.dsl +import graphql.language.Value + // todo this should be a union or sealed class thing data class RemoteArgumentSource( val argumentName: String?, // for OBJECT_FIELD val pathToField: List?, - val sourceType: SourceType?, + val staticValue: Value<*>?, + val sourceType: SourceType, ) { enum class SourceType { ObjectField, FieldArgument, + StaticArgument } } diff --git a/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt b/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt index 512415f82..34b6b586e 100644 --- a/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt +++ b/lib/src/main/java/graphql/nadel/engine/blueprint/NadelExecutionBlueprintFactory.kt @@ -8,10 +8,12 @@ import graphql.Scalars.GraphQLString import graphql.language.EnumTypeDefinition import graphql.language.FieldDefinition import graphql.language.ImplementingTypeDefinition +import graphql.language.StringValue import graphql.nadel.Service import graphql.nadel.dsl.FieldMappingDefinition import graphql.nadel.dsl.RemoteArgumentSource.SourceType.FieldArgument import graphql.nadel.dsl.RemoteArgumentSource.SourceType.ObjectField +import graphql.nadel.dsl.RemoteArgumentSource.SourceType.StaticArgument import graphql.nadel.dsl.TypeMappingDefinition import graphql.nadel.dsl.UnderlyingServiceHydration import graphql.nadel.engine.blueprint.hydration.NadelBatchHydrationMatchStrategy @@ -260,6 +262,7 @@ private class Factory( when (it.valueSource) { is NadelHydrationActorInputDef.ValueSource.ArgumentValue -> null is FieldResultValue -> it.valueSource.queryPathToField + is NadelHydrationActorInputDef.ValueSource.StaticValue -> null } }, ) @@ -358,6 +361,7 @@ private class Factory( when (val hydrationValueSource: NadelHydrationActorInputDef.ValueSource = it.valueSource) { is NadelHydrationActorInputDef.ValueSource.ArgumentValue -> emptyList() is FieldResultValue -> selectSourceFieldQueryPaths(hydrationValueSource) + is NadelHydrationActorInputDef.ValueSource.StaticValue -> emptyList() } }).toSet() @@ -483,7 +487,11 @@ private class Factory( ?: error("No field defined at: ${hydratedFieldParentType.name}.${pathToField.joinToString(".")}"), ) } - else -> error("Unsupported remote argument source type: '$argSourceType'") + StaticArgument -> { + NadelHydrationActorInputDef.ValueSource.StaticValue( + value = remoteArgDef.remoteArgumentSource.staticValue!! + ) + } } NadelHydrationActorInputDef( diff --git a/lib/src/main/java/graphql/nadel/engine/blueprint/hydration/NadelHydrationActorInputDef.kt b/lib/src/main/java/graphql/nadel/engine/blueprint/hydration/NadelHydrationActorInputDef.kt index e803ff340..cd1594c36 100644 --- a/lib/src/main/java/graphql/nadel/engine/blueprint/hydration/NadelHydrationActorInputDef.kt +++ b/lib/src/main/java/graphql/nadel/engine/blueprint/hydration/NadelHydrationActorInputDef.kt @@ -1,5 +1,6 @@ package graphql.nadel.engine.blueprint.hydration +import graphql.language.Value import graphql.nadel.engine.transform.query.NadelQueryPath import graphql.normalized.NormalizedInputValue import graphql.schema.GraphQLArgument @@ -44,6 +45,22 @@ data class NadelHydrationActorInputDef( val argumentDefinition: GraphQLArgument, val defaultValue: NormalizedInputValue?, ) : ValueSource() + + /** + * Represents a static argument value, which is hardcoded in the source code. e.g. + * + * ```graphql + * type Issue { + * id: ID! + * owner: User @hydrated(from: ["issueOwner"], args: [ + * {name: "issueId" value: "issue123"} + * ]) + * } + * ``` + */ + data class StaticValue( + val value: Value<*>, + ) : ValueSource() } } diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt index cac95e5c8..78c47eede 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/NadelHydrationInputBuilder.kt @@ -1,6 +1,7 @@ package graphql.nadel.engine.transform.hydration import graphql.language.NullValue +import graphql.language.Value import graphql.nadel.engine.blueprint.NadelHydrationFieldInstruction import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef import graphql.nadel.engine.blueprint.hydration.NadelHydrationActorInputDef.ValueSource @@ -8,6 +9,7 @@ import graphql.nadel.engine.blueprint.hydration.NadelHydrationStrategy import graphql.nadel.engine.transform.artificial.NadelAliasHelper import graphql.nadel.engine.transform.result.json.JsonNode import graphql.nadel.engine.transform.result.json.JsonNodeExtractor +import graphql.nadel.engine.util.AnyAstValue import graphql.nadel.engine.util.emptyOrSingle import graphql.nadel.engine.util.flatten import graphql.nadel.engine.util.javaValueToAstValue @@ -127,6 +129,7 @@ internal class NadelHydrationInputBuilder private constructor( inputDef, value = getResultValue(valueSource), ) + is ValueSource.StaticValue -> makeInputValue(inputDef, valueSource.value) } } @@ -140,6 +143,16 @@ internal class NadelHydrationInputBuilder private constructor( ) } + private fun makeInputValue( + inputDef: NadelHydrationActorInputDef, + value: Value<*>, + ): NormalizedInputValue { + return makeNormalizedInputValue( + type = inputDef.actorArgumentDef.type, + value = value, + ) + } + private fun getArgumentValue( valueSource: ValueSource.ArgumentValue, ): NormalizedInputValue? { diff --git a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt index e6c51ed9b..f99315acb 100644 --- a/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt +++ b/lib/src/main/java/graphql/nadel/engine/transform/hydration/batch/NadelBatchHydrationInputBuilder.kt @@ -10,6 +10,7 @@ import graphql.nadel.engine.transform.result.json.JsonNodeExtractor import graphql.nadel.engine.util.emptyOrSingle import graphql.nadel.engine.util.flatten import graphql.nadel.engine.util.javaValueToAstValue +import graphql.nadel.engine.util.makeNormalizedInputValue import graphql.nadel.engine.util.mapFrom import graphql.nadel.hooks.ServiceExecutionHooks import graphql.normalized.ExecutableNormalizedField @@ -44,7 +45,7 @@ internal object NadelBatchHydrationInputBuilder { instruction.actorInputValueDefs.mapNotNull { actorFieldArg -> when (val valueSource = actorFieldArg.valueSource) { is NadelHydrationActorInputDef.ValueSource.ArgumentValue -> { - val argValue = hydrationField.normalizedArguments[valueSource.argumentName] + val argValue: NormalizedInputValue? = hydrationField.normalizedArguments[valueSource.argumentName] ?: valueSource.defaultValue if (argValue != null) { actorFieldArg to argValue @@ -54,6 +55,13 @@ internal object NadelBatchHydrationInputBuilder { } // These are batch values, ignore them is NadelHydrationActorInputDef.ValueSource.FieldResultValue -> null + is NadelHydrationActorInputDef.ValueSource.StaticValue -> { + val staticValue: NormalizedInputValue = makeNormalizedInputValue( + type = actorFieldArg.actorArgumentDef.type, + value = valueSource.value, + ) + actorFieldArg to staticValue + } } }, ) diff --git a/lib/src/main/java/graphql/nadel/schema/NadelDirectives.kt b/lib/src/main/java/graphql/nadel/schema/NadelDirectives.kt index ded3f939d..1ce4a261e 100644 --- a/lib/src/main/java/graphql/nadel/schema/NadelDirectives.kt +++ b/lib/src/main/java/graphql/nadel/schema/NadelDirectives.kt @@ -1,5 +1,6 @@ package graphql.nadel.schema +import graphql.scalars.ExtendedScalars import graphql.GraphQLContext import graphql.Scalars import graphql.Scalars.GraphQLString @@ -10,7 +11,9 @@ import graphql.language.DirectiveDefinition.newDirectiveDefinition import graphql.language.EnumTypeDefinition.newEnumTypeDefinition import graphql.language.EnumValueDefinition.newEnumValueDefinition import graphql.language.InputObjectTypeDefinition.newInputObjectDefinition +import graphql.language.ObjectValue import graphql.language.StringValue +import graphql.language.Value import graphql.nadel.dsl.FieldMappingDefinition import graphql.nadel.dsl.RemoteArgumentDefinition import graphql.nadel.dsl.RemoteArgumentSource @@ -72,7 +75,7 @@ object NadelDirectives { ) .inputValueDefinition( name = "value", - type = nonNull(GraphQLString), + type = nonNull(ExtendedScalars.Json), ) .build() @@ -251,8 +254,7 @@ object NadelDirectives { val hydrations = fieldDefinition.getAppliedDirectives(hydratedDirectiveDefinition.name) .asSequence() .map { directive -> - val argumentValues = resolveArgumentValue>(directive.getArgument("arguments")) - val arguments = createRemoteArgs(argumentValues) + val arguments = createRemoteArgs(directive.getArgument("arguments").argumentValue.value as ArrayValue) val inputIdentifiedBy = directive.getArgument("inputIdentifiedBy") val identifiedByValues = resolveArgumentValue>(inputIdentifiedBy) @@ -330,19 +332,19 @@ object NadelDirectives { ) } - private fun createRemoteArgs(arguments: List): List { + private fun createRemoteArgs(arguments: ArrayValue): List { fun Map.requireArgument(key: String): String { return requireNotNull(this[key]) { "${nadelHydrationArgumentDefinition.name} definition requires '$key' to be not-null" } } - return arguments + return arguments.values .map { arg -> @Suppress("UNCHECKED_CAST") // trust GraphQL type system and caller - val argMap = arg as Map - val remoteArgName = argMap.requireArgument("name") - val remoteArgValue = argMap.requireArgument("value") + val argMap = arg as ObjectValue + val remoteArgName = (argMap.objectFields.single { it.name == "name" }.value as StringValue).value + val remoteArgValue = argMap.objectFields.single { it.name == "value" }.value val remoteArgumentSource = createRemoteArgumentSource(remoteArgValue) RemoteArgumentDefinition(remoteArgName, remoteArgumentSource) } @@ -363,21 +365,38 @@ object NadelDirectives { } } - private fun createRemoteArgumentSource(value: String): RemoteArgumentSource { - val values = listFromDottedString(value) - - return when (values.first()) { - "\$source" -> RemoteArgumentSource( + private fun createRemoteArgumentSource(value: Value<*>): RemoteArgumentSource { + if (value is StringValue) { + val values = listFromDottedString(value.value) + return when (values.first()) { + "\$source" -> RemoteArgumentSource( + argumentName = null, + pathToField = values.subList(1, values.size), + staticValue = null, + sourceType = SourceType.ObjectField, + ) + + "\$argument" -> RemoteArgumentSource( + argumentName = values.subList(1, values.size).single(), + pathToField = null, + staticValue = null, + sourceType = SourceType.FieldArgument, + ) + + else -> RemoteArgumentSource( + argumentName = null, + pathToField = null, + staticValue = value, + sourceType = SourceType.StaticArgument, + ) + } + } else { + return RemoteArgumentSource( argumentName = null, - pathToField = values.subList(1, values.size), - sourceType = SourceType.ObjectField, - ) - "\$argument" -> RemoteArgumentSource( - argumentName = values.subList(1, values.size).single(), pathToField = null, - sourceType = SourceType.FieldArgument, + staticValue = value, + sourceType = SourceType.StaticArgument, ) - else -> throw IllegalArgumentException("$value must begin with \$source. or \$argument.") } } @@ -420,12 +439,14 @@ object NadelDirectives { var argumentName: String? = null var path: List? = null + var value: Value<*>? = null when (argumentType) { SourceType.ObjectField -> path = values SourceType.FieldArgument -> argumentName = values.single() + SourceType.StaticArgument -> value = value } - return RemoteArgumentSource(argumentName, path, argumentType) + return RemoteArgumentSource(argumentName, path, value, argumentType) } fun createFieldMapping(fieldDefinition: GraphQLFieldDefinition): FieldMappingDefinition? { diff --git a/lib/src/main/java/graphql/nadel/schema/NeverWiringFactory.kt b/lib/src/main/java/graphql/nadel/schema/NeverWiringFactory.kt index 2bf4d5add..70739f9c0 100644 --- a/lib/src/main/java/graphql/nadel/schema/NeverWiringFactory.kt +++ b/lib/src/main/java/graphql/nadel/schema/NeverWiringFactory.kt @@ -1,6 +1,7 @@ package graphql.nadel.schema import graphql.Assert.assertShouldNeverHappen +import graphql.scalars.ExtendedScalars import graphql.schema.Coercing import graphql.schema.DataFetcher import graphql.schema.GraphQLScalarType @@ -24,6 +25,9 @@ open class NeverWiringFactory : WiringFactory { override fun getScalar(environment: ScalarWiringEnvironment): GraphQLScalarType? { val scalarName = environment.scalarTypeDefinition.name + if (scalarName == ExtendedScalars.Json.name) { + return ExtendedScalars.Json + } return GraphQLScalarType .newScalar() .name(scalarName) diff --git a/lib/src/main/java/graphql/nadel/schema/OverallSchemaGenerator.kt b/lib/src/main/java/graphql/nadel/schema/OverallSchemaGenerator.kt index ffc49aef8..29bd4be9f 100644 --- a/lib/src/main/java/graphql/nadel/schema/OverallSchemaGenerator.kt +++ b/lib/src/main/java/graphql/nadel/schema/OverallSchemaGenerator.kt @@ -4,6 +4,7 @@ import graphql.GraphQLException import graphql.language.FieldDefinition import graphql.language.ObjectTypeDefinition import graphql.language.ObjectTypeDefinition.newObjectTypeDefinition +import graphql.language.ScalarTypeDefinition.newScalarTypeDefinition import graphql.language.SchemaDefinition import graphql.language.SourceLocation import graphql.nadel.NadelDefinitionRegistry @@ -12,6 +13,7 @@ import graphql.nadel.util.AnyNamedNode import graphql.nadel.util.AnySDLDefinition import graphql.nadel.util.AnySDLNamedDefinition import graphql.nadel.util.isExtensionDef +import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLSchema import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator @@ -70,6 +72,9 @@ internal class OverallSchemaGenerator { addIfNotPresent(overallRegistry, allDefinitions, NadelDirectives.nadelHydrationTemplateEnumDefinition) addIfNotPresent(overallRegistry, allDefinitions, NadelDirectives.hydratedFromDirectiveDefinition) addIfNotPresent(overallRegistry, allDefinitions, NadelDirectives.hydratedTemplateDirectiveDefinition) + addIfNotPresent(overallRegistry, allDefinitions, newScalarTypeDefinition() + .name(ExtendedScalars.Json.name) + .build()) for (definition in allDefinitions) { val error = overallRegistry.add(definition) diff --git a/lib/src/main/java/graphql/nadel/validation/NadelHydrationValidation.kt b/lib/src/main/java/graphql/nadel/validation/NadelHydrationValidation.kt index 5b4564064..32a1a9bc0 100644 --- a/lib/src/main/java/graphql/nadel/validation/NadelHydrationValidation.kt +++ b/lib/src/main/java/graphql/nadel/validation/NadelHydrationValidation.kt @@ -20,6 +20,7 @@ import graphql.nadel.validation.NadelSchemaValidationError.MissingHydrationArgum import graphql.nadel.validation.NadelSchemaValidationError.MissingHydrationFieldValueSource import graphql.nadel.validation.NadelSchemaValidationError.MissingRequiredHydrationActorFieldArgument import graphql.nadel.validation.NadelSchemaValidationError.MultipleSourceArgsInBatchHydration +import graphql.nadel.validation.NadelSchemaValidationError.NoSourceArgsInBatchHydration import graphql.nadel.validation.NadelSchemaValidationError.NonExistentHydrationActorFieldArgument import graphql.nadel.validation.util.NadelSchemaUtil.getHydrations import graphql.nadel.validation.util.NadelSchemaUtil.hasRename @@ -193,6 +194,8 @@ internal class NadelHydrationValidation( when { numberOfSourceArgs > 1 -> listOf(MultipleSourceArgsInBatchHydration(parent, overallField)) + numberOfSourceArgs == 0 -> + listOf(NoSourceArgsInBatchHydration(parent, overallField)) else -> emptyList() } @@ -234,4 +237,4 @@ internal class NadelHydrationValidation( } } } -} +} \ No newline at end of file diff --git a/lib/src/main/java/graphql/nadel/validation/NadelSchemaValidationError.kt b/lib/src/main/java/graphql/nadel/validation/NadelSchemaValidationError.kt index e429c2d88..84e2694d9 100644 --- a/lib/src/main/java/graphql/nadel/validation/NadelSchemaValidationError.kt +++ b/lib/src/main/java/graphql/nadel/validation/NadelSchemaValidationError.kt @@ -417,6 +417,20 @@ sealed interface NadelSchemaValidationError { override val subject = overallField } + data class NoSourceArgsInBatchHydration( + val parentType: NadelServiceSchemaElement, + val overallField: GraphQLFieldDefinition, + ) : NadelSchemaValidationError { + val service: Service get() = parentType.service + + override val message = run { + val fieldCoordinates = makeFieldCoordinates(parentType.overall.name, overallField.name) + "No \$source.xxx arguments for batch hydration. Field: $fieldCoordinates" + } + + override val subject = overallField + } + data class MissingArgumentOnUnderlying( val parentType: NadelServiceSchemaElement, val overallField: GraphQLFieldDefinition, diff --git a/lib/src/test/kotlin/graphql/nadel/NadelSchemasTest.kt b/lib/src/test/kotlin/graphql/nadel/NadelSchemasTest.kt index c81c5afd0..f853fd240 100644 --- a/lib/src/test/kotlin/graphql/nadel/NadelSchemasTest.kt +++ b/lib/src/test/kotlin/graphql/nadel/NadelSchemasTest.kt @@ -73,7 +73,7 @@ class NadelSchemasTest : DescribeSpec({ .build() // then - assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query")) + assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query", "JSON")) val testService = schemas.services.single() assert(testService.underlyingSchema.userTypeNames == setOf("World", "Echo", "Query", "Food")) } @@ -149,7 +149,7 @@ class NadelSchemasTest : DescribeSpec({ .build() // then - assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query")) + assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query", "JSON")) val testService = schemas.services.single() assert(testService.underlyingSchema.userTypeNames == setOf("World", "Echo", "Query", "Food")) } @@ -211,7 +211,7 @@ class NadelSchemasTest : DescribeSpec({ .build() // then - assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query", "Issue")) + assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query", "Issue", "JSON")) val issueService = schemas.services.single { it.name == "issue" } assert(issueService.underlyingSchema.userTypeNames == setOf("Query", "Issue")) @@ -272,7 +272,7 @@ class NadelSchemasTest : DescribeSpec({ .build() // then - assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query", "Issue")) + assert(schemas.engineSchema.userTypeNames == setOf("World", "Echo", "Query", "Issue", "JSON")) } it("does not validate the schemas") { @@ -321,7 +321,7 @@ class NadelSchemasTest : DescribeSpec({ .build() // then - assert(schemas.engineSchema.userTypeNames == setOf("Query", "World", "Echo", "Issue")) + assert(schemas.engineSchema.userTypeNames == setOf("Query", "World", "Echo", "Issue", "JSON")) val testService = schemas.services.first { it.name == "test" } assert(testService.definitionRegistry.typeNames == setOf("Query", "Echo", "World")) diff --git a/lib/src/test/kotlin/graphql/nadel/schema/NadelDirectivesTest.kt b/lib/src/test/kotlin/graphql/nadel/schema/NadelDirectivesTest.kt index 32a43ace2..f057b25b7 100644 --- a/lib/src/test/kotlin/graphql/nadel/schema/NadelDirectivesTest.kt +++ b/lib/src/test/kotlin/graphql/nadel/schema/NadelDirectivesTest.kt @@ -10,7 +10,8 @@ import graphql.nadel.schema.NadelDirectives.nadelHydrationComplexIdentifiedBy import graphql.nadel.schema.NadelDirectives.nadelHydrationFromArgumentDefinition import graphql.nadel.schema.NadelDirectives.nadelHydrationTemplateEnumDefinition import graphql.schema.GraphQLSchema -import graphql.schema.idl.RuntimeWiring.MOCKED_WIRING +import graphql.schema.idl.MockedWiringFactory +import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaGenerator import graphql.schema.idl.SchemaParser import io.kotest.core.spec.style.DescribeSpec @@ -29,11 +30,14 @@ class NadelDirectivesTest : DescribeSpec({ ${AstPrinter.printAst(nadelHydrationTemplateEnumDefinition)} ${AstPrinter.printAst(hydratedFromDirectiveDefinition)} ${AstPrinter.printAst(hydratedTemplateDirectiveDefinition)} + scalar JSON """ fun getSchema(schemaText: String): GraphQLSchema { val typeDefs = SchemaParser().parse(commonDefs + "\n" + schemaText) - return SchemaGenerator().makeExecutableSchema(typeDefs, MOCKED_WIRING) + return SchemaGenerator().makeExecutableSchema(typeDefs, RuntimeWiring + .newRuntimeWiring() + .wiringFactory(NeverWiringFactory()).build()) } describe("@hydrated") { diff --git a/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationValidationTest.kt b/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationValidationTest.kt index ea19204df..e2a5dd2c4 100644 --- a/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationValidationTest.kt +++ b/lib/src/test/kotlin/graphql/nadel/validation/NadelHydrationValidationTest.kt @@ -132,6 +132,63 @@ class NadelHydrationValidationTest : DescribeSpec({ assert(errors.single().message == "Multiple \$source.xxx arguments are not supported for batch hydration. Field: JiraIssue.creator") } + it("fails when batch hydration with no \$source args") { + val fixture = NadelValidationTestFixture( + overallSchema = mapOf( + "issues" to """ + type Query { + issue: JiraIssue + } + type JiraIssue @renamed(from: "Issue") { + id: ID! + creator(siteId: ID!): User @hydrated( + service: "users" + field: "users" + arguments: [ + {name: "id", value: "creatorId123"} + {name: "siteId", value: "$argument.siteId"} + ] + ) + } + """.trimIndent(), + "users" to """ + type Query { + users(id: ID!, siteId: ID!): [User] + } + type User { + id: ID! + name: String! + } + """.trimIndent(), + ), + underlyingSchema = mapOf( + "issues" to """ + type Query { + issue: Issue + } + type Issue { + id: ID! + creator: ID! + } + """.trimIndent(), + "users" to """ + type Query { + users(id: ID!, siteId: ID!): [User] + } + type User { + id: ID! + name: String! + } + """.trimIndent(), + ), + ) + + val errors = validate(fixture) + assert(errors.size == 1) + assert(errors.single() is NadelSchemaValidationError.NoSourceArgsInBatchHydration) + assert(errors.single().message == "No \$source.xxx arguments for batch hydration. Field: JiraIssue.creator") + } + it("passes when batch hydration with a single \$source arg and an \$argument arg") { val fixture = NadelValidationTestFixture( overallSchema = mapOf( diff --git a/test/src/test/kotlin/graphql/nadel/tests/hooks/HydrationDetailsHook.kt b/test/src/test/kotlin/graphql/nadel/tests/hooks/HydrationDetailsHook.kt index 2c4c046e5..4b293237b 100644 --- a/test/src/test/kotlin/graphql/nadel/tests/hooks/HydrationDetailsHook.kt +++ b/test/src/test/kotlin/graphql/nadel/tests/hooks/HydrationDetailsHook.kt @@ -29,6 +29,7 @@ class `basic-hydration` : HydrationDetailsHook() { assert(actualHydrationDetails.fieldPath.toString() == "[foo, bar]") } } + @UseHook class `batch-hydration-with-renamed-actor-field` : HydrationDetailsHook() { override fun assertHydrationDetails(actualHydrationDetails: ServiceExecutionHydrationDetails) { diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-array.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-array.yml new file mode 100644 index 000000000..54344dea6 --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-array.yml @@ -0,0 +1,103 @@ +name: "basic hydration with static arg array" +enabled: true +overallSchema: + service2: | + type Query { + barById(id: ID, friendIds: [ID]): Bar + } + type Bar { + id: ID + name: String + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barById" arguments: [ + { name: "id" value: "$source.id" }, + { name: "friendIds" value: ["barId2", "barId3", "barId4"] } + ]) + } +underlyingSchema: + service2: | + type Bar { + id: ID + name: String + } + + type Query { + barById(id: ID, friendIds: [ID]): Bar + } + service1: | + type Foo { + barId: ID + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + hydration__bar__id: id + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo", + "hydration__bar__id": "barId" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barById(id: "barId", friendIds: ["barId2", "barId3", "barId4"]) { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barById": { + "name": "Bar1" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar1" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-boolean.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-boolean.yml new file mode 100644 index 000000000..b5e5b40f0 --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-boolean.yml @@ -0,0 +1,100 @@ +name: "basic hydration with static arg boolean" +enabled: true +overallSchema: + service2: | + type Query { + barWithSomeAttribute(someAttribute: Boolean): Bar + } + type Bar { + id: ID + name: String + someAttribute: Boolean + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barWithSomeAttribute" arguments: [{name: "someAttribute" value: true}]) + } +underlyingSchema: + service2: | + type Bar { + id: ID + name: String + someAttribute: Boolean + } + + type Query { + barWithSomeAttribute(someAttribute: Boolean): Bar + } + service1: | + type Foo { + barId: ID + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barWithSomeAttribute(someAttribute: true) { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barWithSomeAttribute": { + "name": "Bar12345" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar12345" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-float.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-float.yml new file mode 100644 index 000000000..2dff00fcc --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-float.yml @@ -0,0 +1,100 @@ +name: "basic hydration with static arg float" +enabled: true +overallSchema: + service2: | + type Query { + barWithSomeFloat(someFloat: Float): Bar + } + type Bar { + id: ID + name: String + someFloat: Float + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barWithSomeFloat" arguments: [{name: "someFloat" value: 123.45}]) + } +underlyingSchema: + service2: | + type Bar { + id: ID + name: String + someFloat: Float + } + + type Query { + barWithSomeFloat(someFloat: Float): Bar + } + service1: | + type Foo { + barId: ID + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barWithSomeFloat(someFloat: 123.45) { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barWithSomeFloat": { + "name": "Bar12345" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar12345" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-integer.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-integer.yml new file mode 100644 index 000000000..024c93cf6 --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-integer.yml @@ -0,0 +1,98 @@ +name: "basic hydration with static arg integer" +enabled: true +overallSchema: + service2: | + type Query { + barById(id: Int): Bar + } + type Bar { + id: Int + name: String + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barById" arguments: [{name: "id" value: 12345}]) + } +underlyingSchema: + service2: | + type Bar { + id: Int + name: String + } + + type Query { + barById(id: Int): Bar + } + service1: | + type Foo { + barId: Int + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barById(id: 12345) { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barById": { + "name": "Bar12345" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar12345" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-object-array.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-object-array.yml new file mode 100644 index 000000000..fca0cc125 --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-object-array.yml @@ -0,0 +1,140 @@ +name: "basic hydration with static arg object array" +enabled: true +overallSchema: + service2: | + type Query { + barById(id: ID, friends: [FullName]): Bar + } + type Bar { + id: ID + name: String + } + input FullName { + firstName: String + lastName: String + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barById" arguments: [ + { name: "id" value: "$source.id" }, + { name: "friends" + value: [ + { + firstName: "first" + lastName: "last" + }, + { + firstName: "first2" + lastName: "last2" + }, + { + firstName: "first3" + lastName: "last3" + } + ] + } + ]) + } +underlyingSchema: + service2: | + type Bar { + id: ID + name: String + } + + type Query { + barById(id: ID, friends: [FullName]): Bar + } + + input FullName { + firstName: String + lastName: String + } + service1: | + type Foo { + barId: ID + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + hydration__bar__id: id + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo", + "hydration__bar__id": "barId" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barById(id: "barId", friends: [ + { + firstName: "first" + lastName: "last" + }, + { + firstName: "first2" + lastName: "last2" + }, + { + firstName: "first3" + lastName: "last3" + } + ]) { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barById": { + "name": "Bar1" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar1" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-object.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-object.yml new file mode 100644 index 000000000..00cf956bb --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-arg-object.yml @@ -0,0 +1,120 @@ +name: "basic hydration with static arg object" +enabled: true +overallSchema: + service2: | + type Query { + barById(id: ID, fullName: FullName): Bar + } + type Bar { + id: ID + name: String + } + input FullName { + firstName: String + lastName: String + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barById" arguments: [ + { name: "id" value: "$source.id" }, + { name: "fullName" + value: { + firstName: "first" + lastName: "last" + } + } + ]) + } +underlyingSchema: + service2: | + type Bar { + id: ID + name: String + } + + type Query { + barById(id: ID, fullName: FullName): Bar + } + + input FullName { + firstName: String + lastName: String + } + service1: | + type Foo { + barId: ID + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + hydration__bar__id: id + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo", + "hydration__bar__id": "barId" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barById(id: "barId", fullName: { + firstName: "first" + lastName: "last" + }) { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barById": { + "name": "Bar1" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar1" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-args.yml b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-args.yml new file mode 100644 index 000000000..da6fa7415 --- /dev/null +++ b/test/src/test/resources/fixtures/hydration/basic-hydration-with-static-args.yml @@ -0,0 +1,98 @@ +name: "basic hydration with static args" +enabled: true +overallSchema: + service2: | + type Query { + barById(id: ID): Bar + } + type Bar { + id: ID + name: String + } + service1: | + type Query { + foo: Foo + } + type Foo { + id: ID + bar: Bar @hydrated(service: "service2" field: "barById" arguments: [{name: "id" value: "barId"}]) + } +underlyingSchema: + service2: | + type Bar { + id: ID + name: String + } + + type Query { + barById(id: ID): Bar + } + service1: | + type Foo { + barId: ID + id: ID + } + + type Query { + foo: Foo + } +query: | + query { + foo { + bar { + name + } + } + } +variables: { } +serviceCalls: + - serviceName: "service1" + request: + query: | + query { + foo { + __typename__hydration__bar: __typename + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "foo": { + "__typename__hydration__bar": "Foo" + } + }, + "extensions": {} + } + - serviceName: "service2" + request: + query: | + query { + barById(id: "barId") { + name + } + } + variables: { } + # language=JSON + response: |- + { + "data": { + "barById": { + "name": "Bar1" + } + }, + "extensions": {} + } +# language=JSON +response: |- + { + "data": { + "foo": { + "bar": { + "name": "Bar1" + } + } + }, + "extensions": {} + } \ No newline at end of file diff --git a/test/src/test/resources/fixtures/hydration/basic-hydration.yml b/test/src/test/resources/fixtures/hydration/basic-hydration.yml index b19efd2ff..6aa8242c6 100644 --- a/test/src/test/resources/fixtures/hydration/basic-hydration.yml +++ b/test/src/test/resources/fixtures/hydration/basic-hydration.yml @@ -103,4 +103,4 @@ response: |- } }, "extensions": {} - } + } \ No newline at end of file