diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt index a2f0e9303c..129c480f51 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorHooks.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -147,7 +147,7 @@ open class FederatedSchemaGeneratorHooks(private val resolvers: List() - .filter { type -> type.getDirective(KEY_DIRECTIVE_NAME) != null } + .filter { type -> type.hasDirective(KEY_DIRECTIVE_NAME) } .map { it.name } .toSet() } diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/KeyDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/KeyDirective.kt index 391aeaf25c..a43d26edab 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/KeyDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/directives/KeyDirective.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,6 @@ import graphql.introspection.Introspection.DirectiveLocation * well as all the corresponding federated (i.e. extended) types. Key fields specified in the directive field set should correspond to a valid field on the underlying GraphQL interface/object. * Federated extended types should also instrument all the referenced key fields with @external directive. * - * NOTE: The Federation spec specifies that multiple @key directives can be applied on the field. The GraphQL spec has been recently changed to allow this behavior, - * but we are currently blocked and are tracking progress in [this issue](https://github.com/ExpediaGroup/graphql-kotlin/issues/590). - * * Example: * Given * @@ -55,6 +52,7 @@ import graphql.introspection.Introspection.DirectiveLocation * @see ExtendsDirective * @see ExternalDirective */ +@Repeatable @GraphQLDirective( name = KEY_DIRECTIVE_NAME, description = KEY_DIRECTIVE_DESCRIPTION, @@ -70,4 +68,5 @@ internal val KEY_DIRECTIVE_TYPE: graphql.schema.GraphQLDirective = graphql.schem .description(KEY_DIRECTIVE_DESCRIPTION) .validLocations(DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE) .argument(FIELD_SET_ARGUMENT) + .repeatable(true) .build() diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/GraphQLDirectiveContainerExtensions.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/GraphQLDirectiveContainerExtensions.kt index e53c3ce93e..0226c2794f 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/GraphQLDirectiveContainerExtensions.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/GraphQLDirectiveContainerExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,6 @@ import com.expediagroup.graphql.generator.federation.directives.EXTENDS_DIRECTIV import com.expediagroup.graphql.generator.federation.directives.KEY_DIRECTIVE_NAME import graphql.schema.GraphQLDirectiveContainer -internal fun GraphQLDirectiveContainer.isFederatedType() = this.getDirective(KEY_DIRECTIVE_NAME) != null || isExtendedType() +internal fun GraphQLDirectiveContainer.isFederatedType() = this.getDirectives(KEY_DIRECTIVE_NAME).isNotEmpty() || isExtendedType() internal fun GraphQLDirectiveContainer.isExtendedType() = this.getDirective(EXTENDS_DIRECTIVE_NAME) != null diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/graphQLSchemaExtensions.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/graphQLSchemaExtensions.kt index f705fc54b3..a0e21615cb 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/graphQLSchemaExtensions.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/extensions/graphQLSchemaExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ internal fun GraphQLSchema.addDirectivesIfNotPresent(directives: List, directives: Map) { + private fun validate(federatedType: String, fields: List, directiveMap: Map>) { val errors = mutableListOf() val fieldMap = fields.associateBy { it.name } - val extendedType = directives.containsKey(EXTENDS_DIRECTIVE_NAME) + val extendedType = directiveMap.containsKey(EXTENDS_DIRECTIVE_NAME) // [OK] @key directive is specified // [OK] @key references valid existing fields @@ -65,7 +65,7 @@ internal class FederatedSchemaValidator { // [ERROR] @key references fields resulting in list // [ERROR] @key references fields resulting in union // [ERROR] @key references fields resulting in interface - errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directives, fieldMap, extendedType)) + errors.addAll(validateDirective(federatedType, KEY_DIRECTIVE_NAME, directiveMap, fieldMap, extendedType)) for (field in fields) { if (field.getDirective(REQUIRES_DIRECTIVE_NAME) != null) { diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateDirective.kt index e4523e912a..52b89c45a4 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateDirective.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,28 +24,30 @@ import graphql.schema.GraphQLFieldDefinition internal fun validateDirective( validatedType: String, targetDirective: String, - directives: Map, + directiveMap: Map>, fieldMap: Map, extendedType: Boolean ): List { val validationErrors = mutableListOf() - val directive = directives[targetDirective] + val directives = directiveMap[targetDirective] - if (directive == null) { + if (directives == null) { validationErrors.add("@$targetDirective directive is missing on federated $validatedType type") } else { - val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value - val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty() - if (fieldSet.isEmpty()) { - validationErrors.add("@$targetDirective directive on $validatedType is missing field information") - } else { - // validate directive field set selection - val directiveInfo = DirectiveInfo( - directiveName = targetDirective, - fieldSet = fieldSet.joinToString(" "), - typeName = validatedType - ) - validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors) + for (directive in directives) { + val fieldSetValue = (directive.getArgument(FIELD_SET_ARGUMENT_NAME)?.argumentValue?.value as? FieldSet)?.value + val fieldSet = fieldSetValue?.split(" ")?.filter { it.isNotEmpty() }.orEmpty() + if (fieldSet.isEmpty()) { + validationErrors.add("@$targetDirective directive on $validatedType is missing field information") + } else { + // validate directive field set selection + val directiveInfo = DirectiveInfo( + directiveName = targetDirective, + fieldSet = fieldSet.joinToString(" "), + typeName = validatedType + ) + validateFieldSelection(directiveInfo, fieldSet.iterator(), fieldMap, extendedType, validationErrors) + } } } return validationErrors diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateProvidesDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateProvidesDirective.kt index e223229401..7339630f5e 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateProvidesDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateProvidesDirective.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,7 @@ internal fun validateProvidesDirective(federatedType: String, field: GraphQLFiel validateDirective( "$federatedType.${field.name}", PROVIDES_DIRECTIVE_NAME, - field.directivesByName, + field.allDirectivesByName, returnTypeFields, true ) diff --git a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateRequiresDirective.kt b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateRequiresDirective.kt index 00af07877f..be63ae3176 100644 --- a/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateRequiresDirective.kt +++ b/generator/graphql-kotlin-federation/src/main/kotlin/com/expediagroup/graphql/generator/federation/validation/validateRequiresDirective.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import graphql.schema.GraphQLFieldDefinition internal fun validateRequiresDirective(validatedType: String, validatedField: GraphQLFieldDefinition, fieldMap: Map, extendedType: Boolean): List { val errors = mutableListOf() if (extendedType) { - errors.addAll(validateDirective("$validatedType.${validatedField.name}", REQUIRES_DIRECTIVE_NAME, validatedField.directivesByName, fieldMap, extendedType)) + errors.addAll(validateDirective("$validatedType.${validatedField.name}", REQUIRES_DIRECTIVE_NAME, validatedField.allDirectivesByName, fieldMap, extendedType)) } else { errors.add("base $validatedType type has fields marked with @requires directive, validatedField=${validatedField.name}") } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt index 9472e6e823..4847c292cb 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/FederatedSchemaGeneratorTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -58,7 +58,7 @@ private val FEDERATED_SDL = directive @custom on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION "Space separated list of primary keys needed to access federated object" - directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE "Specifies required input field set from the base type for a resolver" directive @requires(fields: _FieldSet!) on FIELD_DEFINITION @@ -75,18 +75,20 @@ private val FEDERATED_SDL = url: String! ) on SCALAR - interface Product @extends @key(fields : "id") { + interface Product @extends @key(fields : "id") @key(fields : "upc") { id: String! @external reviews: [Review!]! + upc: String! @external } union _Entity = Book | User - type Book implements Product @extends @key(fields : "id") { + type Book implements Product @extends @key(fields : "id") @key(fields : "upc") { author: User! @provides(fields : "name") id: String! @external reviews: [Review!]! shippingCost: String! @requires(fields : "weight") + upc: String! @external weight: Float! @external } @@ -136,7 +138,7 @@ class FederatedSchemaGeneratorTest { assertEquals(FEDERATED_SDL, schema.print().trim()) val productType = schema.getObjectType("Book") assertNotNull(productType) - assertNotNull(productType.getDirective(KEY_DIRECTIVE_NAME)) + assertNotNull(productType.hasDirective(KEY_DIRECTIVE_NAME)) val entityUnion = schema.getType(ENTITY_UNION_NAME) as? GraphQLUnionType assertNotNull(entityUnion) @@ -185,7 +187,7 @@ class FederatedSchemaGeneratorTest { directive @provides(fields: _FieldSet!) on FIELD_DEFINITION "Space separated list of primary keys needed to access federated object" - directive @key(fields: _FieldSet!) on OBJECT | INTERFACE + directive @key(fields: _FieldSet!) repeatable on OBJECT | INTERFACE "Marks target object as extending part of the federated schema" directive @extends on OBJECT | INTERFACE diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_1/FederatedMissingKey.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_01/FederatedMissingKey.kt similarity index 95% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_1/FederatedMissingKey.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_01/FederatedMissingKey.kt index e951e7645e..ad75b49f3e 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_1/FederatedMissingKey.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_01/FederatedMissingKey.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._1 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._01 import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective import com.expediagroup.graphql.generator.federation.directives.ExternalDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_2/KeyMissingFieldSelection.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_02/KeyMissingFieldSelection.kt similarity index 95% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_2/KeyMissingFieldSelection.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_02/KeyMissingFieldSelection.kt index 72fdddc7b1..4880724e58 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_2/KeyMissingFieldSelection.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_02/KeyMissingFieldSelection.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._2 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._02 import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_3/InvalidKey.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_03/InvalidKey.kt similarity index 98% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_3/InvalidKey.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_03/InvalidKey.kt index ffbe180e54..59878da6f3 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_3/InvalidKey.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_03/InvalidKey.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._3 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._03 import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_4/BaseKeyReferencingExternalFields.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_04/BaseKeyReferencingExternalFields.kt similarity index 96% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_4/BaseKeyReferencingExternalFields.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_04/BaseKeyReferencingExternalFields.kt index 604f79fa11..dd769b53ea 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_4/BaseKeyReferencingExternalFields.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_04/BaseKeyReferencingExternalFields.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._4 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._04 import com.expediagroup.graphql.generator.federation.directives.ExternalDirective import com.expediagroup.graphql.generator.federation.directives.FieldSet diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_5/ExternalKeyReferencingLocalField.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_05/ExternalKeyReferencingLocalField.kt similarity index 95% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_5/ExternalKeyReferencingLocalField.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_05/ExternalKeyReferencingLocalField.kt index be3077aeca..e18f1d7868 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_5/ExternalKeyReferencingLocalField.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_05/ExternalKeyReferencingLocalField.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._5 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._05 import com.expediagroup.graphql.generator.federation.directives.ExtendsDirective import com.expediagroup.graphql.generator.federation.directives.FieldSet diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_6/KeyReferencingList.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_06/KeyReferencingList.kt similarity index 95% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_6/KeyReferencingList.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_06/KeyReferencingList.kt index 3ba36d1783..ad945a58e6 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_6/KeyReferencingList.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_06/KeyReferencingList.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._6 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._06 import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_7/KeyReferencingInterface.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_07/KeyReferencingInterface.kt similarity index 95% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_7/KeyReferencingInterface.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_07/KeyReferencingInterface.kt index 9f18fba7ad..b937a7141d 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_7/KeyReferencingInterface.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_07/KeyReferencingInterface.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._7 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._07 import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_8/KeyReferencingUnion.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_08/KeyReferencingUnion.kt similarity index 96% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_8/KeyReferencingUnion.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_08/KeyReferencingUnion.kt index a921c7fd6d..d0819e7284 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_8/KeyReferencingUnion.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_08/KeyReferencingUnion.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._8 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._08 import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_9/NestedKeyReferencingScalar.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_09/NestedKeyReferencingScalar.kt similarity index 95% rename from generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_9/NestedKeyReferencingScalar.kt rename to generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_09/NestedKeyReferencingScalar.kt index 2b80882690..a0be4e9aba 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_9/NestedKeyReferencingScalar.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_09/NestedKeyReferencingScalar.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.expediagroup.graphql.generator.federation.data.integration.key.failure._9 +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._09 import com.expediagroup.graphql.generator.federation.directives.FieldSet import com.expediagroup.graphql.generator.federation.directives.KeyDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_10/MultipleKeysOneInvalid.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_10/MultipleKeysOneInvalid.kt new file mode 100644 index 0000000000..e14cb6dd80 --- /dev/null +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/failure/_10/MultipleKeysOneInvalid.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.generator.federation.data.integration.key.failure._10 + +import com.expediagroup.graphql.generator.federation.directives.FieldSet +import com.expediagroup.graphql.generator.federation.directives.KeyDirective +import io.mockk.mockk + +/* +# example invalid usage of @key directive - field set references non-existent field +type MultipleKeysOneInvalid @key(fields : "id") @key(fields : "upc") { + description: String! + id: String! +} + */ +@KeyDirective(fields = FieldSet("id")) +@KeyDirective(fields = FieldSet("upc")) +data class MultipleKeysOneInvalid(val id: String, val description: String) + +class MultipleKeysOneInvalidQuery { + fun multipleKeysOneInvalid(): MultipleKeysOneInvalid = mockk() +} diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/success/_7/MultipleKeys.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/success/_7/MultipleKeys.kt new file mode 100644 index 0000000000..dff75f714b --- /dev/null +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/integration/key/success/_7/MultipleKeys.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2022 Expedia, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.expediagroup.graphql.generator.federation.data.integration.key.success._7 + +import com.expediagroup.graphql.generator.federation.directives.FieldSet +import com.expediagroup.graphql.generator.federation.directives.KeyDirective +import io.mockk.mockk + +/* +# example usage of a valid @key directive referencing single field on a local type +type MultipleKeys @key(fields : "id") @key(fields : "upc") { + description: String! + id: String! + upc: String! +} + */ +@KeyDirective(fields = FieldSet("id")) +@KeyDirective(fields = FieldSet("upc")) +data class MultipleKeys(val id: String, val upc: String, val description: String) + +class MultipleKeyQuery { + fun multipleKeys(): MultipleKeys = mockk() +} diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt index 1f0f47009d..029c3a130d 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/queries/federated/Product.kt @@ -28,22 +28,26 @@ import com.expediagroup.graphql.generator.federation.directives.RequiresDirectiv import kotlin.properties.Delegates /* -interface Product @extends @key(fields : "id") { +interface Product @extends @key(fields : "id") @key(fields : "upc") { id: String! @external + upc: String! @external reviews: [Review!]! } */ @KeyDirective(fields = FieldSet("id")) +@KeyDirective(fields = FieldSet("upc")) @ExtendsDirective interface Product { @ExternalDirective val id: String + @ExternalDirective val upc: String fun reviews(): List } /* -type Book implements Product @extends @key(fields : "id") { +type Book implements Product @extends @key(fields : "id") @key(fields : "upc") { author: User! @provides(fields : "name") id: String! @external + upc: String! @external reviews: [Review!]! shippingCost: String! @requires(fields : "weight") weight: Float! @external @@ -51,10 +55,14 @@ type Book implements Product @extends @key(fields : "id") { */ @ExtendsDirective @KeyDirective(FieldSet("id")) +@KeyDirective(FieldSet("upc")) class Book( - @ExternalDirective override val id: String + @ExternalDirective override val id: String, + @ExternalDirective override val upc: String ) : Product { + constructor(id: String) : this(id, id) + // optionally provided as it is not part of the @key field set // will only be specified if federated query attempts to resolve shippingCost @ExternalDirective diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt index 05aeb6d387..c364b1c491 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,16 +39,18 @@ import kotlin.test.assertNotNull // SDL is returned without _entity and _service queries const val FEDERATED_SERVICE_SDL = """ -interface Product @extends @key(fields : "id") { +interface Product @extends @key(fields : "id") @key(fields : "upc") { id: String! @external reviews: [Review!]! + upc: String! @external } -type Book implements Product @extends @key(fields : "id") { +type Book implements Product @extends @key(fields : "id") @key(fields : "upc") { author: User! @provides(fields : "name") id: String! @external reviews: [Review!]! shippingCost: String! @requires(fields : "weight") + upc: String! @external weight: Float! @external } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/ValidateDirectiveKtTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/ValidateDirectiveKtTest.kt index f02f25f3d0..f60e500e81 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/ValidateDirectiveKtTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/ValidateDirectiveKtTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "", - directives = emptyMap(), + directiveMap = emptyMap(), fieldMap = emptyMap(), extendedType = false ) @@ -54,7 +54,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = emptyMap(), extendedType = false ) @@ -77,7 +77,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = emptyMap(), extendedType = false ) @@ -100,7 +100,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = emptyMap(), extendedType = false ) @@ -125,7 +125,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = emptyMap(), extendedType = false ) @@ -150,7 +150,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = emptyMap(), extendedType = false ) @@ -175,7 +175,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = emptyMap(), extendedType = false ) @@ -207,7 +207,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "MyType", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = mapOf("bar" to graphqlField), extendedType = false ) @@ -245,7 +245,7 @@ internal class ValidateDirectiveKtTest { val validationErrors = validateDirective( validatedType = "MyType", targetDirective = "foo", - directives = mapOf("foo" to directive), + directiveMap = mapOf("foo" to listOf(directive)), fieldMap = mapOf("bar" to graphqlField1, "baz" to graphqlField2), extendedType = false ) diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedKeyDirectiveIT.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedKeyDirectiveIT.kt index 5d10da79f9..dd1e8d2cb8 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedKeyDirectiveIT.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedKeyDirectiveIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,16 +17,18 @@ package com.expediagroup.graphql.generator.federation.validation.integration import com.expediagroup.graphql.generator.TopLevelObject -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._2.KeyMissingFieldSelectionQuery -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._3.InvalidKeyQuery -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._4.BaseKeyReferencingExternalFieldsQuery -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._6.KeyReferencingListQuery -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._7.KeyReferencingInterfaceQuery -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._8.KeyReferencingUnionQuery -import com.expediagroup.graphql.generator.federation.data.integration.key.failure._9.NestedKeyReferencingScalarQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._02.KeyMissingFieldSelectionQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._03.InvalidKeyQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._04.BaseKeyReferencingExternalFieldsQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._06.KeyReferencingListQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._07.KeyReferencingInterfaceQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._08.KeyReferencingUnionQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._09.NestedKeyReferencingScalarQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.failure._10.MultipleKeysOneInvalidQuery import com.expediagroup.graphql.generator.federation.data.integration.key.success._1.SimpleKeyQuery import com.expediagroup.graphql.generator.federation.data.integration.key.success._3.KeyWithMultipleFieldsQuery import com.expediagroup.graphql.generator.federation.data.integration.key.success._5.KeyWithNestedFieldsQuery +import com.expediagroup.graphql.generator.federation.data.integration.key.success._7.MultipleKeyQuery import com.expediagroup.graphql.generator.federation.exception.InvalidFederatedSchema import com.expediagroup.graphql.generator.federation.toFederatedSchema import graphql.schema.GraphQLSchema @@ -35,6 +37,7 @@ import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotNull +import kotlin.test.assertTrue class FederatedKeyDirectiveIT { @@ -98,13 +101,13 @@ class FederatedKeyDirectiveIT { private fun validateTypeWasCreatedWithKeyDirective(schema: GraphQLSchema, typeName: String) { val validatedType = schema.getObjectType(typeName) assertNotNull(validatedType) - assertNotNull(validatedType.getDirective("key")) + assertTrue(validatedType.hasDirective("key")) } @Test fun `@extended type should specify @key directive`() { val exception = assertFailsWith { - toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._1")) + toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._01")) } val expected = "Invalid federated schema:\n - @key directive is missing on federated FederatedMissingKey type" assertEquals(expected, exception.message) @@ -114,7 +117,7 @@ class FederatedKeyDirectiveIT { fun `@key directive field set cannot be empty`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._2"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._02"), queries = listOf(TopLevelObject(KeyMissingFieldSelectionQuery())) ) } @@ -126,7 +129,7 @@ class FederatedKeyDirectiveIT { fun `@key directive needs to reference valid field`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._3"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._03"), queries = listOf(TopLevelObject(InvalidKeyQuery())) ) } @@ -138,7 +141,7 @@ class FederatedKeyDirectiveIT { fun `@key directive on local type cannot reference external fields`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._4"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._04"), queries = listOf(TopLevelObject(BaseKeyReferencingExternalFieldsQuery())) ) } @@ -151,7 +154,7 @@ class FederatedKeyDirectiveIT { @Test fun `@extended type @key directive cannot reference local fields`() { val exception = assertFailsWith { - toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._5")) + toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._05")) } val expected = "Invalid federated schema:\n" + " - @key(fields = id) directive on ExternalKeyReferencingLocalField specifies invalid field set - extended type incorrectly references local field=id" @@ -162,7 +165,7 @@ class FederatedKeyDirectiveIT { fun `@key directive field set cannot reference list field`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._6"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._06"), queries = listOf(TopLevelObject(KeyReferencingListQuery())) ) } @@ -175,7 +178,7 @@ class FederatedKeyDirectiveIT { fun `@key directive field set cannot reference interface field`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._7"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._07"), queries = listOf(TopLevelObject(KeyReferencingInterfaceQuery())) ) } @@ -188,7 +191,7 @@ class FederatedKeyDirectiveIT { fun `@key directive field set cannot reference union field`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._8"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._08"), queries = listOf(TopLevelObject(KeyReferencingUnionQuery())) ) } @@ -201,7 +204,7 @@ class FederatedKeyDirectiveIT { fun `@key directive field set cannot reference scalar complex key`() { val exception = assertFailsWith { toFederatedSchema( - config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._9"), + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._09"), queries = listOf(TopLevelObject(NestedKeyReferencingScalarQuery())) ) } @@ -210,4 +213,32 @@ class FederatedKeyDirectiveIT { " - @key(fields = id { uuid }) directive on NestedKeyReferencingScalar specifies invalid field set - field set specifies field that does not exist, field=uuid" assertEquals(expected, exception.message) } + + @Test + fun `verifies multiple @key directive on a local type`() { + assertDoesNotThrow { + val schema = toFederatedSchema( + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.success._7"), + queries = listOf(TopLevelObject(MultipleKeyQuery())) + ) + val validatedType = schema.getObjectType("MultipleKeys") + assertNotNull(validatedType) + val keyDirectives = validatedType.getDirectives("key") + assertTrue(keyDirectives.isNotEmpty()) + assertEquals(2, keyDirectives.size) + } + } + + @Test + fun `verifies validation runs on multiple @key directives`() { + val exception = assertFailsWith { + toFederatedSchema( + config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.key.failure._10"), + queries = listOf(TopLevelObject(MultipleKeysOneInvalidQuery())) + ) + } + val expected = "Invalid federated schema:\n" + + " - @key(fields = upc) directive on MultipleKeysOneInvalid specifies invalid field set - field set specifies field that does not exist, field=upc" + assertEquals(expected, exception.message) + } } diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedProvidesDirectiveIT.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedProvidesDirectiveIT.kt index 64899de718..f263c4eb28 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedProvidesDirectiveIT.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedProvidesDirectiveIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,6 +34,7 @@ import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotNull +import kotlin.test.assertTrue class FederatedProvidesDirectiveIT { @@ -62,7 +63,7 @@ class FederatedProvidesDirectiveIT { private fun validateTypeWasCreatedWithProvidesDirective(schema: GraphQLSchema, typeName: String) { val validatedType = schema.getObjectType(typeName) assertNotNull(validatedType) - assertNotNull(validatedType.getDirective("key")) + assertTrue(validatedType.hasDirective("key")) val providedField = validatedType.getFieldDefinition("provided") assertNotNull(providedField) assertNotNull(providedField.getDirective("provides")) diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedRequiresDirectiveIT.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedRequiresDirectiveIT.kt index e503b8eaae..c606599b0b 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedRequiresDirectiveIT.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/validation/integration/FederatedRequiresDirectiveIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.junit.jupiter.api.assertDoesNotThrow import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNotNull +import kotlin.test.assertTrue class FederatedRequiresDirectiveIT { @@ -33,7 +34,7 @@ class FederatedRequiresDirectiveIT { assertDoesNotThrow { val schema = toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.success._1")) val validatedType = schema.getObjectType("SimpleRequires") - assertNotNull(validatedType.getDirective("key")) + assertTrue(validatedType.hasDirective("key")) val weightField = validatedType.getFieldDefinition("weight") assertNotNull(weightField) assertNotNull(weightField.getDirective("external")) @@ -90,7 +91,7 @@ class FederatedRequiresDirectiveIT { assertDoesNotThrow { val schema = toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.success._2")) val validatedType = schema.getObjectType("User") - assertNotNull(validatedType.getDirective("key")) + assertTrue(validatedType.hasDirective("key")) val externalField = validatedType.getFieldDefinition("email") assertNotNull(externalField) assertNotNull(externalField.getDirective("external")) @@ -105,7 +106,7 @@ class FederatedRequiresDirectiveIT { assertDoesNotThrow { val schema = toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.success._3")) val validatedType = schema.getObjectType("User") - assertNotNull(validatedType.getDirective("key")) + assertTrue(validatedType.hasDirective("key")) val externalField = validatedType.getFieldDefinition("email") assertNotNull(externalField) assertNotNull(externalField.getDirective("external")) @@ -120,7 +121,7 @@ class FederatedRequiresDirectiveIT { assertDoesNotThrow { val schema = toFederatedSchema(config = federatedTestConfig("com.expediagroup.graphql.generator.federation.data.integration.requires.success._4")) val validatedType = schema.getObjectType("User") - assertNotNull(validatedType.getDirective("key")) + assertTrue(validatedType.hasDirective("key")) val externalField = validatedType.getFieldDefinition("email") assertNotNull(externalField) assertNotNull(externalField.getDirective("external")) diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt index a1b053971f..08039fad8d 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/internal/types/generateDirective.kt @@ -28,6 +28,7 @@ import java.lang.reflect.Field import kotlin.reflect.KAnnotatedElement import kotlin.reflect.KClass import kotlin.reflect.KProperty +import kotlin.reflect.full.hasAnnotation import com.expediagroup.graphql.generator.annotations.GraphQLDirective as GraphQLDirectiveAnnotation internal fun generateDirectives( @@ -59,6 +60,7 @@ private fun getDirective(generator: SchemaGenerator, directiveInfo: DirectiveInf val builder = GraphQLDirective.newDirective() .name(directiveInfo.effectiveName) .description(directiveInfo.directiveAnnotation.description) + .repeatable(directiveInfo.repeatable) directiveInfo.directiveAnnotation.locations.forEach { builder.validLocation(it) @@ -108,10 +110,13 @@ private fun String.normalizeDirectiveName() = this.replaceFirstChar { it.lowerca private fun Annotation.getDirectiveInfo(): DirectiveInfo? = this.annotationClass.annotations .filterIsInstance(GraphQLDirectiveAnnotation::class.java) - .map { DirectiveInfo(this, it) } + .map { graphqlDirective -> + val isRepeatable = this.annotationClass.hasAnnotation() + DirectiveInfo(this, graphqlDirective, isRepeatable) + } .firstOrNull() -private data class DirectiveInfo(val directive: Annotation, val directiveAnnotation: GraphQLDirectiveAnnotation) { +private data class DirectiveInfo(val directive: Annotation, val directiveAnnotation: GraphQLDirectiveAnnotation, val repeatable: Boolean = false) { val effectiveName: String = when { directiveAnnotation.name.isNotEmpty() -> directiveAnnotation.name else -> directive.annotationClass.getSimpleName().normalizeDirectiveName() diff --git a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateDirectiveTest.kt b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateDirectiveTest.kt index 1c43c03d93..1905d1e90f 100644 --- a/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateDirectiveTest.kt +++ b/generator/graphql-kotlin-schema-generator/src/test/kotlin/com/expediagroup/graphql/generator/internal/types/GenerateDirectiveTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2022 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -72,6 +72,10 @@ class GenerateDirectiveTest { @GraphQLDirective(locations = [DirectiveLocation.FIELD_DEFINITION]) annotation class DirectiveOnFieldDefinitionOnly + @Repeatable + @GraphQLDirective(locations = [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE]) + annotation class RepeatableDirective(val value: String) + class MyClass { fun noAnnotation(string: String) = string @@ -102,6 +106,11 @@ class GenerateDirectiveTest { @DirectiveOnObjectOnly @DirectiveOnInputObjectOnly fun invalidDirectives(string: String) = string + + @RepeatableDirective("foo") + @RepeatableDirective("bar") + @RepeatableDirective("baz") + fun repeatedDirectives(string: String) = string } data class MyClassWithConstructorArgs( @@ -191,11 +200,11 @@ class GenerateDirectiveTest { assertEquals(expected = 1, actual = directivesOnSecondField.size) val firstDirective = directivesOnFirstField.first() - val seconDirective = directivesOnSecondField.first() + val secondDirective = directivesOnSecondField.first() assertEquals("directiveWithString", firstDirective.name) - assertEquals("directiveWithString", seconDirective.name) + assertEquals("directiveWithString", secondDirective.name) assertEquals("foo", firstDirective.getArgument("string")?.argumentValue?.value) - assertEquals("bar", seconDirective.getArgument("string")?.argumentValue?.value) + assertEquals("bar", secondDirective.getArgument("string")?.argumentValue?.value) assertEquals(initialCount + 1, basicGenerator.directives.size) } @@ -221,7 +230,7 @@ class GenerateDirectiveTest { } @Test - fun `exlude directive arguments @GraphQLIgnore`() { + fun `exclude directive arguments @GraphQLIgnore`() { val directiveWithIgnoredArgs: KFunction = MyClass::directiveWithIgnoredArgs val directives = generateDirectives(basicGenerator, directiveWithIgnoredArgs, DirectiveLocation.FIELD_DEFINITION) assertEquals(expected = 1, actual = directives.size) @@ -230,7 +239,7 @@ class GenerateDirectiveTest { } @Test - fun `exlude directives with invalid locations`() { + fun `exclude directives with invalid locations`() { val objectDirectives = generateDirectives(basicGenerator, MyExampleObject::class, DirectiveLocation.OBJECT) assertEquals(expected = 1, actual = objectDirectives.size) assertEquals("directiveOnObjectOnly", objectDirectives.first().name) @@ -244,6 +253,18 @@ class GenerateDirectiveTest { assertEquals("directiveOnFieldDefinitionOnly", fieldDirectives.first().name) } + @Test + fun `repeatable directives are supported`() { + val repeatableDirectiveResult = generateDirectives(basicGenerator, MyClass::repeatedDirectives, DirectiveLocation.FIELD_DEFINITION) + assertEquals(3, repeatableDirectiveResult.size) + assertEquals("repeatableDirective", repeatableDirectiveResult[0].name) + assertEquals("foo", repeatableDirectiveResult[0].getArgument("value")?.argumentValue?.value) + assertEquals("repeatableDirective", repeatableDirectiveResult[1].name) + assertEquals("bar", repeatableDirectiveResult[1].getArgument("value")?.argumentValue?.value) + assertEquals("repeatableDirective", repeatableDirectiveResult[2].name) + assertEquals("baz", repeatableDirectiveResult[2].getArgument("value")?.argumentValue?.value) + } + companion object { @AfterAll fun cleanUp(generateDirectiveTest: GenerateDirectiveTest) { diff --git a/website/docs/schema-generator/customizing-schemas/directives.md b/website/docs/schema-generator/customizing-schemas/directives.md index 6c0b4bf7f6..37aad1d243 100644 --- a/website/docs/schema-generator/customizing-schemas/directives.md +++ b/website/docs/schema-generator/customizing-schemas/directives.md @@ -142,6 +142,24 @@ This is a different behavior than `graphql-java` which will first attempt to use For more details please refer to the example usage of directives in our [example app](https://github.com/ExpediaGroup/graphql-kotlin/tree/master/examples/server/spring-server). +### Repeatable Directives + +GraphQL supports repeatable directives (e.g. Apollo federation allows developers to specify multiple `@key` directives). +By default, Kotlin does not allow applying same annotation multiple times. In order to make your directives repeatable, +you need to annotate it with `kotlin.annotation.Repeatable` annotation. + +```kotlin +@Repeatable +@GraphQLDirective(locations = [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE]) +annotation class MyRepeatableDirective(val value: String) +``` + +Generates the above directive as + +```graphql +directive @myRepeatableDirective(value: String!) repeatable on OBJECT | INTERFACE +``` + ## Directive Chaining Directives are applied in the order annotations are declared on the given object. Given diff --git a/website/docs/schema-generator/federation/federated-directives.md b/website/docs/schema-generator/federation/federated-directives.md index 6747fa5d1d..b5df598836 100644 --- a/website/docs/schema-generator/federation/federated-directives.md +++ b/website/docs/schema-generator/federation/federated-directives.md @@ -73,29 +73,28 @@ directive @key(fields: _FieldSet!) on OBJECT | INTERFACE The `@key` directive is used to indicate a combination of fields that can be used to uniquely identify and fetch an object or interface. The specified field set can represent single field (e.g. `"id"`), multiple fields (e.g. `"id name"`) or -nested selection sets (e.g. `"id user { name }"`). +nested selection sets (e.g. `"id user { name }"`). Multiple keys can be specified on a target type. Key directives should be specified on the root base type as well as all the corresponding federated (i.e. extended) types. Key fields specified in the directive field set should correspond to a valid field on the underlying GraphQL interface/object. Federated extended types should also instrument all the referenced key fields with `@external` directive. -> NOTE: The Federation spec specifies that multiple @key directives can be applied on the field. The GraphQL spec has been recently changed to allow this behavior, -> but we are currently blocked and are tracking progress in [this issue](https://github.com/ExpediaGroup/graphql-kotlin/issues/590). - Example ```kotlin @KeyDirective(FieldSet("id")) -class Product(val id: String, val name: String) +@KeyDirective(FieldSet("upc")) +class Product(val id: String, val upc: String, val name: String) ``` will generate ```graphql -type Product @key(fields: "id") { +type Product @key(fields: "id") @key(fields: "upc") { id: String! name: String! + upc: String! } ```