From d730ff5c200dfb44c4217f2786a8729381c6a286 Mon Sep 17 00:00:00 2001 From: Craig Day Date: Fri, 16 Feb 2024 11:54:05 -0800 Subject: [PATCH] ensure scalars are wired up if no existing wiring (#382) Fixes #380 This restructures the scalar transformation logic to ensure that both the type definition and wiring are added correctly if necessary. This allows consumers to define the scalars in their schema, but not assign runtime wiring. This can be nice to support other tools like linters or graphql-faker that require the schema to be a valid schema file. --- .../federation/graphqljava/Federation.java | 21 ++-- .../graphqljava/FederationTest.java | 5 + .../federationV2_defined_scalars.graphql | 84 ++++++++++++++ ...rationV2_defined_scalars_federated.graphql | 104 ++++++++++++++++++ 4 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars.graphql create mode 100644 graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars_federated.graphql diff --git a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java index fba777b1..dad58bfa 100644 --- a/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java +++ b/graphql-java-support/src/main/java/com/apollographql/federation/graphqljava/Federation.java @@ -163,15 +163,18 @@ private static RuntimeWiring ensureFederationV2DirectiveDefinitionsExist( if (def instanceof DirectiveDefinition && !typeRegistry.getDirectiveDefinition(def.getName()).isPresent()) { typeRegistry.add(def); - } else if (def instanceof ScalarTypeDefinition - && !typeRegistry.scalars().containsKey(def.getName())) { - typeRegistry.add(def); - scalarTypesToAdd.add( - GraphQLScalarType.newScalar() - .name(def.getName()) - .description(null) - .coercing(_Any.type.getCoercing()) - .build()); + } else if (def instanceof ScalarTypeDefinition) { + if (!typeRegistry.scalars().containsKey(def.getName())) { + typeRegistry.add(def); + } + if (!runtimeWiring.getScalars().containsKey(def.getName())) { + scalarTypesToAdd.add( + GraphQLScalarType.newScalar() + .name(def.getName()) + .description(null) + .coercing(_Any.type.getCoercing()) + .build()); + } } else if (def instanceof EnumTypeDefinition && !typeRegistry.types().containsKey(def.getName())) { typeRegistry.add(def); diff --git a/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java b/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java index 2c35bcfb..19d4f79c 100644 --- a/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java +++ b/graphql-java-support/src/test/java/com/apollographql/federation/graphqljava/FederationTest.java @@ -356,6 +356,11 @@ public void verifyFederationV2Transformation_progressiveOverride() { () -> Federation.transform(schemaSDL).fetchEntities(env -> null).build()); } + @Test + public void verifyFederationV2Transformation_scalarsDefinedInSchemaButNotWired() { + verifyFederationTransformation("schemas/federationV2_defined_scalars.graphql", true); + } + private GraphQLSchema verifyFederationTransformation( String schemaFileName, boolean isFederationV2) { final RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring().build(); diff --git a/graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars.graphql b/graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars.graphql new file mode 100644 index 00000000..b451f004 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars.graphql @@ -0,0 +1,84 @@ +extend schema + @link( + url: "https://specs.apollo.dev/federation/v2.3" + import: [ + "@composeDirective" + "@extends" + "@external" + "@key" + "@inaccessible" + "@interfaceObject" + "@override" + "@provides" + "@requires" + "@shareable" + "@tag" + ] + ) + @link(url: "https://myspecs.dev/myCustomDirective/v1.0", import: ["@custom"]) + @composeDirective(name: "@custom") + +scalar federation__FieldSet + +directive @custom on OBJECT +directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +type Product + @custom + @key(fields: "id") + @key(fields: "sku package") + @key(fields: "sku variation { id }") { + id: ID! + sku: String + package: String + variation: ProductVariation + dimensions: ProductDimension + createdBy: User @provides(fields: "totalProductsCreated") + notes: String @tag(name: "internal") + research: [ProductResearch!]! +} + +type DeprecatedProduct @key(fields: "sku package") { + sku: String! + package: String! + reason: String + createdBy: User +} + +type ProductVariation { + id: ID! +} + +type ProductResearch @key(fields: "study { caseNumber }") { + study: CaseStudy! + outcome: String +} + +type CaseStudy { + caseNumber: ID! + description: String +} + +type ProductDimension @shareable { + size: String + weight: Float + unit: String @inaccessible +} + +type Query { + product(id: ID!): Product + deprecatedProduct(sku: String!, package: String!): DeprecatedProduct @deprecated(reason: "Use product query instead") +} + +type User @key(fields: "email") { + averageProductsCreatedPerYear: Int @requires(fields: "totalProductsCreated yearsOfEmployment") + email: ID! @external + name: String @override(from: "users") + totalProductsCreated: Int @external + yearsOfEmployment: Int! @external +} + +type Inventory @interfaceObject @key(fields: "id") { + id: ID! + deprecatedProducts: [DeprecatedProduct!]! +} diff --git a/graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars_federated.graphql b/graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars_federated.graphql new file mode 100644 index 00000000..2ffa9871 --- /dev/null +++ b/graphql-java-support/src/test/resources/schemas/federationV2_defined_scalars_federated.graphql @@ -0,0 +1,104 @@ +schema @composeDirective(name : "@custom") @link(import : ["@composeDirective", "@extends", "@external", "@key", "@inaccessible", "@interfaceObject", "@override", "@provides", "@requires", "@shareable", "@tag"], url : "https://specs.apollo.dev/federation/v2.3") @link(import : ["@custom"], url : "https://myspecs.dev/myCustomDirective/v1.0"){ + query: Query +} + +directive @composeDirective(name: String!) repeatable on SCHEMA + +directive @custom on OBJECT + +directive @extends on OBJECT | INTERFACE + +directive @external on OBJECT | FIELD_DEFINITION + +directive @inaccessible on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +directive @interfaceObject on OBJECT + +directive @key(fields: federation__FieldSet!, resolvable: Boolean = true) repeatable on OBJECT | INTERFACE + +directive @link(as: String, for: link__Purpose, import: [link__Import], url: String!) repeatable on SCHEMA + +directive @override(from: String!) on FIELD_DEFINITION + +directive @provides(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @requires(fields: federation__FieldSet!) on FIELD_DEFINITION + +directive @shareable repeatable on OBJECT | FIELD_DEFINITION + +directive @tag(name: String!) repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + +union _Entity = DeprecatedProduct | Inventory | Product | ProductResearch | User + +type CaseStudy { + caseNumber: ID! + description: String +} + +type DeprecatedProduct @key(fields : "sku package", resolvable : true) { + createdBy: User + package: String! + reason: String + sku: String! +} + +type Inventory @interfaceObject @key(fields : "id", resolvable : true) { + deprecatedProducts: [DeprecatedProduct!]! + id: ID! +} + +type Product @custom @key(fields : "id", resolvable : true) @key(fields : "sku package", resolvable : true) @key(fields : "sku variation { id }", resolvable : true) { + createdBy: User @provides(fields : "totalProductsCreated") + dimensions: ProductDimension + id: ID! + notes: String @tag(name : "internal") + package: String + research: [ProductResearch!]! + sku: String + variation: ProductVariation +} + +type ProductDimension @shareable { + size: String + unit: String @inaccessible + weight: Float +} + +type ProductResearch @key(fields : "study { caseNumber }", resolvable : true) { + outcome: String + study: CaseStudy! +} + +type ProductVariation { + id: ID! +} + +type Query { + _entities(representations: [_Any!]!): [_Entity]! + _service: _Service! + deprecatedProduct(package: String!, sku: String!): DeprecatedProduct @deprecated(reason : "Use product query instead") + product(id: ID!): Product +} + +type User @key(fields : "email", resolvable : true) { + averageProductsCreatedPerYear: Int @requires(fields : "totalProductsCreated yearsOfEmployment") + email: ID! @external + name: String @override(from : "users") + totalProductsCreated: Int @external + yearsOfEmployment: Int! @external +} + +type _Service { + sdl: String! +} + +enum link__Purpose { + EXECUTION + SECURITY +} + +scalar _Any + +scalar federation__FieldSet + +scalar link__Import